feat(my_diets): merge conflict fixes

This commit is contained in:
misha
2026-02-11 16:23:48 +03:00
parent 042cc0ea20
commit 5f30aaeb2a
15 changed files with 173 additions and 139 deletions

View File

@@ -180,41 +180,67 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
CustomOutlinedButton(
onPressed: () async {
if (widget.diet != null && widget.isEditing == true) {
if (state.dietState == DietState.draft) {
context.read<CreateDietCubit>().setDietState(DietState.active);
if (state.isDietBeingEdited)
const Loader()
else
CustomOutlinedButton(
onPressed: () async {
if (state.isDietPublishing) return;
if (widget.diet != null && widget.isEditing == true) {
final dietState = state.dietState;
if (dietState == DietState.draft) {
context.read<CreateDietCubit>().setDietState(DietState.active);
} else {
context.read<CreateDietCubit>().setDietState(DietState.draft);
}
final result = await context.read<CreateDietCubit>().updateDiet(widget.diet!.id);
if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');
return;
}
if (dietState == DietState.draft) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета'),
);
});
} else {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
showBottomSheetWithBlur(
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true),
);
});
}
} else {
context.read<CreateDietCubit>().setDietState(DietState.draft);
if (state.dietState != DietState.draft) {
context.read<CreateDietCubit>().setDietState(DietState.draft);
}
final result = await context.read<CreateDietCubit>().createDiet();
if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_publish_diet} ${state.dietData.name}');
}
}
final result = await context.read<CreateDietCubit>().updateDiet(widget.diet!.id);
if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');
}
} else {
if (state.dietState != DietState.draft) {
context.read<CreateDietCubit>().setDietState(DietState.draft);
}
final result = await context.read<CreateDietCubit>().createDiet();
if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_publish_diet} ${state.dietData.name}');
}
}
},
text: state.dietState == DietState.draft && widget.isEditing == true ? s.publish : "В черновик",
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder),
},
text: state.dietState == DietState.draft && widget.isEditing == true ? s.publish : "В черновик",
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder),
),
),
),
const SizedBox(height: 12),
CustomOutlinedButton(
onPressed: state.isDietPublishing
onPressed: state.isDietPublishing || state.isDietBeingEdited
? () {}
: () async {
final diet = await context.read<CreateDietCubit>().getDietForPreview();
@@ -238,6 +264,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
else
CustomOutlinedButton(
onPressed: () async {
if (state.isDietBeingEdited) return;
if (widget.diet != null && widget.isEditing == true) {
final result = await context.read<CreateDietCubit>().updateDiet(widget.diet!.id);
if (result == 'error') {

View File

@@ -66,8 +66,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
imageUrl: appConfig.buildType == AppBuildType.tests ? null : imageUrl,
),
);
emit(state.copyWith(dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration)));
emit(state.copyWith(isLoading: true));
emit(state.copyWith(isLoading: false, isDietBeingEdited: false, dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration)));
}
void setDetails(String text) {
@@ -356,9 +355,12 @@ class CreateDietCubit extends Cubit<CreateDietState> {
}
Future<String?> updateDiet(int dietId) async {
emit(state.copyWith(isDietBeingEdited: true));
if ([DietState.active, DietState.personal].contains(state.dietState) && !_isFromValid()) {
emit(state.copyWith(isDietBeingEdited: false));
return 'error';
} else if (!_isFormValidForDraft()) {
emit(state.copyWith(isDietBeingEdited: false));
return 'error';
}
@@ -380,10 +382,12 @@ class CreateDietCubit extends Cubit<CreateDietState> {
details: state.dietData.details?.isEmpty ?? true ? null : state.dietData.details,
dailySchedule: state.dietData.dailySchedule,
);
emit(state.copyWith(isDietBeingEdited: false));
return null;
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
log(e.toString(), stackTrace: st);
emit(state.copyWith(isDietBeingEdited: false));
return 'error';
}
}

View File

@@ -27,12 +27,11 @@ abstract class CreateDietState with _$CreateDietState {
}) = _CreateDietState;
factory CreateDietState.initial() => CreateDietState(
dailyScheduleErrors: List.filled(21, ''),
isDietPublishing: false,
isLoading: false,
errorMessage: null,
dietData: CreateDietModel(
type: Map.fromIterable(dietTypeValues, value: (element) => false),
),
);
dailyScheduleErrors: List.filled(21, ''),
isDietPublishing: false,
isLoading: false,
errorMessage: null,
isDietBeingEdited: false,
dietData: CreateDietModel(type: Map.fromIterable(dietTypeValues, value: (element) => false)),
);
}

