fix add subspace bugs and plusButton widget

This commit is contained in:
Rafeek Alkhoudare
2025-05-26 02:45:58 -05:00
parent e0951aa13d
commit 0b65c58947
5 changed files with 242 additions and 136 deletions

View File

@ -67,7 +67,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void initState() {
super.initState();
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
connections =
widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
_adjustCanvasSizeForSpaces();
_nameController = TextEditingController(
text: widget.selectedCommunity?.name ?? '',
@ -96,13 +97,15 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
if (oldWidget.spaces != widget.spaces) {
setState(() {
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
connections =
widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
_adjustCanvasSizeForSpaces();
realignTree();
});
}
if (widget.selectedSpace != oldWidget.selectedSpace && widget.selectedSpace != null) {
if (widget.selectedSpace != oldWidget.selectedSpace &&
widget.selectedSpace != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_moveToSpace(widget.selectedSpace!);
});
@ -185,7 +188,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
connection, widget.selectedSpace)
? 1.0
: 0.3, // Adjust opacity
child: CustomPaint(painter: CurvedLinePainter([connection])),
child: CustomPaint(
painter: CurvedLinePainter([connection])),
),
for (var entry in spaces.asMap().entries)
if (entry.value.status != SpaceStatus.deleted &&
@ -195,9 +199,11 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
top: entry.value.position.dy,
child: SpaceCardWidget(
index: entry.key,
onButtonTap: (int index, Offset newPosition, String direction) {
onButtonTap: (int index, Offset newPosition,
String direction) {
_showCreateSpaceDialog(screenSize,
position: spaces[index].position + newPosition,
position:
spaces[index].position + newPosition,
parentIndex: index,
direction: direction,
projectTags: widget.projectTags);
@ -210,7 +216,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
_updateNodePosition(entry.value, newPosition);
},
buildSpaceContainer: (int index) {
final bool isHighlighted = SpaceHelper.isHighlightedSpace(
final bool isHighlighted =
SpaceHelper.isHighlightedSpace(
spaces[index], widget.selectedSpace);
return Opacity(
@ -299,19 +306,25 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
return CreateSpaceDialog(
products: widget.products,
spaceModels: widget.spaceModels,
allTags: TagHelper.getAllTagValues(widget.communities, widget.spaceModels),
allTags:
TagHelper.getAllTagValues(widget.communities, widget.spaceModels),
parentSpace: parentIndex != null ? spaces[parentIndex] : null,
projectTags: projectTags,
onCreateSpace: (String name, String icon, List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel, List<SubspaceModel>? subspaces, List<Tag>? tags) {
onCreateSpace: (String name,
String icon,
List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel,
List<SubspaceModel>? subspaces,
List<Tag>? tags) {
setState(() {
// Set the first space in the center or use passed position
Offset newPosition;
if (parentIndex != null) {
newPosition =
getBalancedChildPosition(spaces[parentIndex]); // Ensure balanced position
newPosition = getBalancedChildPosition(
spaces[parentIndex]); // Ensure balanced position
} else {
newPosition = position ?? ConnectionHelper.getCenterPosition(screenSize);
newPosition =
position ?? ConnectionHelper.getCenterPosition(screenSize);
}
SpaceModel newSpace = SpaceModel(
@ -360,16 +373,21 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
name: widget.selectedSpace!.name,
icon: widget.selectedSpace!.icon,
projectTags: widget.projectTags,
parentSpace:
SpaceHelper.findSpaceByInternalId(widget.selectedSpace?.parent?.internalId, spaces),
parentSpace: SpaceHelper.findSpaceByInternalId(
widget.selectedSpace?.parent?.internalId, spaces),
editSpace: widget.selectedSpace,
currentSpaceModel: widget.selectedSpace?.spaceModel,
tags: widget.selectedSpace?.tags,
subspaces: widget.selectedSpace?.subspaces,
isEdit: true,
allTags: TagHelper.getAllTagValues(widget.communities, widget.spaceModels),
onCreateSpace: (String name, String icon, List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel, List<SubspaceModel>? subspaces, List<Tag>? tags) {
allTags: TagHelper.getAllTagValues(
widget.communities, widget.spaceModels),
onCreateSpace: (String name,
String icon,
List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel,
List<SubspaceModel>? subspaces,
List<Tag>? tags) {
setState(() {
// Update the space's properties
widget.selectedSpace!.name = name;
@ -379,7 +397,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
widget.selectedSpace!.tags = tags;
if (widget.selectedSpace!.status != SpaceStatus.newSpace) {
widget.selectedSpace!.status = SpaceStatus.modified; // Mark as modified
widget.selectedSpace!.status =
SpaceStatus.modified; // Mark as modified
}
for (var space in spaces) {
@ -410,7 +429,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
Map<String, SpaceModel> idToSpace = {};
void flatten(SpaceModel space) {
if (space.status == SpaceStatus.deleted || space.status == SpaceStatus.parentDeleted) {
if (space.status == SpaceStatus.deleted ||
space.status == SpaceStatus.parentDeleted) {
return;
}
result.add(space);
@ -532,13 +552,16 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _selectSpace(BuildContext context, SpaceModel space) {
context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(selectedCommunity: widget.selectedCommunity, selectedSpace: space),
SelectSpaceEvent(
selectedCommunity: widget.selectedCommunity,
selectedSpace: space),
);
}
void _deselectSpace(BuildContext context) {
context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(selectedCommunity: widget.selectedCommunity, selectedSpace: null),
SelectSpaceEvent(
selectedCommunity: widget.selectedCommunity, selectedSpace: null),
);
}
@ -708,7 +731,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
SpaceModel duplicated = _deepCloneSpaceTree(space, parent: parent);
duplicated.position = Offset(space.position.dx + 300, space.position.dy + 100);
duplicated.position =
Offset(space.position.dx + 300, space.position.dy + 100);
List<SpaceModel> duplicatedSubtree = [];
void collectSubtree(SpaceModel node) {
duplicatedSubtree.add(node);
@ -739,7 +763,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
}
SpaceModel _deepCloneSpaceTree(SpaceModel original, {SpaceModel? parent}) {
final duplicatedName = SpaceHelper.generateUniqueSpaceName(original.name, spaces);
final duplicatedName =
SpaceHelper.generateUniqueSpaceName(original.name, spaces);
final newSpace = SpaceModel(
name: duplicatedName,

View File

@ -82,8 +82,10 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
super.initState();
selectedIcon = widget.icon ?? Assets.location;
nameController = TextEditingController(text: widget.name ?? '');
selectedProducts = widget.selectedProducts.isNotEmpty ? widget.selectedProducts : [];
isOkButtonEnabled = enteredName.isNotEmpty || nameController.text.isNotEmpty;
selectedProducts =
widget.selectedProducts.isNotEmpty ? widget.selectedProducts : [];
isOkButtonEnabled =
enteredName.isNotEmpty || nameController.text.isNotEmpty;
if (widget.currentSpaceModel != null) {
subspaces = [];
tags = [];
@ -96,13 +98,15 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
@override
Widget build(BuildContext context) {
bool isSpaceModelDisabled =
(tags != null && tags!.isNotEmpty || subspaces != null && subspaces!.isNotEmpty);
bool isSpaceModelDisabled = (tags != null && tags!.isNotEmpty ||
subspaces != null && subspaces!.isNotEmpty);
bool isTagsAndSubspaceModelDisabled = (selectedSpaceModel != null);
final screenWidth = MediaQuery.of(context).size.width;
return AlertDialog(
title: widget.isEdit ? const Text('Edit Space') : const Text('Create New Space'),
title: widget.isEdit
? const Text('Edit Space')
: const Text('Create New Space'),
backgroundColor: ColorsManager.whiteColors,
content: SizedBox(
width: screenWidth * 0.5,
@ -176,8 +180,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
isNameFieldInvalid = value.isEmpty;
if (!isNameFieldInvalid) {
if (SpaceHelper.isNameConflict(
value, widget.parentSpace, widget.editSpace)) {
if (SpaceHelper.isNameConflict(value,
widget.parentSpace, widget.editSpace)) {
isNameFieldExist = true;
isOkButtonEnabled = false;
} else {
@ -244,7 +248,9 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
padding: EdgeInsets.zero,
),
onPressed: () {
isSpaceModelDisabled ? null : _showLinkSpaceModelDialog(context);
isSpaceModelDisabled
? null
: _showLinkSpaceModelDialog(context);
},
child: ButtonContentWidget(
svgAssets: Assets.link,
@ -254,7 +260,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
)
: Container(
width: screenWidth * 0.25,
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
padding: const EdgeInsets.symmetric(
vertical: 10.0, horizontal: 16.0),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(10),
@ -269,7 +276,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: ColorsManager.spaceColor),
.copyWith(
color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
@ -340,12 +348,12 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
onPressed: () async {
isTagsAndSubspaceModelDisabled
? null
: _showSubSpaceDialog(
context, enteredName, [], false, widget.products, subspaces);
: _showSubSpaceDialog(context, enteredName,
[], false, widget.products, subspaces);
},
child: ButtonContentWidget(
icon: Icons.add,
label: 'Create Sub Space',
label: 'Create Sub Spaces',
disabled: isTagsAndSubspaceModelDisabled,
),
)
@ -368,16 +376,22 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
if (subspaces != null)
...subspaces!.map((subspace) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
SubspaceNameDisplayWidget(
text: subspace.subspaceName,
validateName: (updatedName) {
bool nameExists = subspaces!.any((s) {
bool isSameId = s.internalId == subspace.internalId;
bool isSameName =
s.subspaceName.trim().toLowerCase() ==
updatedName.trim().toLowerCase();
bool nameExists =
subspaces!.any((s) {
bool isSameId = s.internalId ==
subspace.internalId;
bool isSameName = s.subspaceName
.trim()
.toLowerCase() ==
updatedName
.trim()
.toLowerCase();
return !isSameId && isSameName;
});
@ -386,7 +400,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
},
onNameChanged: (updatedName) {
setState(() {
subspace.subspaceName = updatedName;
subspace.subspaceName =
updatedName;
});
},
),
@ -395,8 +410,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
}),
EditChip(
onTap: () async {
_showSubSpaceDialog(context, enteredName, [], true,
widget.products, subspaces);
_showSubSpaceDialog(context, enteredName,
[], true, widget.products, subspaces);
},
)
],
@ -405,7 +420,9 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
),
const SizedBox(height: 10),
(tags?.isNotEmpty == true ||
subspaces?.any((subspace) => subspace.tags?.isNotEmpty == true) == true)
subspaces?.any((subspace) =>
subspace.tags?.isNotEmpty == true) ==
true)
? SizedBox(
width: screenWidth * 0.25,
child: Container(
@ -425,14 +442,16 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
// Combine tags from spaceModel and subspaces
...TagHelper.groupTags([
...?tags,
...?subspaces?.expand((subspace) => subspace.tags ?? [])
...?subspaces?.expand(
(subspace) => subspace.tags ?? [])
]).entries.map(
(entry) => Chip(
avatar: SizedBox(
width: 24,
height: 24,
child: SvgPicture.asset(
entry.key.icon ?? 'assets/icons/gateway.svg',
entry.key.icon ??
'assets/icons/gateway.svg',
fit: BoxFit.contain,
),
),
@ -441,11 +460,15 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
?.copyWith(
color: ColorsManager
.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
backgroundColor:
ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
borderRadius:
BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor,
),
@ -460,15 +483,18 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
products: widget.products,
subspaces: subspaces,
allTags: widget.allTags,
addedProducts:
TagHelper.createInitialSelectedProductsForTags(
addedProducts: TagHelper
.createInitialSelectedProductsForTags(
tags ?? [], subspaces),
title: 'Edit Device',
initialTags: TagHelper.generateInitialForTags(
spaceTags: tags, subspaces: subspaces),
initialTags:
TagHelper.generateInitialForTags(
spaceTags: tags,
subspaces: subspaces),
spaceName: widget.name ?? '',
projectTags: widget.projectTags,
onSave: (updatedTags, updatedSubspaces) {
onSave:
(updatedTags, updatedSubspaces) {
setState(() {
tags = updatedTags;
subspaces = updatedSubspaces;
@ -529,17 +555,25 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
} else if (isNameFieldExist) {
return;
} else {
String newName = enteredName.isNotEmpty ? enteredName : (widget.name ?? '');
String newName = enteredName.isNotEmpty
? enteredName
: (widget.name ?? '');
if (newName.isNotEmpty) {
widget.onCreateSpace(newName, selectedIcon, selectedProducts,
selectedSpaceModel, subspaces, tags);
widget.onCreateSpace(
newName,
selectedIcon,
selectedProducts,
selectedSpaceModel,
subspaces,
tags);
Navigator.of(context).pop();
}
}
},
borderRadius: 10,
backgroundColor:
isOkButtonEnabled ? ColorsManager.secondaryColor : ColorsManager.grayColor,
backgroundColor: isOkButtonEnabled
? ColorsManager.secondaryColor
: ColorsManager.grayColor,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
),
@ -586,24 +620,31 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
);
}
void _showSubSpaceDialog(BuildContext context, String name, final List<Tag>? spaceTags,
bool isEdit, List<ProductModel>? products, final List<SubspaceModel>? existingSubSpaces) {
void _showSubSpaceDialog(
BuildContext context,
String name,
final List<Tag>? spaceTags,
bool isEdit,
List<ProductModel>? products,
final List<SubspaceModel>? existingSubSpaces) {
showDialog(
context: context,
builder: (BuildContext context) {
return CreateSubSpaceDialog(
spaceName: name,
dialogTitle: isEdit ? 'Edit Sub-space' : 'Create Sub-space',
dialogTitle: isEdit ? 'Edit Sub-spaces' : 'Create Sub-spaces',
products: products,
existingSubSpaces: existingSubSpaces,
onSave: (slectedSubspaces) {
final List<Tag> tagsToAppendToSpace = [];
if (slectedSubspaces != null) {
final updatedIds = slectedSubspaces.map((s) => s.internalId).toSet();
if (slectedSubspaces != null && slectedSubspaces.isNotEmpty) {
final updatedIds =
slectedSubspaces.map((s) => s.internalId).toSet();
if (existingSubSpaces != null) {
final deletedSubspaces =
existingSubSpaces.where((s) => !updatedIds.contains(s.internalId)).toList();
final deletedSubspaces = existingSubSpaces
.where((s) => !updatedIds.contains(s.internalId))
.toList();
for (var s in deletedSubspaces) {
if (s.tags != null) {
tagsToAppendToSpace.addAll(s.tags!);
@ -623,15 +664,16 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
);
}
void _showTagCreateDialog(
BuildContext context, String name, bool isEdit, List<ProductModel>? products) {
void _showTagCreateDialog(BuildContext context, String name, bool isEdit,
List<ProductModel>? products) {
isEdit
? showDialog(
context: context,
builder: (BuildContext context) {
return AssignTagDialog(
title: 'Edit Device',
addedProducts: TagHelper.createInitialSelectedProductsForTags(tags, subspaces),
addedProducts: TagHelper.createInitialSelectedProductsForTags(
tags, subspaces),
spaceName: name,
products: products,
subspaces: subspaces,
@ -646,7 +688,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
if (subspaces != null) {
for (final subspace in subspaces!) {
for (final selectedSubspace in selectedSubspaces) {
if (subspace.subspaceName == selectedSubspace.subspaceName) {
if (subspace.subspaceName ==
selectedSubspace.subspaceName) {
subspace.tags = selectedSubspace.tags;
}
}
@ -670,7 +713,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
allTags: widget.allTags,
projectTags: widget.projectTags,
initialSelectedProducts:
TagHelper.createInitialSelectedProductsForTags(tags, subspaces),
TagHelper.createInitialSelectedProductsForTags(
tags, subspaces),
onSave: (selectedSpaceTags, selectedSubspaces) {
setState(() {
tags = selectedSpaceTags;
@ -680,7 +724,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
if (subspaces != null) {
for (final subspace in subspaces!) {
for (final selectedSubspace in selectedSubspaces) {
if (subspace.subspaceName == selectedSubspace.subspaceName) {
if (subspace.subspaceName ==
selectedSubspace.subspaceName) {
subspace.tags = selectedSubspace.tags;
}
}

View File

@ -17,10 +17,7 @@ class PlusButtonWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Positioned(
left: offset.dx,
top: offset.dy,
child: GestureDetector(
return GestureDetector(
onTap: () {
Offset newPosition;
switch (direction) {
@ -45,8 +42,8 @@ class PlusButtonWidget extends StatelessWidget {
color: ColorsManager.spaceColor,
shape: BoxShape.circle,
),
child: const Icon(Icons.add, color: ColorsManager.whiteColors, size: 20),
),
child:
const Icon(Icons.add, color: ColorsManager.whiteColors, size: 20),
),
);
}

View File

@ -25,35 +25,34 @@ class SpaceCardWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onPanUpdate: (details) {
// Call the provided callback to update the position
final newPosition = position + details.delta;
onPositionChanged(newPosition);
},
child: MouseRegion(
onEnter: (_) {
// Call the provided callback to handle hover state
onHoverChanged(index, true);
},
onExit: (_) {
// Call the provided callback to handle hover state
onHoverChanged(index, false);
},
return MouseRegion(
onEnter: (_) => onHoverChanged(index, true),
onExit: (_) => onHoverChanged(index, false),
child: SizedBox(
width: 140, // Make sure this covers both card and plus button
height: 90,
child: Stack(
clipBehavior: Clip
.none, // Allow hovering elements to be displayed outside the boundary
clipBehavior: Clip.none,
children: [
buildSpaceContainer(index), // Build the space container
if (isHovered) ...[
PlusButtonWidget(
// Main card
Container(
width: 140,
height: 80,
alignment: Alignment.center,
color: Colors.transparent,
child: buildSpaceContainer(index),
),
// Plus button (NO inner Positioned!)
if (isHovered)
Align(
alignment: Alignment.bottomCenter,
child: PlusButtonWidget(
index: index,
direction: 'down',
offset: const Offset(63, 50),
offset: Offset.zero,
onButtonTap: onButtonTap,
),
],
),
],
),
),

View File

@ -79,6 +79,22 @@ class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> {
color: ColorsManager.blackColor,
),
),
Row(
children: [
Text(
'press Enter to Save',
style: context.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 10,
color: ColorsManager.grayColor,
),
),
const SizedBox(
width: 5,
),
const Icon(Icons.save_as_sharp, size: 10),
],
),
const SizedBox(height: 16),
Container(
width: context.screenWidth * 0.35,
@ -101,13 +117,15 @@ class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> {
final index = entry.key;
final subSpace = entry.value;
final lowerName = subSpace.subspaceName.toLowerCase();
final lowerName =
subSpace.subspaceName.toLowerCase();
final duplicateIndices = state.subSpaces
.asMap()
.entries
.where((e) =>
e.value.subspaceName.toLowerCase() == lowerName)
e.value.subspaceName.toLowerCase() ==
lowerName)
.map((e) => e.key)
.toList();
final isDuplicate = duplicateIndices.length > 1 &&
@ -182,10 +200,32 @@ class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> {
Expanded(
child: DefaultButton(
onPressed: state.errorMessage.isEmpty
? () {
final subSpacesBloc = context.read<SubSpaceBloc>();
final subSpaces = subSpacesBloc.state.subSpaces;
? () async {
final trimmedValue =
_subspaceNameController.text.trim();
final subSpacesBloc =
context.read<SubSpaceBloc>();
if (trimmedValue.isNotEmpty) {
subSpacesBloc.add(
AddSubSpace(
SubspaceModel(
subspaceName: trimmedValue,
disabled: false,
),
),
);
_subspaceNameController.clear();
}
await Future.delayed(
const Duration(milliseconds: 10));
final subSpaces =
subSpacesBloc.state.subSpaces;
// if (subSpaces.isNotEmpty) {
widget.onSave?.call(subSpaces);
// }
Navigator.of(context).pop();
}
: null,