Merge branch 'SP-1717-FE-Draw-Create-Edit-Space-Dialog' of https://github.com/SyncrowIOT/web into SP-1717-FE-Draw-Create-Edit-Space-Dialog

This commit is contained in:
Faris Armoush
2025-07-14 10:16:31 +03:00
20 changed files with 495 additions and 193 deletions

View File

@ -0,0 +1,14 @@
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';
class SpaceReorderDataModel {
const SpaceReorderDataModel({
required this.space,
this.parent,
this.community,
});
final SpaceModel space;
final SpaceModel? parent;
final CommunityModel? community;
}

View File

@ -1,13 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_reorder_data_model.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.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/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/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/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/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';
class CommunityStructureCanvas extends StatefulWidget { class CommunityStructureCanvas extends StatefulWidget {
const CommunityStructureCanvas({ const CommunityStructureCanvas({
@ -31,8 +35,9 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final double _horizontalSpacing = 150.0; final double _horizontalSpacing = 150.0;
final double _verticalSpacing = 120.0; final double _verticalSpacing = 120.0;
late TransformationController _transformationController; late final TransformationController _transformationController;
late AnimationController _animationController; late final AnimationController _animationController;
SpaceReorderDataModel? _draggedData;
@override @override
void initState() { void initState() {
@ -97,7 +102,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final position = _positions[space.uuid]; final position = _positions[space.uuid];
if (position == null) return; if (position == null) return;
const scale = 1.5; const scale = 1;
final viewSize = context.size; final viewSize = context.size;
if (viewSize == null) return; if (viewSize == null) return;
@ -112,16 +117,33 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
_runAnimation(matrix); _runAnimation(matrix);
} }
void _onReorder(SpaceReorderDataModel data, int newIndex) {
final newCommunity = widget.community.copyWith();
final children = data.parent?.children ?? newCommunity.spaces;
final oldIndex = children.indexWhere((s) => s.uuid == data.space.uuid);
if (oldIndex != -1) {
final item = children.removeAt(oldIndex);
if (newIndex > oldIndex) {
children.insert(newIndex - 1, item);
} else {
children.insert(newIndex, item);
}
}
context.read<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(newCommunity),
);
}
void _onSpaceTapped(SpaceModel? space) { void _onSpaceTapped(SpaceModel? space) {
context.read<CommunitiesTreeSelectionBloc>().add( context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(community: widget.community, space: space), SelectSpaceEvent(community: widget.community, space: space),
); );
} }
void _resetSelectionAndZoom() { void _resetSelectionAndZoom([CommunityModel? community]) {
context.read<CommunitiesTreeSelectionBloc>().add( context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent( SelectSpaceEvent(
community: widget.community, community: community ?? widget.community,
space: null, space: null,
), ),
); );
@ -182,7 +204,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
_positions.clear(); _positions.clear();
final community = widget.community; final community = widget.community;
_calculateLayout(community.spaces, 0, {}); final levelXOffset = <int, double>{};
_calculateLayout(community.spaces, 0, levelXOffset);
final selectedSpace = widget.selectedSpace; final selectedSpace = widget.selectedSpace;
final highlightedUuids = <String>{}; final highlightedUuids = <String>{};
@ -193,7 +216,24 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final widgets = <Widget>[]; final widgets = <Widget>[];
final connections = <SpaceConnectionModel>[]; final connections = <SpaceConnectionModel>[];
_generateWidgets(community.spaces, widgets, connections, highlightedUuids); _generateWidgets(
widget.community.spaces,
widgets,
connections,
highlightedUuids,
community: widget.community,
);
final createButtonX = levelXOffset[0] ?? 0.0;
const createButtonY = 0.0;
widgets.add(
Positioned(
left: createButtonX,
top: createButtonY,
child: CreateSpaceButton(communityUuid: widget.community.uuid),
),
);
return [ return [
CustomPaint( CustomPaint(
@ -211,58 +251,178 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
List<SpaceModel> spaces, List<SpaceModel> spaces,
List<Widget> widgets, List<Widget> widgets,
List<SpaceConnectionModel> connections, List<SpaceConnectionModel> connections,
Set<String> highlightedUuids, Set<String> highlightedUuids, {
) { CommunityModel? community,
for (final space in spaces) { SpaceModel? parent,
}) {
if (spaces.isNotEmpty) {
final firstChildPos = _positions[spaces.first.uuid]!;
final targetPos = Offset(
firstChildPos.dx - (_horizontalSpacing / 4),
firstChildPos.dy,
);
widgets.add(_buildDropTarget(parent, community, 0, targetPos));
}
for (var i = 0; i < spaces.length; i++) {
final space = spaces[i];
final position = _positions[space.uuid]; final position = _positions[space.uuid];
if (position == null) continue; if (position == null) {
continue;
}
final isHighlighted = highlightedUuids.contains(space.uuid); final isHighlighted = highlightedUuids.contains(space.uuid);
final hasNoSelectedSpace = widget.selectedSpace == null; final hasNoSelectedSpace = widget.selectedSpace == null;
final spaceCard = SpaceCardWidget(
buildSpaceContainer: () {
return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
child: Tooltip(
message: space.spaceName,
preferBelow: false,
child: SpaceCell(
onTap: () => _onSpaceTapped(space),
icon: space.icon,
name: space.spaceName,
),
),
);
},
onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
communityUuid: widget.community.uuid,
),
);
final reorderData = SpaceReorderDataModel(
space: space,
parent: parent,
community: community,
);
widgets.add( widgets.add(
Positioned( Positioned(
left: position.dx, left: position.dx,
top: position.dy, top: position.dy,
width: _cardWidth, width: _cardWidth,
height: _cardHeight, height: _cardHeight,
child: SpaceCardWidget( child: Draggable<SpaceReorderDataModel>(
buildSpaceContainer: () { data: reorderData,
return Opacity( feedback: Material(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, color: Colors.transparent,
child: Tooltip( child: Opacity(
message: space.spaceName, opacity: 0.2,
preferBelow: false, child: SizedBox(
child: SpaceCell( width: _cardWidth,
onTap: () => _onSpaceTapped(space), height: _cardHeight,
icon: space.icon, child: spaceCard,
name: space.spaceName,
),
), ),
); ),
}, ),
onTap: () => SpaceDetailsDialogHelper.showCreate(context), onDragStarted: () => setState(() => _draggedData = reorderData),
onDragEnd: (_) => setState(() => _draggedData = null),
onDraggableCanceled: (_, __) => setState(() => _draggedData = null),
childWhenDragging: Opacity(opacity: 0.4, child: spaceCard),
child: spaceCard,
), ),
), ),
); );
final targetPos = Offset(
position.dx + _cardWidth + (_horizontalSpacing / 4) - 20,
position.dy,
);
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
for (final child in space.children) { for (final child in space.children) {
connections.add( connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid));
SpaceConnectionModel(from: space.uuid, to: child.uuid), }
if (space.children.isNotEmpty) {
_generateWidgets(
space.children,
widgets,
connections,
highlightedUuids,
parent: space,
); );
} }
_generateWidgets(space.children, widgets, connections, highlightedUuids);
} }
} }
Widget _buildDropTarget(
SpaceModel? parent,
CommunityModel? community,
int index,
Offset position,
) {
return Positioned(
left: position.dx,
top: position.dy,
width: 40,
height: _cardHeight,
child: DragTarget<SpaceReorderDataModel>(
builder: (context, candidateData, rejectedData) {
if (_draggedData == null) {
return const SizedBox();
}
final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid &&
_draggedData?.community == null) ||
(_draggedData?.community?.uuid == community?.uuid &&
_draggedData?.parent == null);
if (!isTargetForDragged) {
return const SizedBox();
}
return Container(
width: 40,
height: _cardHeight,
decoration: BoxDecoration(
color: context.theme.colorScheme.primary.withValues(
alpha: candidateData.isNotEmpty ? 0.7 : 0.3,
),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.add,
color: context.theme.colorScheme.onPrimary,
),
);
},
onWillAcceptWithDetails: (data) {
final children = parent?.children ?? community?.spaces ?? [];
final isSameParent = (data.data.parent?.uuid == parent?.uuid &&
data.data.community == null) ||
(data.data.community?.uuid == community?.uuid &&
data.data.parent == null);
if (!isSameParent) {
return false;
}
final oldIndex =
children.indexWhere((s) => s.uuid == data.data.space.uuid);
if (oldIndex == index || oldIndex == index - 1) {
return false;
}
return true;
},
onAcceptWithDetails: (data) => _onReorder(data.data, index),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final treeWidgets = _buildTreeWidgets(); final treeWidgets = _buildTreeWidgets();
return InteractiveViewer( return InteractiveViewer(
transformationController: _transformationController, transformationController: _transformationController,
boundaryMargin: EdgeInsets.symmetric( boundaryMargin: EdgeInsets.symmetric(
horizontal: MediaQuery.sizeOf(context).width * 0.3, horizontal: context.screenWidth * 0.3,
vertical: MediaQuery.sizeOf(context).height * 0.3, vertical: context.screenHeight * 0.3,
), ),
minScale: 0.5, minScale: 0.5,
maxScale: 3.0, maxScale: 3.0,
@ -270,8 +430,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
child: GestureDetector( child: GestureDetector(
onTap: _resetSelectionAndZoom, onTap: _resetSelectionAndZoom,
child: SizedBox( child: SizedBox(
width: MediaQuery.sizeOf(context).width * 5, width: context.screenWidth * 5,
height: MediaQuery.sizeOf(context).height * 5, height: context.screenHeight * 5,
child: Stack(children: treeWidgets), child: Stack(children: treeWidgets),
), ),
), ),

View File

@ -55,8 +55,9 @@ class CommunityStructureHeader extends StatelessWidget {
children: [ children: [
Text( Text(
'Community Structure', 'Community Structure',
style: theme.textTheme.headlineLarge style: theme.textTheme.headlineLarge?.copyWith(
?.copyWith(color: ColorsManager.blackColor), color: ColorsManager.blackColor,
),
), ),
if (selectedCommunity != null) if (selectedCommunity != null)
Row( Row(
@ -67,8 +68,9 @@ class CommunityStructureHeader extends StatelessWidget {
Flexible( Flexible(
child: SelectableText( child: SelectableText(
selectedCommunity.name, selectedCommunity.name,
style: theme.textTheme.bodyLarge style: theme.textTheme.bodyLarge?.copyWith(
?.copyWith(color: ColorsManager.blackColor), color: ColorsManager.blackColor,
),
maxLines: 1, maxLines: 1,
), ),
), ),
@ -93,12 +95,11 @@ class CommunityStructureHeader extends StatelessWidget {
CommunityStructureHeaderActionButtons( CommunityStructureHeaderActionButtons(
onDelete: (space) {}, onDelete: (space) {},
onDuplicate: (space) {}, onDuplicate: (space) {},
onEdit: (space) { onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
SpaceDetailsDialogHelper.showEdit( context,
context, spaceModel: selectedSpace!,
spaceModel: selectedSpace!, communityUuid: selectedCommunity.uuid,
); ),
},
selectedSpace: selectedSpace, selectedSpace: selectedSpace,
), ),
], ],

View File

@ -2,38 +2,66 @@ import 'package:flutter/material.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/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class CreateSpaceButton extends StatelessWidget { class CreateSpaceButton extends StatefulWidget {
const CreateSpaceButton({super.key}); const CreateSpaceButton({
required this.communityUuid,
super.key,
});
final String communityUuid;
@override
State<CreateSpaceButton> createState() => _CreateSpaceButtonState();
}
class _CreateSpaceButtonState extends State<CreateSpaceButton> {
bool _isHovered = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return Tooltip(
onTap: () => SpaceDetailsDialogHelper.showCreate(context), margin: const EdgeInsets.symmetric(vertical: 24),
child: Container( message: 'Create a new space',
height: 60, child: GestureDetector(
decoration: BoxDecoration( onTap: () => SpaceDetailsDialogHelper.showCreate(
color: Colors.white, context,
borderRadius: BorderRadius.circular(20), communityUuid: widget.communityUuid,
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3),
),
],
), ),
child: Center( child: MouseRegion(
child: Container( onEnter: (_) => setState(() => _isHovered = true),
width: 40, onExit: (_) => setState(() => _isHovered = false),
height: 40, child: AnimatedOpacity(
decoration: const BoxDecoration( duration: const Duration(milliseconds: 100),
color: ColorsManager.boxColor, opacity: _isHovered ? 1.0 : 0.45,
shape: BoxShape.circle, child: Container(
), width: 150,
child: const Icon( height: 90,
Icons.add, decoration: BoxDecoration(
color: Colors.blue, color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.2),
spreadRadius: 3,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.borderColor, width: 2),
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.add,
color: Colors.blue,
),
),
),
), ),
), ),
), ),

View File

@ -22,22 +22,20 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
return MouseRegion( return MouseRegion(
onEnter: (_) => setState(() => isHovered = true), onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false), onExit: (_) => setState(() => isHovered = false),
child: SizedBox( child: Stack(
child: Stack( clipBehavior: Clip.none,
clipBehavior: Clip.none, alignment: Alignment.center,
alignment: Alignment.center, children: [
children: [ widget.buildSpaceContainer(),
widget.buildSpaceContainer(), if (isHovered)
if (isHovered) Positioned(
Positioned( bottom: 0,
bottom: 0, child: PlusButtonWidget(
child: PlusButtonWidget( offset: Offset.zero,
offset: Offset.zero, onButtonTap: widget.onTap,
onButtonTap: widget.onTap,
),
), ),
], ),
), ],
), ),
); );
} }

View File

@ -13,11 +13,17 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state; final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
final selectedCommunity = selectionBloc.selectedCommunity; final selectedCommunity = selectionBloc.selectedCommunity;
final selectedSpace = selectionBloc.selectedSpace; final selectedSpace = selectionBloc.selectedSpace;
const spacer = Spacer(flex: 10); const spacer = Spacer(flex: 6);
return Visibility( return Visibility(
visible: selectedCommunity!.spaces.isNotEmpty, visible: selectedCommunity!.spaces.isNotEmpty,
replacement: const Row( replacement: Row(
children: [spacer, Expanded(child: CreateSpaceButton()), spacer], children: [
spacer,
Expanded(
child: CreateSpaceButton(communityUuid: selectedCommunity.uuid),
),
spacer
],
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@ -13,14 +13,14 @@ class CommunitiesTreeFailureWidget extends StatelessWidget {
return Expanded( return Expanded(
child: Center( child: Center(
child: Column( child: Column(
spacing: 16,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( SelectableText(
errorMessage ?? 'Something went wrong', errorMessage ?? 'Something went wrong',
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 16), FilledButton(
ElevatedButton(
onPressed: () => context.read<CommunitiesBloc>().add( onPressed: () => context.read<CommunitiesBloc>().add(
LoadCommunities( LoadCommunities(
LoadCommunitiesParam( LoadCommunitiesParam(

View File

@ -40,16 +40,6 @@ class SpaceDetailsModel extends Equatable {
); );
} }
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'spaceName': spaceName,
'icon': icon,
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
'subspaces': subspaces.map((e) => e.toJson()).toList(),
};
}
SpaceDetailsModel copyWith({ SpaceDetailsModel copyWith({
String? uuid, String? uuid,
String? spaceName, String? spaceName,
@ -89,14 +79,6 @@ class ProductAllocation extends Equatable {
); );
} }
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'product': product.toJson(),
'tag': tag.toJson(),
};
}
ProductAllocation copyWith({ ProductAllocation copyWith({
String? uuid, String? uuid,
Product? product, Product? product,
@ -134,14 +116,6 @@ class Subspace extends Equatable {
); );
} }
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'name': name,
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
};
}
Subspace copyWith({ Subspace copyWith({
String? uuid, String? uuid,
String? name, String? name,

View File

@ -2,23 +2,37 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_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/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/domain/models/space_details_model.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';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
abstract final class SpaceDetailsDialogHelper { abstract final class SpaceDetailsDialogHelper {
static void showCreate(BuildContext context) { static void showCreate(
BuildContext context, {
required String communityUuid,
}) {
showDialog<void>( showDialog<void>(
context: context, context: context,
builder: (_) => BlocProvider( builder: (_) => MultiBlocProvider(
create: (context) => SpaceDetailsBloc( providers: [
RemoteSpaceDetailsService(httpService: HTTPService()), BlocProvider(
), create: (context) => SpaceDetailsBloc(
child: SpaceDetailsDialog( RemoteSpaceDetailsService(httpService: HTTPService()),
context: context, ),
title: const SelectableText('Create Space'), ),
spaceModel: SpaceModel.empty(), ],
onSave: (space) {}, child: Builder(
builder: (context) => SpaceDetailsDialog(
context: context,
title: const SelectableText('Create Space'),
spaceModel: SpaceModel.empty(),
onSave: (space) {},
communityUuid: communityUuid,
),
), ),
), ),
); );
@ -27,20 +41,82 @@ abstract final class SpaceDetailsDialogHelper {
static void showEdit( static void showEdit(
BuildContext context, { BuildContext context, {
required SpaceModel spaceModel, required SpaceModel spaceModel,
required String communityUuid,
}) { }) {
showDialog<void>( showDialog<void>(
context: context, context: context,
builder: (_) => BlocProvider( builder: (_) => MultiBlocProvider(
create: (context) => SpaceDetailsBloc( providers: [
RemoteSpaceDetailsService(httpService: HTTPService()), BlocProvider(
), create: (context) => SpaceDetailsBloc(
child: SpaceDetailsDialog( RemoteSpaceDetailsService(httpService: HTTPService()),
context: context, ),
title: const SelectableText('Edit Space'), ),
spaceModel: spaceModel, BlocProvider(
onSave: (space) {}, create: (context) => UpdateSpaceBloc(
RemoteUpdateSpaceService(HTTPService()),
),
),
],
child: Builder(
builder: (context) => BlocListener<UpdateSpaceBloc, UpdateSpaceState>(
listener: _updateListener,
child: SpaceDetailsDialog(
context: context,
title: const SelectableText('Edit Space'),
spaceModel: spaceModel,
onSave: (space) => context.read<UpdateSpaceBloc>().add(
UpdateSpace(
UpdateSpaceParam(
communityUuid: communityUuid,
space: space,
),
),
),
communityUuid: communityUuid,
),
),
), ),
), ),
); );
} }
static void _updateListener(BuildContext context, UpdateSpaceState state) {
return switch (state) {
UpdateSpaceInitial() => null,
UpdateSpaceLoading() => _onLoading(context),
UpdateSpaceSuccess(:final space) => _onUpdateSuccess(context, space),
UpdateSpaceFailure(:final errorMessage) => _onError(context, errorMessage),
};
}
static void _onUpdateSuccess(BuildContext context, SpaceDetailsModel space) {
Navigator.of(context).pop();
}
static void _onLoading(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()),
);
}
static void _onError(BuildContext context, String errorMessage) {
Navigator.of(context).pop();
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => AlertDialog(
title: const Text('Error'),
content: Text(errorMessage),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('OK'),
),
],
),
);
}
} }

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_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/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
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/params/load_space_details_param.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';
@ -15,6 +14,7 @@ class SpaceDetailsDialog extends StatefulWidget {
required this.spaceModel, required this.spaceModel,
required this.onSave, required this.onSave,
required this.context, required this.context,
required this.communityUuid,
super.key, super.key,
}); });
@ -22,6 +22,7 @@ class SpaceDetailsDialog extends StatefulWidget {
final SpaceModel spaceModel; final SpaceModel spaceModel;
final void Function(SpaceDetailsModel space) onSave; final void Function(SpaceDetailsModel space) onSave;
final BuildContext context; final BuildContext context;
final String communityUuid;
@override @override
State<SpaceDetailsDialog> createState() => _SpaceDetailsDialogState(); State<SpaceDetailsDialog> createState() => _SpaceDetailsDialogState();
@ -35,11 +36,7 @@ class _SpaceDetailsDialogState extends State<SpaceDetailsDialog> {
if (!isCreateMode) { if (!isCreateMode) {
final param = LoadSpaceDetailsParam( final param = LoadSpaceDetailsParam(
spaceUuid: widget.spaceModel.uuid, spaceUuid: widget.spaceModel.uuid,
communityUuid: widget.context communityUuid: widget.communityUuid,
.read<CommunitiesTreeSelectionBloc>()
.state
.selectedCommunity!
.uuid,
); );
widget.context.read<SpaceDetailsBloc>().add(LoadSpaceDetails(param)); widget.context.read<SpaceDetailsBloc>().add(LoadSpaceDetails(param));
} }

View File

@ -37,7 +37,7 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
..._subspaces, ..._subspaces,
Subspace( Subspace(
name: name, name: name,
uuid: const Uuid().v4(), uuid: '${const Uuid().v4()}-NewTag',
productAllocations: const [], productAllocations: const [],
), ),
]; ];

View File

@ -3,41 +3,19 @@ import 'package:equatable/equatable.dart';
class Tag extends Equatable { class Tag extends Equatable {
final String uuid; final String uuid;
final String name; final String name;
final String createdAt;
final String updatedAt;
const Tag({ const Tag({
required this.uuid, required this.uuid,
required this.name, required this.name,
required this.createdAt,
required this.updatedAt,
}); });
factory Tag.empty() => const Tag(
uuid: '',
name: '',
createdAt: '',
updatedAt: '',
);
factory Tag.fromJson(Map<String, dynamic> json) { factory Tag.fromJson(Map<String, dynamic> json) {
return Tag( return Tag(
uuid: json['uuid'] as String, uuid: json['uuid'] as String,
name: json['name'] as String, name: json['name'] as String,
createdAt: json['createdAt'] as String,
updatedAt: json['updatedAt'] as String,
); );
} }
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'name': name,
'createdAt': createdAt,
'updatedAt': updatedAt,
};
}
@override @override
List<Object?> get props => [uuid, name, createdAt, updatedAt]; List<Object?> get props => [uuid, name];
} }

View File

@ -214,9 +214,12 @@ class _AssignTagsDialogState extends State<AssignTagsDialog> {
for (final product in newProducts) { for (final product in newProducts) {
_space.productAllocations.add( _space.productAllocations.add(
ProductAllocation( ProductAllocation(
uuid: const Uuid().v4(), uuid: '${const Uuid().v4()}-NewProductUuid',
product: product, product: product,
tag: Tag.empty(), tag: Tag(
uuid: '${const Uuid().v4()}-NewTag',
name: '',
),
), ),
); );
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:uuid/uuid.dart';
class ProductTagField extends StatefulWidget { class ProductTagField extends StatefulWidget {
final List<Tag> items; final List<Tag> items;
@ -53,13 +54,8 @@ class _ProductTagFieldState extends State<ProductTagField> {
void _submit(String value) { void _submit(String value) {
final lowerCaseValue = value.toLowerCase(); final lowerCaseValue = value.toLowerCase();
final selectedTag = widget.items.firstWhere( final selectedTag = widget.items.firstWhere(
(tag) => tag.name.toLowerCase() == lowerCaseValue, (e) => e.name.toLowerCase() == lowerCaseValue,
orElse: () => Tag( orElse: () => Tag(uuid: '${const Uuid().v4()}-NewTag', name: value),
name: value,
uuid: '',
createdAt: '',
updatedAt: '',
),
); );
widget.onSelected(selectedTag); widget.onSelected(selectedTag);
_closeDropdown(); _closeDropdown();

View File

@ -1,5 +1,7 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
@ -12,17 +14,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
static const _defaultErrorMessage = 'Failed to update space'; static const _defaultErrorMessage = 'Failed to update space';
@override @override
Future<SpaceDetailsModel> updateSpace(SpaceDetailsModel space) async { Future<SpaceDetailsModel> updateSpace(UpdateSpaceParam param) async {
try { try {
final response = await _httpService.put( final path = await _makeUrl(param);
path: 'endpoint', await _httpService.put(
body: space.toJson(), path: path,
expectedResponseModel: (data) => SpaceDetailsModel.fromJson( body: param.toJson(),
data as Map<String, dynamic>, expectedResponseModel: (data) {
), final response = data as Map<String, dynamic>;
final isSuccess = response['success'] as bool;
if (!isSuccess) {
throw APIException(response['error'] as String);
}
return isSuccess;
},
); );
return response; return param.space;
} on DioException catch (e) { } on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?; final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?; final error = message?['error'] as Map<String, dynamic>?;
@ -37,4 +45,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
throw APIException(formattedErrorMessage); throw APIException(formattedErrorMessage);
} }
} }
Future<String> _makeUrl(UpdateSpaceParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null || projectUuid.isEmpty) {
throw APIException('Project UUID is not set');
}
final spaceUuid = param.space.uuid;
if (spaceUuid.isEmpty) {
throw APIException('Space UUID is not set');
}
final communityUuid = param.communityUuid;
if (communityUuid.isEmpty) {
throw APIException('Community UUID is not set');
}
return '/projects/$projectUuid/communities/$communityUuid/spaces/$spaceUuid';
}
} }

View File

@ -0,0 +1,42 @@
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
class UpdateSpaceParam {
UpdateSpaceParam({
required this.space,
required this.communityUuid,
});
final SpaceDetailsModel space;
final String communityUuid;
Map<String, dynamic> toJson() {
return {
'spaceName': space.spaceName,
'icon': space.icon,
'subspaces': space.subspaces.map((e) => e._toJson()).toList(),
'productAllocations':
space.productAllocations.map((e) => e._toJson()).toList(),
};
}
}
extension _ProductAllocationToJson on ProductAllocation {
Map<String, dynamic> _toJson() {
final isNewTag = tag.uuid.isEmpty;
return <String, dynamic>{
if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid,
'productUuid': product.uuid,
};
}
}
extension _SubspaceToJson on Subspace {
Map<String, dynamic> _toJson() {
final isNewSubspace = uuid.endsWith('-NewTag');
return <String, dynamic>{
if (!isNewSubspace) 'uuid': uuid,
'subspaceName': name,
'productAllocations': productAllocations.map((e) => e._toJson()).toList(),
};
}
}

View File

@ -1,5 +1,6 @@
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
abstract class UpdateSpaceService { abstract interface class UpdateSpaceService {
Future<SpaceDetailsModel> updateSpace(SpaceDetailsModel space); Future<SpaceDetailsModel> updateSpace(UpdateSpaceParam param);
} }

View File

@ -1,6 +1,7 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/api_exception.dart';
@ -20,7 +21,7 @@ class UpdateSpaceBloc extends Bloc<UpdateSpaceEvent, UpdateSpaceState> {
) async { ) async {
emit(UpdateSpaceLoading()); emit(UpdateSpaceLoading());
try { try {
final updatedSpace = await _updateSpaceService.updateSpace(event.space); final updatedSpace = await _updateSpaceService.updateSpace(event.param);
emit(UpdateSpaceSuccess(updatedSpace)); emit(UpdateSpaceSuccess(updatedSpace));
} on APIException catch (e) { } on APIException catch (e) {
emit(UpdateSpaceFailure(e.message)); emit(UpdateSpaceFailure(e.message));

View File

@ -8,10 +8,10 @@ sealed class UpdateSpaceEvent extends Equatable {
} }
final class UpdateSpace extends UpdateSpaceEvent { final class UpdateSpace extends UpdateSpaceEvent {
const UpdateSpace(this.space); const UpdateSpace(this.param);
final SpaceDetailsModel space; final UpdateSpaceParam param;
@override @override
List<Object> get props => [space]; List<Object> get props => [param];
} }

View File

@ -21,10 +21,10 @@ final class UpdateSpaceSuccess extends UpdateSpaceState {
} }
final class UpdateSpaceFailure extends UpdateSpaceState { final class UpdateSpaceFailure extends UpdateSpaceState {
final String message; final String errorMessage;
const UpdateSpaceFailure(this.message); const UpdateSpaceFailure(this.errorMessage);
@override @override
List<Object> get props => [message]; List<Object> get props => [errorMessage];
} }