mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-08-24 23:42:28 +00:00
Compare commits
7 Commits
0f9cbd22a2
...
9a4fdb2f88
Author | SHA1 | Date | |
---|---|---|---|
9a4fdb2f88 | |||
1558806cc3 | |||
a4391aa73e | |||
2d69e3c72f | |||
cd8ffc99ea | |||
83895d3dda | |||
f1cf8d88d3 |
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,9 +32,10 @@ class DuplicateSpaceDialog extends StatelessWidget {
|
||||
child: BlocListener<DuplicateSpaceBloc, DuplicateSpaceState>(
|
||||
listener: _listener,
|
||||
child: DuplicateSpaceDialogForm(
|
||||
initialName: initialName,
|
||||
selectedSpaceUuid: selectedSpaceUuid,
|
||||
selectedCommunityUuid: selectedCommunityUuid,
|
||||
initialName: _getInitialName(),
|
||||
selectedSpaceUuid: selectedSpace.uuid,
|
||||
selectedCommunityUuid: selectedCommunity.uuid,
|
||||
siblingNames: _getSiblingNames(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -66,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})';
|
||||
}
|
||||
}
|
||||
|
@ -3,18 +3,22 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/domain/params/duplicate_space_param.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/widgets/duplicate_space_text_field.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class DuplicateSpaceDialogForm extends StatefulWidget {
|
||||
const DuplicateSpaceDialogForm({
|
||||
required this.initialName,
|
||||
required this.selectedSpaceUuid,
|
||||
required this.selectedCommunityUuid,
|
||||
required this.siblingNames,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String initialName;
|
||||
final String selectedSpaceUuid;
|
||||
final String selectedCommunityUuid;
|
||||
final List<String> siblingNames;
|
||||
|
||||
@override
|
||||
State<DuplicateSpaceDialogForm> createState() => _DuplicateSpaceDialogFormState();
|
||||
@ -22,18 +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}(1)');
|
||||
_nameController = TextEditingController(
|
||||
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() {
|
||||
@ -44,27 +63,32 @@ class _DuplicateSpaceDialogFormState extends State<DuplicateSpaceDialogForm> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const SelectableText('Duplicate Space'),
|
||||
title: const SelectableText(
|
||||
'Duplicate Space',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 16,
|
||||
children: [
|
||||
const SelectableText('Enter a new name for the duplicated space:'),
|
||||
DuplicateSpaceTextField(
|
||||
nameController: _nameController,
|
||||
isNameValid: _isNameValid,
|
||||
initialName: widget.initialName,
|
||||
errorText: _errorText,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isNameValid ? () => _submit(context) : null,
|
||||
child: const Text('Duplicate'),
|
||||
SpaceDetailsActionButtons(
|
||||
spacerFlex: 2,
|
||||
onSave: _errorText == null ? () => _submit(context) : null,
|
||||
onCancel: Navigator.of(context).pop,
|
||||
saveButtonLabel: 'Duplicate',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -5,16 +5,12 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
class DuplicateSpaceTextField extends StatelessWidget {
|
||||
const DuplicateSpaceTextField({
|
||||
required this.nameController,
|
||||
required this.isNameValid,
|
||||
required this.initialName,
|
||||
required 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) {
|
||||
@ -24,9 +20,10 @@ class DuplicateSpaceTextField extends StatelessWidget {
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
label: const Text('Space Name'),
|
||||
border: _border(),
|
||||
enabledBorder: _border(),
|
||||
filled: true,
|
||||
fillColor: ColorsManager.boxColor,
|
||||
border: _border(ColorsManager.transparentColor),
|
||||
enabledBorder: _border(ColorsManager.transparentColor),
|
||||
focusedBorder: _border(ColorsManager.primaryColor),
|
||||
errorBorder: _border(context.theme.colorScheme.error),
|
||||
focusedErrorBorder: _border(context.theme.colorScheme.error),
|
||||
@ -34,14 +31,14 @@ class DuplicateSpaceTextField extends StatelessWidget {
|
||||
color: context.theme.colorScheme.error,
|
||||
fontSize: 8,
|
||||
),
|
||||
errorText: isNameValid ? null : _errorText,
|
||||
errorText: errorText,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
OutlineInputBorder _border([Color? color]) {
|
||||
return OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: color ?? ColorsManager.blackColor,
|
||||
width: 0.5,
|
||||
|
@ -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/extension/build_context_x.dart';
|
||||
|
||||
class SpaceDetailsForm extends StatelessWidget {
|
||||
class SpaceDetailsForm extends StatefulWidget {
|
||||
const SpaceDetailsForm({
|
||||
required this.title,
|
||||
required this.space,
|
||||
@ -22,24 +22,52 @@ class SpaceDetailsForm extends StatelessWidget {
|
||||
final SpaceDetailsModel space;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SpaceDetailsModelBloc(initialState: space),
|
||||
create: (context) => SpaceDetailsModelBloc(initialState: widget.space),
|
||||
child: BlocBuilder<SpaceDetailsModelBloc, SpaceDetailsModel>(
|
||||
buildWhen: (previous, current) => previous != current,
|
||||
builder: (context, space) {
|
||||
return AlertDialog(
|
||||
title: DefaultTextStyle(
|
||||
style: context.textTheme.titleLarge!.copyWith(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
child: title,
|
||||
buildWhen: (previous, current) => previous != current,
|
||||
builder: (context, space) {
|
||||
return AlertDialog(
|
||||
title: DefaultTextStyle(
|
||||
style: context.textTheme.titleLarge!.copyWith(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
backgroundColor: ColorsManager.white,
|
||||
content: SizedBox(
|
||||
child: widget.title,
|
||||
),
|
||||
backgroundColor: ColorsManager.white,
|
||||
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,
|
||||
width: context.screenWidth * 0.4,
|
||||
child: Row(
|
||||
@ -70,14 +98,16 @@ class SpaceDetailsForm extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
SpaceDetailsActionButtons(
|
||||
onSave: () => onSave(space),
|
||||
onCancel: Navigator.of(context).pop,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
actions: [
|
||||
SpaceDetailsActionButtons(
|
||||
onSave: _isFormValid ? () => widget.onSave(space) : null,
|
||||
onCancel: Navigator.of(context).pop,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -33,12 +33,13 @@ class _SpaceNameTextFieldState extends State<SpaceNameTextField> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
String? _validateName(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '*Space name should not be empty.';
|
||||
}
|
||||
if (value.length > 50) {
|
||||
return '*Space name cannot be longer than 50 characters.';
|
||||
}
|
||||
if (widget.isNameFieldExist(value)) {
|
||||
return '*Name already exists';
|
||||
}
|
||||
@ -47,30 +48,26 @@ class _SpaceNameTextFieldState extends State<SpaceNameTextField> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: TextFormField(
|
||||
controller: _controller,
|
||||
onChanged: (value) => context.read<SpaceDetailsModelBloc>().add(
|
||||
UpdateSpaceDetailsName(value),
|
||||
),
|
||||
validator: _validateName,
|
||||
style: context.textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Please enter the name',
|
||||
hintStyle: context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: ColorsManager.boxColor,
|
||||
enabledBorder: _buildBorder(context, ColorsManager.vividBlue),
|
||||
focusedBorder: _buildBorder(context, ColorsManager.primaryColor),
|
||||
errorBorder: _buildBorder(context, context.theme.colorScheme.error),
|
||||
focusedErrorBorder: _buildBorder(context, context.theme.colorScheme.error),
|
||||
errorStyle: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.theme.colorScheme.error,
|
||||
return TextFormField(
|
||||
controller: _controller,
|
||||
onChanged: (value) => context.read<SpaceDetailsModelBloc>().add(
|
||||
UpdateSpaceDetailsName(value),
|
||||
),
|
||||
validator: _validateName,
|
||||
style: context.textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Please enter the name',
|
||||
hintStyle: context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: ColorsManager.boxColor,
|
||||
enabledBorder: _buildBorder(context, ColorsManager.vividBlue),
|
||||
focusedBorder: _buildBorder(context, ColorsManager.primaryColor),
|
||||
errorBorder: _buildBorder(context, context.theme.colorScheme.error),
|
||||
focusedErrorBorder: _buildBorder(context, context.theme.colorScheme.error),
|
||||
errorStyle: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -89,28 +89,35 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
|
||||
),
|
||||
child: const SelectableText('Create Sub-Space'),
|
||||
),
|
||||
content: Column(
|
||||
spacing: 12,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SubSpacesInput(
|
||||
subSpaces: _subspaces,
|
||||
onSubspaceAdded: _handleSubspaceAdded,
|
||||
onSubspaceDeleted: _handleSubspaceDeleted,
|
||||
controller: _subspaceNameController,
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: Visibility(
|
||||
key: ValueKey(_hasDuplicateNames),
|
||||
visible: _hasDuplicateNames,
|
||||
child: const SelectableText(
|
||||
'Error: Duplicate subspace names are not allowed.',
|
||||
style: TextStyle(color: Colors.red),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: context.screenHeight * 0.4,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SubSpacesInput(
|
||||
subSpaces: _subspaces,
|
||||
onSubspaceAdded: _handleSubspaceAdded,
|
||||
onSubspaceDeleted: _handleSubspaceDeleted,
|
||||
controller: _subspaceNameController,
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: Visibility(
|
||||
key: ValueKey(_hasDuplicateNames),
|
||||
visible: _hasDuplicateNames,
|
||||
child: const SelectableText(
|
||||
'Error: Duplicate subspace names are not allowed.',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
SpaceDetailsActionButtons(
|
||||
|
@ -182,15 +182,20 @@ class _AssignTagsDialogState extends State<AssignTagsDialog> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: AssignTagsTable(
|
||||
productAllocations: allProductAllocations,
|
||||
subspaces: _space.subspaces,
|
||||
productLocations: productLocations,
|
||||
onTagSelected: _handleTagChange,
|
||||
onLocationSelected: _handleLocationChange,
|
||||
onProductDeleted: _handleProductDelete,
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: double.infinity,
|
||||
maxHeight: context.screenHeight * 0.6,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: AssignTagsTable(
|
||||
productAllocations: allProductAllocations,
|
||||
subspaces: _space.subspaces,
|
||||
productLocations: productLocations,
|
||||
onTagSelected: _handleTagChange,
|
||||
onLocationSelected: _handleLocationChange,
|
||||
onProductDeleted: _handleProductDelete,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasErrors)
|
||||
|
Reference in New Issue
Block a user