Diallows naming duplication when duplicating a space, and adds a proper suffix to the name in case of another spaces having a name match.

This commit is contained in:
Faris Armoush
2025-07-27 10:51:53 +03:00
parent f1cf8d88d3
commit 83895d3dda
5 changed files with 118 additions and 25 deletions

View File

@ -68,4 +68,20 @@ abstract final class SpacesRecursiveHelper {
return space; return space;
}).toList(); }).toList();
} }
static SpaceModel? findParent(
List<SpaceModel> spaces,
String targetUuid,
) {
for (final space in spaces) {
if (space.children.any((child) => child.uuid == targetUuid)) {
return space;
}
final parent = findParent(space.children, targetUuid);
if (parent != null) {
return parent;
}
}
return null;
}
} }

View File

@ -49,8 +49,8 @@ class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget {
context: context, context: context,
builder: (_) => DuplicateSpaceDialog( builder: (_) => DuplicateSpaceDialog(
initialName: space.spaceName, initialName: space.spaceName,
selectedSpaceUuid: space.uuid, selectedSpace: space,
selectedCommunityUuid: selectedCommunity.uuid, selectedCommunity: selectedCommunity,
onSuccess: (spaces) { onSuccess: (spaces) {
final updatedCommunity = selectedCommunity.copyWith( final updatedCommunity = selectedCommunity.copyWith(
spaces: spaces, spaces: spaces,

View File

@ -1,6 +1,7 @@
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/common/widgets/app_loading_indicator.dart'; import 'package:syncrow_web/common/widgets/app_loading_indicator.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/duplicate_space/data/services/remote_duplicate_space_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/data/services/remote_duplicate_space_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_bloc.dart';
@ -12,15 +13,15 @@ class DuplicateSpaceDialog extends StatelessWidget {
const DuplicateSpaceDialog({ const DuplicateSpaceDialog({
required this.initialName, required this.initialName,
required this.onSuccess, required this.onSuccess,
required this.selectedSpaceUuid, required this.selectedSpace,
required this.selectedCommunityUuid, required this.selectedCommunity,
super.key, super.key,
}); });
final String initialName; final String initialName;
final void Function(List<SpaceModel> spaces) onSuccess; final void Function(List<SpaceModel> spaces) onSuccess;
final String selectedSpaceUuid; final SpaceModel selectedSpace;
final String selectedCommunityUuid; final CommunityModel selectedCommunity;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -31,10 +32,10 @@ class DuplicateSpaceDialog extends StatelessWidget {
child: BlocListener<DuplicateSpaceBloc, DuplicateSpaceState>( child: BlocListener<DuplicateSpaceBloc, DuplicateSpaceState>(
listener: _listener, listener: _listener,
child: DuplicateSpaceDialogForm( child: DuplicateSpaceDialogForm(
initialNameSuffix: '(1)', initialName: _getInitialName(),
initialName: initialName, selectedSpaceUuid: selectedSpace.uuid,
selectedSpaceUuid: selectedSpaceUuid, selectedCommunityUuid: selectedCommunity.uuid,
selectedCommunityUuid: selectedCommunityUuid, siblingNames: _getSiblingNames(),
), ),
), ),
); );
@ -67,4 +68,69 @@ class DuplicateSpaceDialog extends StatelessWidget {
break; break;
} }
} }
List<SpaceModel> _findSiblings(
List<SpaceModel> spaces,
String targetUuid,
) {
final parent = _findParent(spaces, targetUuid);
if (parent != null) {
return parent.children.where((s) => s.uuid != targetUuid).toList();
} else {
if (spaces.any((s) => s.uuid == targetUuid)) {
return spaces.where((s) => s.uuid != targetUuid).toList();
}
}
return [];
}
SpaceModel? _findParent(List<SpaceModel> allSpaces, String childUuid) {
for (final space in allSpaces) {
if (space.children.any((child) => child.uuid == childUuid)) {
return space;
}
final parent = _findParent(space.children, childUuid);
if (parent != null) {
return parent;
}
}
return null;
}
List<String> _getSiblingNames() {
final siblings = _findSiblings(selectedCommunity.spaces, selectedSpace.uuid);
final names = siblings.map((s) => s.spaceName).toList();
names.add(initialName);
return names;
}
String _getInitialName() {
final allRelevantNames = _getSiblingNames();
final nameRegex = RegExp(r'^(.*?) ?\((\d+)\)$');
final baseNameMatch = nameRegex.firstMatch(initialName);
final baseName = baseNameMatch?.group(1)?.trim() ?? initialName;
var maxSuffix = 0;
for (final name in allRelevantNames) {
if (name == baseName) {
continue;
}
final match = nameRegex.firstMatch(name);
if (match != null) {
final currentBaseName = match.group(1)!.trim();
if (currentBaseName == baseName) {
final suffix = int.parse(match.group(2)!);
if (suffix > maxSuffix) {
maxSuffix = suffix;
}
}
}
}
return '$baseName(${maxSuffix + 1})';
}
} }

View File

@ -11,14 +11,14 @@ class DuplicateSpaceDialogForm extends StatefulWidget {
required this.initialName, required this.initialName,
required this.selectedSpaceUuid, required this.selectedSpaceUuid,
required this.selectedCommunityUuid, required this.selectedCommunityUuid,
required this.initialNameSuffix, required this.siblingNames,
super.key, super.key,
}); });
final String initialName; final String initialName;
final String selectedSpaceUuid; final String selectedSpaceUuid;
final String selectedCommunityUuid; final String selectedCommunityUuid;
final String initialNameSuffix; final List<String> siblingNames;
@override @override
State<DuplicateSpaceDialogForm> createState() => _DuplicateSpaceDialogFormState(); State<DuplicateSpaceDialogForm> createState() => _DuplicateSpaceDialogFormState();
@ -26,20 +26,33 @@ class DuplicateSpaceDialogForm extends StatefulWidget {
class _DuplicateSpaceDialogFormState extends State<DuplicateSpaceDialogForm> { class _DuplicateSpaceDialogFormState extends State<DuplicateSpaceDialogForm> {
late final TextEditingController _nameController; late final TextEditingController _nameController;
bool _isNameValid = true; String? _errorText;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_nameController = TextEditingController( _nameController = TextEditingController(
text: '${widget.initialName}${widget.initialNameSuffix}', text: widget.initialName,
); );
_nameController.addListener(_validateName); _nameController.addListener(_validateName);
} }
void _validateName() => setState( void _validateName() {
() => _isNameValid = _nameController.text.trim() != widget.initialName, final name = _nameController.text.trim();
); if (name.isEmpty) {
setState(() {
_errorText = 'Name cannot be empty';
});
} else if (widget.siblingNames.contains(name)) {
setState(() {
_errorText = 'Name must be unique';
});
} else {
setState(() {
_errorText = null;
});
}
}
@override @override
void dispose() { void dispose() {
@ -66,15 +79,15 @@ class _DuplicateSpaceDialogFormState extends State<DuplicateSpaceDialogForm> {
const SelectableText('Enter a new name for the duplicated space:'), const SelectableText('Enter a new name for the duplicated space:'),
DuplicateSpaceTextField( DuplicateSpaceTextField(
nameController: _nameController, nameController: _nameController,
isNameValid: _isNameValid, isNameValid: _errorText == null,
initialName: widget.initialName, errorText: _errorText,
), ),
], ],
), ),
actions: [ actions: [
SpaceDetailsActionButtons( SpaceDetailsActionButtons(
spacerFlex: 2, spacerFlex: 2,
onSave: _isNameValid ? () => _submit(context) : null, onSave: _errorText == null ? () => _submit(context) : null,
onCancel: Navigator.of(context).pop, onCancel: Navigator.of(context).pop,
saveButtonLabel: 'Duplicate', saveButtonLabel: 'Duplicate',
), ),

View File

@ -6,15 +6,13 @@ class DuplicateSpaceTextField extends StatelessWidget {
const DuplicateSpaceTextField({ const DuplicateSpaceTextField({
required this.nameController, required this.nameController,
required this.isNameValid, required this.isNameValid,
required this.initialName, this.errorText,
super.key, super.key,
}); });
final TextEditingController nameController; final TextEditingController nameController;
final bool isNameValid; final bool isNameValid;
final String initialName; final String? errorText;
String get _errorText => 'Name must be different from "$initialName"';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -35,7 +33,7 @@ class DuplicateSpaceTextField extends StatelessWidget {
color: context.theme.colorScheme.error, color: context.theme.colorScheme.error,
fontSize: 8, fontSize: 8,
), ),
errorText: isNameValid ? null : _errorText, errorText: isNameValid ? null : errorText,
), ),
); );
} }