forked from eatvio/eatvio_app
feat(recipe): drafts
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -20,4 +20,6 @@ env/.env
|
||||
.env
|
||||
android/key.properties
|
||||
app/upload-keystore.jks
|
||||
coverage/
|
||||
coverage/
|
||||
.fvm/
|
||||
.fvmrc
|
||||
File diff suppressed because one or more lines are too long
@@ -186,7 +186,22 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
|
||||
}
|
||||
|
||||
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<File> downloadImageAsFile(String imageUrl) async {
|
||||
|
||||
@@ -24,40 +24,42 @@ class RecipeRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<FormData> _buildFormData(RecipeModel recipe, {String state = 'active'}) async {
|
||||
Future<List> _buildStepsMap(List<Step?>? 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<FormData> _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<Map<String, dynamic>>()
|
||||
.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<FormData> _buildDraftFormData(RecipeModel recipe) async {
|
||||
final ingredientsMap = recipe.ingredients
|
||||
?.map((i) => i?.toJson())
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
final stepsMap = await _buildStepsMap(recipe.steps);
|
||||
|
||||
final formMap = <String, dynamic>{
|
||||
'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<void> 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<void> 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);
|
||||
|
||||
@@ -56,4 +56,10 @@ abstract class CreateRecipeService {
|
||||
Future<void> draftRecipe({
|
||||
@Path() required int id,
|
||||
});
|
||||
|
||||
@POST('/api/recipes/create/drafted')
|
||||
@MultiPart()
|
||||
Future<HttpResponse<dynamic>> createDraftedRecipe({
|
||||
@Body() required FormData formData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user