Add SpaceDetails dialog and related widgets for creating and editing spaces, including SpaceDetailsDevicesBox and SpaceSubSpacesBox for managing devices and subspaces.

This commit is contained in:
Faris Armoush
2025-07-02 15:03:23 +03:00
parent 68b6c9b18c
commit 63353af38b
9 changed files with 614 additions and 12 deletions

View File

@ -1,11 +1,37 @@
import 'package:flutter/material.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/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
abstract final class SpaceDetailsDialogHelper {
static void showCreate(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => const SpaceDetailsDialog(),
builder: (_) => SpaceDetailsDialog(
title: const Text('Create Space'),
space: SpaceDetailsModel.empty(),
onSave: (space) {},
),
);
}
static void showEdit(
BuildContext context, {
required SpaceModel spaceModel,
}) {
showDialog<void>(
context: context,
builder: (_) => SpaceDetailsDialog(
title: const Text('Edit Space'),
space: SpaceDetailsModel(
uuid: spaceModel.uuid,
spaceName: spaceModel.spaceName,
icon: spaceModel.icon,
productAllocations: const [],
subspaces: const [],
),
onSave: (space) {},
),
);
}
}

View File

@ -33,14 +33,12 @@ class SpaceDetailsActionButtons extends StatelessWidget {
}
Widget _buildSaveButton() {
return Expanded(
child: DefaultButton(
return DefaultButton(
onPressed: onSave,
borderRadius: 10,
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
),
);
}
}

View File

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/common/edit_chip.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceDetailsDevicesBox extends StatelessWidget {
const SpaceDetailsDevicesBox({super.key, required this.space});
final SpaceDetailsModel space;
@override
Widget build(BuildContext context) {
return Column(
children: [
if (space.productAllocations.isNotEmpty ||
space.subspaces
.any((subspace) => subspace.productAllocations.isNotEmpty))
SizedBox(
width: context.screenWidth * 0.25,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0, // Border width
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
// Combine tags from spaceModel and subspaces
// ...TagHelper.groupTags([
// ...?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',
// fit: BoxFit.contain,
// ),
// ),
// label: Text(
// 'x${entry.value}', // Show count
// style: Theme.of(context)
// .textTheme
// .bodySmall
// ?.copyWith(color: ColorsManager.spaceColor),
// ),
// backgroundColor: ColorsManager.whiteColors,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// side: const BorderSide(
// color: ColorsManager.spaceColor,
// ),
// ),
// ),
// ),
EditChip(
onTap: () {},
),
],
),
),
)
else
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
child: const ButtonContentWidget(
svgAssets: Assets.addIcon,
label: 'Add Devices',
// disabled: isTagsAndSubspaceModelDisabled,
),
)
],
);
}
}

View File

@ -1,12 +1,78 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'
show SpaceDetailsModel;
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceDetailsDialog extends StatelessWidget {
const SpaceDetailsDialog({super.key});
const SpaceDetailsDialog({
required this.title,
required this.space,
required this.onSave,
super.key,
});
final Widget title;
final SpaceDetailsModel space;
final void Function(SpaceDetailsModel space) onSave;
@override
Widget build(BuildContext context) {
return const Dialog(
child: Text('Create Space'),
return AlertDialog(
title: title,
backgroundColor: ColorsManager.whiteColors,
content: SizedBox(
height: context.screenHeight * 0.25,
child: Row(
spacing: 20,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 1,
child: SpaceIconPicker(
iconPath: space.icon,
),
),
Expanded(
flex: 2,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Text(context.watch<SpaceDetailsModelBloc>().state.toString()),
SpaceNameTextField(
initialValue: space.spaceName,
isNameFieldExist: (value) {
final subspaces = space.subspaces;
if (subspaces.isEmpty) return false;
return subspaces.any(
(subspace) => subspace.name == value,
);
},
),
const Spacer(),
SpaceSubSpacesBox(
subspaces: space.subspaces,
),
const SizedBox(height: 16),
SpaceDetailsDevicesBox(space: space),
],
),
),
],
),
),
actions: [
SpaceDetailsActionButtons(
onSave: () => onSave(space),
onCancel: Navigator.of(context).pop,
),
],
);
}
}

View File

@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceNameTextField extends StatefulWidget {
const SpaceNameTextField({
required this.initialValue,
required this.isNameFieldExist,
super.key,
});
final String? initialValue;
final bool Function(String value) isNameFieldExist;
@override
State<SpaceNameTextField> createState() => _SpaceNameTextFieldState();
}
class _SpaceNameTextFieldState extends State<SpaceNameTextField> {
late final TextEditingController _controller;
@override
void initState() {
_controller = TextEditingController(text: widget.initialValue);
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
final _formKey = GlobalKey<FormState>();
String? _validateName(String? value) {
if (value == null || value.isEmpty) {
return '*Space name should not be empty.';
}
if (widget.isNameFieldExist(value)) {
return '*Name already exists';
}
return null;
}
@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: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
hintText: 'Please enter the name',
hintStyle: context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.lightGrayColor,
),
filled: true,
fillColor: ColorsManager.boxColor,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(width: 1.5),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: ColorsManager.boxColor,
),
),
errorStyle: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.red,
),
),
),
);
}
}

View File

