Compare commits

..

11 Commits

Author SHA1 Message Date
5b33a8617e Merge branch 'dev' of https://github.com/SyncrowIOT/web into hotfix/communities_loading 2025-06-18 15:25:43 +03:00
34565a7dab hotfix/communities_loading v2. 2025-06-18 15:25:32 +03:00
caf1ff5c7e Fix energy device condition and community and space dialog (#268)
<!--
  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 energy dialog and fix reset value in text form and fix create dialog
routine

## 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-18 15:01:08 +03:00
01e8002c43 fix: adjust spacing in create new routines dialog for improved layout 2025-06-18 14:55:12 +03:00
63da660ece refactor: update function handling in routine dialogs 2025-06-18 14:40:25 +03:00
567d0e2d20 hotfix/communities_loading (#267)
<!--
  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

hotfix/ loading communities.
## 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)
- [x]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-18 14:37:00 +03:00
45e6ea3259 hotfix/communities_loading 2025-06-18 14:33:39 +03:00
b9a3b9c719 fix: update dropdown styles and dimensions for better UI consistency 2025-06-18 12:14:17 +03:00
f5500dfe50 bug fixed it is locally change the state now (#264)
<!--
  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:
-->

## Jira Ticket

[SP-SP-1413](https://syncrow.atlassian.net/jira/software/projects/SP/boards/5?assignee=712020%3A71e88a7f-7752-44b3-8177-4ab51a950811&selectedIssue=SP-1413)

## Description

state now changes without fetching API it is locallly

## 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-18 09:51:06 +03:00
1ba1aba54e PR fixes and tested 2025-06-18 08:42:18 +03:00
09f2123946 bug fixed it is locally change the state now 2025-06-17 16:21:01 +03:00
57 changed files with 428 additions and 1568 deletions

View File

@ -1,10 +0,0 @@
import 'package:flutter/material.dart';
class AppLoadingIndicator extends StatelessWidget {
const AppLoadingIndicator({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: CircularProgressIndicator());
}
}

View File

@ -7,10 +7,10 @@ import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options_prod.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/services/locator.dart';
import 'package:syncrow_web/utils/app_routes.dart';
@ -59,7 +59,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
@ -67,7 +67,7 @@ class MyApp extends StatelessWidget {
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(

View File

@ -7,10 +7,10 @@ import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options_dev.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/services/locator.dart';
import 'package:syncrow_web/utils/app_routes.dart';
@ -59,7 +59,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
@ -67,7 +67,7 @@ class MyApp extends StatelessWidget {
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(

View File

@ -7,10 +7,10 @@ import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options_prod.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/services/locator.dart';
import 'package:syncrow_web/utils/app_routes.dart';
@ -56,7 +56,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
@ -64,7 +64,7 @@ class MyApp extends StatelessWidget {
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(

View File

@ -31,6 +31,8 @@ import 'package:syncrow_web/pages/analytics/services/range_of_aqi/remote_range_o
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
@ -130,9 +132,19 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
}
}
class AnalyticsPageForm extends StatelessWidget {
class AnalyticsPageForm extends StatefulWidget {
const AnalyticsPageForm({super.key});
@override
State<AnalyticsPageForm> createState() => _AnalyticsPageFormState();
}
class _AnalyticsPageFormState extends State<AnalyticsPageForm> {
@override
void initState() {
context.read<SpaceTreeBloc>().add(InitialEvent());
super.initState();
}
@override
Widget build(BuildContext context) {
return WebScaffold(

View File

@ -8,15 +8,28 @@ import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routi
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/view/create_new_routine_view.dart';
import 'package:syncrow_web/pages/routines/view/routines_view.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
class DeviceManagementPage extends StatefulWidget with HelperResponsiveLayout {
const DeviceManagementPage({super.key});
@override
State<DeviceManagementPage> createState() => _DeviceManagementPageState();
}
class _DeviceManagementPageState extends State<DeviceManagementPage> {
@override
void initState() {
context.read<SpaceTreeBloc>().add(InitialEvent());
super.initState();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(

View File

@ -13,19 +13,25 @@ class HomePage extends StatefulWidget {
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with HelperResponsiveLayout{
class _HomePageState extends State<HomePage> with HelperResponsiveLayout {
@override
void initState() {
context.read<HomeBloc>().add(const FetchUserInfo());
_fetchUserInfo();
super.initState();
}
@override
Widget build(BuildContext context) {
final isSmallScreen = isSmallScreenSize(context);
final isMediumScreen = isMediumScreenSize(context);
return isSmallScreen || isMediumScreen
? HomeMobilePage()
: const HomeWebPage();
if (isSmallScreenSize(context) || isMediumScreenSize(context)) {
return HomeMobilePage();
}
return const HomeWebPage();
}
void _fetchUserInfo() {
final bloc = context.read<HomeBloc>();
if (bloc.user == null) bloc.add(const FetchUserInfo());
}
}

View File

@ -13,6 +13,35 @@ class FunctionBloc extends Bloc<FunctionBlocEvent, FunctionBlocState> {
on<AddFunction>(_onAddFunction);
on<SelectFunction>(_onSelectFunction);
}
// void _onAddFunction(AddFunction event, Emitter<FunctionBlocState> emit) {
// final functions = List<DeviceFunctionData>.from(state.addedFunctions);
// final existingIndex = functions.indexWhere(
// (f) => f.functionCode == event.functionData.functionCode,
// );
// if (existingIndex != -1) {
// final existingData = functions[existingIndex];
// functions[existingIndex] = DeviceFunctionData(
// entityId: event.functionData.entityId,
// functionCode: event.functionData.functionCode,
// operationName: event.functionData.operationName,
// value: event.functionData.value ?? existingData.value,
// valueDescription: event.functionData.valueDescription ??
// existingData.valueDescription,
// condition: event.functionData.condition ?? existingData.condition,
// step: event.functionData.step ?? existingData.step,
// );
// } else {
// functions.clear();
// functions.add(event.functionData);
// }
// emit(state.copyWith(
// addedFunctions: functions,
// selectedFunction: event.functionData.functionCode,
// ));
// }
void _onAddFunction(AddFunction event, Emitter<FunctionBlocState> emit) {
final functions = List<DeviceFunctionData>.from(state.addedFunctions);
final existingIndex = functions.indexWhere(
@ -20,19 +49,10 @@ class FunctionBloc extends Bloc<FunctionBlocEvent, FunctionBlocState> {
);
if (existingIndex != -1) {
final existingData = functions[existingIndex];
functions[existingIndex] = DeviceFunctionData(
entityId: event.functionData.entityId,
functionCode: event.functionData.functionCode,
operationName: event.functionData.operationName,
value: event.functionData.value ?? existingData.value,
valueDescription: event.functionData.valueDescription ??
existingData.valueDescription,
condition: event.functionData.condition ?? existingData.condition,
step: event.functionData.step ?? existingData.step,
);
// Update the function value
functions[existingIndex] = event.functionData;
} else {
functions.clear();
// Add new function value
functions.add(event.functionData);
}

View File

@ -1419,15 +1419,17 @@ Future<void> _onLoadScenes(
event.automationId, event.automationStatusUpdate, projectId);
if (success) {
final updatedAutomations = await SceneApi.getAutomationByUnitId(
event.automationStatusUpdate.spaceUuid,
event.communityId,
projectId);
// await SceneApi.getAutomationByUnitId(
// event.automationStatusUpdate.spaceUuid,
// event.communityId,
// projectId);
// Remove from loading set safely
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
final updatedAutomations = changeItemStateOnToggelingSceen(
state.automations, event.automationId);
emit(state.copyWith(
automations: updatedAutomations,
loadingAutomationIds: updatedLoadingIds,
@ -1449,4 +1451,24 @@ Future<void> _onLoadScenes(
));
}
}
List<ScenesModel> changeItemStateOnToggelingSceen(
List<ScenesModel> oldSceen, String automationId) {
return oldSceen.map((scene) {
if (scene.id == automationId) {
return ScenesModel(
id: scene.id,
sceneTuyaId: scene.sceneTuyaId,
name: scene.name,
status: scene.status == 'enable' ? 'disable' : 'enable',
type: scene.type,
spaceName: scene.spaceName,
spaceId: scene.spaceId,
communityId: scene.communityId,
icon: scene.icon,
);
}
return scene;
}).toList();
}
}

View File

@ -117,7 +117,7 @@ class _DropdownContentState extends State<_DropdownContent> {
final selectedCommunity = _findCommunity(state, state.selectedSpaceId);
return Container(
height: 46,
height: 40,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
@ -149,7 +149,7 @@ class _DropdownContentState extends State<_DropdownContent> {
),
),
height: 45,
width: 33,
width: 44,
child: const Icon(
Icons.keyboard_arrow_down,
color: ColorsManager.textGray,

View File

@ -44,144 +44,156 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
_selectedSpace = null;
_selectedCommunity = _selectedId;
}
return AlertDialog(
return Dialog(
backgroundColor: Colors.white,
insetPadding: EdgeInsets.zero,
contentPadding: EdgeInsets.zero,
insetPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
title: Text(
'Create New Routines',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.spaceColor,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
content: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
const Divider(),
const SizedBox(height: 20),
Column(
children: [
Padding(
padding:
const EdgeInsets.only(left: 13, right: 8),
child: Column(
children: [
SpaceTreeDropdown(
selectedSpaceId: _selectedId,
child: Container(
width: 450,
child: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 20),
Text(
'Create New Routines',
textAlign: TextAlign.center,
style:
Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.spaceColor,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
const Divider(),
const SizedBox(height: 20),
Column(
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 13, right: 10),
child: Column(
children: [
SpaceTreeDropdown(
selectedSpaceId: _selectedId,
onChanged: (String? newValue) {
setState(
() => _selectedId = newValue!);
if (_selectedId != null) {
_bloc.add(
SpaceOnlyWithDevicesEvent(
_selectedId!));
}
},
),
],
)),
const SizedBox(height: 21),
Padding(
padding: const EdgeInsets.only(
left: 15, right: 20),
child: SpaceDropdown(
hintMessage: spaceHint,
spaces: spaces,
selectedValue: _selectedSpace,
onChanged: (String? newValue) {
setState(() => _selectedId = newValue!);
if (_selectedId != null) {
_bloc.add(SpaceOnlyWithDevicesEvent(
_selectedId!));
}
setState(() {
_selectedSpace = newValue;
});
},
),
],
)),
const SizedBox(height: 5),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(left: 15, right: 15),
child: SpaceDropdown(
hintMessage: spaceHint,
spaces: spaces,
selectedValue: _selectedSpace,
onChanged: (String? newValue) {
setState(() {
_selectedSpace = newValue;
});
},
),
],
),
),
],
),
const SizedBox(height: 20),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'Cancel',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
color: ColorsManager.blackColor,
),
],
),
const SizedBox(height: 20),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'Cancel',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
color: ColorsManager.blackColor,
),
),
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextButton(
onPressed: _selectedCommunity != null &&
_selectedSpace != null
? () {
Navigator.of(context).pop({
'community': _selectedCommunity,
'space': _selectedSpace,
});
}
: null,
child: Text(
'Next',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
color: _selectedCommunity != null &&
_selectedSpace != null
? ColorsManager.blueColor
: Colors.blue.shade100,
),
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextButton(
onPressed: _selectedCommunity != null &&
_selectedSpace != null
? () {
Navigator.of(context).pop({
'community': _selectedCommunity,
'space': _selectedSpace,
});
}
: null,
child: Text(
'Next',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
color: _selectedCommunity != null &&
_selectedSpace != null
? ColorsManager.blueColor
: Colors.blue.shade100,
),
),
),
),
),
],
),
const SizedBox(height: 10),
],
),
if (isLoadingCommunities)
const SizedBox(
height: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: CircularProgressIndicator(
color: ColorsManager.primaryColor,
),
),
],
),
],
),
const SizedBox(height: 10),
],
),
],
if (isLoadingCommunities)
const SizedBox(
height: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: CircularProgressIndicator(
color: ColorsManager.primaryColor,
),
),
],
),
),
],
),
),
);
},

