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;
}).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,
builder: (_) => DuplicateSpaceDialog(
initialName: space.spaceName,
selectedSpaceUuid: space.uuid,
selectedCommunityUuid: selectedCommunity.uuid,
selectedSpace: space,
selectedCommunity: selectedCommunity,
onSuccess: (spaces) {
final updatedCommunity = selectedCommunity.copyWith(
spaces: spaces,

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/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';
@ -12,15 +13,15 @@ class DuplicateSpaceDialog extends StatelessWidget {
const DuplicateSpaceDialog({
required this.initialName,
required this.onSuccess,
required this.selectedSpaceUuid,
required this.selectedCommunityUuid,
required this.selectedSpace,
required this.selectedCommunity,
super.key,
});
final String initialName;
final void Function(List<SpaceModel> spaces) onSuccess;
final String selectedSpaceUuid;
final String selectedCommunityUuid;
final SpaceModel selectedSpace;
final CommunityModel selectedCommunity;
@override
Widget build(BuildContext context) {
@ -31,10 +32,10 @@ class DuplicateSpaceDialog extends StatelessWidget {
child: BlocListener<DuplicateSpaceBloc, DuplicateSpaceState>(
listener: _listener,
child: DuplicateSpaceDialogForm(
initialNameSuffix: '(1)',
initialName: initialName,
selectedSpaceUuid: selectedSpaceUuid,
selectedCommunityUuid: selectedCommunityUuid,
initialName: _getInitialName(),
selectedSpaceUuid: selectedSpace.uuid,
selectedCommunityUuid: selectedCommunity.uuid,
siblingNames: _getSiblingNames(),
),
),
);
@ -67,4 +68,69 @@ class DuplicateSpaceDialog extends StatelessWidget {
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.selectedSpaceUuid,
required this.selectedCommunityUuid,
required this.initialNameSuffix,
required this.siblingNames,
super.key,
});
final String initialName;
final String selectedSpaceUuid;
final String selectedCommunityUuid;
final String initialNameSuffix;
final List<String> siblingNames;
@override
State<DuplicateSpaceDialogForm> createState() => _DuplicateSpaceDialogFormState();
@ -26,20 +26,33 @@ class DuplicateSpaceDialogForm extends StatefulWidget {
class _DuplicateSpaceDialogFormState extends State<DuplicateSpaceDialogForm> {
late final TextEditingController _nameController;
bool _isNameValid = true;
String? _errorText;
@override
void initState() {
super.initState();
_nameController = TextEditingController(
text: '${widget.initialName}${widget.initialNameSuffix}',
text: widget.initialName,
);
_nameController.addListener(_validateName);
}
void _validateName() => setState(
() => _isNameValid = _nameController.text.trim() != widget.initialName,
);
void _validateName() {
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
void dispose() {
@ -66,15 +79,15 @@ class _DuplicateSpaceDialogFormState extends State<DuplicateSpaceDialogForm> {
const SelectableText('Enter a new name for the duplicated space:'),
DuplicateSpaceTextField(
nameController: _nameController,
isNameValid: _isNameValid,
initialName: widget.initialName,
isNameValid: _errorText == null,
errorText: _errorText,
),
],
),
actions: [
SpaceDetailsActionButtons(
spacerFlex: 2,
onSave: _isNameValid ? () => _submit(context) : null,
onSave: _errorText == null ? () => _submit(context) : null,
onCancel: Navigator.of(context).pop,
saveButtonLabel: 'Duplicate',
),

View File

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