From 39db35b4e43daee4550029788f4654779dadd2fc Mon Sep 17 00:00:00 2001 From: Jonas Sander <29028262+Jonas-Sander@users.noreply.github.com> Date: Thu, 12 Oct 2023 14:16:18 +0200 Subject: [PATCH] Remove NewHomeworkDialog code. --- .../blocs/homework/homework_dialog_bloc.dart | 1 - .../homework/new_homework_dialog_bloc.dart | 411 --------- .../homework/shared/open_homework_dialog.dart | 3 +- .../homework_details/homework_details.dart | 1 - .../pages/homework/new_homework_dialog.dart | 809 ------------------ app/lib/pages/homework_page.dart | 3 +- .../new_homework_dialog_bloc_test.dart | 249 ------ .../lib/src/models/course.dart | 1 - 8 files changed, 2 insertions(+), 1476 deletions(-) delete mode 100644 app/lib/blocs/homework/new_homework_dialog_bloc.dart delete mode 100644 app/lib/pages/homework/new_homework_dialog.dart delete mode 100644 app/test/homework/new_homework_dialog_bloc_test.dart diff --git a/app/lib/blocs/homework/homework_dialog_bloc.dart b/app/lib/blocs/homework/homework_dialog_bloc.dart index 6f5d2793c..367ed950f 100644 --- a/app/lib/blocs/homework/homework_dialog_bloc.dart +++ b/app/lib/blocs/homework/homework_dialog_bloc.dart @@ -393,7 +393,6 @@ class HomeworkDialogApi { Future create(UserInput userInput) async { final localFiles = userInput.localFiles; - // TODO: Is cache used? final course = (await _api.course.streamCourse(userInput.courseId.id).first)!; final authorReference = _api.references.users.doc(_api.user.authUser!.uid); diff --git a/app/lib/blocs/homework/new_homework_dialog_bloc.dart b/app/lib/blocs/homework/new_homework_dialog_bloc.dart deleted file mode 100644 index 4321f2332..000000000 --- a/app/lib/blocs/homework/new_homework_dialog_bloc.dart +++ /dev/null @@ -1,411 +0,0 @@ -// Copyright (c) 2023 Sharezone UG (haftungsbeschränkt) -// Licensed under the EUPL-1.2-or-later. -// -// You may obtain a copy of the Licence at: -// https://joinup.ec.europa.eu/software/page/eupl -// -// SPDX-License-Identifier: EUPL-1.2 - -import 'package:bloc/bloc.dart'; -import 'package:bloc_presentation/bloc_presentation.dart'; -import 'package:common_domain_models/common_domain_models.dart'; -import 'package:date/date.dart'; -import 'package:equatable/equatable.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:files_basics/files_models.dart'; -import 'package:files_basics/local_file.dart'; -import 'package:filesharing_logic/filesharing_logic_models.dart'; -import 'package:firebase_hausaufgabenheft_logik/firebase_hausaufgabenheft_logik.dart'; -import 'package:group_domain_models/group_domain_models.dart'; -import 'package:meta/meta.dart'; -import 'package:sharezone/blocs/homework/homework_dialog_bloc.dart'; -import 'package:time/time.dart'; - -sealed class HomeworkDialogEvent extends Equatable { - const HomeworkDialogEvent(); -} - -class Submit extends HomeworkDialogEvent { - const Submit(); - - @override - List get props => []; -} - -class TitleChanged extends HomeworkDialogEvent { - final String newTitle; - - const TitleChanged(this.newTitle); - - @override - List get props => [newTitle]; -} - -class DueDateChanged extends HomeworkDialogEvent { - final Date newDueDate; - - const DueDateChanged(this.newDueDate); - - @override - List get props => [newDueDate]; -} - -class CourseChanged extends HomeworkDialogEvent { - final CourseId newCourseId; - - const CourseChanged(this.newCourseId); - - @override - List get props => [newCourseId]; -} - -class SubmissionsChanged extends HomeworkDialogEvent { - final ({bool enabled, Time? submissionTime}) newSubmissionsOptions; - - const SubmissionsChanged(this.newSubmissionsOptions); - - @override - List get props => [newSubmissionsOptions]; -} - -class DescriptionChanged extends HomeworkDialogEvent { - final String newDescription; - - const DescriptionChanged(this.newDescription); - - @override - List get props => [newDescription]; -} - -class AttachmentsAdded extends HomeworkDialogEvent { - final IList newFiles; - - const AttachmentsAdded(this.newFiles); - - @override - List get props => [newFiles]; -} - -class AttachmentRemoved extends HomeworkDialogEvent { - // TODO: Don't know if this is really what we need. - final LocalFile? localFile; - final CloudFile? cloudFile; - - const AttachmentRemoved({this.localFile, this.cloudFile}); - - @override - List get props => [localFile, cloudFile]; -} - -class NotifyCourseMembersChanged extends HomeworkDialogEvent { - final bool newNotifyCourseMembers; - - const NotifyCourseMembersChanged(this.newNotifyCourseMembers); - - @override - List get props => [newNotifyCourseMembers]; -} - -class IsPrivateChanged extends HomeworkDialogEvent { - final bool newIsPrivate; - - const IsPrivateChanged(this.newIsPrivate); - - @override - List get props => [newIsPrivate]; -} - -sealed class HomeworkDialogState extends Equatable { - final bool isEditing; - @override - List get props => [isEditing]; - - const HomeworkDialogState({required this.isEditing}); -} - -class Ready extends HomeworkDialogState { - // TODO: Add error states (title, course, Due date?) - final String title; - final CourseState course; - final DateTime? dueDate; - final SubmissionState submissions; - final String description; - final IList attachments; - final bool notifyCourseMembers; - final (bool, {bool isChangeable}) isPrivate; - final bool hasModifiedData; - - @override - List get props => [ - title, - course, - dueDate, - submissions, - description, - attachments, - notifyCourseMembers, - isPrivate, - hasModifiedData, - super.isEditing, - ]; - - const Ready({ - required this.title, - required this.course, - required this.dueDate, - required this.submissions, - required this.description, - required this.attachments, - required this.notifyCourseMembers, - required this.isPrivate, - required this.hasModifiedData, - required super.isEditing, - }); -} - -class SavedSucessfully extends HomeworkDialogState { - // TODO: Remove? - @override - List get props => [super.isEditing]; - - const SavedSucessfully({required super.isEditing}); -} - -class FileView extends Equatable { - final FileId fileId; - final String fileName; - final FileFormat format; - final LocalFile? localFile; - final CloudFile? cloudFile; - - @override - List get props => [fileId, fileName, format, localFile, cloudFile]; - - const FileView({ - required this.fileId, - required this.fileName, - required this.format, - this.localFile, - this.cloudFile, - }) : assert((localFile != null && cloudFile == null) || - (localFile == null && cloudFile != null)); -} - -sealed class SubmissionState extends Equatable { - bool get isChangeable; - bool get isEnabled => this is SubmissionsEnabled; - const SubmissionState(); -} - -class SubmissionsDisabled extends SubmissionState { - /// If the user can update the [SubmissionState], i.e. turn on submissions. - /// - /// If a homework is private submissions can not be turned on. This field - /// would be `true` in this case. - @override - final bool isChangeable; - @override - List get props => [isChangeable]; - - const SubmissionsDisabled({required this.isChangeable}); -} - -class SubmissionsEnabled extends SubmissionState { - @override - bool get isChangeable => true; - - final Time deadline; - @override - List get props => [isChangeable, deadline]; - - const SubmissionsEnabled({required this.deadline}); -} - -sealed class CourseState extends Equatable { - const CourseState(); -} - -class NoCourseChosen extends CourseState { - @override - List get props => []; - const NoCourseChosen(); -} - -class CourseChosen extends CourseState { - final CourseId courseId; - final String courseName; - - /// If the user can update the [CourseState].. - /// - /// If editing an existing homework this will be `false`, since one can't move - /// a homework from course a to course b. - final bool isChangeable; - - @override - List get props => [courseId, courseName, isChangeable]; - - const CourseChosen({ - required this.courseId, - required this.courseName, - required this.isChangeable, - }); -} - -class LoadingHomework extends HomeworkDialogState { - final HomeworkId homework; - @override - List get props => [homework, super.isEditing]; - - const LoadingHomework(this.homework, {required super.isEditing}); -} - -@visibleForTesting -final emptyCreateHomeworkDialogState = Ready( - title: '', - course: const NoCourseChosen(), - dueDate: null, - submissions: const SubmissionsDisabled(isChangeable: true), - description: '', - attachments: IList(), - notifyCourseMembers: false, - isPrivate: (false, isChangeable: true), - hasModifiedData: false, - isEditing: false, -); - -class _LoadedHomeworkData extends HomeworkDialogEvent { - @override - List get props => []; -} - -sealed class HomeworkDialogBlocPresentationEvent extends Equatable { - const HomeworkDialogBlocPresentationEvent(); -} - -// TODO: Maybe two different classes - one if trying to save with invalid data -// and one if saving failed due to some other error? -// TODO: test -class SavingFailed extends HomeworkDialogBlocPresentationEvent { - const SavingFailed(); - - @override - List get props => []; -} - -class NewHomeworkDialogBloc - extends Bloc - with - BlocPresentationMixin { - final HomeworkDialogApi api; - late HomeworkDto _initialHomework; - late List _initialAttachments; - - NewHomeworkDialogBloc({required this.api, HomeworkId? homeworkId}) - : super(homeworkId != null - ? LoadingHomework(homeworkId, isEditing: true) - : emptyCreateHomeworkDialogState) { - on<_LoadedHomeworkData>( - (event, emit) => emit(Ready( - title: _initialHomework.title, - course: CourseChosen( - courseId: CourseId(_initialHomework.courseID), - courseName: _initialHomework.courseName, - isChangeable: false, - ), - dueDate: _initialHomework.todoUntil, - submissions: _initialHomework.withSubmissions - ? SubmissionsEnabled(deadline: _initialHomework.todoUntil.toTime()) - : const SubmissionsDisabled(isChangeable: true), - description: _initialHomework.description, - attachments: IList([ - for (final attachment in _initialAttachments) - FileView( - fileId: FileId(attachment.id!), - fileName: attachment.name, - format: attachment.fileFormat, - cloudFile: attachment, - ) - ]), - notifyCourseMembers: false, - isPrivate: (_initialHomework.private, isChangeable: homeworkId == null), - hasModifiedData: false, - isEditing: true, - )), - ); - on( - (event, emit) async { - await api.create(UserInput( - 'S. 32 8a)', - CourseId('maths_course'), - DateTime(2023, 10, 12), - '', - false, - [], - false, - null, - false, - )); - - emit(SavedSucessfully(isEditing: false)); - }, - ); - on( - (event, emit) { - // TODO - }, - ); - on( - (event, emit) { - // TODO - }, - ); - on( - (event, emit) { - // TODO - }, - ); - on( - (event, emit) { - // TODO - }, - ); - on( - (event, emit) { - // TODO - }, - ); - on( - (event, emit) { - // TODO - }, - ); - on( - (event, emit) { - // TODO - }, - ); - on( - (event, emit) { - // TODO - }, - ); - on( - (event, emit) { - // TODO - }, - ); - - if (homeworkId != null) { - _loadExistingData(homeworkId); - } - } - - Future _loadExistingData(HomeworkId homeworkId) async { - _initialHomework = await api.loadHomework(homeworkId); - _initialAttachments = await api.loadCloudFiles( - homeworkId: _initialHomework.id, - ); - add(_LoadedHomeworkData()); - } -} diff --git a/app/lib/homework/shared/open_homework_dialog.dart b/app/lib/homework/shared/open_homework_dialog.dart index eaae9bf7a..bf47044c4 100644 --- a/app/lib/homework/shared/open_homework_dialog.dart +++ b/app/lib/homework/shared/open_homework_dialog.dart @@ -10,7 +10,6 @@ import 'package:common_domain_models/common_domain_models.dart'; import 'package:firebase_hausaufgabenheft_logik/firebase_hausaufgabenheft_logik.dart'; import 'package:flutter/material.dart'; import 'package:sharezone/pages/homework/homework_dialog.dart'; -import 'package:sharezone/pages/homework/new_homework_dialog.dart'; import 'package:sharezone_widgets/sharezone_widgets.dart'; Future openHomeworkDialogAndShowConfirmationIfSuccessful( @@ -20,7 +19,7 @@ Future openHomeworkDialogAndShowConfirmationIfSuccessful( final successful = await Navigator.push( context, IgnoreWillPopScopeWhenIosSwipeBackRoute( - builder: (context) => NewHomeworkDialog( + builder: (context) => HomeworkDialog( id: homework?.id != null ? HomeworkId(homework!.id) : null, ), settings: const RouteSettings(name: HomeworkDialog.tag), diff --git a/app/lib/pages/homework/homework_details/homework_details.dart b/app/lib/pages/homework/homework_details/homework_details.dart index 6c1ff3a6d..90354606b 100644 --- a/app/lib/pages/homework/homework_details/homework_details.dart +++ b/app/lib/pages/homework/homework_details/homework_details.dart @@ -20,7 +20,6 @@ import 'package:sharezone/navigation/logic/navigation_bloc.dart'; import 'package:sharezone/navigation/models/navigation_item.dart'; import 'package:sharezone/pages/homework/homework_details/homework_details_view_factory.dart'; import 'package:sharezone/pages/homework/homework_dialog.dart'; -import 'package:sharezone/pages/homework/new_homework_dialog.dart'; import 'package:sharezone/pages/homework_page.dart'; import 'package:sharezone/report/report_icon.dart'; import 'package:sharezone/report/report_item.dart'; diff --git a/app/lib/pages/homework/new_homework_dialog.dart b/app/lib/pages/homework/new_homework_dialog.dart deleted file mode 100644 index a57a214ce..000000000 --- a/app/lib/pages/homework/new_homework_dialog.dart +++ /dev/null @@ -1,809 +0,0 @@ -// Copyright (c) 2022 Sharezone UG (haftungsbeschränkt) -// Licensed under the EUPL-1.2-or-later. -// -// You may obtain a copy of the Licence at: -// https://joinup.ec.europa.eu/software/page/eupl -// -// SPDX-License-Identifier: EUPL-1.2 - -import 'dart:async'; -import 'dart:developer'; - -import 'package:bloc_provider/bloc_provider.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:common_domain_models/common_domain_models.dart'; -import 'package:date/date.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:firebase_hausaufgabenheft_logik/firebase_hausaufgabenheft_logik.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart' as bloc_lib show BlocProvider; -import 'package:flutter_bloc/flutter_bloc.dart' hide BlocProvider; -import 'package:sharezone/blocs/application_bloc.dart'; -import 'package:sharezone/blocs/dashbord_widgets_blocs/holiday_bloc.dart'; -import 'package:sharezone/blocs/homework/homework_dialog_bloc.dart' - hide HomeworkDialogBloc; -import 'package:sharezone/blocs/homework/new_homework_dialog_bloc.dart'; -import 'package:sharezone/filesharing/dialog/attach_file.dart'; -import 'package:sharezone/filesharing/dialog/course_tile.dart'; -import 'package:sharezone/markdown/markdown_analytics.dart'; -import 'package:sharezone/markdown/markdown_support.dart'; -import 'package:sharezone/timetable/src/edit_time.dart'; -import 'package:sharezone/util/next_lesson_calculator/next_lesson_calculator.dart'; -import 'package:sharezone/widgets/material/list_tile_with_description.dart'; -import 'package:sharezone/widgets/material/save_button.dart'; -import 'package:sharezone_utils/platform.dart'; -import 'package:sharezone_widgets/sharezone_widgets.dart'; -import 'package:time/time.dart'; - -class NewHomeworkDialog extends StatefulWidget { - const NewHomeworkDialog({ - Key? key, - required this.id, - this.homeworkDialogApi, - this.nextLessonCalculator, - }) : super(key: key); - - static const tag = "homework-dialog"; - - final HomeworkId? id; - final HomeworkDialogApi? homeworkDialogApi; - final NextLessonCalculator? nextLessonCalculator; - - @override - State createState() => _HomeworkDialogState(); -} - -class _HomeworkDialogState extends State { - late NewHomeworkDialogBloc bloc; - late Future homework; - - @override - void initState() { - super.initState(); - final markdownAnalytics = BlocProvider.of(context); - final szContext = BlocProvider.of(context); - final analytics = szContext.analytics; - - late NextLessonCalculator nextLessonCalculator; - if (widget.nextLessonCalculator != null) { - widget.nextLessonCalculator!; - } else { - final holidayManager = - BlocProvider.of(context).holidayManager; - nextLessonCalculator = NextLessonCalculator( - timetableGateway: szContext.api.timetable, - userGateway: szContext.api.user, - holidayManager: holidayManager); - } - - if (widget.id != null) { - homework = szContext.api.homework - .singleHomework(widget.id!.id, source: Source.cache) - .then((value) { - bloc = NewHomeworkDialogBloc( - homeworkId: widget.id, - api: widget.homeworkDialogApi ?? HomeworkDialogApi(szContext.api), - ); - return value; - }); - } else { - homework = Future.value(null); - bloc = NewHomeworkDialogBloc( - api: widget.homeworkDialogApi ?? HomeworkDialogApi(szContext.api), - ); - } - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: homework, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Container(); - } - if (snapshot.hasError) { - return Center(child: Text(snapshot.error!.toString())); - } - return bloc_lib.BlocProvider( - create: (context) => bloc, - child: __HomeworkDialog( - isEditing: snapshot.data != null, - bloc: bloc, - ), - ); - }, - ); - } -} - -class HwDialogKeys { - static const Key titleTextField = Key("title-field"); - static const Key courseTile = Key("course-tile"); - static const Key todoUntilTile = Key("todo-until-tile"); - static const Key submissionTile = Key("submission-tile"); - static const Key submissionTimeTile = Key("submission-time-tile"); - static const Key descriptionField = Key("description-field"); - static const Key addAttachmentTile = Key("add-attachment-tile"); - static const Key attachmentOverflowMenuIcon = Key("attachment-overflow-menu"); - static const Key notifyCourseMembersTile = Key("notify-course-members-tile"); - static const Key isPrivateTile = Key("is-private-tile"); - static const Key saveButton = Key("save-button"); -} - -class __HomeworkDialog extends StatefulWidget { - const __HomeworkDialog( - {Key? key, required this.isEditing, required this.bloc}) - : super(key: key); - - final bool isEditing; - final NewHomeworkDialogBloc bloc; - - @override - __HomeworkDialogState createState() => __HomeworkDialogState(); -} - -class __HomeworkDialogState extends State<__HomeworkDialog> { - final titleNode = FocusNode(); - - @override - void initState() { - delayKeyboard(context: context, focusNode: titleNode); - super.initState(); - } - - bool hasModifiedData() { - final state = widget.bloc.state; - return state is Ready && state.hasModifiedData; - } - - Future leaveDialog() async { - if (hasModifiedData()) { - final confirmedLeave = await warnUserAboutLeavingForm(context); - if (confirmedLeave && context.mounted) Navigator.pop(context); - } else { - Navigator.pop(context); - } - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return switch (state) { - LoadingHomework() => const Center(child: CircularProgressIndicator()), - // TODO - SavedSucessfully() => Container(), - Ready() => WillPopScope( - onWillPop: () async => hasModifiedData() - ? warnUserAboutLeavingForm(context) - : Future.value(true), - child: Scaffold( - body: Column( - children: [ - _AppBar( - editMode: widget.isEditing, - focusNodeTitle: titleNode, - onCloseTap: () => leaveDialog(), - titleField: _TitleField( - focusNode: titleNode, - state: state, - )), - Expanded( - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - _CourseTile(state: state), - const _MobileDivider(), - _TodoUntilPicker(state: state), - const _MobileDivider(), - _SubmissionsSwitch(state: state), - const _MobileDivider(), - _DescriptionField(state: state), - const _MobileDivider(), - _AttachFile(state: state), - const _MobileDivider(), - _SendNotification(state: state), - const _MobileDivider(), - _PrivateHomeworkSwitch(state: state), - const _MobileDivider(), - ], - ), - ), - ), - ], - ), - ), - ) - }; - }, - ); - } -} - -class _MobileDivider extends StatelessWidget { - const _MobileDivider({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - if (PlatformCheck.isDesktopOrWeb) return Container(); - return const Divider(height: 0); - } -} - -class _ErrorStrings { - static const String emptyTitle = - "Bitte gib einen Titel für die Hausaufgabe an!"; - static const String emptyCourse = - "Bitte gib einen Kurs für die Hausaufgabe an!"; - static const String emptyTodoUntil = - "Bitte gib ein Fälligkeitsdatum für die Hausaufgabe an!"; -} - -class _SaveButton extends StatelessWidget { - const _SaveButton({Key? key, this.editMode = false}) : super(key: key); - - final bool editMode; - - Future onPressed(BuildContext context) async { - final bloc = bloc_lib.BlocProvider.of(context); - try { - // bloc.validateInputOrThrow(); - sendDataToFrankfurtSnackBar(context); - // TODO: How can we handle errors that might occure when submitting? - bloc.add(const Submit()); - - if (!context.mounted) return; - hideSendDataToFrankfurtSnackBar(context); - Navigator.pop(context, true); - } on InvalidHomeworkInputException catch (e) { - showSnackSec( - text: switch (e) { - EmptyTitleException() => _ErrorStrings.emptyTitle, - EmptyCourseException() => _ErrorStrings.emptyCourse, - EmptyTodoUntilException() => _ErrorStrings.emptyTodoUntil - }, - context: context, - ); - } on Exception catch (e) { - log("Exception when submitting: $e", error: e); - showSnackSec( - text: - "Es gab einen unbekannten Fehler (${e.toString()}) 😖 Bitte kontaktiere den Support!", - context: context, - seconds: 5, - ); - } - } - - void hideSendDataToFrankfurtSnackBar(BuildContext context) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - } - - @override - Widget build(BuildContext context) { - return SaveButton( - tooltip: "Hausaufgabe speichern", - onPressed: () => onPressed(context), - ); - } -} - -class _TodoUntilPicker extends StatelessWidget { - final Ready state; - - const _TodoUntilPicker({required this.state}); - - @override - Widget build(BuildContext context) { - final bloc = bloc_lib.BlocProvider.of(context); - return MaxWidthConstraintBox( - child: SafeArea( - top: false, - bottom: false, - child: DefaultTextStyle.merge( - style: const TextStyle( - color: null, - // TODO: - // color: snapshot.hasError ? Colors.red : null, - ), - child: DatePicker( - key: HwDialogKeys.todoUntilTile, - padding: const EdgeInsets.all(12), - selectedDate: state.dueDate, - selectDate: (newDate) { - bloc.add(DueDateChanged(Date.fromDateTime(newDate))); - }, - ), - ), - ), - ); - } -} - -class _AppBar extends StatelessWidget { - const _AppBar({ - Key? key, - required this.editMode, - required this.focusNodeTitle, - required this.onCloseTap, - required this.titleField, - }) : super(key: key); - - final bool editMode; - final VoidCallback onCloseTap; - final Widget titleField; - - final FocusNode focusNodeTitle; - - @override - Widget build(BuildContext context) { - return Material( - color: Theme.of(context).isDarkTheme - ? Theme.of(context).appBarTheme.backgroundColor - : Theme.of(context).primaryColor, - elevation: 1, - child: SafeArea( - top: true, - bottom: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(4, 6, 6, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: const Icon(Icons.close, color: Colors.white), - onPressed: onCloseTap, - tooltip: "Schließen", - ), - _SaveButton( - key: HwDialogKeys.saveButton, - editMode: editMode, - ), - ], - ), - ), - titleField, - ], - ), - ), - ); - } -} - -class _TitleField extends StatelessWidget { - const _TitleField({ - required this.focusNode, - required this.state, - }); - - final Ready state; - final FocusNode focusNode; - - @override - Widget build(BuildContext context) { - final bloc = bloc_lib.BlocProvider.of(context); - return MaxWidthConstraintBox( - child: _TitleFieldBase( - // TODO: Will always rebuild with state change, fix. - prefilledTitle: state.title, - focusNode: focusNode, - onChanged: (newTitle) { - bloc.add(TitleChanged(newTitle)); - }, - // TODO - errorText: null, - ), - ); - // return MaxWidthConstraintBox( - // child: StreamBuilder( - // stream: bloc.title, - // builder: (context, snapshot) { - // final errorText = switch (snapshot.error) { - // EmptyTitleException => _ErrorStrings.emptyTitle, - // _ => snapshot.error?.toString(), - // }; - - // return _TitleFieldBase( - // prefilledTitle: prefilledTitle, - // focusNode: focusNode, - // onChanged: bloc.changeTitle, - // errorText: errorText, - // ); - // }), - // ); - } -} - -class _TitleFieldBase extends StatelessWidget { - const _TitleFieldBase({ - Key? key, - required this.prefilledTitle, - required this.onChanged, - this.errorText, - this.focusNode, - }) : super(key: key); - - final String? prefilledTitle; - final String? errorText; - final FocusNode? focusNode; - final Function(String) onChanged; - - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height / 3, - ), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PrefilledTextField( - key: HwDialogKeys.titleTextField, - prefilledText: prefilledTitle, - focusNode: focusNode, - cursorColor: Colors.white, - maxLines: null, - style: const TextStyle(color: Colors.white, fontSize: 22), - decoration: const InputDecoration( - hintText: "Titel eingeben (z.B. AB Nr. 1 - 3)", - hintStyle: TextStyle(color: Colors.white), - border: InputBorder.none, - ), - onChanged: onChanged, - textCapitalization: TextCapitalization.sentences, - ), - Text( - errorText ?? "", - style: TextStyle(color: Colors.red[700], fontSize: 12), - ), - const SizedBox(height: 10), - ], - ), - ), - ), - ); - } -} - -class _CourseTile extends StatelessWidget { - const _CourseTile({Key? key, required this.state}) : super(key: key); - - final Ready state; - - @override - Widget build(BuildContext context) { - final bloc = bloc_lib.BlocProvider.of(context); - final courseState = state.course; - return MaxWidthConstraintBox( - child: SafeArea( - top: false, - bottom: false, - child: CourseTileBase( - key: HwDialogKeys.courseTile, - courseName: - courseState is CourseChosen ? courseState.courseName : null, - // TODO: - errorText: null, - onTap: () => CourseTile.onTap(context, onChangedId: (course) { - bloc.add(CourseChanged(course)); - }), - ), - ), - ); - } -} - -class _SendNotification extends StatelessWidget { - const _SendNotification({Key? key, required this.state}) : super(key: key); - - final Ready state; - - @override - Widget build(BuildContext context) { - final bloc = bloc_lib.BlocProvider.of(context); - - return MaxWidthConstraintBox( - child: SafeArea( - top: false, - bottom: false, - child: _SendNotificationBase( - title: - "Kursmitglieder ${state.isEditing ? "über die Änderungen " : ""}benachrichtigen", - onChanged: (newValue) => - bloc.add(NotifyCourseMembersChanged(newValue)), - sendNotification: state.notifyCourseMembers, - description: state.isEditing - ? null - : "Sende eine Benachrichtigung an deine Kursmitglieder, dass du eine neue Hausaufgabe erstellt hast.", - ), - ), - ); - } -} - -class _SendNotificationBase extends StatelessWidget { - const _SendNotificationBase({ - required this.title, - required this.sendNotification, - required this.onChanged, - this.description, - }); - - final String title; - final String? description; - final bool sendNotification; - final Function(bool) onChanged; - - @override - Widget build(BuildContext context) { - return ListTileWithDescription( - key: HwDialogKeys.notifyCourseMembersTile, - leading: const Icon(Icons.notifications_active), - title: Text(title), - trailing: Switch.adaptive( - onChanged: onChanged, - value: sendNotification, - ), - onTap: () => onChanged(!sendNotification), - description: description != null ? Text(description!) : null, - ); - } -} - -class _DescriptionField extends StatelessWidget { - const _DescriptionField({required this.state}); - - final Ready state; - - @override - Widget build(BuildContext context) { - final bloc = bloc_lib.BlocProvider.of(context); - return _DescriptionFieldBase( - onChanged: (newDescription) => - bloc.add(DescriptionChanged(newDescription)), - // TODO: Will update with each state change, fix. - prefilledDescription: state.description, - ); - } -} - -class _DescriptionFieldBase extends StatelessWidget { - const _DescriptionFieldBase({ - required this.onChanged, - required this.prefilledDescription, - }); - - final Function(String) onChanged; - final String? prefilledDescription; - - @override - Widget build(BuildContext context) { - return MaxWidthConstraintBox( - child: SafeArea( - top: false, - bottom: false, - child: Padding( - padding: const EdgeInsets.only(top: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - leading: const Icon(Icons.subject), - title: PrefilledTextField( - key: HwDialogKeys.descriptionField, - prefilledText: prefilledDescription, - maxLines: null, - scrollPadding: const EdgeInsets.all(16.0), - keyboardType: TextInputType.multiline, - decoration: const InputDecoration( - hintText: "Zusatzinformationen eingeben", - border: InputBorder.none, - ), - onChanged: onChanged, - textCapitalization: TextCapitalization.sentences, - ), - ), - const Padding( - padding: EdgeInsets.fromLTRB(16, 0, 16, 12), - child: MarkdownSupport(), - ), - ], - ), - ), - ), - ); - } -} - -class _AttachFile extends StatelessWidget { - const _AttachFile({required this.state}); - - final Ready state; - - @override - Widget build(BuildContext context) { - final bloc = bloc_lib.BlocProvider.of(context); - return MaxWidthConstraintBox( - child: SafeArea( - top: false, - bottom: false, - child: AttachFileBase( - key: HwDialogKeys.addAttachmentTile, - onLocalFilesAdded: (localFiles) => - bloc.add(AttachmentsAdded(localFiles.toIList())), - onLocalFileRemoved: (localFile) => - bloc.add(AttachmentRemoved(localFile: localFile)), - onCloudFileRemoved: (cloudFile) => - bloc.add(AttachmentRemoved(cloudFile: cloudFile)), - cloudFiles: state.attachments - .where((file) => file.cloudFile != null) - .map((file) => file.cloudFile!) - .toList(), - localFiles: state.attachments - .where((file) => file.localFile != null) - .map((file) => file.localFile!) - .toList(), - ), - ), - ); - } -} - -class _SubmissionsSwitch extends StatelessWidget { - final Ready state; - - const _SubmissionsSwitch({required this.state}); - - @override - Widget build(BuildContext context) { - final submissionsState = state.submissions; - final bloc = bloc_lib.BlocProvider.of(context); - final submissionTime = submissionsState is SubmissionsEnabled - ? submissionsState.deadline - : null; - - return MaxWidthConstraintBox( - child: _SubmissionsSwitchBase( - key: HwDialogKeys.submissionTile, - isWidgetEnabled: state.submissions.isChangeable, - submissionsEnabled: state.submissions.isEnabled, - onChanged: (newIsEnabled) => bloc.add(SubmissionsChanged(( - enabled: newIsEnabled, - submissionTime: newIsEnabled ? submissionTime : null - ))), - onTimeChanged: (newTime) => bloc - .add(SubmissionsChanged((enabled: true, submissionTime: newTime))), - time: submissionTime, - ), - ); - } -} - -class _SubmissionsSwitchBase extends StatelessWidget { - const _SubmissionsSwitchBase({ - Key? key, - required this.isWidgetEnabled, - required this.submissionsEnabled, - required this.onChanged, - required this.onTimeChanged, - required this.time, - }) : super(key: key); - - final bool isWidgetEnabled; - final bool submissionsEnabled; - final Function(bool) onChanged; - final Function(Time) onTimeChanged; - final Time? time; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - ListTile( - leading: const Icon(Icons.folder_open), - title: const Text("Mit Abgabe"), - onTap: isWidgetEnabled ? () => onChanged(!submissionsEnabled) : null, - trailing: Switch.adaptive( - value: submissionsEnabled, - onChanged: isWidgetEnabled ? onChanged : null, - ), - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: submissionsEnabled - ? ListTile( - key: HwDialogKeys.submissionTimeTile, - title: const Text("Abgabe-Uhrzeit"), - onTap: () async { - await hideKeyboardWithDelay(context: context); - if (!context.mounted) return; - - final initialTime = time == Time(hour: 23, minute: 59) - ? Time(hour: 18, minute: 0) - : time; - final newTime = - await selectTime(context, initialTime: initialTime); - if (newTime != null) { - onTimeChanged(newTime); - } - }, - trailing: Padding( - padding: const EdgeInsets.only(right: 8), - child: Text(time.toString()), - ), - ) - : Container(), - ), - ], - ); - } -} - -class _PrivateHomeworkSwitch extends StatelessWidget { - const _PrivateHomeworkSwitch({ - Key? key, - required this.state, - }) : super(key: key); - - final Ready state; - - @override - Widget build(BuildContext context) { - final bloc = bloc_lib.BlocProvider.of(context); - return MaxWidthConstraintBox( - child: SafeArea( - top: false, - bottom: false, - child: _PrivateHomeworkSwitchBase( - key: HwDialogKeys.isPrivateTile, - isPrivate: state.isPrivate.$1, - onChanged: state.isPrivate.isChangeable - ? (newVal) => bloc.add(IsPrivateChanged(newVal)) - : null, - ), - ), - ); - } -} - -class _PrivateHomeworkSwitchBase extends StatelessWidget { - const _PrivateHomeworkSwitchBase({ - required this.isPrivate, - this.onChanged, - super.key, - }); - - final bool isPrivate; - - /// Called when the user changes if the homework is private. - /// - /// Passing `null` disables the tile. - final Function(bool)? onChanged; - - @override - Widget build(BuildContext context) { - final isEnabled = onChanged != null; - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - leading: const Icon(Icons.security), - title: const Text("Privat"), - subtitle: const Text("Hausaufgabe nicht mit dem Kurs teilen."), - enabled: isEnabled, - trailing: Switch.adaptive( - value: isPrivate, - onChanged: isEnabled ? onChanged! : null, - ), - onTap: isEnabled ? () => onChanged!(!isPrivate) : null, - ); - } -} diff --git a/app/lib/pages/homework_page.dart b/app/lib/pages/homework_page.dart index baa7dc8cf..82ddea307 100644 --- a/app/lib/pages/homework_page.dart +++ b/app/lib/pages/homework_page.dart @@ -24,7 +24,6 @@ import 'package:sharezone/navigation/scaffold/app_bar_configuration.dart'; import 'package:sharezone/navigation/scaffold/sharezone_main_scaffold.dart'; import 'package:sharezone/pages/homework/homework_archived.dart'; import 'package:sharezone/pages/homework/homework_dialog.dart'; -import 'package:sharezone/pages/homework/new_homework_dialog.dart'; import 'package:sharezone/widgets/homework/homework_card.dart'; import 'package:sharezone_common/translations.dart'; import 'package:sharezone_widgets/sharezone_widgets.dart'; @@ -44,7 +43,7 @@ Future openHomeworkDialogAndShowConfirmationIfSuccessful( final successful = await Navigator.push( context, IgnoreWillPopScopeWhenIosSwipeBackRoute( - builder: (context) => NewHomeworkDialog( + builder: (context) => HomeworkDialog( id: homework?.id != null ? HomeworkId(homework!.id) : null, ), settings: const RouteSettings(name: HomeworkDialog.tag), diff --git a/app/test/homework/new_homework_dialog_bloc_test.dart b/app/test/homework/new_homework_dialog_bloc_test.dart deleted file mode 100644 index f004a8ada..000000000 --- a/app/test/homework/new_homework_dialog_bloc_test.dart +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright (c) 2023 Sharezone UG (haftungsbeschränkt) -// Licensed under the EUPL-1.2-or-later. -// -// You may obtain a copy of the Licence at: -// https://joinup.ec.europa.eu/software/page/eupl -// -// SPDX-License-Identifier: EUPL-1.2 - -import 'package:analytics/analytics.dart'; -import 'package:common_domain_models/common_domain_models.dart'; -import 'package:date/date.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:files_basics/files_models.dart'; -import 'package:filesharing_logic/filesharing_logic_models.dart'; -import 'package:firebase_hausaufgabenheft_logik/firebase_hausaufgabenheft_logik.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:group_domain_models/group_domain_models.dart'; -import 'package:mockito/mockito.dart'; -import 'package:sharezone/blocs/homework/homework_dialog_bloc.dart'; -import 'package:sharezone/blocs/homework/new_homework_dialog_bloc.dart'; -import 'package:time/time.dart'; - -import '../analytics/analytics_test.dart'; -import 'homework_dialog_test.dart'; -import 'homework_dialog_test.mocks.dart'; - -void main() { - group('HomeworkDialogBloc', () { - late MockSharezoneGateway sharezoneGateway; - late MockCourseGateway courseGateway; - late MockHomeworkDialogApi homeworkDialogApi; - late MockNextLessonCalculator nextLessonCalculator; - late MockSharezoneContext sharezoneContext; - late LocalAnalyticsBackend analyticsBackend; - late Analytics analytics; - - setUp(() { - courseGateway = MockCourseGateway(); - sharezoneGateway = MockSharezoneGateway(); - homeworkDialogApi = MockHomeworkDialogApi(); - nextLessonCalculator = MockNextLessonCalculator(); - sharezoneContext = MockSharezoneContext(); - analyticsBackend = LocalAnalyticsBackend(); - analytics = Analytics(analyticsBackend); - }); - - test('Returns empty dialog when called for creating a new homework', () { - final bloc = NewHomeworkDialogBloc( - api: MockHomeworkDialogApi(), - ); - expect(bloc.state, emptyCreateHomeworkDialogState); - }); - test('Sucessfully saves simple homework', () async { - final bloc = NewHomeworkDialogBloc( - api: homeworkDialogApi, - ); - - final mathCourse = Course.create().copyWith( - id: 'maths_course', - name: 'Maths', - subject: 'Math', - abbreviation: 'M', - myRole: MemberRole.admin, - ); - - when(courseGateway.streamCourses()) - .thenAnswer((_) => Stream.value([mathCourse])); - - bloc.add(TitleChanged('S. 32 8a)')); - bloc.add(CourseChanged(CourseId(mathCourse.id))); - bloc.add(DueDateChanged(Date.parse('2023-10-12'))); - bloc.add(Submit()); - - await pumpEventQueue(); - - expect(bloc.state, SavedSucessfully(isEditing: false)); - expect( - homeworkDialogApi.userInputToBeCreated, - UserInput( - 'S. 32 8a)', - CourseId(mathCourse.id), - DateTime(2023, 10, 12), - '', - false, - [], - false, - null, - false, - )); - }); - test('Returns loading state when called for an existing homework', () { - final homeworkId = HomeworkId('foo'); - - final homeworkDialogApi = MockHomeworkDialogApi(); - homeworkDialogApi.homeworkToReturn = - HomeworkDto.create(courseID: 'courseID'); - final bloc = - NewHomeworkDialogBloc(api: homeworkDialogApi, homeworkId: homeworkId); - expect(bloc.state, LoadingHomework(homeworkId, isEditing: true)); - }); - test('Returns homework data when called for existing homework', () async { - final homeworkId = HomeworkId('foo_homework_id'); - - final fooCourse = Course.create().copyWith( - id: 'foo_course', - name: 'Foo course', - subject: 'Foo subject', - abbreviation: 'F', - myRole: MemberRole.admin, - ); - - when(courseGateway.streamCourses()) - .thenAnswer((_) => Stream.value([fooCourse])); - when(sharezoneContext.analytics).thenReturn(analytics); - final nextLessonDate = Date('2024-03-08'); - nextLessonCalculator.dateToReturn = nextLessonDate; - - homeworkDialogApi.loadCloudFilesResult.addAll([ - CloudFile.create( - id: 'foo_attachment_id1', - creatorName: 'Assignment Creator Name 1', - courseID: 'foo_course', - creatorID: 'foo_creator_id', - path: FolderPath.fromPathString( - '/foo_course/${FolderPath.attachments}')) - .copyWith( - name: 'foo_attachment1.png', fileFormat: FileFormat.image), - CloudFile.create( - id: 'foo_attachment_id2', - creatorName: 'Assignment Creator Name 2', - courseID: 'foo_course', - creatorID: 'foo_creator_id', - path: FolderPath.fromPathString( - '/foo_course/${FolderPath.attachments}')) - .copyWith(name: 'foo_attachment2.pdf', fileFormat: FileFormat.pdf), - ]); - - final mockDocumentReference = MockDocumentReference(); - when(mockDocumentReference.id).thenReturn('foo_course'); - final homework = HomeworkDto.create( - courseID: 'foo_course', courseReference: mockDocumentReference) - .copyWith( - id: homeworkId.id, - title: 'title text', - courseID: 'foo_course', - courseName: 'Foo course', - subject: 'Foo subject', - withSubmissions: false, - todoUntil: DateTime(2024, 03, 12), - description: 'description text', - attachments: ['foo_attachment_id1', 'foo_attachment2.png'], - private: true, - ); - homeworkDialogApi.homeworkToReturn = homework; - - final bloc = - NewHomeworkDialogBloc(api: homeworkDialogApi, homeworkId: homeworkId); - await pumpEventQueue(); - expect( - bloc.state, - Ready( - title: 'title text', - course: CourseChosen( - courseId: CourseId('foo_course'), - courseName: 'Foo course', - isChangeable: false, - ), - dueDate: DateTime(2024, 03, 12), - submissions: const SubmissionsDisabled(isChangeable: true), - description: 'description text', - attachments: IList([ - FileView( - fileId: FileId('foo_attachment_id1'), - fileName: 'foo_attachment1.png', - format: FileFormat.image, - cloudFile: homeworkDialogApi.loadCloudFilesResult[0]), - FileView( - fileId: FileId('foo_attachment_id2'), - fileName: 'foo_attachment2.pdf', - format: FileFormat.pdf, - cloudFile: homeworkDialogApi.loadCloudFilesResult[1]), - ]), - notifyCourseMembers: false, - isPrivate: (true, isChangeable: false), - hasModifiedData: false, - isEditing: true, - ), - ); - }); - test('Returns homework data when called for existing homework 2', () async { - final homeworkId = HomeworkId('bar_homework_id'); - - final barCourse = Course.create().copyWith( - id: 'bar_course', - name: 'Bar course', - subject: 'Bar subject', - abbreviation: 'B', - myRole: MemberRole.admin, - ); - - when(courseGateway.streamCourses()) - .thenAnswer((_) => Stream.value([barCourse])); - when(sharezoneContext.analytics).thenReturn(analytics); - final nextLessonDate = Date('2024-03-08'); - nextLessonCalculator.dateToReturn = nextLessonDate; - - final mockDocumentReference = MockDocumentReference(); - when(mockDocumentReference.id).thenReturn('bar_course'); - final homework = HomeworkDto.create( - courseID: 'bar_course', courseReference: mockDocumentReference) - .copyWith( - id: homeworkId.id, - title: 'title text', - courseID: 'bar_course', - courseName: 'Bar course', - subject: 'Bar subject', - withSubmissions: true, - todoUntil: DateTime(2024, 03, 12, 16, 35), - description: 'description text', - attachments: [], - private: false, - ); - homeworkDialogApi.homeworkToReturn = homework; - - final bloc = - NewHomeworkDialogBloc(api: homeworkDialogApi, homeworkId: homeworkId); - await pumpEventQueue(); - expect( - bloc.state, - Ready( - title: 'title text', - course: CourseChosen( - courseId: CourseId('bar_course'), - courseName: 'Bar course', - isChangeable: false, - ), - dueDate: DateTime(2024, 03, 12, 16, 35), - submissions: SubmissionsEnabled(deadline: Time(hour: 16, minute: 35)), - description: 'description text', - attachments: IList(), - notifyCourseMembers: false, - isPrivate: (false, isChangeable: false), - hasModifiedData: false, - isEditing: true, - ), - ); - }); - }); -} diff --git a/lib/group_domain_models/lib/src/models/course.dart b/lib/group_domain_models/lib/src/models/course.dart index ac1469703..2ecfdd2af 100644 --- a/lib/group_domain_models/lib/src/models/course.dart +++ b/lib/group_domain_models/lib/src/models/course.dart @@ -6,7 +6,6 @@ // // SPDX-License-Identifier: EUPL-1.2 -import 'package:collection/collection.dart'; import 'package:common_domain_models/common_domain_models.dart'; import 'package:design/design.dart'; import 'package:flutter/foundation.dart';