View File

@ -34,7 +34,9 @@ class SpaceDropdown extends StatelessWidget {
),
SizedBox(
child: Container(
height: 40,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(10),
),
child: DropdownButton2<String>(
@ -45,7 +47,7 @@ class SpaceDropdown extends StatelessWidget {
value: space.uuid,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
' ${space.name}',
@ -88,7 +90,7 @@ class SpaceDropdown extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 6,
flex: 8,
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
@ -129,6 +131,7 @@ class SpaceDropdown extends StatelessWidget {
dropdownStyleData: DropdownStyleData(
maxHeight: MediaQuery.of(context).size.height * 0.4,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(10),
),
),

View File

@ -17,9 +17,10 @@ class SaveRoutineHelper {
builder: (context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
final selectedConditionLabel = state.selectedAutomationOperator == 'and'
? 'All Conditions are met'
: 'Any Condition is met';
final selectedConditionLabel =
state.selectedAutomationOperator == 'and'
? 'All Conditions are met'
: 'Any Condition is met';
return AlertDialog(
contentPadding: EdgeInsets.zero,
@ -37,10 +38,11 @@ class SaveRoutineHelper {
Text(
'Create a scene: ${state.routineName ?? ""}',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.bold,
),
style:
Theme.of(context).textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 18),
_buildDivider(),
@ -58,7 +60,8 @@ class SaveRoutineHelper {
_buildIfConditions(state, context),
Container(
width: 1,
color: ColorsManager.greyColor.withValues(alpha: 0.8),
color: ColorsManager.greyColor
.withValues(alpha: 0.8),
),
_buildThenActions(state, context),
],
@ -97,7 +100,8 @@ class SaveRoutineHelper {
child: Row(
spacing: 16,
children: [
Expanded(child: Text('IF: $selectedConditionLabel', style: textStyle)),
Expanded(
child: Text('IF: $selectedConditionLabel', style: textStyle)),
const Expanded(child: Text('THEN:', style: textStyle)),
],
),
@ -109,7 +113,7 @@ class SaveRoutineHelper {
spacing: 16,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
DialogFooterButton(
DialogFooterButton(
text: 'Back',
onTap: () => Navigator.pop(context),
),
@ -143,7 +147,8 @@ class SaveRoutineHelper {
child: ListView(
// shrinkWrap: true,
children: state.thenItems.map((item) {
final functions = state.selectedFunctions[item['uniqueCustomId']] ?? [];
final functions =
state.selectedFunctions[item['uniqueCustomId']] ?? [];
return functionRow(item, context, functions);
}).toList(),
),
@ -203,19 +208,20 @@ class SaveRoutineHelper {
),
),
child: Center(
child: item['type'] == 'tap_to_run' || item['type'] == 'scene'
? Image.memory(
base64Decode(item['icon']),
width: 12,
height: 22,
fit: BoxFit.scaleDown,
)
: SvgPicture.asset(
item['imagePath'],
width: 12,
height: 12,
fit: BoxFit.scaleDown,
),
child:
item['type'] == 'tap_to_run' || item['type'] == 'scene'
? Image.memory(
base64Decode(item['icon']),
width: 12,
height: 22,
fit: BoxFit.scaleDown,
)
: SvgPicture.asset(
item['imagePath'],
width: 12,
height: 12,
fit: BoxFit.scaleDown,
),
),
),
Flexible(

View File

@ -405,8 +405,8 @@ class PowerFactorCStatusFunction extends EnergyClampFunctions {
code: 'PowerFactorC',
operationName: 'Power Factor C',
icon: Assets.speedoMeter,
min: 0.00,
max: 1.00,
min: 0.0,
max: 1.0,
step: 0.1,
unit: "",
);

View File

@ -117,10 +117,22 @@ class ACHelper {
},
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData =
state.addedFunctions.firstWhere(
(f) =>
f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
/// add the functions to the routine bloc
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
uniqueCustomId,
),
);

View File

@ -78,12 +78,22 @@ class _CeilingSensorDialogState extends State<CeilingSensorDialog> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData =
state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
final functions = _updateValuesForAddedFunctions(
state.addedFunctions,
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
functions,
[selectedFunctionData],
'${widget.uniqueCustomId}',
),
);

View File

@ -192,9 +192,18 @@ class _WallPresenceSensorState extends State<FlushPresenceSensor> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId!,
),
);

View File

@ -115,9 +115,18 @@ class _GatewayDialogState extends State<GatewayDialog> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId ?? '-1',
),
);

View File

@ -147,7 +147,7 @@ class OneGangSwitchHelper {
// }
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
uniqueCustomId,
),
);

View File

@ -250,9 +250,18 @@ class _EnergyClampDialogState extends State<EnergyClampDialog> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId!,
),
);

