mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-08-25 18:49:41 +00:00
Compare commits
10 Commits
b223194950
...
d65f9ceea9
Author | SHA1 | Date | |
---|---|---|---|
d65f9ceea9 | |||
f539b0ac8d | |||
5a3cf93748 | |||
e740652507 | |||
c60078c96a | |||
903c5dd29b | |||
df39fca050 | |||
f832c5d884 | |||
fa930571dc | |||
acefe7b355 |
@ -7,13 +7,15 @@ abstract final class SpacesRecursiveHelper {
|
||||
SpaceDetailsModel updatedSpace,
|
||||
) {
|
||||
return spaces.map((space) {
|
||||
if (space.uuid == updatedSpace.uuid) {
|
||||
final isUpdatedSpace = space.uuid == updatedSpace.uuid;
|
||||
if (isUpdatedSpace) {
|
||||
return space.copyWith(
|
||||
spaceName: updatedSpace.spaceName,
|
||||
icon: updatedSpace.icon,
|
||||
);
|
||||
}
|
||||
if (space.children.isNotEmpty) {
|
||||
final hasChildren = space.children.isNotEmpty;
|
||||
if (hasChildren) {
|
||||
return space.copyWith(
|
||||
children: recusrivelyUpdate(space.children, updatedSpace),
|
||||
);
|
||||
@ -26,7 +28,7 @@ abstract final class SpacesRecursiveHelper {
|
||||
List<SpaceModel> spaces,
|
||||
String spaceUuid,
|
||||
) {
|
||||
final s = spaces.map((space) {
|
||||
final updatedSpaces = spaces.map((space) {
|
||||
if (space.uuid == spaceUuid) return null;
|
||||
if (space.children.isNotEmpty) {
|
||||
return space.copyWith(
|
||||
@ -35,7 +37,7 @@ abstract final class SpacesRecursiveHelper {
|
||||
}
|
||||
return space;
|
||||
}).toList();
|
||||
|
||||
return s.whereType<SpaceModel>().toList();
|
||||
final nonNullSpaces = updatedSpaces.whereType<SpaceModel>().toList();
|
||||
return nonNullSpaces;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_space_details_spaces_decorator_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||
@ -49,7 +49,7 @@ class _SpaceManagementPageState extends State<SpaceManagementPage> {
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
UniqueSubspacesDecorator(
|
||||
UniqueSpaceDetailsSpacesDecoratorService(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
),
|
||||
|
@ -53,6 +53,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
@override
|
||||
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedSpace == null) return;
|
||||
if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
@ -452,17 +453,17 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final treeWidgets = _buildTreeWidgets();
|
||||
return InteractiveViewer(
|
||||
transformationController: _transformationController,
|
||||
boundaryMargin: EdgeInsets.symmetric(
|
||||
horizontal: context.screenWidth * 0.3,
|
||||
vertical: context.screenHeight * 0.3,
|
||||
),
|
||||
minScale: 0.5,
|
||||
maxScale: 3.0,
|
||||
constrained: false,
|
||||
child: GestureDetector(
|
||||
onTap: _resetSelectionAndZoom,
|
||||
return GestureDetector(
|
||||
onTap: _resetSelectionAndZoom,
|
||||
child: InteractiveViewer(
|
||||
transformationController: _transformationController,
|
||||
boundaryMargin: EdgeInsets.symmetric(
|
||||
horizontal: context.screenWidth * 0.3,
|
||||
vertical: context.screenHeight * 0.3,
|
||||
),
|
||||
minScale: 0.5,
|
||||
maxScale: 3.0,
|
||||
constrained: false,
|
||||
child: SizedBox(
|
||||
width: context.screenWidth * 5,
|
||||
height: context.screenHeight * 5,
|
||||
|
@ -19,27 +19,27 @@ class CommunityStructureHeaderActionButtons extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (selectedSpace == null) return const SizedBox.shrink();
|
||||
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.end,
|
||||
spacing: 10,
|
||||
children: [
|
||||
if (selectedSpace != null) ...[
|
||||
CommunityStructureHeaderButton(
|
||||
label: 'Edit',
|
||||
svgAsset: Assets.editSpace,
|
||||
onPressed: () => onEdit(selectedSpace!),
|
||||
),
|
||||
CommunityStructureHeaderButton(
|
||||
label: 'Duplicate',
|
||||
svgAsset: Assets.duplicate,
|
||||
onPressed: () => onDuplicate(selectedSpace!),
|
||||
),
|
||||
CommunityStructureHeaderButton(
|
||||
label: 'Delete',
|
||||
svgAsset: Assets.spaceDelete,
|
||||
onPressed: () => onDelete(selectedSpace!),
|
||||
),
|
||||
],
|
||||
CommunityStructureHeaderButton(
|
||||
label: 'Edit',
|
||||
svgAsset: Assets.editSpace,
|
||||
onPressed: () => onEdit(selectedSpace!),
|
||||
),
|
||||
CommunityStructureHeaderButton(
|
||||
label: 'Duplicate',
|
||||
svgAsset: Assets.duplicate,
|
||||
onPressed: () => onDuplicate(selectedSpace!),
|
||||
),
|
||||
CommunityStructureHeaderButton(
|
||||
label: 'Delete',
|
||||
svgAsset: Assets.spaceDelete,
|
||||
onPressed: () => onDelete(selectedSpace!),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -2,31 +2,22 @@ import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class PlusButtonWidget extends StatelessWidget {
|
||||
final Offset offset;
|
||||
final void Function() onButtonTap;
|
||||
final void Function() onTap;
|
||||
|
||||
const PlusButtonWidget({
|
||||
required this.onTap,
|
||||
super.key,
|
||||
required this.offset,
|
||||
required this.onButtonTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onButtonTap,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.spaceColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: ColorsManager.whiteColors,
|
||||
size: 20,
|
||||
),
|
||||
return IconButton.filled(
|
||||
onPressed: onTap,
|
||||
style: IconButton.styleFrom(backgroundColor: ColorsManager.spaceColor),
|
||||
icon: const Icon(
|
||||
Icons.add,
|
||||
color: ColorsManager.whiteColors,
|
||||
size: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -29,10 +29,9 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
|
||||
widget.buildSpaceContainer(),
|
||||
if (isHovered)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
bottom: -5,
|
||||
child: PlusButtonWidget(
|
||||
offset: Offset.zero,
|
||||
onButtonTap: widget.onTap,
|
||||
onTap: widget.onTap,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -3,6 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.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';
|
||||
|
||||
class SpaceManagementCommunityStructure extends StatelessWidget {
|
||||
@ -13,28 +15,44 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
|
||||
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
|
||||
final selectedCommunity = selectionBloc.selectedCommunity;
|
||||
final selectedSpace = selectionBloc.selectedSpace;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CommunityStructureHeader(),
|
||||
Visibility(
|
||||
visible: selectedCommunity!.spaces.isNotEmpty,
|
||||
replacement: _buildEmptyWidget(selectedCommunity),
|
||||
child: _buildCanvas(selectedCommunity, selectedSpace),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCanvas(
|
||||
CommunityModel selectedCommunity,
|
||||
SpaceModel? selectedSpace,
|
||||
) {
|
||||
return Expanded(
|
||||
child: CommunityStructureCanvas(
|
||||
community: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyWidget(CommunityModel selectedCommunity) {
|
||||
const spacer = Spacer(flex: 6);
|
||||
return Visibility(
|
||||
visible: selectedCommunity!.spaces.isNotEmpty,
|
||||
replacement: Row(
|
||||
|
||||
return Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
spacer,
|
||||
Expanded(
|
||||
child: CreateSpaceButton(communityUuid: selectedCommunity.uuid),
|
||||
),
|
||||
spacer
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CommunityStructureHeader(),
|
||||
Expanded(
|
||||
child: CommunityStructureCanvas(
|
||||
community: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
child: CreateSpaceButton(
|
||||
communityUuid: selectedCommunity.uuid,
|
||||
),
|
||||
),
|
||||
spacer,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -4,18 +4,17 @@ import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domai
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/services/delete_space_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';
|
||||
|
||||
final class RemoteDeleteSpaceService implements DeleteSpaceService {
|
||||
RemoteDeleteSpaceService({
|
||||
required this.httpService,
|
||||
});
|
||||
const RemoteDeleteSpaceService(this._httpService);
|
||||
|
||||
final HTTPService httpService;
|
||||
final HTTPService _httpService;
|
||||
|
||||
@override
|
||||
Future<void> delete(DeleteSpaceParam param) async {
|
||||
try {
|
||||
await httpService.delete(
|
||||
await _httpService.delete(
|
||||
path: await _makeUrl(param),
|
||||
expectedResponseModel: (json) {
|
||||
final response = json as Map<String, dynamic>;
|
||||
@ -56,6 +55,10 @@ final class RemoteDeleteSpaceService implements DeleteSpaceService {
|
||||
if (param.spaceUuid.isEmpty) {
|
||||
throw APIException('Space UUID is not set');
|
||||
}
|
||||
return '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}';
|
||||
|
||||
return ApiEndpoints.deleteSpace
|
||||
.replaceAll('{projectId}', projectUuid)
|
||||
.replaceAll('{communityId}', param.communityUuid)
|
||||
.replaceAll('{spaceId}', param.spaceUuid);
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class DeleteSpaceDialog extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => DeleteSpaceBloc(
|
||||
RemoteDeleteSpaceService(httpService: HTTPService()),
|
||||
RemoteDeleteSpaceService(HTTPService()),
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) => Dialog(
|
||||
|
@ -2,23 +2,27 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/doma
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
|
||||
|
||||
class UniqueSubspacesDecorator implements SpaceDetailsService {
|
||||
class UniqueSpaceDetailsSpacesDecoratorService implements SpaceDetailsService {
|
||||
final SpaceDetailsService _decoratee;
|
||||
|
||||
const UniqueSubspacesDecorator(this._decoratee);
|
||||
const UniqueSpaceDetailsSpacesDecoratorService(this._decoratee);
|
||||
|
||||
@override
|
||||
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
|
||||
final response = await _decoratee.getSpaceDetails(param);
|
||||
|
||||
final uniqueSubspaces = <String, Subspace>{};
|
||||
final duplicateNames = <String>{};
|
||||
|
||||
for (final subspace in response.subspaces) {
|
||||
final normalizedName = subspace.name.trim().toLowerCase();
|
||||
if (!uniqueSubspaces.containsKey(normalizedName)) {
|
||||
if (uniqueSubspaces.containsKey(normalizedName)) {
|
||||
duplicateNames.add(normalizedName);
|
||||
} else {
|
||||
uniqueSubspaces[normalizedName] = subspace;
|
||||
}
|
||||
}
|
||||
duplicateNames.forEach(uniqueSubspaces.remove);
|
||||
|
||||
return response.copyWith(
|
||||
subspaces: uniqueSubspaces.values.toList(),
|
@ -10,7 +10,12 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AddDeviceTypeWidget extends StatefulWidget {
|
||||
const AddDeviceTypeWidget({super.key});
|
||||
const AddDeviceTypeWidget({
|
||||
super.key,
|
||||
this.initialProducts = const [],
|
||||
});
|
||||
|
||||
final List<Product> initialProducts;
|
||||
|
||||
@override
|
||||
State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState();
|
||||
@ -18,6 +23,16 @@ class AddDeviceTypeWidget extends StatefulWidget {
|
||||
|
||||
class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
|
||||
final Map<Product, int> _selectedProducts = {};
|
||||
final Map<Product, int> _initialProductCounts = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
for (final product in widget.initialProducts) {
|
||||
_initialProductCounts[product] = (_initialProductCounts[product] ?? 0) + 1;
|
||||
}
|
||||
_selectedProducts.addAll(_initialProductCounts);
|
||||
}
|
||||
|
||||
void _onIncrement(Product product) {
|
||||
setState(() {
|
||||
@ -27,8 +42,12 @@ class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
|
||||
|
||||
void _onDecrement(Product product) {
|
||||
setState(() {
|
||||
if ((_selectedProducts[product] ?? 0) > 0) {
|
||||
_selectedProducts[product] = _selectedProducts[product]! - 1;
|
||||
final initialCount = _initialProductCounts[product] ?? 0;
|
||||
final currentCount = _selectedProducts[product] ?? 0;
|
||||
if (currentCount > initialCount) {
|
||||
_selectedProducts[product] = currentCount - 1;
|
||||
} else if (currentCount > 0 && initialCount == 0) {
|
||||
_selectedProducts[product] = currentCount - 1;
|
||||
if (_selectedProducts[product] == 0) {
|
||||
_selectedProducts.remove(product);
|
||||
}
|
||||
@ -63,7 +82,22 @@ class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
|
||||
actions: [
|
||||
SpaceDetailsActionButtons(
|
||||
onSave: () {
|
||||
final result = _selectedProducts.entries
|
||||
final resultMap = <Product, int>{};
|
||||
resultMap.addAll(_selectedProducts);
|
||||
|
||||
for (final entry in _initialProductCounts.entries) {
|
||||
final product = entry.key;
|
||||
final initialCount = entry.value;
|
||||
final currentCount = resultMap[product] ?? 0;
|
||||
|
||||
if (currentCount > initialCount) {
|
||||
resultMap[product] = currentCount - initialCount;
|
||||
} else {
|
||||
resultMap.remove(product);
|
||||
}
|
||||
}
|
||||
|
||||
final result = resultMap.entries
|
||||
.expand((entry) => List.generate(entry.value, (_) => entry.key))
|
||||
.toList();
|
||||
Navigator.of(context).pop(result);
|
||||
|
@ -205,7 +205,14 @@ class _AssignTagsDialogState extends State<AssignTagsDialog> {
|
||||
onCancel: () async {
|
||||
final newProducts = await showDialog<List<Product>>(
|
||||
context: context,
|
||||
builder: (context) => const AddDeviceTypeWidget(),
|
||||
builder: (context) => AddDeviceTypeWidget(
|
||||
initialProducts: [
|
||||
..._space.productAllocations.map((e) => e.product),
|
||||
..._space.subspaces
|
||||
.expand((s) => s.productAllocations)
|
||||
.map((e) => e.product),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (newProducts == null || newProducts.isEmpty) return;
|
||||
|
Reference in New Issue
Block a user