Compare commits

...

5 Commits

9 changed files with 232 additions and 103 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,14 @@ 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, errorText: _errorText,
initialName: widget.initialName,
), ),
], ],
), ),
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

@ -5,16 +5,12 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
class DuplicateSpaceTextField extends StatelessWidget { class DuplicateSpaceTextField extends StatelessWidget {
const DuplicateSpaceTextField({ const DuplicateSpaceTextField({
required this.nameController, required this.nameController,
required this.isNameValid, required this.errorText,
required this.initialName,
super.key, super.key,
}); });
final TextEditingController nameController; final TextEditingController nameController;
final bool isNameValid; final String? errorText;
final String initialName;
String get _errorText => 'Name must be different from "$initialName"';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -35,7 +31,7 @@ class DuplicateSpaceTextField extends StatelessWidget {
color: context.theme.colorScheme.error, color: context.theme.colorScheme.error,
fontSize: 8, fontSize: 8,
), ),
errorText: isNameValid ? null : _errorText, errorText: errorText,
), ),
); );
} }

View File

@ -10,7 +10,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/update_space/prese
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';
class SpaceDetailsForm extends StatelessWidget { class SpaceDetailsForm extends StatefulWidget {
const SpaceDetailsForm({ const SpaceDetailsForm({
required this.title, required this.title,
required this.space, required this.space,
@ -22,10 +22,27 @@ class SpaceDetailsForm extends StatelessWidget {
final SpaceDetailsModel space; final SpaceDetailsModel space;
final void Function(SpaceDetailsModel space) onSave; final void Function(SpaceDetailsModel space) onSave;
@override
State<SpaceDetailsForm> createState() => _SpaceDetailsFormState();
}
class _SpaceDetailsFormState extends State<SpaceDetailsForm> {
final _formKey = GlobalKey<FormState>();
bool _isFormValid = false;
@override
void initState() {
super.initState();
final initialName = widget.space.spaceName;
_isFormValid = initialName.isNotEmpty &&
initialName.length <= 50 &&
!widget.space.subspaces.any((subspace) => subspace.name == initialName);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => SpaceDetailsModelBloc(initialState: space), create: (context) => SpaceDetailsModelBloc(initialState: widget.space),
child: BlocBuilder<SpaceDetailsModelBloc, SpaceDetailsModel>( child: BlocBuilder<SpaceDetailsModelBloc, SpaceDetailsModel>(
buildWhen: (previous, current) => previous != current, buildWhen: (previous, current) => previous != current,
builder: (context, space) { builder: (context, space) {
@ -36,10 +53,21 @@ class SpaceDetailsForm extends StatelessWidget {
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
), ),
child: title, child: widget.title,
), ),
backgroundColor: ColorsManager.white, backgroundColor: ColorsManager.white,
content: SizedBox( content: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: () {
final isValid = _formKey.currentState?.validate() ?? false;
if (_isFormValid != isValid) {
setState(() {
_isFormValid = isValid;
});
}
},
child: SizedBox(
height: context.screenHeight * 0.3, height: context.screenHeight * 0.3,
width: context.screenWidth * 0.4, width: context.screenWidth * 0.4,
child: Row( child: Row(
@ -70,14 +98,16 @@ class SpaceDetailsForm extends StatelessWidget {
], ],
), ),
), ),
),
actions: [ actions: [
SpaceDetailsActionButtons( SpaceDetailsActionButtons(
onSave: () => onSave(space), onSave: _isFormValid ? () => widget.onSave(space) : null,
onCancel: Navigator.of(context).pop, onCancel: Navigator.of(context).pop,
), ),
], ],
); );
}), },
),
); );
} }
} }

View File

@ -33,12 +33,13 @@ class _SpaceNameTextFieldState extends State<SpaceNameTextField> {
super.dispose(); super.dispose();
} }
final _formKey = GlobalKey<FormState>();
String? _validateName(String? value) { String? _validateName(String? value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return '*Space name should not be empty.'; return '*Space name should not be empty.';
} }
if (value.length > 50) {
return '*Space name cannot be longer than 50 characters.';
}
if (widget.isNameFieldExist(value)) { if (widget.isNameFieldExist(value)) {
return '*Name already exists'; return '*Name already exists';
} }
@ -47,10 +48,7 @@ class _SpaceNameTextFieldState extends State<SpaceNameTextField> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Form( return TextFormField(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: TextFormField(
controller: _controller, controller: _controller,
onChanged: (value) => context.read<SpaceDetailsModelBloc>().add( onChanged: (value) => context.read<SpaceDetailsModelBloc>().add(
UpdateSpaceDetailsName(value), UpdateSpaceDetailsName(value),
@ -72,7 +70,6 @@ class _SpaceNameTextFieldState extends State<SpaceNameTextField> {
color: context.theme.colorScheme.error, color: context.theme.colorScheme.error,
), ),
), ),
),
); );
} }

View File

@ -89,7 +89,12 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
), ),
child: const SelectableText('Create Sub-Space'), child: const SelectableText('Create Sub-Space'),
), ),
content: Column( content: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: context.screenHeight * 0.4,
),
child: SingleChildScrollView(
child: Column(
spacing: 12, spacing: 12,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -112,6 +117,8 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
), ),
], ],
), ),
),
),
actions: [ actions: [
SpaceDetailsActionButtons( SpaceDetailsActionButtons(
onSave: _hasDuplicateNames ? null : _handleSave, onSave: _hasDuplicateNames ? null : _handleSave,

View File

@ -182,8 +182,12 @@ class _AssignTagsDialogState extends State<AssignTagsDialog> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( ConstrainedBox(
width: double.infinity, constraints: BoxConstraints(
minWidth: double.infinity,
maxHeight: context.screenHeight * 0.6,
),
child: SingleChildScrollView(
child: AssignTagsTable( child: AssignTagsTable(
productAllocations: allProductAllocations, productAllocations: allProductAllocations,
subspaces: _space.subspaces, subspaces: _space.subspaces,
@ -193,6 +197,7 @@ class _AssignTagsDialogState extends State<AssignTagsDialog> {
onProductDeleted: _handleProductDelete, onProductDeleted: _handleProductDelete,
), ),
), ),
),
if (hasErrors) if (hasErrors)
AssignTagsErrorMessages( AssignTagsErrorMessages(
errorMessages: _validationErrors.values.toSet().toList(), errorMessages: _validationErrors.values.toSet().toList(),