View File

@ -27,17 +27,16 @@ class EnergyValueSelectorWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final selectedFn =
functions.firstWhere((f) => f.code == selectedFunction);
final selectedFn = functions.firstWhere((f) => f.code == selectedFunction);
final values = selectedFn.getOperationalValues();
final step = selectedFn.step ?? 1.0;
final step = selectedFn.step;
final _unit = selectedFn.unit ?? '';
final (double, double) sliderRange =
(selectedFn.min ?? 0.0, selectedFn.max ?? 100.0);
if (_isSliderFunction(selectedFunction)) {
return CustomRoutinesTextbox(
withSpecialChar: false,
withSpecialChar: true,
currentCondition: functionData.condition,
dialogType: dialogType,
sliderRange: sliderRange,
@ -60,14 +59,14 @@ class EnergyValueSelectorWidget extends StatelessWidget {
entityId: device?.uuid ?? '',
functionCode: selectedFunction,
operationName: functionData.operationName,
value: value.toInt(),
value: value,
condition: functionData.condition,
),
),
),
unit: _unit,
dividendOfRange: 1,
stepIncreaseAmount: step,
stepIncreaseAmount: step!,
);
}

View File

@ -145,9 +145,22 @@ class TwoGangSwitchHelper {
// ),
// );
// }
final selectedFunctionData =
state.addedFunctions.firstWhere(
(f) =>
f.functionCode ==
state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode:
state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
uniqueCustomId,
),
);

View File

@ -210,9 +210,18 @@ class _WallPresenceSensorState extends State<WallPresenceSensor> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId!,
),
);

View File

@ -188,9 +188,18 @@ class _WaterHeaterDialogRoutinesState extends State<WaterHeaterDialogRoutines> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId!,
),
);

View File

@ -1,43 +0,0 @@
import 'package:equatable/equatable.dart';
class PaginatedDataModel<T> extends Equatable {
const PaginatedDataModel({
required this.data,
required this.page,
required this.size,
required this.hasNext,
required this.totalItems,
required this.totalPages,
});
final List<T> data;
final int page;
final int size;
final bool hasNext;
final int totalItems;
final int totalPages;
factory PaginatedDataModel.fromJson(
Map<String, dynamic> json,
List<T> Function(List<dynamic>) fromJsonList,
) {
return PaginatedDataModel<T>(
data: fromJsonList(json['data'] as List<dynamic>),
page: json['page'] as int? ?? 1,
size: json['size'] as int? ?? 25,
hasNext: json['hasNext'] as bool? ?? false,
totalItems: json['totalItem'] as int? ?? 0,
totalPages: json['totalPage'] as int? ?? 0,
);
}
@override
List<Object?> get props => [
data,
page,
size,
hasNext,
totalItems,
totalPages,
];
}