View File

@@ -106,7 +106,7 @@ class CreateDietRepository {
}
}
Future<void> updateDiet({
Future<int> updateDiet({
required int dietId,
required DietState state,
String? name,
@@ -164,12 +164,17 @@ class CreateDietRepository {
try {
switch (state) {
case DietState.active:
return await createDietService.updatePublished(diet: dietId, formData: formData);
final response = await createDietService.updatePublished(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
case DietState.draft:
return await createDietService.updateDrafted(diet: dietId, formData: formData);
final response = await createDietService.updateDrafted(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
case DietState.personal:
return await createDietService.updatePersonal(diet: dietId, formData: formData);
final response = await createDietService.updatePersonal(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
}
} on DioException catch (e) {
return e.response?.statusCode ?? 0;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;

View File

@@ -28,21 +28,21 @@ abstract class CreateDietService {
@POST('/api/diets/update/published/{diet}')
@MultiPart()
Future<void> updatePublished({
Future<HttpResponse> updatePublished({
@Path() required int diet,
@Body() required FormData formData,
});
@POST('/api/diets/update/drafted/{diet}')
@MultiPart()
Future<void> updateDrafted({
Future<HttpResponse> updateDrafted({
@Path() required int diet,
@Body() required FormData formData,
});
@POST('/api/diets/update/personalized/{diet}')
@MultiPart()
Future<void> updatePersonal({
Future<HttpResponse> updatePersonal({
@Path() required int diet,
@Body() required FormData formData,
});

View File

@@ -291,16 +291,16 @@ class MyDietsCubit extends Cubit<MyDietsState> {
}
}
Future<bool> publishDiet(int dietId) async {
Future<int> publishDiet(int dietId) async {
try {
await createDietRepository.updateDiet(
final statusCode = await createDietRepository.updateDiet(
dietId: dietId,
state: DietState.active,
);
return true;
return statusCode;
} catch (e, st) {
log(e.toString(), stackTrace: st);
return false;
return 0;
}
}

View File

@@ -142,7 +142,8 @@ class MyDietsScreenContent extends StatelessWidget {
}
final diets = switch (state.selectedStateIndex) {
1 => state.personalDiets,
2 => state.personalDiets,
1 => state.draftDiets,
_ => state.activeDiets,
};

View File

@@ -36,8 +36,11 @@ class MyDietWidget extends StatefulWidget {
}
class _MyDietWidgetState extends State<MyDietWidget> {
void _editDiet(Diet diet) {
GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: {'diet': diet, 'isEditing': true});
Future<void> _editDiet(Diet diet, BuildContext context) async{
await GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: {'diet': diet, 'isEditing': true});
if (!context.mounted) return;
await context.read<MyDietsCubit>().loadAllDiets();
}
void _suggestDiet(Diet diet) {
@@ -49,16 +52,19 @@ class _MyDietWidgetState extends State<MyDietWidget> {
}
Future<void> _publishDiet(Diet diet, BuildContext context) async {
final result = await context.read<MyDietsCubit>().publishDiet(widget.diet.id);
if (result) {
if (!context.mounted) return;
showBottomSheetWithBlur(
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: widget.diet.name),
);
await context.read<MyDietsCubit>().loadAllDiets();
} else {
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
final statusCode = await context.read<MyDietsCubit>().publishDiet(widget.diet.id);
switch (statusCode) {
case 201:
if (!context.mounted) return;
showBottomSheetWithBlur(
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: widget.diet.name),
);
await context.read<MyDietsCubit>().loadAllDiets();
case 422:
showErrorToast('Пожалуйста, заполните все обязательные поля для публикации');
default:
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
}
}
@@ -169,35 +175,8 @@ class _MyDietWidgetState extends State<MyDietWidget> {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
// Верхний ряд: Удалить + Редактировать (или + Поделиться для опубликованных)
Row(
children: [
// Кнопка удаления (слева)
CustomOutlinedButton(
onPressed: () async {
final result = await context.read<MyDietsCubit>().deleteDiet(widget.diet.id);
if (result) {
if (!context.mounted) return;
showBottomSheetWithBlur(
context: context,
child: DietDeletedBottomSheet(title: widget.diet.name),
);
await context.read<MyDietsCubit>().loadAllDiets();
}
},
height: 52,
width: 52,
leftIcon: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 20),
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
side: const WidgetStatePropertyAll(BorderSide.none),
),
text: '',
),
const SizedBox(width: 6),
// Кнопка редактирования (центр)
Expanded(
child: CustomOutlinedButton(
onPressed: () async {
@@ -206,20 +185,23 @@ class _MyDietWidgetState extends State<MyDietWidget> {
} else if (state.selectedStateIndex == 2) {
_suggestDiet(widget.diet);
} else {
_editDiet(widget.diet);
}
_editDiet(widget.diet, context);
}
},
text: switch (state.selectedStateIndex) {
1 => s.publish,
2 => s.toOffer,
_ => s.edit,
},
text: s.edit,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
// Кнопка поделиться (справа) для опубликованных
if (state.selectedStateIndex == 0) ...[
const SizedBox(width: 6),
CustomOutlinedButton(
@@ -315,7 +297,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
if (state.selectedStateIndex == 0) {
_unpublishDiet(widget.diet, context);
} else {
_editDiet(widget.diet);
_editDiet(widget.diet, context);
}
},
text: switch (state.selectedStateIndex) {

View File

@@ -21,6 +21,7 @@ class _MyDietsTabState extends State<MyDietsTab> {
final s = S.of(context);
if (tabTitles.isEmpty) {
tabTitles.add(s.published);
tabTitles.add(s.drafted);
tabTitles.add(s.personal);
}
super.didChangeDependencies();
@@ -30,29 +31,37 @@ class _MyDietsTabState extends State<MyDietsTab> {
Widget build(BuildContext context) {
return BlocBuilder<MyDietsCubit, MyDietsState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Container(
height: 48,
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(100),
),
padding: const EdgeInsets.all(4),
return Align(
alignment: Alignment.centerLeft,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
...List.generate(
2,
(index) => Expanded(
child: AnimatedOptionButton(
textStyle: TextStyles.textSMedium,
isSelected: state.selectedStateIndex == index,
text: tabTitles[index],
onTap: () {
widget.additionalOnTap?.call();
context.read<MyDietsCubit>().changeSelectedStateIndex(index);
},
),
Container(
width: 484,
height: 48,
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(100),
),
padding: const EdgeInsets.all(4),
child: Row(
children: [
...List.generate(
3,
(index) => Expanded(
child: AnimatedOptionButton(
textStyle: TextStyles.textSMedium,
isSelected: state.selectedStateIndex == index,
text: tabTitles[index],
onTap: () {
widget.additionalOnTap?.call();
context.read<MyDietsCubit>().changeSelectedStateIndex(index);
},
),
),
),
],
),
),
],

View File

@@ -326,7 +326,7 @@ void main() {
expect(find.byType(DietTypeChoiceWidget), findsOneWidget);
expect(find.byType(DietImagePickerWidget), findsOneWidget);
// Action buttons
expect(find.byType(CustomOutlinedButton), findsNWidgets(2));
expect(find.byType(CustomOutlinedButton), findsNWidgets(3));
});
});
}

View File

@@ -432,7 +432,7 @@ void main() {
await cubit.setInitialData(testDiet);
expect(cubit.dietId, 1);
expect(cubit.state.isDietBeingEdited, true);
expect(cubit.state.isDietBeingEdited, false);
expect(cubit.state.dietData.name, 'Test Diet');
expect(cubit.state.dietData.details, 'Test details');
expect(cubit.state.dietData.duration, 7);

View File

@@ -6,10 +6,16 @@ import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:retrofit/dio.dart';
import 'package:retrofit/retrofit.dart';
import 'create_diet_repository_test.mocks.dart';
HttpResponse<dynamic> _okResponse() => HttpResponse(
null,
Response(requestOptions: RequestOptions(path: ''), statusCode: 200),
);
@GenerateNiceMocks([MockSpec<CreateDietService>()])
void main() {
group('CreateDietRepository', () {
@@ -152,7 +158,7 @@ void main() {
final testFile = File('${Directory.systemTemp.path}/test_file.jpg');
await testFile.writeAsString('test content');
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
await repository.updateDiet(dietId: dietId, state: DietState.active, name: 'Updated Diet', duration: 14, details: 'Updated details', image: testFile);
@@ -167,7 +173,7 @@ void main() {
test('updateDiet - обновляет черновик диеты (draft)', () async {
const dietId = 1;
when(mockService.updateDrafted(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
when(mockService.updateDrafted(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
await repository.updateDiet(dietId: dietId, state: DietState.draft, name: 'Updated Diet');
@@ -177,7 +183,7 @@ void main() {
test('updateDiet - обновляет персональную диету (personal)', () async {
const dietId = 1;
when(mockService.updatePersonal(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
when(mockService.updatePersonal(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
await repository.updateDiet(dietId: dietId, state: DietState.personal, name: 'Updated Diet');
@@ -242,7 +248,7 @@ void main() {
],
];
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
await repository.updateDiet(
dietId: dietId,
@@ -257,7 +263,7 @@ void main() {
test('updateDiet - обновляет диету без image', () async {
const dietId = 1;
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
await repository.updateDiet(
dietId: dietId,
@@ -273,7 +279,7 @@ void main() {
test('updateDiet - обновляет диету только с типами', () async {
const dietId = 1;
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
await repository.updateDiet(
dietId: dietId,
@@ -305,7 +311,7 @@ void main() {
],
];
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
await repository.updateDiet(
dietId: dietId,

View File

@@ -269,11 +269,11 @@ void main() {
when(mockCreateDietRepository.updateDiet(
dietId: 1,
state: DietState.active,
)).thenAnswer((_) async => {});
)).thenAnswer((_) async => 200);
final result = await myDietsCubit!.publishDiet(1);
expect(result, true);
expect(result, 200);
verify(mockCreateDietRepository.updateDiet(dietId: 1, state: DietState.active)).called(1);
});
@@ -290,7 +290,7 @@ void main() {
final result = await myDietsCubit!.publishDiet(1);
expect(result, false);
expect(result, 0);
});
test('unpublishDiet - снимает диету с публикации', () async {
@@ -302,7 +302,7 @@ void main() {
when(mockCreateDietRepository.updateDiet(
dietId: 1,
state: DietState.draft,
)).thenAnswer((_) async => {});
)).thenAnswer((_) async => 200);
final result = await myDietsCubit!.unpublishDiet(1);

View File

@@ -106,7 +106,7 @@ void main() {
// Assert
expect(find.byType(MyDietsTab), findsOneWidget);
expect(find.byType(AnimatedOptionButton), findsNWidgets(2));
expect(find.byType(AnimatedOptionButton), findsNWidgets(3));
});
testWidgets('should handle tab selection in MyDietsTab', (WidgetTester tester) async {
@@ -119,7 +119,7 @@ void main() {
// Act - Find and tap the second tab
final tabButtons = find.byType(AnimatedOptionButton);
expect(tabButtons, findsNWidgets(2));
expect(tabButtons, findsNWidgets(3));
// Tap the second tab (index 1)
await tester.tap(tabButtons.at(1));
@@ -221,7 +221,7 @@ void main() {
// Assert
expect(find.byType(MyDietsTab), findsOneWidget);
expect(find.byType(AnimatedOptionButton), findsNWidgets(2));
expect(find.byType(AnimatedOptionButton), findsNWidgets(3));
});
testWidgets('should handle MyDietsTab additionalOnTap callback', (WidgetTester tester) async {

View File

@@ -104,7 +104,7 @@ class FixtureCreateDietRepository extends CreateDietRepository {
}
@override
Future<void> updateDiet({
Future<int> updateDiet({
required int dietId,
required DietState state,
String? name,
@@ -131,6 +131,7 @@ class FixtureCreateDietRepository extends CreateDietRepository {
if (lightType != null) 'light': lightType == '1',
},
});
return 201;
}
}
@@ -166,17 +167,17 @@ class _NoopCreateDietService implements CreateDietService {
}
@override
Future<void> updateDrafted({required int diet, required FormData formData}) {
Future<HttpResponse> updateDrafted({required int diet, required FormData formData}) {
throw UnimplementedError();
}
@override
Future<void> updatePersonal({required int diet, required FormData formData}) {
Future<HttpResponse> updatePersonal({required int diet, required FormData formData}) {
throw UnimplementedError();
}
@override
Future<void> updatePublished({required int diet, required FormData formData}) {
Future<HttpResponse> updatePublished({required int diet, required FormData formData}) {
throw UnimplementedError();
}
}