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_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_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/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_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/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/space_details/presentation/helpers/space_details_dialog_helper.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityStructureCanvas extends StatefulWidget {
const CommunityStructureCanvas({
@ -31,8 +35,9 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final double _horizontalSpacing = 150.0;
final double _verticalSpacing = 120.0;
late TransformationController _transformationController;
late AnimationController _animationController;
late final TransformationController _transformationController;
late final AnimationController _animationController;
SpaceReorderDataModel? _draggedData;
@override
void initState() {
@ -97,7 +102,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final position = _positions[space.uuid];
if (position == null) return;
const scale = 1.5;
const scale = 1;
final viewSize = context.size;
if (viewSize == null) return;
@ -112,16 +117,33 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
_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) {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(community: widget.community, space: space),
);
}
void _resetSelectionAndZoom() {
void _resetSelectionAndZoom([CommunityModel? community]) {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(
community: widget.community,
community: community ?? widget.community,
space: null,
),
);
@ -182,7 +204,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
_positions.clear();
final community = widget.community;
_calculateLayout(community.spaces, 0, {});
final levelXOffset = <int, double>{};
_calculateLayout(community.spaces, 0, levelXOffset);
final selectedSpace = widget.selectedSpace;
final highlightedUuids = <String>{};
@ -193,7 +216,24 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final widgets = <Widget>[];
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 [
CustomPaint(
@ -211,22 +251,30 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
List<SpaceModel> spaces,
List<Widget> widgets,
List<SpaceConnectionModel> connections,
Set<String> highlightedUuids,
) {
for (final space in spaces) {
Set<String> highlightedUuids, {
CommunityModel? community,
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];
if (position == null) continue;
if (position == null) {
continue;
}
final isHighlighted = highlightedUuids.contains(space.uuid);
final hasNoSelectedSpace = widget.selectedSpace == null;
widgets.add(
Positioned(
left: position.dx,
top: position.dy,
width: _cardWidth,
height: _cardHeight,
child: SpaceCardWidget(
final spaceCard = SpaceCardWidget(
buildSpaceContainer: () {
return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
@ -241,28 +289,140 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
),
);
},
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
communityUuid: widget.community.uuid,
),
);
final reorderData = SpaceReorderDataModel(
space: space,
parent: parent,
community: community,
);
widgets.add(
Positioned(
left: position.dx,
top: position.dy,
width: _cardWidth,
height: _cardHeight,
child: Draggable<SpaceReorderDataModel>(
data: reorderData,
feedback: Material(
color: Colors.transparent,
child: Opacity(
opacity: 0.2,
child: SizedBox(
width: _cardWidth,
height: _cardHeight,
child: spaceCard,
),
),
),
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) {
connections.add(
SpaceConnectionModel(from: space.uuid, to: child.uuid),
connections.add(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
Widget build(BuildContext context) {
final treeWidgets = _buildTreeWidgets();
return InteractiveViewer(
transformationController: _transformationController,
boundaryMargin: EdgeInsets.symmetric(
horizontal: MediaQuery.sizeOf(context).width * 0.3,
vertical: MediaQuery.sizeOf(context).height * 0.3,
horizontal: context.screenWidth * 0.3,
vertical: context.screenHeight * 0.3,
),
minScale: 0.5,
maxScale: 3.0,
@ -270,8 +430,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
child: GestureDetector(
onTap: _resetSelectionAndZoom,
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 5,
height: MediaQuery.sizeOf(context).height * 5,
width: context.screenWidth * 5,
height: context.screenHeight * 5,
child: Stack(children: treeWidgets),
),
),

View File

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

View File

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

View File

@ -22,7 +22,6 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
return MouseRegion(
onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false),
child: SizedBox(
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
@ -38,7 +37,6 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
),
],
),
),
);
}
}

View File

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

View File

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

View File

@ -2,23 +2,37 @@ import 'package:flutter/material.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/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/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';
abstract final class SpaceDetailsDialogHelper {
static void showCreate(BuildContext context) {
static void showCreate(
BuildContext context, {
required String communityUuid,
}) {
showDialog<void>(
context: context,
builder: (_) => BlocProvider(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceDetailsBloc(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
child: SpaceDetailsDialog(
),
],
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(
BuildContext context, {
required SpaceModel spaceModel,
required String communityUuid,
}) {
showDialog<void>(
context: context,
builder: (_) => BlocProvider(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceDetailsBloc(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),
BlocProvider(
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) {},
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_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/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/params/load_space_details_param.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.onSave,
required this.context,
required this.communityUuid,
super.key,
});
@ -22,6 +22,7 @@ class SpaceDetailsDialog extends StatefulWidget {
final SpaceModel spaceModel;
final void Function(SpaceDetailsModel space) onSave;
final BuildContext context;
final String communityUuid;
@override
State<SpaceDetailsDialog> createState() => _SpaceDetailsDialogState();
@ -35,11 +36,7 @@ class _SpaceDetailsDialogState extends State<SpaceDetailsDialog> {
if (!isCreateMode) {
final param = LoadSpaceDetailsParam(
spaceUuid: widget.spaceModel.uuid,
communityUuid: widget.context
.read<CommunitiesTreeSelectionBloc>()
.state
.selectedCommunity!
.uuid,
communityUuid: widget.communityUuid,
);
widget.context.read<SpaceDetailsBloc>().add(LoadSpaceDetails(param));
}

View File

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

View File

@ -3,41 +3,19 @@ import 'package:equatable/equatable.dart';
class Tag extends Equatable {
final String uuid;
final String name;
final String createdAt;
final String updatedAt;
const Tag({
required this.uuid,
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) {
return Tag(
uuid: json['uuid'] 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
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) {
_space.productAllocations.add(
ProductAllocation(
uuid: const Uuid().v4(),
uuid: '${const Uuid().v4()}-NewProductUuid',
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/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:uuid/uuid.dart';
class ProductTagField extends StatefulWidget {
final List<Tag> items;
@ -53,13 +54,8 @@ class _ProductTagFieldState extends State<ProductTagField> {
void _submit(String value) {
final lowerCaseValue = value.toLowerCase();
final selectedTag = widget.items.firstWhere(
(tag) => tag.name.toLowerCase() == lowerCaseValue,
orElse: () => Tag(
name: value,
uuid: '',
createdAt: '',
updatedAt: '',
),
(e) => e.name.toLowerCase() == lowerCaseValue,
orElse: () => Tag(uuid: '${const Uuid().v4()}-NewTag', name: value),
);
widget.onSelected(selectedTag);
_closeDropdown();

View File

@ -1,5 +1,7 @@
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/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/services/api/api_exception.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';
@override
Future<SpaceDetailsModel> updateSpace(SpaceDetailsModel space) async {
Future<SpaceDetailsModel> updateSpace(UpdateSpaceParam param) async {
try {
final response = await _httpService.put(
path: 'endpoint',
body: space.toJson(),
expectedResponseModel: (data) => SpaceDetailsModel.fromJson(
data as Map<String, dynamic>,
),
final path = await _makeUrl(param);
await _httpService.put(
path: path,
body: param.toJson(),
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) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
@ -37,4 +45,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
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/update_space/domain/params/update_space_param.dart';
abstract class UpdateSpaceService {
Future<SpaceDetailsModel> updateSpace(SpaceDetailsModel space);
abstract interface class UpdateSpaceService {
Future<SpaceDetailsModel> updateSpace(UpdateSpaceParam param);
}

View File

@ -1,6 +1,7 @@
import 'package:bloc/bloc.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/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/services/api/api_exception.dart';
@ -20,7 +21,7 @@ class UpdateSpaceBloc extends Bloc<UpdateSpaceEvent, UpdateSpaceState> {
) async {
emit(UpdateSpaceLoading());
try {
final updatedSpace = await _updateSpaceService.updateSpace(event.space);
final updatedSpace = await _updateSpaceService.updateSpace(event.param);
emit(UpdateSpaceSuccess(updatedSpace));
} on APIException catch (e) {
emit(UpdateSpaceFailure(e.message));

View File

@ -8,10 +8,10 @@ sealed class UpdateSpaceEvent extends Equatable {
}
final class UpdateSpace extends UpdateSpaceEvent {
const UpdateSpace(this.space);
const UpdateSpace(this.param);
final SpaceDetailsModel space;
final UpdateSpaceParam param;
@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 String message;
final String errorMessage;
const UpdateSpaceFailure(this.message);
const UpdateSpaceFailure(this.errorMessage);
@override
List<Object> get props => [message];
List<Object> get props => [errorMessage];
}