View File

@ -1,47 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class SpaceManagementPage extends StatelessWidget {
const SpaceManagementPage({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => CommunitiesBloc(
communitiesService: DebouncedCommunitiesService(
RemoteCommunitiesService(HTTPService()),
),
)..add(const LoadCommunities(LoadCommunitiesParam())),
),
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
],
child: WebScaffold(
appBarTitle: Text(
'Space Management',
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
),
enableMenuSidebar: false,
centerBody: Text(
'Community Structure',
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: FontWeight.bold,
),
),
rightBody: const NavigateHomeGridView(),
scaffoldBody: const SpaceManagementBody(),
),
);
}
}

View File

@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart';
class SpaceManagementBody extends StatelessWidget {
const SpaceManagementBody({super.key});
@override
Widget build(BuildContext context) {
return const Row(
children: [
SpaceManagementCommunitiesTree(),
],
);
}
}

View File

@ -1,42 +0,0 @@
import 'dart:async';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart';
final class DebouncedCommunitiesService implements CommunitiesService {
DebouncedCommunitiesService(
this._decoratee, {
this.debounceDuration = const Duration(milliseconds: 500),
});
final CommunitiesService _decoratee;
final Duration debounceDuration;
Timer? _debounceTimer;
late Completer<CommunitiesPaginationModel>? _completer;
@override
Future<CommunitiesPaginationModel> getCommunity(
LoadCommunitiesParam param,
) async {
_debounceTimer?.cancel();
_completer = Completer<CommunitiesPaginationModel>();
final currentCompleter = _completer!;
_debounceTimer = Timer(debounceDuration, () async {
try {
final result = await _decoratee.getCommunity(param);
if (!currentCompleter.isCompleted) {
currentCompleter.complete(result);
}
} catch (error) {
if (!currentCompleter.isCompleted) {
currentCompleter.completeError(error);
}
}
});
return currentCompleter.future;
}
}

View File

