From 79ec14161a03e6389f29726865bdefaceb696eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20M=C3=A4gel?= Date: Fri, 22 Dec 2023 10:35:35 +0100 Subject: [PATCH] refactor(TagDialog): Redesign tag dialog --- lib/common_widgets/contexts_dialog.dart | 56 +++++- lib/common_widgets/input_dialog.dart | 1 + lib/common_widgets/key_values_dialog.dart | 53 ++++++ lib/common_widgets/priorities_dialog.dart | 2 +- lib/common_widgets/projects_dialog.dart | 56 +++++- lib/common_widgets/tag_dialog.dart | 178 +++++++++++++++++ .../filter/pages/filter_create_edit_page.dart | 1 + .../todo/pages/todo_create_edit_page.dart | 2 +- .../todo/pages/todo_search_page.dart | 1 + lib/presentation/todo/states/todo_cubit.dart | 12 +- .../todo/widgets/todo_detail_items.dart | 81 +++----- .../todo/widgets/todo_tag_dialog.dart | 180 ------------------ .../todo/widgets/todo_text_field.dart | 1 + .../filter/states/filter_cubit_test.dart | 110 ++++++++--- .../todo/pages/todo_create_page_test.dart | 31 +-- .../todo/pages/todo_edit_page_test.dart | 30 +-- .../todo/states/todo_cubit_test.dart | 58 +++--- 17 files changed, 502 insertions(+), 351 deletions(-) create mode 100644 lib/common_widgets/key_values_dialog.dart create mode 100644 lib/common_widgets/tag_dialog.dart delete mode 100644 lib/presentation/todo/widgets/todo_tag_dialog.dart diff --git a/lib/common_widgets/contexts_dialog.dart b/lib/common_widgets/contexts_dialog.dart index 6debc77..37da07f 100644 --- a/lib/common_widgets/contexts_dialog.dart +++ b/lib/common_widgets/contexts_dialog.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:ntodotxt/common_widgets/chip.dart'; +import 'package:ntodotxt/common_widgets/tag_dialog.dart'; import 'package:ntodotxt/presentation/filter/states/filter_cubit.dart' show FilterCubit; +import 'package:ntodotxt/presentation/todo/states/todo_cubit.dart'; class ContextListDialog extends StatefulWidget { final FilterCubit cubit; @@ -34,8 +36,8 @@ class _ContextListDialogState extends State { @override void initState() { - selectedItems = {...widget.cubit.state.filter.contexts}; super.initState(); + selectedItems = {...widget.cubit.state.filter.contexts}; } @override @@ -44,7 +46,7 @@ class _ContextListDialogState extends State { title: const Text('Contexts'), content: GenericChipGroup( children: [ - for (String item in widget.items) + for (String item in widget.items.toList()..sort()) GenericChoiceChip( label: Text(item), selected: selectedItems.contains(item), @@ -76,3 +78,53 @@ class _ContextListDialogState extends State { ); } } + +class ContextTagDialog extends TagDialog { + const ContextTagDialog({ + required super.cubit, + super.title = 'Contexts', + super.tagName = 'context', + super.availableTags, + super.key = const Key('addContextTagDialog'), + }); + + @override + RegExp get regex => RegExp(r'^\S+$'); + + static Future dialog({ + required BuildContext context, + required TodoCubit cubit, + required Set availableTags, + }) async { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) => ContextTagDialog( + cubit: cubit, + availableTags: availableTags, + ), + ); + } + + @override + void onSubmit(BuildContext context, Set values) => + cubit.updateContexts(values); + + @override + State createState() => _ContextTagDialogState(); +} + +class _ContextTagDialogState extends TagDialogState { + @override + void initState() { + super.initState(); + super.tags = { + ...widget.availableTags.map( + (String t) => Tag(name: t, selected: false), + ), + ...widget.cubit.state.todo.contexts.map( + (String t) => Tag(name: t, selected: true), + ), + }; + } +} diff --git a/lib/common_widgets/input_dialog.dart b/lib/common_widgets/input_dialog.dart index 3f94943..c846649 100644 --- a/lib/common_widgets/input_dialog.dart +++ b/lib/common_widgets/input_dialog.dart @@ -36,6 +36,7 @@ class InputDialog extends StatelessWidget { border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, errorBorder: InputBorder.none, disabledBorder: InputBorder.none, ), diff --git a/lib/common_widgets/key_values_dialog.dart b/lib/common_widgets/key_values_dialog.dart new file mode 100644 index 0000000..e2295fe --- /dev/null +++ b/lib/common_widgets/key_values_dialog.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:ntodotxt/common_widgets/tag_dialog.dart'; +import 'package:ntodotxt/presentation/todo/states/todo_cubit.dart'; + +class KeyValueTagDialog extends TagDialog { + const KeyValueTagDialog({ + required super.cubit, + super.title = 'Key Values', + super.tagName = 'key:value', + super.availableTags, + super.key = const Key('addKeyValueTagDialog'), + }); + + @override + RegExp get regex => RegExp(r'^([^\+\@].*[^:\s]):(.*[^:\s])$'); + + static Future dialog({ + required BuildContext context, + required TodoCubit cubit, + required Set availableTags, + }) async { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) => KeyValueTagDialog( + cubit: cubit, + availableTags: availableTags, + ), + ); + } + + @override + void onSubmit(BuildContext context, Set values) => + cubit.updateKeyValues(values); + + @override + State createState() => _KeyValueTagDialogState(); +} + +class _KeyValueTagDialogState extends TagDialogState { + @override + void initState() { + super.initState(); + super.tags = { + ...widget.availableTags.map( + (String t) => Tag(name: t, selected: false), + ), + ...widget.cubit.state.todo.fmtKeyValues.map( + (String t) => Tag(name: t, selected: true), + ), + }; + } +} diff --git a/lib/common_widgets/priorities_dialog.dart b/lib/common_widgets/priorities_dialog.dart index bde6d46..24f4dde 100644 --- a/lib/common_widgets/priorities_dialog.dart +++ b/lib/common_widgets/priorities_dialog.dart @@ -35,8 +35,8 @@ class _PriorityListDialogState extends State { @override void initState() { - selectedItems = {...widget.cubit.state.filter.priorities}; super.initState(); + selectedItems = {...widget.cubit.state.filter.priorities}; } @override diff --git a/lib/common_widgets/projects_dialog.dart b/lib/common_widgets/projects_dialog.dart index 82d60f2..e76120f 100644 --- a/lib/common_widgets/projects_dialog.dart +++ b/lib/common_widgets/projects_dialog.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:ntodotxt/common_widgets/chip.dart'; +import 'package:ntodotxt/common_widgets/tag_dialog.dart'; import 'package:ntodotxt/presentation/filter/states/filter_cubit.dart' show FilterCubit; +import 'package:ntodotxt/presentation/todo/states/todo_cubit.dart'; class ProjectListDialog extends StatefulWidget { final FilterCubit cubit; @@ -34,8 +36,8 @@ class _ProjectListDialogState extends State { @override void initState() { - selectedItems = {...widget.cubit.state.filter.projects}; super.initState(); + selectedItems = {...widget.cubit.state.filter.projects}; } @override @@ -44,7 +46,7 @@ class _ProjectListDialogState extends State { title: const Text('Projects'), content: GenericChipGroup( children: [ - for (String item in widget.items) + for (String item in widget.items.toList()..sort()) GenericChoiceChip( label: Text(item), selected: selectedItems.contains(item), @@ -76,3 +78,53 @@ class _ProjectListDialogState extends State { ); } } + +class ProjectTagDialog extends TagDialog { + const ProjectTagDialog({ + required super.cubit, + super.title = 'Projects', + super.tagName = 'project', + super.availableTags, + super.key = const Key('addProjectTagDialog'), + }); + + @override + RegExp get regex => RegExp(r'^\S+$'); + + static Future dialog({ + required BuildContext context, + required TodoCubit cubit, + required Set availableTags, + }) async { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) => ProjectTagDialog( + cubit: cubit, + availableTags: availableTags, + ), + ); + } + + @override + void onSubmit(BuildContext context, Set values) => + cubit.updateProjects(values); + + @override + State createState() => _ProjectTagDialogState(); +} + +class _ProjectTagDialogState extends TagDialogState { + @override + void initState() { + super.initState(); + super.tags = { + ...widget.availableTags.map( + (String t) => Tag(name: t, selected: false), + ), + ...widget.cubit.state.todo.projects.map( + (String t) => Tag(name: t, selected: true), + ), + }; + } +} diff --git a/lib/common_widgets/tag_dialog.dart b/lib/common_widgets/tag_dialog.dart new file mode 100644 index 0000000..c77239f --- /dev/null +++ b/lib/common_widgets/tag_dialog.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:ntodotxt/common_widgets/chip.dart'; +import 'package:ntodotxt/presentation/todo/states/todo_cubit.dart'; + +class Tag { + String name; + bool selected; + + Tag({ + required this.name, + required this.selected, + }); + + @override + String toString() => name; +} + +class TagDialog extends StatefulWidget { + final TodoCubit cubit; + final String title; + final String tagName; + final Set availableTags; + + const TagDialog({ + required this.cubit, + required this.title, + required this.tagName, + this.availableTags = const {}, + super.key, + }); + + RegExp get regex => RegExp(r'^\S+$'); + + void onSubmit(BuildContext context, Set values) {} + + @override + State createState() => TagDialogState(); +} + +class TagDialogState extends State { + // Holds the selected tags before adding to the regular state. + Set tags = {}; + + late GlobalKey _formKey; + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _formKey = GlobalKey(); + _controller = TextEditingController(); + } + + @override + void dispose() { + // Clean up the controller when the widget is disposed. + _controller.dispose(); + super.dispose(); + } + + Set get sortedTags { + List t = tags.toList() + ..sort( + (Tag a, Tag b) => a.toString().compareTo(b.toString()), + ); + return t.toSet(); + } + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.4, + minChildSize: 0.15, + maxChildSize: 0.6, + expand: false, + builder: (BuildContext context, ScrollController scrollController) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), + title: Text( + widget.title, + style: Theme.of(context).textTheme.titleLarge, + ), + trailing: TextButton( + child: const Text('Apply'), + onPressed: () { + widget.onSubmit(context, { + for (Tag t in tags) + if (t.selected) t.name + }); + Navigator.pop(context); + }, + ), + ), + ), + const Divider(), + if (tags.isNotEmpty) + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), + title: GenericChipGroup( + children: [ + for (var t in sortedTags) + GenericChoiceChip( + label: Text(t.name), + selected: t.selected, + onSelected: (bool selected) { + setState(() { + t.selected = selected; + }); + }, + ), + ], + ), + ), + ), + if (tags.isNotEmpty) const Divider(), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), + title: Form( + key: _formKey, + child: TextFormField( + controller: _controller, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Enter <${widget.tagName}> tag ...', + isDense: true, + filled: false, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + ), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Missing tag name'; + } + if (!widget.regex.hasMatch(value)) { + return 'Invalid tag format'; + } + return null; + }, + ), + ), + trailing: TextButton( + child: const Text('Add'), + onPressed: () { + if (_formKey.currentState!.validate()) { + setState(() { + tags.add(Tag( + name: _controller.text, + selected: true, + )); + }); + _controller.text = ''; + } + }, + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/presentation/filter/pages/filter_create_edit_page.dart b/lib/presentation/filter/pages/filter_create_edit_page.dart index 1b982ed..939f92a 100644 --- a/lib/presentation/filter/pages/filter_create_edit_page.dart +++ b/lib/presentation/filter/pages/filter_create_edit_page.dart @@ -341,6 +341,7 @@ class _FilterNameTextFieldState extends State { border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, errorBorder: InputBorder.none, disabledBorder: InputBorder.none, ), diff --git a/lib/presentation/todo/pages/todo_create_edit_page.dart b/lib/presentation/todo/pages/todo_create_edit_page.dart index 04bf309..53334df 100644 --- a/lib/presentation/todo/pages/todo_create_edit_page.dart +++ b/lib/presentation/todo/pages/todo_create_edit_page.dart @@ -42,7 +42,7 @@ class TodoCreateEditPage extends StatelessWidget { builder: (BuildContext context, TodoState state) { return Scaffold( appBar: MainAppBar( - title: createMode ? 'Add' : 'Edit', + title: createMode ? 'Create' : 'Edit', toolbar: createMode ? null : Row( diff --git a/lib/presentation/todo/pages/todo_search_page.dart b/lib/presentation/todo/pages/todo_search_page.dart index 659d821..53993ab 100644 --- a/lib/presentation/todo/pages/todo_search_page.dart +++ b/lib/presentation/todo/pages/todo_search_page.dart @@ -16,6 +16,7 @@ class TodoSearchPage extends SearchDelegate { border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, errorBorder: InputBorder.none, disabledBorder: InputBorder.none, ), diff --git a/lib/presentation/todo/states/todo_cubit.dart b/lib/presentation/todo/states/todo_cubit.dart index 8684778..9edd178 100644 --- a/lib/presentation/todo/states/todo_cubit.dart +++ b/lib/presentation/todo/states/todo_cubit.dart @@ -74,12 +74,12 @@ class TodoCubit extends Cubit { } } - void addMultipleProjects(List projects) { + void updateProjects(Set projects) { try { emit( state.success( todo: state.todo.copyWith( - projects: {...state.todo.projects}..addAll(projects), + projects: {...projects}, ), ), ); @@ -116,12 +116,12 @@ class TodoCubit extends Cubit { } } - void addMultipleContexts(List contexts) { + void updateContexts(Set contexts) { try { emit( state.success( todo: state.todo.copyWith( - contexts: {...state.todo.contexts}..addAll(contexts), + contexts: {...contexts}, ), ), ); @@ -162,9 +162,9 @@ class TodoCubit extends Cubit { } } - void addMultipleKeyValues(List kvs) { + void updateKeyValues(Set kvs) { try { - Map keyValues = {...state.todo.keyValues}; + Map keyValues = {}; for (var kv in kvs) { if (!Todo.patternKeyValue.hasMatch(kv)) { throw TodoInvalidKeyValueTag(tag: kv); diff --git a/lib/presentation/todo/widgets/todo_detail_items.dart b/lib/presentation/todo/widgets/todo_detail_items.dart index 7c81c8e..909c0e8 100644 --- a/lib/presentation/todo/widgets/todo_detail_items.dart +++ b/lib/presentation/todo/widgets/todo_detail_items.dart @@ -2,10 +2,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/common_widgets/chip.dart'; +import 'package:ntodotxt/common_widgets/contexts_dialog.dart'; +import 'package:ntodotxt/common_widgets/key_values_dialog.dart'; +import 'package:ntodotxt/common_widgets/projects_dialog.dart'; import 'package:ntodotxt/domain/todo/todo_model.dart'; import 'package:ntodotxt/presentation/todo/states/todo_cubit.dart'; import 'package:ntodotxt/presentation/todo/states/todo_state.dart'; -import 'package:ntodotxt/presentation/todo/widgets/todo_tag_dialog.dart'; abstract class TodoTagSection extends StatelessWidget { final Icon leadingIcon; @@ -15,17 +17,6 @@ abstract class TodoTagSection extends StatelessWidget { super.key, }); - void _showDialog({ - required BuildContext context, - required Widget child, - }) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (BuildContext context) => child, - ); - } - void _onSelected(BuildContext context, String value, bool selected); Widget _buildChips({ @@ -100,24 +91,12 @@ class TodoProjectTags extends TodoTagSection { @override void _onSelected(BuildContext context, String value, bool selected) { if (selected) { - context.read().addMultipleProjects([value]); + context.read().addProject(value); } else { context.read().removeProject(value); } } - void _openDialog(BuildContext context) { - _showDialog( - context: context, - child: BlocProvider.value( - value: BlocProvider.of(context), - child: TodoProjectTagDialog( - availableTags: availableTags, - ), - ), - ); - } - @override Widget build(BuildContext context) { return BlocBuilder( @@ -141,7 +120,11 @@ class TodoProjectTags extends TodoTagSection { trailing: IconButton( icon: const Icon(Icons.add), tooltip: 'Add project tag', - onPressed: () => _openDialog(context), + onPressed: () => ProjectTagDialog.dialog( + context: context, + cubit: BlocProvider.of(context), + availableTags: availableTags, + ), ), ), ); @@ -162,24 +145,12 @@ class TodoContextTags extends TodoTagSection { @override void _onSelected(BuildContext context, String value, bool selected) { if (selected) { - context.read().addMultipleContexts([value]); + context.read().addContext(value); } else { context.read().removeContext(value); } } - void _openDialog(BuildContext context) { - _showDialog( - context: context, - child: BlocProvider.value( - value: BlocProvider.of(context), - child: TodoContextTagDialog( - availableTags: availableTags, - ), - ), - ); - } - @override Widget build(BuildContext context) { return BlocBuilder( @@ -203,7 +174,11 @@ class TodoContextTags extends TodoTagSection { trailing: IconButton( icon: const Icon(Icons.add), tooltip: 'Add context tag', - onPressed: () => _openDialog(context), + onPressed: () => ContextTagDialog.dialog( + context: context, + cubit: BlocProvider.of(context), + availableTags: availableTags, + ), ), ), ); @@ -224,24 +199,12 @@ class TodoKeyValueTags extends TodoTagSection { @override void _onSelected(BuildContext context, String value, bool selected) { if (selected) { - context.read().addMultipleKeyValues([value]); + context.read().addKeyValue(value); } else { context.read().removeKeyValue(value); } } - void _openDialog(BuildContext context) { - _showDialog( - context: context, - child: BlocProvider.value( - value: BlocProvider.of(context), - child: TodoKeyValueTagDialog( - availableTags: availableTags, - ), - ), - ); - } - @override Widget build(BuildContext context) { return BlocBuilder( @@ -266,7 +229,11 @@ class TodoKeyValueTags extends TodoTagSection { trailing: IconButton( icon: const Icon(Icons.add), tooltip: 'Add key:value tag', - onPressed: () => _openDialog(context), + onPressed: () => KeyValueTagDialog.dialog( + context: context, + cubit: BlocProvider.of(context), + availableTags: availableTags, + ), ), ), ); @@ -392,9 +359,9 @@ class TodoDueDateItem extends StatelessWidget { if (date != null) { final String? formattedDate = Todo.date2Str(date); if (formattedDate != null) { - context.read().addMultipleKeyValues( - ['due:$formattedDate'], - ); + context.read().addKeyValue( + 'due:$formattedDate', + ); } } }, diff --git a/lib/presentation/todo/widgets/todo_tag_dialog.dart b/lib/presentation/todo/widgets/todo_tag_dialog.dart deleted file mode 100644 index b6f6b24..0000000 --- a/lib/presentation/todo/widgets/todo_tag_dialog.dart +++ /dev/null @@ -1,180 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:ntodotxt/common_widgets/chip.dart'; -import 'package:ntodotxt/presentation/todo/states/todo_cubit.dart'; - -class TodoTagDialog extends StatefulWidget { - final String tagName; - final Set availableTags; - - const TodoTagDialog({ - required this.tagName, - this.availableTags = const {}, - super.key, - }); - - void onSubmit(BuildContext context, List values) {} - - @override - State createState() => _TodoTagDialogState(); -} - -class _TodoTagDialogState extends State { - // Holds the selected tags before adding to the regular state. - List selectedTags = []; - - late GlobalKey _textFormKey; - late TextEditingController _controller; - - @override - void initState() { - super.initState(); - _textFormKey = GlobalKey(); - _controller = TextEditingController(); - } - - @override - void dispose() { - // Clean up the controller when the widget is disposed. - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BottomSheet( - enableDrag: false, - showDragHandle: false, - onClosing: () {}, - builder: (BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - contentPadding: const EdgeInsets.only(left: 8.0), - title: TextFormField( - key: _textFormKey, - controller: _controller, - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - hintText: - 'Enter <${widget.tagName}> tags seperated by whitespace ...', - isDense: true, - filled: false, - contentPadding: EdgeInsets.zero, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - ), - ), - trailing: Tooltip( - message: 'Add ${widget.tagName} tags', - child: TextButton( - child: const Text('Apply'), - onPressed: () { - // Remove duplicate whitespaces from input - // and split string by whitespaces. - final List addedTags = _controller.text - .trim() - .replaceAllMapped(RegExp(r'\s+'), (match) { - return ' '; - }).split(' ') - ..removeWhere((value) => value.isEmpty); - widget.onSubmit(context, [...addedTags, ...selectedTags]); - Navigator.pop(context); - }, - ), - ), - ), - if (widget.availableTags.isNotEmpty) - ListTile( - contentPadding: const EdgeInsets.only(left: 8.0), - title: GenericChipGroup( - children: [ - for (var t in widget.availableTags) - GenericChoiceChip( - label: Text(t), - selected: selectedTags.contains(t), - onSelected: (bool selected) { - if (selected) { - setState(() { - selectedTags.add(t); - }); - } else { - setState(() { - selectedTags.remove(t); - }); - } - }, - ), - ], - ), - ), - ], - ), - ); - }, - ); - } -} - -class TodoProjectTagDialog extends TodoTagDialog { - const TodoProjectTagDialog({ - super.tagName = 'project', - super.availableTags, - super.key = const Key('addProjectTagDialog'), - }); - - @override - void onSubmit(BuildContext context, List values) { - context.read().addMultipleProjects(values); - } - - @override - State createState() => _TodoProjectTagDialogState(); -} - -class _TodoProjectTagDialogState - extends _TodoTagDialogState {} - -class TodoContextTagDialog extends TodoTagDialog { - const TodoContextTagDialog({ - super.tagName = 'context', - super.availableTags, - super.key = const Key('addContextTagDialog'), - }); - - @override - void onSubmit(BuildContext context, List values) { - context.read().addMultipleContexts(values); - } - - @override - State createState() => _TodoContextTagDialogState(); -} - -class _TodoContextTagDialogState - extends _TodoTagDialogState {} - -class TodoKeyValueTagDialog extends TodoTagDialog { - const TodoKeyValueTagDialog({ - super.tagName = 'key:value', - super.availableTags, - super.key = const Key('addKeyValueTagDialog'), - }); - - @override - void onSubmit(BuildContext context, List values) { - context.read().addMultipleKeyValues(values); - } - - @override - State createState() => _TodoKeyValueTagDialogState(); -} - -class _TodoKeyValueTagDialogState - extends _TodoTagDialogState {} diff --git a/lib/presentation/todo/widgets/todo_text_field.dart b/lib/presentation/todo/widgets/todo_text_field.dart index 1616feb..c858d29 100644 --- a/lib/presentation/todo/widgets/todo_text_field.dart +++ b/lib/presentation/todo/widgets/todo_text_field.dart @@ -78,6 +78,7 @@ class _TodoStringTextFieldState extends State { border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, errorBorder: InputBorder.none, disabledBorder: InputBorder.none, ), diff --git a/test/presentation/filter/states/filter_cubit_test.dart b/test/presentation/filter/states/filter_cubit_test.dart index 31ceb57..0e8002b 100644 --- a/test/presentation/filter/states/filter_cubit_test.dart +++ b/test/presentation/filter/states/filter_cubit_test.dart @@ -91,7 +91,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter'), + filter: const Filter(), ); cubit.updateOrder(ListOrder.descending); @@ -99,7 +99,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.descending, filter: ListFilter.all, group: ListGroup.none, @@ -112,7 +111,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter'), + filter: const Filter(), ); cubit.updateFilter(ListFilter.completedOnly); @@ -120,7 +119,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.completedOnly, group: ListGroup.none, @@ -128,12 +126,12 @@ void main() { ), ); }); - test('filter', () async { + test('group', () async { final FilterCubit cubit = FilterCubit( repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter'), + filter: const Filter(), ); cubit.updateGroup(ListGroup.priority); @@ -141,7 +139,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.priority, @@ -157,7 +154,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter'), + filter: const Filter(), ); cubit.addPriority(Priority.A); @@ -165,7 +162,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, @@ -179,7 +175,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter', priorities: {Priority.A}), + filter: const Filter(priorities: {Priority.A}), ); cubit.addPriority(Priority.A); @@ -187,7 +183,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, @@ -201,7 +196,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter', priorities: {Priority.A}), + filter: const Filter(priorities: {Priority.A}), ); cubit.removePriority(Priority.A); @@ -209,7 +204,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, @@ -223,7 +217,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter'), + filter: const Filter(), ); cubit.removePriority(Priority.A); @@ -231,7 +225,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, @@ -240,6 +233,27 @@ void main() { ), ); }); + test('update multiple', () async { + final FilterCubit cubit = FilterCubit( + repository: FilterRepository( + FilterController(inMemoryDatabasePath), + ), + filter: const Filter(), + ); + cubit.updatePriorities({Priority.A, Priority.B}); + + expect( + cubit.state, + const FilterSuccess( + filter: Filter( + order: ListOrder.ascending, + filter: ListFilter.all, + group: ListGroup.none, + priorities: {Priority.A, Priority.B}, + ), + ), + ); + }); }); group('project', () { @@ -248,7 +262,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter'), + filter: const Filter(), ); cubit.addProject('project1'); @@ -256,7 +270,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, @@ -270,7 +283,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter', projects: {'project1'}), + filter: const Filter(projects: {'project1'}), ); cubit.addProject('project1'); @@ -278,7 +291,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, @@ -292,7 +304,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter', projects: {'project1'}), + filter: const Filter(projects: {'project1'}), ); cubit.removeProject('project1'); @@ -300,7 +312,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, @@ -314,7 +325,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter'), + filter: const Filter(), ); cubit.removeProject('project1'); @@ -322,7 +333,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, @@ -331,6 +341,27 @@ void main() { ), ); }); + test('update multiple', () async { + final FilterCubit cubit = FilterCubit( + repository: FilterRepository( + FilterController(inMemoryDatabasePath), + ), + filter: const Filter(), + ); + cubit.updateProjects({'project1', 'project2'}); + + expect( + cubit.state, + const FilterSuccess( + filter: Filter( + order: ListOrder.ascending, + filter: ListFilter.all, + group: ListGroup.none, + projects: {'project1', 'project2'}, + ), + ), + ); + }); }); group('context', () { @@ -339,7 +370,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter'), + filter: const Filter(), ); cubit.addContext('context1'); @@ -347,7 +378,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, @@ -361,7 +391,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter', contexts: {'context1'}), + filter: const Filter(contexts: {'context1'}), ); cubit.addContext('context1'); @@ -369,7 +399,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, @@ -383,7 +412,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter', contexts: {'context1'}), + filter: const Filter(contexts: {'context1'}), ); cubit.removeContext('context1'); @@ -391,7 +420,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, @@ -405,7 +433,7 @@ void main() { repository: FilterRepository( FilterController(inMemoryDatabasePath), ), - filter: const Filter(name: 'filter'), + filter: const Filter(), ); cubit.removeContext('context1'); @@ -413,7 +441,6 @@ void main() { cubit.state, const FilterSuccess( filter: Filter( - name: 'filter', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, @@ -422,5 +449,26 @@ void main() { ), ); }); + test('update multiple', () async { + final FilterCubit cubit = FilterCubit( + repository: FilterRepository( + FilterController(inMemoryDatabasePath), + ), + filter: const Filter(), + ); + cubit.updateContexts({'context1', 'context2'}); + + expect( + cubit.state, + const FilterSuccess( + filter: Filter( + order: ListOrder.ascending, + filter: ListFilter.all, + group: ListGroup.none, + contexts: {'context1', 'context2'}, + ), + ), + ); + }); }); } diff --git a/test/presentation/todo/pages/todo_create_page_test.dart b/test/presentation/todo/pages/todo_create_page_test.dart index 28965f4..9ca5774 100644 --- a/test/presentation/todo/pages/todo_create_page_test.dart +++ b/test/presentation/todo/pages/todo_create_page_test.dart @@ -56,13 +56,10 @@ void main() { ), 'project1', ); - await safeTapByFinder( - tester, - find.descendant( - of: addProjectTagDialogFinder, - matching: find.byTooltip('Add project tags'), - ), - ); + + await safeTapByFinder(tester, find.text('Add')); + await tester.pump(); + await safeTapByFinder(tester, find.text('Apply')); await tester.pump(); expect( @@ -139,13 +136,9 @@ void main() { ), 'context1', ); - await safeTapByFinder( - tester, - find.descendant( - of: addContextTagDialogFinder, - matching: find.byTooltip('Add context tags'), - ), - ); + await safeTapByFinder(tester, find.text('Add')); + await tester.pump(); + await safeTapByFinder(tester, find.text('Apply')); await tester.pump(); expect( @@ -221,13 +214,9 @@ void main() { ), 'foo:bar', ); - await safeTapByFinder( - tester, - find.descendant( - of: addKeyValueTagDialogFinder, - matching: find.byTooltip('Add key:value tags'), - ), - ); + await safeTapByFinder(tester, find.text('Add')); + await tester.pump(); + await safeTapByFinder(tester, find.text('Apply')); await tester.pump(); expect( diff --git a/test/presentation/todo/pages/todo_edit_page_test.dart b/test/presentation/todo/pages/todo_edit_page_test.dart index 69cad6e..6da2056 100644 --- a/test/presentation/todo/pages/todo_edit_page_test.dart +++ b/test/presentation/todo/pages/todo_edit_page_test.dart @@ -459,13 +459,9 @@ void main() { ), 'project1', ); - await safeTapByFinder( - tester, - find.descendant( - of: addProjectTagDialogFinder, - matching: find.byTooltip('Add project tags'), - ), - ); + await safeTapByFinder(tester, find.text('Add')); + await tester.pump(); + await safeTapByFinder(tester, find.text('Apply')); await tester.pump(); expect( @@ -630,13 +626,9 @@ void main() { ), 'context1', ); - await safeTapByFinder( - tester, - find.descendant( - of: addContextTagDialogFinder, - matching: find.byTooltip('Add context tags'), - ), - ); + await safeTapByFinder(tester, find.text('Add')); + await tester.pump(); + await safeTapByFinder(tester, find.text('Apply')); await tester.pump(); expect( @@ -801,13 +793,9 @@ void main() { ), 'foo:bar', ); - await safeTapByFinder( - tester, - find.descendant( - of: addKeyValueTagDialogFinder, - matching: find.byTooltip('Add key:value tags'), - ), - ); + await safeTapByFinder(tester, find.text('Add')); + await tester.pump(); + await safeTapByFinder(tester, find.text('Apply')); await tester.pump(); expect( diff --git a/test/presentation/todo/states/todo_cubit_test.dart b/test/presentation/todo/states/todo_cubit_test.dart index 16c7f90..7642bd2 100644 --- a/test/presentation/todo/states/todo_cubit_test.dart +++ b/test/presentation/todo/states/todo_cubit_test.dart @@ -157,7 +157,7 @@ void main() { test('initial', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleProjects(['project1']); + bloc.addProject('project1'); expect( bloc.state, @@ -175,7 +175,7 @@ void main() { projects: const {'project1'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleProjects(['project2']); + bloc.addProject('project2'); expect( bloc.state, @@ -190,7 +190,7 @@ void main() { test('invalid format', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleProjects(['project 2']); + bloc.addProject('project 2'); expect( bloc.state, @@ -209,7 +209,7 @@ void main() { projects: const {'project1'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleProjects(['project1']); + bloc.addProject('project1'); expect( bloc.state, @@ -227,7 +227,7 @@ void main() { projects: const {'project1'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleProjects(['Project1']); + bloc.addProject('Project1'); expect( bloc.state, @@ -242,7 +242,7 @@ void main() { test('multiple entries', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleProjects(['project1', 'project2']); + bloc.updateProjects({'project1', 'project2'}); expect( bloc.state, @@ -257,7 +257,7 @@ void main() { test('multiple entries / invalid format', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleProjects(['project1', 'project 2']); + bloc.updateProjects({'project1', 'project 2'}); expect( bloc.state, @@ -276,7 +276,7 @@ void main() { projects: const {'project1'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleProjects(['project1', 'project2']); + bloc.updateProjects({'project1', 'project2'}); expect( bloc.state, @@ -294,7 +294,7 @@ void main() { projects: const {'project1'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleProjects(['Project1', 'Project2']); + bloc.updateProjects({'Project1', 'Project2'}); expect( bloc.state, @@ -367,7 +367,7 @@ void main() { test('initial', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleContexts(['context1']); + bloc.addContext('context1'); expect( bloc.state, @@ -385,7 +385,7 @@ void main() { contexts: const {'context1'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleContexts(['context2']); + bloc.addContext('context2'); expect( bloc.state, @@ -400,7 +400,7 @@ void main() { test('invalid format', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleContexts(['context 2']); + bloc.addContext('context 2'); expect( bloc.state, @@ -419,7 +419,7 @@ void main() { contexts: const {'context1'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleContexts(['context1']); + bloc.addContext('context1'); expect( bloc.state, @@ -437,7 +437,7 @@ void main() { contexts: const {'context1'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleContexts(['Context1']); + bloc.addContext('Context1'); expect( bloc.state, @@ -452,7 +452,7 @@ void main() { test('multiple entries', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleContexts(['context1', 'context2']); + bloc.updateContexts({'context1', 'context2'}); expect( bloc.state, @@ -467,7 +467,7 @@ void main() { test('multiple entries / invalid format', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleContexts(['context1', 'context 2']); + bloc.updateContexts({'context1', 'context 2'}); expect( bloc.state, @@ -486,7 +486,7 @@ void main() { contexts: const {'context1'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleContexts(['context1', 'context2']); + bloc.updateContexts({'context1', 'context2'}); expect( bloc.state, @@ -504,7 +504,7 @@ void main() { contexts: const {'context1'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleContexts(['context1', 'Context2']); + bloc.updateContexts({'context1', 'Context2'}); expect( bloc.state, @@ -577,7 +577,7 @@ void main() { test('initial', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleKeyValues(['key:val']); + bloc.addKeyValue('key:val'); expect( bloc.state, @@ -595,7 +595,7 @@ void main() { keyValues: const {'foo': 'bar'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleKeyValues(['key:val']); + bloc.addKeyValue('key:val'); expect( bloc.state, @@ -610,7 +610,7 @@ void main() { test('invalid format', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleKeyValues(['key_val']); + bloc.addKeyValue('key_val'); expect( bloc.state, @@ -629,7 +629,7 @@ void main() { keyValues: const {'foo': 'bar'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleKeyValues(['foo:bar']); + bloc.addKeyValue('foo:bar'); expect( bloc.state, @@ -648,7 +648,7 @@ void main() { keyValues: const {'foo': 'bar'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleKeyValues(['Foo:bar']); + bloc.addKeyValue('Foo:bar'); expect( bloc.state, @@ -667,7 +667,7 @@ void main() { keyValues: const {'foo': 'bar'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleKeyValues(['foo:new']); + bloc.addKeyValue('foo:new'); expect( bloc.state, @@ -683,7 +683,7 @@ void main() { test('multiple entries', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleKeyValues(['key1:val1', 'key2:val2']); + bloc.updateKeyValues({'key1:val1', 'key2:val2'}); expect( bloc.state, @@ -699,7 +699,7 @@ void main() { test('multiple entries / invalid format', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleKeyValues(['key1:val1', 'key2_val2']); + bloc.updateKeyValues({'key1:val1', 'key2_val2'}); expect( bloc.state, @@ -719,7 +719,7 @@ void main() { keyValues: const {'foo': 'bar'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleKeyValues(['key1:val1', 'foo:bar']); + bloc.updateKeyValues({'key1:val1', 'foo:bar'}); expect( bloc.state, @@ -738,7 +738,7 @@ void main() { keyValues: const {'foo': 'bar'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleKeyValues(['Key1:val1', 'Foo:bar']); + bloc.updateKeyValues({'Key1:val1', 'Foo:bar'}); expect( bloc.state, @@ -757,7 +757,7 @@ void main() { keyValues: const {'foo': 'bar'}, ); final TodoCubit bloc = TodoCubit(todo: todo); - bloc.addMultipleKeyValues(['key1:val1', 'foo:new']); + bloc.updateKeyValues({'key1:val1', 'foo:new'}); expect( bloc.state,