mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-08-25 07:22:27 +00:00
Feature/reorder spaces api integration (#362)
<!-- 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 Integrated reordering spaces with the API. Fixed drop target bug, where the canvas wouldn't show the first drop target in the tree. ## Type of Change <!--- Put an `x` in all the boxes that apply: --> - [ x ] ✨ 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
This commit is contained in:
@ -9,6 +9,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen
|
|||||||
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/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
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/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/products/presentation/bloc/products_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_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/remote_space_details_service.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/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/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||||
@ -25,15 +27,16 @@ class SpaceManagementPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _SpaceManagementPageState extends State<SpaceManagementPage> {
|
class _SpaceManagementPageState extends State<SpaceManagementPage> {
|
||||||
late final CommunitiesBloc communitiesBloc;
|
late final CommunitiesBloc communitiesBloc;
|
||||||
|
late final HTTPService _httpService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
_httpService = HTTPService();
|
||||||
communitiesBloc = CommunitiesBloc(
|
communitiesBloc = CommunitiesBloc(
|
||||||
communitiesService: DebouncedCommunitiesService(
|
communitiesService: DebouncedCommunitiesService(
|
||||||
RemoteCommunitiesService(HTTPService()),
|
RemoteCommunitiesService(_httpService),
|
||||||
),
|
),
|
||||||
)..add(const LoadCommunities(LoadCommunitiesParam()));
|
)..add(const LoadCommunities(LoadCommunitiesParam()));
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,13 +53,18 @@ class _SpaceManagementPageState extends State<SpaceManagementPage> {
|
|||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => SpaceDetailsBloc(
|
create: (context) => SpaceDetailsBloc(
|
||||||
UniqueSpaceDetailsSpacesDecoratorService(
|
UniqueSpaceDetailsSpacesDecoratorService(
|
||||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
RemoteSpaceDetailsService(httpService: _httpService),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => ProductsBloc(
|
create: (context) => ProductsBloc(
|
||||||
RemoteProductsService(HTTPService()),
|
RemoteProductsService(_httpService),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => ReorderSpacesBloc(
|
||||||
|
RemoteReorderSpacesService(_httpService),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -11,6 +11,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain
|
|||||||
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/domain/models/space_model.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/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/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
@ -164,6 +166,16 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
context.read<CommunitiesBloc>().add(
|
context.read<CommunitiesBloc>().add(
|
||||||
CommunitiesUpdateCommunity(newCommunity),
|
CommunitiesUpdateCommunity(newCommunity),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
context.read<ReorderSpacesBloc>().add(
|
||||||
|
ReorderSpacesEvent(
|
||||||
|
ReorderSpacesParam(
|
||||||
|
communityUuid: widget.community.uuid,
|
||||||
|
parentSpaceUuid: data.parent?.uuid ?? '',
|
||||||
|
spaces: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSpaceTapped(SpaceModel? space) {
|
void _onSpaceTapped(SpaceModel? space) {
|
||||||
@ -245,6 +257,13 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
final levelXOffset = <int, double>{};
|
final levelXOffset = <int, double>{};
|
||||||
_calculateLayout(community.spaces, 0, levelXOffset);
|
_calculateLayout(community.spaces, 0, levelXOffset);
|
||||||
|
|
||||||
|
const horizontalCanvasPadding = 100.0;
|
||||||
|
final originalPositions = Map.of(_positions);
|
||||||
|
_positions.clear();
|
||||||
|
for (final entry in originalPositions.entries) {
|
||||||
|
_positions[entry.key] = entry.value.translate(horizontalCanvasPadding, 0);
|
||||||
|
}
|
||||||
|
|
||||||
final selectedSpace = widget.selectedSpace;
|
final selectedSpace = widget.selectedSpace;
|
||||||
final highlightedUuids = <String>{};
|
final highlightedUuids = <String>{};
|
||||||
if (selectedSpace != null) {
|
if (selectedSpace != null) {
|
||||||
@ -262,7 +281,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
community: widget.community,
|
community: widget.community,
|
||||||
);
|
);
|
||||||
|
|
||||||
final createButtonX = levelXOffset[0] ?? 0.0;
|
final createButtonX = (levelXOffset[0] ?? 0.0) + horizontalCanvasPadding;
|
||||||
const createButtonY = 0.0;
|
const createButtonY = 0.0;
|
||||||
|
|
||||||
widgets.add(
|
widgets.add(
|
||||||
@ -294,10 +313,12 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
CommunityModel? community,
|
CommunityModel? community,
|
||||||
SpaceModel? parent,
|
SpaceModel? parent,
|
||||||
}) {
|
}) {
|
||||||
|
const targetWidth = 40.0;
|
||||||
|
final padding = (_horizontalSpacing - targetWidth) / 2;
|
||||||
if (spaces.isNotEmpty) {
|
if (spaces.isNotEmpty) {
|
||||||
final firstChildPos = _positions[spaces.first.uuid]!;
|
final firstChildPos = _positions[spaces.first.uuid]!;
|
||||||
final targetPos = Offset(
|
final targetPos = Offset(
|
||||||
firstChildPos.dx - (_horizontalSpacing / 4),
|
firstChildPos.dx - padding - targetWidth,
|
||||||
firstChildPos.dy,
|
firstChildPos.dy,
|
||||||
);
|
);
|
||||||
widgets.add(_buildDropTarget(parent, community, 0, targetPos));
|
widgets.add(_buildDropTarget(parent, community, 0, targetPos));
|
||||||
@ -379,7 +400,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
);
|
);
|
||||||
|
|
||||||
final targetPos = Offset(
|
final targetPos = Offset(
|
||||||
position.dx + cardWidth + (_horizontalSpacing / 4) - 20,
|
position.dx + cardWidth + padding,
|
||||||
position.dy,
|
position.dy,
|
||||||
);
|
);
|
||||||
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
|
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
|
||||||
@ -414,24 +435,33 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
child: DragTarget<SpaceReorderDataModel>(
|
child: DragTarget<SpaceReorderDataModel>(
|
||||||
builder: (context, candidateData, rejectedData) {
|
builder: (context, candidateData, rejectedData) {
|
||||||
if (_draggedData == null) {
|
if (_draggedData == null) {
|
||||||
return const SizedBox();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid &&
|
final children = parent?.children ?? community?.spaces ?? [];
|
||||||
_draggedData?.community == null) ||
|
final isSameParent = (_draggedData!.parent?.uuid == parent?.uuid &&
|
||||||
(_draggedData?.community?.uuid == community?.uuid &&
|
_draggedData!.community == null) ||
|
||||||
_draggedData?.parent == null);
|
(_draggedData!.community?.uuid == community?.uuid &&
|
||||||
|
_draggedData!.parent == null);
|
||||||
|
|
||||||
if (!isTargetForDragged) {
|
if (!isSameParent) {
|
||||||
return const SizedBox();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
final oldIndex =
|
||||||
|
children.indexWhere((s) => s.uuid == _draggedData!.space.uuid);
|
||||||
|
if (oldIndex != -1 && (oldIndex == index || oldIndex == index - 1)) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
width: 40,
|
width: 40,
|
||||||
|
alignment: Alignment.center,
|
||||||
height: _cardHeight,
|
height: _cardHeight,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.theme.colorScheme.primary.withValues(
|
color: context.theme.colorScheme.primary.withValues(
|
||||||
alpha: candidateData.isNotEmpty ? 0.7 : 0.3,
|
alpha: candidateData.isNotEmpty ? 0.9 : 0.3,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
@ -454,6 +484,9 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
|
|
||||||
final oldIndex =
|
final oldIndex =
|
||||||
children.indexWhere((s) => s.uuid == data.data.space.uuid);
|
children.indexWhere((s) => s.uuid == data.data.space.uuid);
|
||||||
|
if (oldIndex == -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (oldIndex == index || oldIndex == index - 1) {
|
if (oldIndex == index || oldIndex == index - 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -481,7 +514,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: context.screenWidth * 5,
|
width: context.screenWidth * 5,
|
||||||
height: context.screenHeight * 5,
|
height: context.screenHeight * 5,
|
||||||
child: Stack(children: treeWidgets),
|
child: Stack(clipBehavior: Clip.none, children: treeWidgets),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_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 RemoteReorderSpacesService implements ReorderSpacesService {
|
||||||
|
RemoteReorderSpacesService(this._httpClient);
|
||||||
|
|
||||||
|
final HTTPService _httpClient;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> reorderSpaces(ReorderSpacesParam param) async {
|
||||||
|
try {
|
||||||
|
await _httpClient.post(
|
||||||
|
path: await _makeUrl(param),
|
||||||
|
body: param.toJson(),
|
||||||
|
expectedResponseModel: (json) => json,
|
||||||
|
);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
final message = e.response?.data as Map<String, dynamic>?;
|
||||||
|
throw APIException(_getErrorMessageFromBody(message));
|
||||||
|
} catch (e) {
|
||||||
|
throw APIException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getErrorMessageFromBody(Map<String, dynamic>? body) {
|
||||||
|
if (body == null) return 'Failed to delete space';
|
||||||
|
final error = body['error'] as Map<String, dynamic>?;
|
||||||
|
final errorMessage = error?['message'] as String? ?? '';
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _makeUrl(ReorderSpacesParam param) async {
|
||||||
|
final projectUuid = await ProjectManager.getProjectUUID();
|
||||||
|
final communityUuid = param.communityUuid;
|
||||||
|
|
||||||
|
if (projectUuid == null || projectUuid.isEmpty) {
|
||||||
|
throw APIException('Project UUID is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (communityUuid.isEmpty) {
|
||||||
|
throw APIException('Community UUID is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param.parentSpaceUuid.isEmpty) {
|
||||||
|
throw APIException('Parent Space UUID is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiEndpoints.reorderSpaces
|
||||||
|
.replaceAll('{projectUuid}', projectUuid)
|
||||||
|
.replaceAll('{communityUuid}', communityUuid)
|
||||||
|
.replaceAll('{parentSpaceUuid}', param.parentSpaceUuid);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||||
|
|
||||||
|
class ReorderSpacesParam extends Equatable {
|
||||||
|
const ReorderSpacesParam({
|
||||||
|
required this.communityUuid,
|
||||||
|
required this.parentSpaceUuid,
|
||||||
|
required this.spaces,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String communityUuid;
|
||||||
|
final String parentSpaceUuid;
|
||||||
|
final List<SpaceModel> spaces;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [spaces, communityUuid, parentSpaceUuid];
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'spacesUuids': spaces.map((space) => space.uuid).toList(),
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart';
|
||||||
|
|
||||||
|
abstract interface class ReorderSpacesService {
|
||||||
|
Future<void> reorderSpaces(ReorderSpacesParam param);
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart';
|
||||||
|
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||||
|
|
||||||
|
part 'reorder_spaces_event.dart';
|
||||||
|
part 'reorder_spaces_state.dart';
|
||||||
|
|
||||||
|
class ReorderSpacesBloc extends Bloc<ReorderSpacesEvent, ReorderSpacesState> {
|
||||||
|
ReorderSpacesBloc(
|
||||||
|
this._reorderSpacesService,
|
||||||
|
) : super(const ReorderSpacesInitial()) {
|
||||||
|
on<ReorderSpacesEvent>(_onReorderSpacesEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ReorderSpacesService _reorderSpacesService;
|
||||||
|
|
||||||
|
Future<void> _onReorderSpacesEvent(
|
||||||
|
ReorderSpacesEvent event,
|
||||||
|
Emitter<ReorderSpacesState> emit,
|
||||||
|
) async {
|
||||||
|
emit(const ReorderSpacesLoading());
|
||||||
|
try {
|
||||||
|
await _reorderSpacesService.reorderSpaces(event.param);
|
||||||
|
emit(const ReorderSpacesSuccess());
|
||||||
|
} on APIException catch (e) {
|
||||||
|
emit(ReorderSpacesFailure(e.message));
|
||||||
|
} catch (e) {
|
||||||
|
emit(ReorderSpacesFailure(e.toString()));
|
||||||
|
} finally {
|
||||||
|
emit(const ReorderSpacesInitial());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
part of 'reorder_spaces_bloc.dart';
|
||||||
|
|
||||||
|
final class ReorderSpacesEvent extends Equatable {
|
||||||
|
const ReorderSpacesEvent(this.param);
|
||||||
|
|
||||||
|
final ReorderSpacesParam param;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [param];
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
part of 'reorder_spaces_bloc.dart';
|
||||||
|
|
||||||
|
sealed class ReorderSpacesState extends Equatable {
|
||||||
|
const ReorderSpacesState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ReorderSpacesInitial extends ReorderSpacesState {
|
||||||
|
const ReorderSpacesInitial();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ReorderSpacesLoading extends ReorderSpacesState {
|
||||||
|
const ReorderSpacesLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ReorderSpacesSuccess extends ReorderSpacesState {
|
||||||
|
const ReorderSpacesSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ReorderSpacesFailure extends ReorderSpacesState {
|
||||||
|
const ReorderSpacesFailure(this.errorMessage);
|
||||||
|
|
||||||
|
final String errorMessage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [errorMessage];
|
||||||
|
}
|
@ -41,6 +41,8 @@ abstract class ApiEndpoints {
|
|||||||
'/projects/{projectId}/communities/{communityId}/spaces/{spaceId}';
|
'/projects/{projectId}/communities/{communityId}/spaces/{spaceId}';
|
||||||
static const String getSpaceHierarchy =
|
static const String getSpaceHierarchy =
|
||||||
'/projects/{projectId}/communities/{communityId}/spaces';
|
'/projects/{projectId}/communities/{communityId}/spaces';
|
||||||
|
static const String reorderSpaces =
|
||||||
|
'/projects/{projectUuid}/communities/{communityUuid}/spaces/{parentSpaceUuid}/spaces/order';
|
||||||
|
|
||||||
// Community Module
|
// Community Module
|
||||||
static const String createCommunity = '/projects/{projectId}/communities';
|
static const String createCommunity = '/projects/{projectId}/communities';
|
||||||
|
Reference in New Issue
Block a user