@ -1,11 +1,9 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
class RemoteCommunitiesService implements CommunitiesService {
const RemoteCommunitiesService(this._httpService);
@ -15,26 +13,14 @@ class RemoteCommunitiesService implements CommunitiesService {
static const _defaultErrorMessage = 'Failed to load communities';
@override
Future<CommunitiesPaginationModel> getCommunity(
LoadCommunitiesParam param,
) async {
Future<List<CommunityModel>> getCommunity(LoadCommunitiesParam param) async {
try {
final response = await _httpService.get(
path: await _makeUrl(),
queryParameters: {
'page': param.page,
'size': param.size,
'includeSpaces': param.includeSpaces,
if (param.search.isNotEmpty && param.search != 'null')
'search': param.search,
},
expectedResponseModel: (json) => CommunitiesPaginationModel.fromJson(
json as Map<String, dynamic>,
CommunityModel.fromJsonList,
),
return _httpService.get(
path: '/api/communities/',
expectedResponseModel: (json) => (json as List<dynamic>)
.map((e) => CommunityModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
@ -45,13 +31,4 @@ class RemoteCommunitiesService implements CommunitiesService {
throw APIException(formattedErrorMessage);
}
}
Future<String> _makeUrl() async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null) throw APIException('Project UUID is required');
return ApiEndpoints.getCommunityListv2.replaceAll(
'{projectId}',
projectUuid,
);
}
}

View File

@ -4,19 +4,11 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain
class CommunityModel extends Equatable {
final String uuid;
final String name;
final DateTime createdAt;
final DateTime updatedAt;
final String description;
final String externalId;
final List<SpaceModel> spaces;
const CommunityModel({
required this.uuid,
required this.name,
required this.createdAt,
required this.updatedAt,
required this.description,
required this.externalId,
required this.spaces,
});
@ -24,20 +16,11 @@ class CommunityModel extends Equatable {
return CommunityModel(
uuid: json['uuid'] as String,
name: json['name'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
description: json['description'] as String,
externalId: json['externalId']?.toString() ?? '',
spaces: (json['spaces'] as List<dynamic>? ?? <dynamic>[])
spaces: (json['spaces'] as List<dynamic>)
.map((e) => SpaceModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
static List<CommunityModel> fromJsonList(List<dynamic> json) {
return json
.map((e) => CommunityModel.fromJson(e as Map<String, dynamic>))
.toList();
}
@override
List<Object?> get props => [uuid, name, spaces];

View File

@ -2,37 +2,26 @@ import 'package:equatable/equatable.dart';
class SpaceModel extends Equatable {
final String uuid;
final DateTime? createdAt;
final DateTime? updatedAt;
final String spaceName;
final String icon;
final List<SpaceModel> children;
final SpaceModel? parent;
const SpaceModel({
required this.uuid,
required this.createdAt,
required this.updatedAt,
required this.spaceName,
required this.icon,
required this.children,
required this.parent,
});
factory SpaceModel.fromJson(Map<String, dynamic> json) {
return SpaceModel(
uuid: json['uuid'] as String? ?? '',
createdAt: DateTime.tryParse(json['createdAt'] as String? ?? ''),
updatedAt: DateTime.tryParse(json['updatedAt'] as String? ?? ''),
spaceName: json['spaceName'] as String? ?? '',
icon: json['icon'] as String? ?? 'assets/icons/location_icon.svg',
uuid: json['uuid'] as String,
spaceName: json['spaceName'] as String,
icon: json['icon'] as String,
children: (json['children'] as List<dynamic>?)
?.map((e) => SpaceModel.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
parent: json['parent'] != null
? SpaceModel.fromJson(json['parent'] as Map<String, dynamic>)
: null,
);
}

View File

@ -1,32 +1,3 @@
import 'package:equatable/equatable.dart';
class LoadCommunitiesParam extends Equatable {
const LoadCommunitiesParam({
this.page = 1,
this.size = 25,
this.search = '',
this.includeSpaces = true,
});
final int page;
final int size;
final String search;
final bool includeSpaces;
LoadCommunitiesParam copyWith({
int? page,
int? size,
String? search,
bool? includeSpaces,
}) {
return LoadCommunitiesParam(
page: page ?? this.page,
size: size ?? this.size,
search: search ?? this.search,
includeSpaces: includeSpaces ?? this.includeSpaces,
);
}
@override
List<Object?> get props => [page, size, search, includeSpaces];
class LoadCommunitiesParam {
const LoadCommunitiesParam();
}

View File

@ -1,9 +1,6 @@
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
typedef CommunitiesPaginationModel = PaginatedDataModel<CommunityModel>;
abstract class CommunitiesService {
Future<CommunitiesPaginationModel> getCommunity(LoadCommunitiesParam param);
Future<List<CommunityModel>> getCommunity(LoadCommunitiesParam param);
}

View File

@ -14,8 +14,6 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
}) : _communitiesService = communitiesService,
super(const CommunitiesState()) {
on<LoadCommunities>(_onLoadCommunities);
on<LoadMoreCommunities>(_onLoadMoreCommunities);
on<InsertCommunity>(_onInsertCommunity);
}
final CommunitiesService _communitiesService;
@ -25,93 +23,28 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
Emitter<CommunitiesState> emit,
) async {
try {
emit(
state.copyWith(status: CommunitiesStatus.loading),
);
final paginationResponse = await _communitiesService.getCommunity(
event.param,
);
emit(const CommunitiesState(status: CommunitiesStatus.loading));
final communities = await _communitiesService.getCommunity(event.param);
emit(
CommunitiesState(
status: CommunitiesStatus.success,
communities: paginationResponse.data,
hasNext: paginationResponse.hasNext,
currentPage: paginationResponse.page,
searchQuery: event.param.search,
isLoadingMore: false,
communities: communities,
),
);
} on APIException catch (e) {
_onApiException(e, emit);
} catch (e) {
_onError(e, emit);
}
}
Future<void> _onLoadMoreCommunities(
LoadMoreCommunities event,
Emitter<CommunitiesState> emit,
) async {
if (!state.hasNext || state.isLoadingMore) return;
try {
emit(state.copyWith(isLoadingMore: true));
final param = LoadCommunitiesParam(
page: state.currentPage + 1,
search: state.searchQuery,
);
final paginationResponse = await _communitiesService.getCommunity(param);
final updatedCommunities = List<CommunityModel>.from(state.communities)
..addAll(paginationResponse.data);
emit(
state.copyWith(
status: CommunitiesStatus.success,
communities: updatedCommunities,
hasNext: paginationResponse.hasNext,
currentPage: paginationResponse.page,
isLoadingMore: false,
CommunitiesState(
status: CommunitiesStatus.failure,
errorMessage: e.message,
),
);
} on APIException catch (e) {
_onApiException(e, emit);
} catch (e) {
_onError(e, emit);
emit(
CommunitiesState(
status: CommunitiesStatus.failure,
errorMessage: e.toString(),
),
);
}
}
void _onApiException(
APIException e,
Emitter<CommunitiesState> emit,
) {
emit(
state.copyWith(
status: CommunitiesStatus.failure,
isLoadingMore: false,
errorMessage: e.message,
),
);
}
void _onError(Object e, Emitter<CommunitiesState> emit) {
emit(
state.copyWith(
status: CommunitiesStatus.failure,
isLoadingMore: false,
errorMessage: e.toString(),
),
);
}
void _onInsertCommunity(
InsertCommunity event,
Emitter<CommunitiesState> emit,
) {
emit(state.copyWith(communities: [event.community, ...state.communities]));
}
}

View File

@ -15,19 +15,3 @@ class LoadCommunities extends CommunitiesEvent {
@override
List<Object?> get props => [param];
}
class LoadMoreCommunities extends CommunitiesEvent {
const LoadMoreCommunities();
@override
List<Object?> get props => [];
}
final class InsertCommunity extends CommunitiesEvent {
const InsertCommunity(this.community);
final CommunityModel community;
@override
List<Object?> get props => [community];
}

View File

@ -7,48 +7,12 @@ final class CommunitiesState extends Equatable {
this.status = CommunitiesStatus.initial,
this.communities = const [],
this.errorMessage,
this.isLoadingMore = false,
this.hasNext = false,
this.currentPage = 1,
this.searchQuery = '',
});
final CommunitiesStatus status;
final List<CommunityModel> communities;
final String? errorMessage;
final bool isLoadingMore;
final bool hasNext;
final int currentPage;
final String searchQuery;
CommunitiesState copyWith({
CommunitiesStatus? status,
List<CommunityModel>? communities,
String? errorMessage,
bool? isLoadingMore,
bool? hasNext,
int? currentPage,
String? searchQuery,
}) {
return CommunitiesState(
status: status ?? this.status,
communities: communities ?? this.communities,
errorMessage: errorMessage ?? this.errorMessage,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
hasNext: hasNext ?? this.hasNext,
currentPage: currentPage ?? this.currentPage,
searchQuery: searchQuery ?? this.searchQuery,
);
}
@override
List<Object?> get props => [
status,
communities,
errorMessage,
isLoadingMore,
hasNext,
currentPage,
searchQuery,
];
List<Object?> get props => [status, communities, errorMessage];
}

View File

@ -1,47 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
part 'communities_tree_selection_event.dart';
part 'communities_tree_selection_state.dart';
class CommunitiesTreeSelectionBloc
extends Bloc<CommunitiesTreeSelectionEvent, CommunitiesTreeSelectionState> {
CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) {
on<SelectCommunityEvent>(_onSelectCommunity);
on<SelectSpaceEvent>(_onSelectSpace);
on<ClearCommunitiesTreeSelectionEvent>(_onClearSelection);
}
void _onSelectCommunity(
SelectCommunityEvent event,
Emitter<CommunitiesTreeSelectionState> emit,
) {
emit(
CommunitiesTreeSelectionState(
selectedCommunity: event.community,
selectedSpace: null,
),
);
}
void _onSelectSpace(
SelectSpaceEvent event,
Emitter<CommunitiesTreeSelectionState> emit,
) {
emit(
CommunitiesTreeSelectionState(
selectedCommunity: null,
selectedSpace: event.space,
),
);
}
void _onClearSelection(
ClearCommunitiesTreeSelectionEvent event,
Emitter<CommunitiesTreeSelectionState> emit,
) {
emit(const CommunitiesTreeSelectionState());
}
}

View File

@ -1,30 +0,0 @@
part of 'communities_tree_selection_bloc.dart';
sealed class CommunitiesTreeSelectionEvent extends Equatable {
const CommunitiesTreeSelectionEvent();
@override
List<Object?> get props => [];
}
final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent {
final CommunityModel? community;
const SelectCommunityEvent({required this.community});
@override
List<Object?> get props => [community];
}
final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent {
final SpaceModel? space;
const SelectSpaceEvent({required this.space});
@override
List<Object?> get props => [space];
}
final class ClearCommunitiesTreeSelectionEvent
extends CommunitiesTreeSelectionEvent {
const ClearCommunitiesTreeSelectionEvent();
}

View File

@ -1,29 +0,0 @@
part of 'communities_tree_selection_bloc.dart';
final class CommunitiesTreeSelectionState extends Equatable {
const CommunitiesTreeSelectionState({
this.selectedCommunity,
this.selectedSpace,
});
final CommunityModel? selectedCommunity;
final SpaceModel? selectedSpace;
CommunitiesTreeSelectionState copyWith({
CommunityModel? selectedCommunity,
SpaceModel? selectedSpace,
List<CommunityModel>? expandedCommunities,
List<SpaceModel>? expandedSpaces,
}) {
return CommunitiesTreeSelectionState(
selectedCommunity: selectedCommunity ?? this.selectedCommunity,
selectedSpace: selectedSpace ?? this.selectedSpace,
);
}
@override
List<Object?> get props => [
selectedCommunity,
selectedSpace,
];
}

View File

@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
class CommunitiesTreeFailureWidget extends StatelessWidget {
const CommunitiesTreeFailureWidget({super.key, this.errorMessage});
final String? errorMessage;
@override
Widget build(BuildContext context) {
return Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
errorMessage ?? 'Something went wrong',
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.read<CommunitiesBloc>().add(
LoadCommunities(
LoadCommunitiesParam(
search: context.read<CommunitiesBloc>().state.searchQuery,
),
),
),
child: const Text('Retry'),
),
],
),
),
);
}
}

View File

@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart';
class CommunityTile extends StatelessWidget {
final String title;
final List<Widget>? children;
final bool isExpanded;
final bool isSelected;
final void Function(String, bool isExpanded) onExpansionChanged;
final void Function() onItemSelected;
const CommunityTile({
super.key,
required this.title,
required this.isExpanded,
required this.onExpansionChanged,
required this.onItemSelected,
required this.isSelected,
this.children,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: CustomExpansionTile(
title: title,
initiallyExpanded: isExpanded,
isSelected: isSelected,
onExpansionChanged: (bool expanded) {
onExpansionChanged(title, expanded);
},
onItemSelected: onItemSelected,
children: children ?? [],
));
}
}

View File

@ -1,22 +0,0 @@
import 'package:flutter/material.dart';
class EmptyCommunitiesTreeSearchResultWidget extends StatelessWidget {
const EmptyCommunitiesTreeSearchResultWidget({
required this.searchQuery,
super.key,
});
final String searchQuery;
@override
Widget build(BuildContext context) {
return Center(
child: Text(
searchQuery.isEmpty
? 'No communities found'
: 'No communities found for "$searchQuery"',
textAlign: TextAlign.center,
),
);
}
}

View File

@ -1,112 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/widgets/app_loading_indicator.dart';
import 'package:syncrow_web/common/widgets/search_bar.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart';
import 'package:syncrow_web/utils/style.dart';
class SpaceManagementCommunitiesTree extends StatefulWidget {
const SpaceManagementCommunitiesTree({super.key});
@override
State<SpaceManagementCommunitiesTree> createState() =>
_SpaceManagementCommunitiesTreeState();
}
class _SpaceManagementCommunitiesTreeState
extends State<SpaceManagementCommunitiesTree> {
@override
void initState() {
context.read<CommunitiesBloc>().add(
const LoadCommunities(LoadCommunitiesParam()),
);
super.initState();
}
void _onSearchChanged(String searchQuery) {
context
.read<CommunitiesBloc>()
.add(LoadCommunities(LoadCommunitiesParam(search: searchQuery.trim())));
}
void _onLoadMore() {
context.read<CommunitiesBloc>().add(const LoadMoreCommunities());
}
@override
Widget build(BuildContext context) {
return BlocBuilder<CommunitiesBloc, CommunitiesState>(
builder: (context, state) => Container(
width: 320,
decoration: subSectionContainerDecoration,
child: Column(
children: [
const SpaceManagementSidebarHeader(),
CustomSearchBar(
onSearchChanged: _onSearchChanged,
),
const SizedBox(height: 16),
switch (state.status) {
CommunitiesStatus.initial => const AppLoadingIndicator(),
CommunitiesStatus.loading => state.communities.isEmpty
? const AppLoadingIndicator()
: _buildCommunitiesTree(context, state),
CommunitiesStatus.success => _buildCommunitiesTree(context, state),
CommunitiesStatus.failure => CommunitiesTreeFailureWidget(
errorMessage: state.errorMessage,
),
},
Visibility(
visible: state.isLoadingMore,
child: const AppLoadingIndicator(),
),
],
),
),
);
}
Widget _buildCommunitiesTree(
BuildContext context,
CommunitiesState state,
) {
final communitiesIsEmpty = state.communities.isEmpty;
final statusIsSuccess = state.status == CommunitiesStatus.success;
return Expanded(
child: Visibility(
visible: statusIsSuccess && communitiesIsEmpty,
replacement: Stack(
children: [
SpaceManagementSidebarCommunitiesList(
communities: state.communities,
onLoadMore: state.hasNext ? _onLoadMore : null,
isLoadingMore: state.isLoadingMore,
hasNext: state.hasNext,
itemBuilder: (context, index) {
return SpaceManagementCommunitiesTreeCommunityTile(
community: state.communities[index],
);
},
),
if (state.status == CommunitiesStatus.loading &&
state.communities.isNotEmpty)
ColoredBox(
color: Colors.white.withValues(alpha: 0.7),
child: const AppLoadingIndicator(),
),
],
),
child: EmptyCommunitiesTreeSearchResultWidget(
searchQuery: state.searchQuery,
),
),
);
}
}

View File

@ -1,45 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart';
class SpaceManagementCommunitiesTreeCommunityTile extends StatelessWidget {
const SpaceManagementCommunitiesTreeCommunityTile({
required this.community,
super.key,
});
final CommunityModel community;
@override
Widget build(BuildContext context) {
final spaces = community.spaces
.map(
(space) => SpaceManagementCommunitiesTreeSpaceTile(
space: space,
community: community,
),
)
.toList();
return CommunityTile(
title: community.name,
key: ValueKey(community.uuid),
isSelected: context
.watch<CommunitiesTreeSelectionBloc>()
.state
.selectedCommunity
?.uuid ==
community.uuid,
isExpanded: false,
onItemSelected: () {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
onExpansionChanged: (title, expanded) {},
children: spaces,
);
}
}

View File

@ -1,56 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart';
class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget {
const SpaceManagementCommunitiesTreeSpaceTile({
required this.space,
required this.community,
super.key,
});
final SpaceModel space;
final CommunityModel community;
@override
Widget build(BuildContext context) {
final spaceIsExpanded = _isSpaceOrChildSelected(context, space);
final isSelected =
context.watch<CommunitiesTreeSelectionBloc>().state.selectedSpace?.uuid ==
space.uuid;
return Padding(
padding: const EdgeInsetsDirectional.only(start: 16.0),
child: SpaceTile(
title: space.spaceName,
key: ValueKey(space.uuid),
isSelected: isSelected,
initiallyExpanded: spaceIsExpanded,
onExpansionChanged: (expanded) {},
onItemSelected: () => context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(space: space),
),
children: space.children
.map(
(childSpace) => SpaceManagementCommunitiesTreeSpaceTile(
space: childSpace,
community: community,
),
)
.toList(),
),
);
}
bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) {
final selectedSpace =
context.read<CommunitiesTreeSelectionBloc>().state.selectedSpace;
final isSpaceSelected = selectedSpace?.uuid == space.uuid;
final anySubSpaceIsSelected = space.children.any(
(child) => _isSpaceOrChildSelected(context, child),
);
return isSpaceSelected || anySubSpaceIsSelected;
}
}