@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/common/edit_chip.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceSubSpacesBox extends StatelessWidget {
const SpaceSubSpacesBox({super.key, required this.subspaces});
final List<Subspace> subspaces;
@override
Widget build(BuildContext context) {
return Column(
children: [
if (subspaces.isEmpty)
TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
overlayColor: ColorsManager.transparentColor,
),
onPressed: () => showDialog<void>(
context: context,
builder: (_) => SpaceSubSpacesDialog(subspaces: subspaces),
),
child: const ButtonContentWidget(
svgAssets: Assets.addIcon,
label: 'Create Sub Spaces',
// disabled: widget.isTagsAndSubspaceModelDisabled,
disabled: false,
),
)
else
SizedBox(
width: context.screenWidth * 0.25,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0,
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
...subspaces.map(
(e) => SubspaceNameDisplayWidget(subSpace: e),
),
EditChip(
onTap: () {},
),
],
),
),
),
],
);
}
}

View File

@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceSubSpacesDialog extends StatelessWidget {
const SpaceSubSpacesDialog({
required this.subspaces,
super.key,
});
final List<Subspace> subspaces;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Create Sub Spaces'),
content: TextFieldSubSpaceDialogWidget(
subSpaces: subspaces,
),
actions: [
SpaceDetailsActionButtons(
onSave: () {},
onCancel: Navigator.of(context).pop,
)
],
);
}
}
class TextFieldSubSpaceDialogWidget extends StatefulWidget {
const TextFieldSubSpaceDialogWidget({
super.key,
required this.subSpaces,
});
final List<Subspace> subSpaces;
@override
State<TextFieldSubSpaceDialogWidget> createState() => _TextFieldSubSpaceDialogWidgetState();
}
class _TextFieldSubSpaceDialogWidgetState extends State<TextFieldSubSpaceDialogWidget> {
late final TextEditingController _subspaceNameController;
@override
void initState() {
super.initState();
_subspaceNameController = TextEditingController();
}
@override
void dispose() {
_subspaceNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: context.screenWidth * 0.35,
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(10),
),
child: Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
...widget.subSpaces.asMap().entries.map(
(entry) {
final index = entry.key;
final subSpace = entry.value;
final lowerName = subSpace.name.toLowerCase();
final duplicateIndices = widget.subSpaces
.asMap()
.entries
.where((e) => e.value.name.toLowerCase() == lowerName)
.map((e) => e.key)
.toList();
final isDuplicate = duplicateIndices.length > 1 &&
duplicateIndices.indexOf(index) != 0;
return SubspaceChip(
subSpace: subSpace,
isDuplicate: isDuplicate,
onDeleted: () => context.read<SpaceDetailsModelBloc>().add(
UpdateSpaceDetailsSubspaces(
widget.subSpaces.where((e) => e.uuid != subSpace.uuid).toList(),
),
),
);
},
),
SizedBox(
width: 200,
child: TextField(
controller: _subspaceNameController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null,
hintStyle: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGrayColor,
),
),
onSubmitted: (value) {
final trimmedValue = value.trim();
if (trimmedValue.isNotEmpty) {
context.read<SpaceDetailsModelBloc>().add(
UpdateSpaceDetailsSubspaces(
[
...widget.subSpaces,
Subspace(
name: trimmedValue,
uuid: '',
productAllocations: const [],
),
],
),
);
_subspaceNameController.clear();
}
},
style: context.textTheme.bodyMedium,
),
),
],
),
);
}
}

View File

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SubspaceChip extends StatelessWidget {
const SubspaceChip({
required this.subSpace,
required this.isDuplicate,
required this.onDeleted,
super.key,
});
final Subspace subSpace;
final bool isDuplicate;
final void Function() onDeleted;
@override
Widget build(BuildContext context) {
return Chip(
label: Text(
subSpace.name,
style: context.textTheme.bodySmall?.copyWith(
color: isDuplicate ? ColorsManager.red : ColorsManager.spaceColor,
),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(
color: isDuplicate ? ColorsManager.red : ColorsManager.transparentColor,
width: 0,
),
),
deleteIcon: Container(
padding: const EdgeInsetsDirectional.all(1),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.5,
),
),
child: const FittedBox(
fit: BoxFit.scaleDown,
child: Icon(
Icons.close,
color: ColorsManager.lightGrayColor,
),
),
),
onDeleted: onDeleted,
);
}
}

View File

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SubspaceNameDisplayWidget extends StatefulWidget {
const SubspaceNameDisplayWidget({super.key, required this.subSpace});
final Subspace subSpace;
@override
State<SubspaceNameDisplayWidget> createState() =>
_SubspaceNameDisplayWidgetState();
}
class _SubspaceNameDisplayWidgetState extends State<SubspaceNameDisplayWidget> {
late final TextEditingController _controller;
bool isEditing = false;
@override
void initState() {
_controller = TextEditingController(text: widget.subSpace.name);
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final textStyle = context.textTheme.bodySmall?.copyWith(
color: ColorsManager.spaceColor,
);
return InkWell(
onTap: () => setState(() => isEditing = true),
child: Visibility(
visible: isEditing,
replacement: Text(
widget.subSpace.name,
style: textStyle,
),
child: TextField(
controller: _controller,
style: textStyle,
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsetsDirectional.symmetric(
horizontal: 8,
),
),
onSubmitted: (value) {
final bloc = context.read<SpaceDetailsModelBloc>();
bloc.add(
UpdateSpaceDetailsSubspaces(
bloc.state.subspaces
.map(
(e) => e.uuid == widget.subSpace.uuid
? e.copyWith(name: value)
: e,
)
.toList(),
),
);
},
),
),
);
}
}