From 97536445703afa50feaa0ef4991b5c8c59472de8 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 18 Feb 2026 18:04:01 +0300 Subject: [PATCH] feat(recipe): drafts --- .gitignore | 4 +- .../reports/problems/problems-report.html | 2 +- .../create_recipe_cubit.dart | 17 +++- .../reporitories/recipe_repository.dart | 98 ++++++++++++------- .../services/recipe_service.dart | 6 ++ .../create_recipe_cubit_test.dart | 39 ++++++++ 6 files changed, 127 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 530ea713..1cd6d5b0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,6 @@ env/.env .env android/key.properties app/upload-keystore.jks -coverage/ \ No newline at end of file +coverage/ +.fvm/ +.fvmrc \ No newline at end of file diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index 746fadde..379f47c9 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart index 482d7d56..e7537af7 100644 --- a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart +++ b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart @@ -186,7 +186,22 @@ class CreateRecipeCubit extends Cubit { } bool validateForDraft() { - return validateAll(); + final isTitleValid = validateTitle(); + _clearNonTitleValidationErrors(); + return isTitleValid; + } + + void _clearNonTitleValidationErrors() { + emit(state.copyWith( + descriptionValidationError: null, + cookingTimeValidationError: null, + activeTimeValidationError: null, + servingsValidationError: null, + energyPerServingValidationError: null, + imageValidationError: null, + stepsValidationError: null, + ingredientsValidationError: null, + )); } Future downloadImageAsFile(String imageUrl) async { diff --git a/lib/features/create_recipe/reporitories/recipe_repository.dart b/lib/features/create_recipe/reporitories/recipe_repository.dart index f079d810..34d2599f 100644 --- a/lib/features/create_recipe/reporitories/recipe_repository.dart +++ b/lib/features/create_recipe/reporitories/recipe_repository.dart @@ -24,40 +24,42 @@ class RecipeRepository { } } - Future _buildFormData(RecipeModel recipe, {String state = 'active'}) async { + Future _buildStepsMap(List? steps) async { + final stepsMap = []; + for (final Step? step in steps ?? []) { + if (step != null) { + if ((step.image == null || (step.image?.isEmpty ?? true)) && step.description != null) { + stepsMap.add({'description_md': step.description ?? ''}); + } else if (step.image != null && (step.image?.isNotEmpty ?? false)) { + final compressed = await ImageCompressUtility.compressFile(File(step.image ?? '')); + final multipartFile = await MultipartFile.fromFile(compressed.path); + stepsMap.add({ + 'description_md': step.description ?? '', + 'image': multipartFile, + }); + } + } + } + return stepsMap; + } + + Future _buildPublishFormData(RecipeModel recipe) async { if (recipe.image == null || (recipe.image?.isEmpty ?? true)) { throw Exception('Recipe image is required'); } final image = await ImageCompressUtility.compressFile(File(recipe.image ?? '')); final multiPartFile = await MultipartFile.fromFile(image.path); - final ingredientsMap = recipe.ingredients?.map((ingredient) { - if (ingredient != null) { - return ingredient.toJson(); - } - }).toList() ?? + final ingredientsMap = recipe.ingredients + ?.map((i) => i?.toJson()) + .whereType>() + .toList() ?? []; - final stepsMap = []; - for (final Step? step in recipe.steps ?? []) { - if (step != null) { - if ((step.image == null || (step.image?.isEmpty ?? true)) && step.description != null) { - stepsMap.add({ - 'description_md': step.description ?? "", - }); - } else if (step.image != null && (step.image?.isNotEmpty ?? false)) { - final image = await ImageCompressUtility.compressFile(File(step.image ?? '')); - final multipartFile = await MultipartFile.fromFile(image.path); - stepsMap.add({ - 'description_md': step.description ?? "", - 'image': multipartFile, - }); - } - } - } + final stepsMap = await _buildStepsMap(recipe.steps); + final formMap = { 'type': recipe.type, - 'state': state, 'title': recipe.title, 'tags[]': recipe.tags ?? [], 'description_md': recipe.description, @@ -74,9 +76,41 @@ class RecipeRepository { return FormData.fromMap(formMap); } + Future _buildDraftFormData(RecipeModel recipe) async { + final ingredientsMap = recipe.ingredients + ?.map((i) => i?.toJson()) + .whereType>() + .toList() ?? + []; + + final stepsMap = await _buildStepsMap(recipe.steps); + + final formMap = { + 'type': recipe.type, + 'title': recipe.title, + 'tags[]': recipe.tags ?? [], + 'description_md': recipe.description, + 'video_url': recipe.videoUrl, + 'ingredients': ingredientsMap, + 'steps': stepsMap, + 'cooking_time': recipe.totalCookingTime, + 'active_time': recipe.activeCookTime, + 'servings': recipe.servings, + 'energy_per_serving': recipe.energyPerServing, + }; + + if (recipe.image != null && (recipe.image?.isNotEmpty ?? false)) { + final compressed = await ImageCompressUtility.compressFile(File(recipe.image ?? '')); + formMap['image'] = await MultipartFile.fromFile(compressed.path); + } + + formMap.removeWhere((k, v) => v == null || (v is String && v.isEmpty)); + return FormData.fromMap(formMap); + } + Future publishRecipe(RecipeModel recipe, [bool update = false, String? slug]) async { try { - final formData = await _buildFormData(recipe, state: 'active'); + final formData = await _buildPublishFormData(recipe); if (update) { await recipeService.updateRecipe(slug: slug!, formData: formData); } else { @@ -90,20 +124,12 @@ class RecipeRepository { Future saveAsDraft(RecipeModel recipe, [bool update = false, String? slug]) async { try { - final formData = await _buildFormData(recipe, state: 'archived'); + final formData = await _buildDraftFormData(recipe); log(formData.fields.toString()); if (update) { - final response = await recipeService.updateRecipe(slug: slug!, formData: formData); - final recipeId = response.data['data']?['id'] ?? response.data['id']; - if (recipeId != null) { - await recipeService.draftRecipe(id: recipeId as int); - } + await recipeService.updateRecipe(slug: slug!, formData: formData); } else { - final response = await recipeService.publishRecipe(formData: formData); - final recipeId = response.data['data']?['id'] ?? response.data['id']; - if (recipeId != null) { - await recipeService.draftRecipe(id: recipeId as int); - } + await recipeService.createDraftedRecipe(formData: formData); } } catch (e, s) { await Sentry.captureException(e, stackTrace: s); diff --git a/lib/features/create_recipe/services/recipe_service.dart b/lib/features/create_recipe/services/recipe_service.dart index 2341d1d2..4bed540a 100644 --- a/lib/features/create_recipe/services/recipe_service.dart +++ b/lib/features/create_recipe/services/recipe_service.dart @@ -56,4 +56,10 @@ abstract class CreateRecipeService { Future draftRecipe({ @Path() required int id, }); + + @POST('/api/recipes/create/drafted') + @MultiPart() + Future> createDraftedRecipe({ + @Body() required FormData formData, + }); } diff --git a/test/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit_test.dart b/test/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit_test.dart index 2fb3fb7b..eb4bd3b8 100644 --- a/test/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit_test.dart +++ b/test/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit_test.dart @@ -438,5 +438,44 @@ void main() { expect(cubit.validateAll(), true); }); + + // validateForDraft tests + test('validateForDraft - возвращает false когда title не задан', () { + expect(cubit.validateForDraft(), false); + expect(cubit.state.titleValidationError, 'Поле не заполнено'); + }); + + test('validateForDraft - возвращает false когда title слишком короткий', () { + cubit.replaceTitle('AB'); + expect(cubit.validateForDraft(), false); + expect(cubit.state.titleValidationError, 'Минимальная длина 3 символа '); + }); + + test('validateForDraft - возвращает true когда задан только title', () { + cubit.replaceTitle('Мой черновик'); + expect(cubit.validateForDraft(), true); + expect(cubit.state.titleValidationError, isNull); + }); + + test('validateForDraft - сбрасывает ошибки остальных полей', () { + // Сначала вызываем validateAll, чтобы появились ошибки + cubit.validateAll(); + expect(cubit.state.descriptionValidationError, isNotNull); + expect(cubit.state.imageValidationError, isNotNull); + + // Теперь задаём title и вызываем validateForDraft + cubit.replaceTitle('Мой черновик'); + final result = cubit.validateForDraft(); + + expect(result, true); + expect(cubit.state.descriptionValidationError, isNull); + expect(cubit.state.cookingTimeValidationError, isNull); + expect(cubit.state.activeTimeValidationError, isNull); + expect(cubit.state.servingsValidationError, isNull); + expect(cubit.state.energyPerServingValidationError, isNull); + expect(cubit.state.imageValidationError, isNull); + expect(cubit.state.stepsValidationError, isNull); + expect(cubit.state.ingredientsValidationError, isNull); + }); }); }