View File

@ -1,34 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class SpaceManagementSidebarAddCommunityButton extends StatelessWidget {
const SpaceManagementSidebarAddCommunityButton({
required this.onTap,
super.key,
});
final void Function() onTap;
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: 30,
child: IconButton(
style: IconButton.styleFrom(
iconSize: 20,
backgroundColor: ColorsManager.circleImageBackground,
shape: const CircleBorder(
side: BorderSide(
color: ColorsManager.lightGrayBorderColor,
width: 3,
),
),
),
onPressed: onTap,
icon: SvgPicture.asset(Assets.addIcon),
),
);
}
}

View File

@ -1,104 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceManagementSidebarCommunitiesList extends StatefulWidget {
const SpaceManagementSidebarCommunitiesList({
required this.communities,
required this.itemBuilder,
this.onLoadMore,
this.isLoadingMore = false,
this.hasNext = false,
super.key,
});
final List<CommunityModel> communities;
final Widget Function(BuildContext context, int index) itemBuilder;
final VoidCallback? onLoadMore;
final bool isLoadingMore;
final bool hasNext;
@override
State<SpaceManagementSidebarCommunitiesList> createState() =>
_SpaceManagementSidebarCommunitiesListState();
}
class _SpaceManagementSidebarCommunitiesListState
extends State<SpaceManagementSidebarCommunitiesList> {
late final ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100) {
if (widget.hasNext && !widget.isLoadingMore && widget.onLoadMore != null) {
widget.onLoadMore!();
}
}
}
bool _onNotification(ScrollEndNotification notification) {
final hasReachedEnd = notification.metrics.extentAfter == 0;
if (hasReachedEnd &&
widget.hasNext &&
!widget.isLoadingMore &&
widget.onLoadMore != null) {
widget.onLoadMore!();
return true;
}
return false;
}
@override
void dispose() {
_scrollController
..removeListener(_onScroll)
..dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final itemCount = widget.communities.length + (widget.isLoadingMore ? 1 : 0);
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: context.screenWidth * 0.5,
child: Scrollbar(
scrollbarOrientation: ScrollbarOrientation.left,
thumbVisibility: true,
controller: _scrollController,
child: NotificationListener<ScrollEndNotification>(
onNotification: _onNotification,
child: ListView.builder(
shrinkWrap: true,
padding: const EdgeInsetsDirectional.only(start: 16),
itemCount: itemCount,
controller: _scrollController,
itemBuilder: (context, index) {
if (index == widget.communities.length) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(),
),
);
}
return widget.itemBuilder(context, index);
},
),
),
),
),
);
}
}

View File

@ -1,68 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
class SpaceManagementSidebarHeader extends StatelessWidget {
const SpaceManagementSidebarHeader({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: subSectionContainerDecoration,
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Communities',
style: context.textTheme.titleMedium?.copyWith(
color: ColorsManager.blackColor,
),
),
SpaceManagementSidebarAddCommunityButton(
onTap: () => _onAddCommunity(context),
),
],
),
);
}
void _onAddCommunity(BuildContext context) {
final bloc = context.read<CommunitiesTreeSelectionBloc>();
final selectedCommunity = bloc.state.selectedCommunity;
final isSelected = selectedCommunity?.uuid.isNotEmpty ?? false;
if (isSelected) {
_clearSelection(context);
} else {
_showCreateCommunityDialog(context);
}
}
void _clearSelection(BuildContext context) {
context.read<CommunitiesTreeSelectionBloc>().add(
const ClearCommunitiesTreeSelectionEvent(),
);
}
void _showCreateCommunityDialog(BuildContext context) => showDialog<void>(
context: context,
builder: (_) => CreateCommunityDialog(
title: const Text('Community Name'),
onCreateCommunity: (community) {
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
),
);
}

View File

@ -1,54 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart';
class SpaceTile extends StatefulWidget {
final String title;
final bool isSelected;
final bool initiallyExpanded;
final ValueChanged<bool> onExpansionChanged;
final List<Widget>? children;
final void Function() onItemSelected;
const SpaceTile({
super.key,
required this.title,
required this.initiallyExpanded,
required this.onExpansionChanged,
required this.onItemSelected,
required this.isSelected,
this.children,
});
@override
State<SpaceTile> createState() => _SpaceTileState();
}
class _SpaceTileState extends State<SpaceTile> {
late bool _isExpanded;
@override
void initState() {
super.initState();
_isExpanded = widget.initiallyExpanded;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0),
child: CustomExpansionTile(
isSelected: widget.isSelected,
title: widget.title,
initiallyExpanded: _isExpanded,
onItemSelected: widget.onItemSelected,
onExpansionChanged: (bool expanded) {
setState(() {
_isExpanded = expanded;
});
widget.onExpansionChanged(expanded);
},
children: widget.children ?? [],
),
);
}
}

View File

@ -1,5 +1,4 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/services/create_community_service.dart';
@ -17,51 +16,24 @@ class RemoteCreateCommunityService implements CreateCommunityService {
Future<CommunityModel> createCommunity(CreateCommunityParam param) async {
try {
final response = await _httpService.post(
path: await _makeUrl(),
body: {
'name': param.name,
'description': param.description,
},
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>;
if (json['success'] == true) {
return CommunityModel.fromJson(
json['data'] as Map<String, dynamic>,
);
}
return null;
},
path: 'endpoint',
expectedResponseModel: (data) => CommunityModel.fromJson(
data as Map<String, dynamic>,
),
);
if (response == null) {
throw APIException(
_getErrorMessageFromBody(response as Map<String, dynamic>?),
);
}
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
throw APIException(_getErrorMessageFromBody(message));
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [
_defaultErrorMessage,
errorMessage,
].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) {
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
}
}
String _getErrorMessageFromBody(Map<String, dynamic>? body) {
if (body == null) {
return _defaultErrorMessage;
}
final error = body['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
return errorMessage;
}
Future<String> _makeUrl() async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null) {
throw APIException('Project UUID is not set');
}
return '/projects/$projectUuid/communities';
}
}

View File

@ -1,13 +1,9 @@
import 'package:equatable/equatable.dart';
class CreateCommunityParam extends Equatable {
const CreateCommunityParam({
required this.name,
this.description = '',
});
const CreateCommunityParam({required this.name});
final String name;
final String description;
@override
List<Object> get props => [name];

View File

@ -1,61 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class CreateCommunityDialog extends StatelessWidget {
final void Function(CommunityModel community) onCreateCommunity;
final String? initialName;
final Widget title;
const CreateCommunityDialog({
super.key,
required this.onCreateCommunity,
required this.title,
this.initialName,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())),
child: BlocListener<CreateCommunityBloc, CreateCommunityState>(
listener: (context, state) {
switch (state) {
case CreateCommunityLoading():
showDialog<void>(
context: context,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
break;
case CreateCommunitySuccess(:final community):
Navigator.of(context).pop();
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Community created successfully')),
);
onCreateCommunity.call(community);
break;
case CreateCommunityFailure(:final message):
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
break;
default:
break;
}
},
child: CreateCommunityDialogWidget(
title: title,
initialName: initialName,
),
),
);
}
}

View File

@ -1,144 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CreateCommunityDialogWidget extends StatefulWidget {
final String? initialName;
final Widget title;
const CreateCommunityDialogWidget({
super.key,
required this.title,
this.initialName,
});
@override
State<CreateCommunityDialogWidget> createState() =>
_CreateCommunityDialogWidgetState();
}
class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidget> {
late final TextEditingController _nameController;
@override
void initState() {
_nameController = TextEditingController(text: widget.initialName ?? '');
super.initState();
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: ColorsManager.transparentColor,
child: Container(
width: MediaQuery.of(context).size.width * 0.3,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: ColorsManager.blackColor.withValues(alpha: 0.25),
blurRadius: 20,
spreadRadius: 5,
offset: const Offset(0, 5),
),
],
),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: BlocBuilder<CreateCommunityBloc, CreateCommunityState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DefaultTextStyle(
style: Theme.of(context).textTheme.headlineMedium!,
child: widget.title,
),
const SizedBox(height: 18),
CreateCommunityNameTextField(
nameController: _nameController,
),
if (state case CreateCommunityFailure(:final message))
Padding(
padding: const EdgeInsets.only(top: 18),
child: SelectableText(
'* $message',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
),
const SizedBox(height: 24),
_buildActionButtons(context),
],
);
},
),
),
),
),
);
}
Widget _buildActionButtons(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () => Navigator.of(context).pop(),
),
),
const SizedBox(width: 16),
_buildCreateCommunityButton(context),
],
);
}
Widget _buildCreateCommunityButton(BuildContext context) {
return Expanded(
child: DefaultButton(
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
_onSubmit(context);
}
},
borderRadius: 10,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
),
);
}
void _onSubmit(BuildContext context) {
if (_formKey.currentState?.validate() ?? false) {
context.read<CreateCommunityBloc>().add(
CreateCommunity(
CreateCommunityParam(
name: _nameController.text.trim(),
),
),
);
}
}
}

View File

@ -1,48 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CreateCommunityNameTextField extends StatelessWidget {
const CreateCommunityNameTextField({
required this.nameController,
super.key,
});
final TextEditingController nameController;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: nameController,
validator: _validator,
style: context.textTheme.bodyMedium,
decoration: InputDecoration(
hintText: 'Please enter the community name',
filled: true,
fillColor: ColorsManager.boxColor,
enabledBorder: _buildBorder(ColorsManager.boxColor),
focusedBorder: _buildBorder(),
focusedErrorBorder: _buildBorder(Theme.of(context).colorScheme.error),
errorBorder: _buildBorder(Theme.of(context).colorScheme.error),
),
);
}
String? _validator(String? value) {
if (value == null || value.isEmpty) {
return '*Name should not be empty.';
}
return null;
}
InputBorder _buildBorder([Color? color]) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: color ?? ColorsManager.vividBlue.withValues(alpha: 0.5),
width: 1,
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
@ -25,6 +26,12 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
final CommunitySpaceManagementApi _api = CommunitySpaceManagementApi();
final ProductApi _productApi = ProductApi();
final SpaceModelManagementApi _spaceModelApi = SpaceModelManagementApi();
@override
void initState() {
context.read<SpaceTreeBloc>().add(InitialEvent());
super.initState();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(

View File

@ -4,10 +4,11 @@ import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart';
class SpaceTile extends StatefulWidget {
final String title;
final bool isSelected;
final bool initiallyExpanded;
final ValueChanged<bool> onExpansionChanged;
final List<Widget>? children;
final void Function() onItemSelected;
final Function() onItemSelected;
const SpaceTile({
super.key,

View File

@ -46,7 +46,6 @@ abstract class ApiEndpoints {
// Community Module
static const String createCommunity = '/projects/{projectId}/communities';
static const String getCommunityList = '/projects/{projectId}/communities';
static const String getCommunityListv2 = '/projects/{projectId}/communities/v2';
static const String getCommunityById =
'/projects/{projectId}/communities/{communityId}';
static const String updateCommunity =