From b946942625d2292c4b6ba891b30d6a4390a2053f Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 4 Dec 2019 15:22:31 -0700 Subject: [PATCH 01/34] Use main.dart for example entrypoint(s) + This is what dartlang expects, and we get docked for not doing it this way when pana analyzes the repo. --- example/context/{index.dart => main.dart} | 0 example/index.html | 4 ++-- example/{index.dart => main.dart} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename example/context/{index.dart => main.dart} (100%) rename example/{index.dart => main.dart} (100%) diff --git a/example/context/index.dart b/example/context/main.dart similarity index 100% rename from example/context/index.dart rename to example/context/main.dart diff --git a/example/index.html b/example/index.html index 93813b78a..87241a464 100644 --- a/example/index.html +++ b/example/index.html @@ -3,7 +3,7 @@ - over_react codegen testing + over_react builder testing @@ -20,6 +20,6 @@ - + diff --git a/example/index.dart b/example/main.dart similarity index 100% rename from example/index.dart rename to example/main.dart From 1f1b5d721cde4289c668636b4fe07d7faeec808e Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 4 Dec 2019 15:36:41 -0700 Subject: [PATCH 02/34] Split up existing examples into sub-directories --- example/builder/index.html | 25 +++++++++++++++++++ example/{ => builder}/main.dart | 10 ++++---- .../{ => src}/abstract_inheritance.dart | 0 .../abstract_inheritance.over_react.g.dart | 0 example/builder/{ => src}/basic.dart | 0 .../builder/{ => src}/basic.over_react.g.dart | 0 example/builder/{ => src}/basic_library.dart | 0 .../{ => src}/basic_library.over_react.g.dart | 0 .../builder/{ => src}/basic_with_state.dart | 0 .../basic_with_state.over_react.g.dart | 0 .../{ => src}/basic_with_type_params.dart | 0 .../basic_with_type_params.over_react.g.dart | 0 .../{ => src}/generic_inheritance_sub.dart | 0 .../generic_inheritance_sub.over_react.g.dart | 0 .../{ => src}/generic_inheritance_super.dart | 0 ...eneric_inheritance_super.over_react.g.dart | 0 .../{ => src}/part_of_basic_library.dart | 0 .../{ => src}/part_of_basic_library_2.dart | 0 .../builder/{ => src}/private_component.dart | 0 .../private_component.over_react.g.dart | 0 .../private_factory_public_component.dart | 0 ...factory_public_component.over_react.g.dart | 0 example/builder/{ => src}/props_mixin.dart | 0 .../{ => src}/props_mixin.over_react.g.dart | 0 example/builder/{ => src}/state_mixin.dart | 0 .../{ => src}/state_mixin.over_react.g.dart | 0 example/context/index.html | 25 +++++++++++++++++++ example/index.html | 13 +++++----- 28 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 example/builder/index.html rename example/{ => builder}/main.dart (87%) rename example/builder/{ => src}/abstract_inheritance.dart (100%) rename example/builder/{ => src}/abstract_inheritance.over_react.g.dart (100%) rename example/builder/{ => src}/basic.dart (100%) rename example/builder/{ => src}/basic.over_react.g.dart (100%) rename example/builder/{ => src}/basic_library.dart (100%) rename example/builder/{ => src}/basic_library.over_react.g.dart (100%) rename example/builder/{ => src}/basic_with_state.dart (100%) rename example/builder/{ => src}/basic_with_state.over_react.g.dart (100%) rename example/builder/{ => src}/basic_with_type_params.dart (100%) rename example/builder/{ => src}/basic_with_type_params.over_react.g.dart (100%) rename example/builder/{ => src}/generic_inheritance_sub.dart (100%) rename example/builder/{ => src}/generic_inheritance_sub.over_react.g.dart (100%) rename example/builder/{ => src}/generic_inheritance_super.dart (100%) rename example/builder/{ => src}/generic_inheritance_super.over_react.g.dart (100%) rename example/builder/{ => src}/part_of_basic_library.dart (100%) rename example/builder/{ => src}/part_of_basic_library_2.dart (100%) rename example/builder/{ => src}/private_component.dart (100%) rename example/builder/{ => src}/private_component.over_react.g.dart (100%) rename example/builder/{ => src}/private_factory_public_component.dart (100%) rename example/builder/{ => src}/private_factory_public_component.over_react.g.dart (100%) rename example/builder/{ => src}/props_mixin.dart (100%) rename example/builder/{ => src}/props_mixin.over_react.g.dart (100%) rename example/builder/{ => src}/state_mixin.dart (100%) rename example/builder/{ => src}/state_mixin.over_react.g.dart (100%) create mode 100644 example/context/index.html diff --git a/example/builder/index.html b/example/builder/index.html new file mode 100644 index 000000000..fdf4d1ff3 --- /dev/null +++ b/example/builder/index.html @@ -0,0 +1,25 @@ + + + + + + over_react builder testing + + + + + + + + + + + +
+ + + + + + + diff --git a/example/main.dart b/example/builder/main.dart similarity index 87% rename from example/main.dart rename to example/builder/main.dart index 5d8b9fcb5..8568a37a8 100644 --- a/example/main.dart +++ b/example/builder/main.dart @@ -3,11 +3,11 @@ import 'dart:html'; import 'package:over_react/over_react.dart'; import 'package:react/react_dom.dart' as react_dom; -import './builder/abstract_inheritance.dart'; -import './builder/basic.dart'; -import './builder/basic_library.dart'; -import './builder/generic_inheritance_sub.dart'; -import './builder/generic_inheritance_super.dart'; +import './src/abstract_inheritance.dart'; +import './src/basic.dart'; +import './src/basic_library.dart'; +import './src/generic_inheritance_sub.dart'; +import './src/generic_inheritance_super.dart'; main() { setClientConfiguration(); diff --git a/example/builder/abstract_inheritance.dart b/example/builder/src/abstract_inheritance.dart similarity index 100% rename from example/builder/abstract_inheritance.dart rename to example/builder/src/abstract_inheritance.dart diff --git a/example/builder/abstract_inheritance.over_react.g.dart b/example/builder/src/abstract_inheritance.over_react.g.dart similarity index 100% rename from example/builder/abstract_inheritance.over_react.g.dart rename to example/builder/src/abstract_inheritance.over_react.g.dart diff --git a/example/builder/basic.dart b/example/builder/src/basic.dart similarity index 100% rename from example/builder/basic.dart rename to example/builder/src/basic.dart diff --git a/example/builder/basic.over_react.g.dart b/example/builder/src/basic.over_react.g.dart similarity index 100% rename from example/builder/basic.over_react.g.dart rename to example/builder/src/basic.over_react.g.dart diff --git a/example/builder/basic_library.dart b/example/builder/src/basic_library.dart similarity index 100% rename from example/builder/basic_library.dart rename to example/builder/src/basic_library.dart diff --git a/example/builder/basic_library.over_react.g.dart b/example/builder/src/basic_library.over_react.g.dart similarity index 100% rename from example/builder/basic_library.over_react.g.dart rename to example/builder/src/basic_library.over_react.g.dart diff --git a/example/builder/basic_with_state.dart b/example/builder/src/basic_with_state.dart similarity index 100% rename from example/builder/basic_with_state.dart rename to example/builder/src/basic_with_state.dart diff --git a/example/builder/basic_with_state.over_react.g.dart b/example/builder/src/basic_with_state.over_react.g.dart similarity index 100% rename from example/builder/basic_with_state.over_react.g.dart rename to example/builder/src/basic_with_state.over_react.g.dart diff --git a/example/builder/basic_with_type_params.dart b/example/builder/src/basic_with_type_params.dart similarity index 100% rename from example/builder/basic_with_type_params.dart rename to example/builder/src/basic_with_type_params.dart diff --git a/example/builder/basic_with_type_params.over_react.g.dart b/example/builder/src/basic_with_type_params.over_react.g.dart similarity index 100% rename from example/builder/basic_with_type_params.over_react.g.dart rename to example/builder/src/basic_with_type_params.over_react.g.dart diff --git a/example/builder/generic_inheritance_sub.dart b/example/builder/src/generic_inheritance_sub.dart similarity index 100% rename from example/builder/generic_inheritance_sub.dart rename to example/builder/src/generic_inheritance_sub.dart diff --git a/example/builder/generic_inheritance_sub.over_react.g.dart b/example/builder/src/generic_inheritance_sub.over_react.g.dart similarity index 100% rename from example/builder/generic_inheritance_sub.over_react.g.dart rename to example/builder/src/generic_inheritance_sub.over_react.g.dart diff --git a/example/builder/generic_inheritance_super.dart b/example/builder/src/generic_inheritance_super.dart similarity index 100% rename from example/builder/generic_inheritance_super.dart rename to example/builder/src/generic_inheritance_super.dart diff --git a/example/builder/generic_inheritance_super.over_react.g.dart b/example/builder/src/generic_inheritance_super.over_react.g.dart similarity index 100% rename from example/builder/generic_inheritance_super.over_react.g.dart rename to example/builder/src/generic_inheritance_super.over_react.g.dart diff --git a/example/builder/part_of_basic_library.dart b/example/builder/src/part_of_basic_library.dart similarity index 100% rename from example/builder/part_of_basic_library.dart rename to example/builder/src/part_of_basic_library.dart diff --git a/example/builder/part_of_basic_library_2.dart b/example/builder/src/part_of_basic_library_2.dart similarity index 100% rename from example/builder/part_of_basic_library_2.dart rename to example/builder/src/part_of_basic_library_2.dart diff --git a/example/builder/private_component.dart b/example/builder/src/private_component.dart similarity index 100% rename from example/builder/private_component.dart rename to example/builder/src/private_component.dart diff --git a/example/builder/private_component.over_react.g.dart b/example/builder/src/private_component.over_react.g.dart similarity index 100% rename from example/builder/private_component.over_react.g.dart rename to example/builder/src/private_component.over_react.g.dart diff --git a/example/builder/private_factory_public_component.dart b/example/builder/src/private_factory_public_component.dart similarity index 100% rename from example/builder/private_factory_public_component.dart rename to example/builder/src/private_factory_public_component.dart diff --git a/example/builder/private_factory_public_component.over_react.g.dart b/example/builder/src/private_factory_public_component.over_react.g.dart similarity index 100% rename from example/builder/private_factory_public_component.over_react.g.dart rename to example/builder/src/private_factory_public_component.over_react.g.dart diff --git a/example/builder/props_mixin.dart b/example/builder/src/props_mixin.dart similarity index 100% rename from example/builder/props_mixin.dart rename to example/builder/src/props_mixin.dart diff --git a/example/builder/props_mixin.over_react.g.dart b/example/builder/src/props_mixin.over_react.g.dart similarity index 100% rename from example/builder/props_mixin.over_react.g.dart rename to example/builder/src/props_mixin.over_react.g.dart diff --git a/example/builder/state_mixin.dart b/example/builder/src/state_mixin.dart similarity index 100% rename from example/builder/state_mixin.dart rename to example/builder/src/state_mixin.dart diff --git a/example/builder/state_mixin.over_react.g.dart b/example/builder/src/state_mixin.over_react.g.dart similarity index 100% rename from example/builder/state_mixin.over_react.g.dart rename to example/builder/src/state_mixin.over_react.g.dart diff --git a/example/context/index.html b/example/context/index.html new file mode 100644 index 000000000..c76de450d --- /dev/null +++ b/example/context/index.html @@ -0,0 +1,25 @@ + + + + + + over_react context example + + + + + + + + + + + +
+ + + + + + + diff --git a/example/index.html b/example/index.html index 87241a464..c0bd086f6 100644 --- a/example/index.html +++ b/example/index.html @@ -3,7 +3,7 @@ - over_react builder testing + OverReact Examples @@ -15,11 +15,10 @@ -
- - - - - +

OverReact Examples

+ From 2fe31795cf9c0ba5caeb552422a2b21adb270401 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Thu, 5 Dec 2019 16:07:52 -0700 Subject: [PATCH 03/34] Add over_react_redux todo example app --- analysis_options.yaml | 1 + app/over_react_redux/todo_client/.gitignore | 23 ++ app/over_react_redux/todo_client/README.md | 27 ++ .../todo_client/analysis_options.yaml | 90 ++++++ app/over_react_redux/todo_client/build.yaml | 0 .../todo_client/lib/sass/src/_constants.scss | 1 + .../todo_client/lib/sass/src/_empty_view.scss | 61 +++++ .../lib/sass/src/_scaffolding.scss | 13 + .../todo_client/lib/sass/src/_todo_list.scss | 16 ++ .../todo_client/lib/sass/styles.css | 93 +++++++ .../todo_client/lib/sass/styles.css.map | 1 + .../todo_client/lib/sass/styles.scss | 4 + .../todo_client/lib/src/actions.dart | 112 ++++++++ .../todo_client/lib/src/actions.g.dart | 20 ++ .../todo_client/lib/src/components/app.dart | 114 ++++++++ .../lib/src/components/app_bar/app_bar.dart | 39 +++ .../app_bar/app_bar_local_storage_menu.dart | 146 ++++++++++ .../local_storage_menu_item_input.dart | 104 +++++++ .../components/app_bar/save_as_menu_item.dart | 73 +++++ .../app_bar/saved_data_menu_item.dart | 163 +++++++++++ .../lib/src/components/create_input.dart | 55 ++++ .../components/shared/avatar_with_colors.dart | 137 ++++++++++ .../src/components/shared/display_list.dart | 51 ++++ .../lib/src/components/shared/empty_view.dart | 147 ++++++++++ .../hoverable_item_component_mixin.dart | 52 ++++ .../shared/hoverable_item_mixin.dart | 19 ++ .../shared/list_item_component_mixin.dart | 121 ++++++++ .../list_item_expansion_panel_summary.dart | 95 +++++++ .../components/shared/list_item_mixin.dart | 39 +++ .../src/components/shared/material_ui.dart | 209 ++++++++++++++ .../src/components/shared/menu_overlay.dart | 99 +++++++ .../shared/todo_item_text_field.dart | 82 ++++++ .../lib/src/components/task_count.dart | 83 ++++++ .../lib/src/components/todo_list.dart | 53 ++++ .../lib/src/components/todo_list_item.dart | 258 ++++++++++++++++++ .../lib/src/components/user_list.dart | 52 ++++ .../lib/src/components/user_list_item.dart | 216 +++++++++++++++ .../lib/src/components/user_selector.dart | 99 +++++++ .../src/components/user_selector_trigger.dart | 42 +++ .../todo_client/lib/src/local_storage.dart | 120 ++++++++ .../lib/src/models/base_model.dart | 6 + .../todo_client/lib/src/models/todo.dart | 51 ++++ .../todo_client/lib/src/models/todo.g.dart | 26 ++ .../todo_client/lib/src/models/user.dart | 35 +++ .../todo_client/lib/src/models/user.g.dart | 20 ++ .../todo_client/lib/src/store.dart | 190 +++++++++++++ .../todo_client/lib/src/store.g.dart | 45 +++ .../todo_client/lib/src/utils.dart | 23 ++ .../todo_client/lib/todo_client.dart | 4 + app/over_react_redux/todo_client/pubspec.yaml | 20 ++ app/over_react_redux/todo_client/smithy.yml | 19 ++ .../todo_client/web/index.html | 24 ++ .../todo_client/web/js/material-ui-config.js | 24 ++ .../todo_client/web/main.dart | 18 ++ doc/over_react_redux_documentation.md | 26 +- example/index.html | 6 + pubspec.yaml | 2 +- 57 files changed, 3663 insertions(+), 6 deletions(-) create mode 100644 app/over_react_redux/todo_client/.gitignore create mode 100644 app/over_react_redux/todo_client/README.md create mode 100644 app/over_react_redux/todo_client/analysis_options.yaml create mode 100644 app/over_react_redux/todo_client/build.yaml create mode 100644 app/over_react_redux/todo_client/lib/sass/src/_constants.scss create mode 100644 app/over_react_redux/todo_client/lib/sass/src/_empty_view.scss create mode 100644 app/over_react_redux/todo_client/lib/sass/src/_scaffolding.scss create mode 100644 app/over_react_redux/todo_client/lib/sass/src/_todo_list.scss create mode 100644 app/over_react_redux/todo_client/lib/sass/styles.css create mode 100644 app/over_react_redux/todo_client/lib/sass/styles.css.map create mode 100644 app/over_react_redux/todo_client/lib/sass/styles.scss create mode 100644 app/over_react_redux/todo_client/lib/src/actions.dart create mode 100644 app/over_react_redux/todo_client/lib/src/actions.g.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/app.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar_local_storage_menu.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/app_bar/local_storage_menu_item_input.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/app_bar/save_as_menu_item.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/create_input.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/shared/display_list.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/shared/empty_view.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/shared/hoverable_item_component_mixin.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/shared/hoverable_item_mixin.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/shared/list_item_component_mixin.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/shared/list_item_mixin.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/shared/menu_overlay.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/shared/todo_item_text_field.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/task_count.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/todo_list.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/user_list.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/user_list_item.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/user_selector.dart create mode 100644 app/over_react_redux/todo_client/lib/src/components/user_selector_trigger.dart create mode 100644 app/over_react_redux/todo_client/lib/src/local_storage.dart create mode 100644 app/over_react_redux/todo_client/lib/src/models/base_model.dart create mode 100644 app/over_react_redux/todo_client/lib/src/models/todo.dart create mode 100644 app/over_react_redux/todo_client/lib/src/models/todo.g.dart create mode 100644 app/over_react_redux/todo_client/lib/src/models/user.dart create mode 100644 app/over_react_redux/todo_client/lib/src/models/user.g.dart create mode 100644 app/over_react_redux/todo_client/lib/src/store.dart create mode 100644 app/over_react_redux/todo_client/lib/src/store.g.dart create mode 100644 app/over_react_redux/todo_client/lib/src/utils.dart create mode 100644 app/over_react_redux/todo_client/lib/todo_client.dart create mode 100644 app/over_react_redux/todo_client/pubspec.yaml create mode 100644 app/over_react_redux/todo_client/smithy.yml create mode 100644 app/over_react_redux/todo_client/web/index.html create mode 100644 app/over_react_redux/todo_client/web/js/material-ui-config.js create mode 100644 app/over_react_redux/todo_client/web/main.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 9bde00518..2acb77cbe 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,6 +4,7 @@ analyzer: exclude: - _site/** - test_fixtures/** + - app/** linter: rules: # ------------------- diff --git a/app/over_react_redux/todo_client/.gitignore b/app/over_react_redux/todo_client/.gitignore new file mode 100644 index 000000000..6646773d0 --- /dev/null +++ b/app/over_react_redux/todo_client/.gitignore @@ -0,0 +1,23 @@ +.packages +.pub +packages +*.over_react.g.dart + +# Files created by dart2js +*.dart.js +*.part.js +*.js.deps +*.js.map +*.info.json + +# Directory created by dartdoc +doc/api/ + +# JetBrains IDEs +.idea/ +*.iml +*.ipr +*.iws +.arcconfig +.dart_tool +.vscode diff --git a/app/over_react_redux/todo_client/README.md b/app/over_react_redux/todo_client/README.md new file mode 100644 index 000000000..42e73a791 --- /dev/null +++ b/app/over_react_redux/todo_client/README.md @@ -0,0 +1,27 @@ +# OverReact Redux Example "Todo" Application + +## Running +``` +pub get +webdev serve +``` + +Open [http://localhost:8080](http://localhost:8080). + +## Using Redux Dev Tools +This example app is already set up to utilize a `DevToolsStore` so +that you can inspect the data using the Redux Devtools extension! + +Download the Redux Devtools extension: + - [Google Chrome Extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en) + - [Firefox Extension](https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/) + +> Additional information about `redux_dev_tools` and `DevToolsStore`s can be found [here](https://github.com/brianegan/redux_dev_tools#redux_dev_tools) + +## Compiling Sass + +To compile the `.scss` file(s) into CSS, run + +``` +pub run w_common:compile_sass -s expanded +``` diff --git a/app/over_react_redux/todo_client/analysis_options.yaml b/app/over_react_redux/todo_client/analysis_options.yaml new file mode 100644 index 000000000..61454ab90 --- /dev/null +++ b/app/over_react_redux/todo_client/analysis_options.yaml @@ -0,0 +1,90 @@ +include: package:pedantic/analysis_options.1.8.0.yaml + +linter: + rules: + # ------------------- + # Pedantic + # ------------------- + avoid_empty_else: true + avoid_init_to_null: true + avoid_relative_lib_imports: true + avoid_return_types_on_setters: true + avoid_shadowing_type_parameters: true + avoid_types_as_parameter_names: true + curly_braces_in_flow_control_structures: true + empty_catches: true + empty_constructor_bodies: true + empty_statements: true + library_names: true + library_prefixes: true + no_duplicate_case_values: true + null_closures: true + prefer_contains: true + prefer_equal_for_default_values: true + prefer_is_empty: true + prefer_is_not_empty: true + prefer_iterable_whereType: true + recursive_getters: true + slash_for_doc_comments: true + type_init_formals: true + unawaited_futures: true + unnecessary_const: true + unnecessary_new: true + unnecessary_null_in_if_null_operators: true + unrelated_type_equality_checks: true + use_rethrow_when_possible: true + valid_regexps: true + + # ------------------- + # Other + # ------------------- + annotate_overrides: true + avoid_bool_literals_in_conditional_expressions: true + avoid_classes_with_only_static_members: true + avoid_double_and_int_checks: true + avoid_null_checks_in_equality_operators: true + avoid_returning_null_for_void: true + avoid_returning_this: true + avoid_setters_without_getters: true + avoid_single_cascade_in_expression_statements: true + avoid_slow_async_io: true + avoid_types_on_closure_parameters: false + avoid_unused_constructor_parameters: true + avoid_void_async: true + await_only_futures: true + camel_case_types: true + cancel_subscriptions: true + close_sinks: true + comment_references: true + hash_and_equals: true + implementation_imports: true + iterable_contains_unrelated_type: true + list_remove_unrelated_type: true + literal_only_boolean_expressions: true + no_adjacent_strings_in_list: true + package_names: true + prefer_collection_literals: true + prefer_conditional_assignment: true + prefer_const_declarations: true + prefer_constructors_over_static_methods: true + prefer_function_declarations_over_variables: true + prefer_generic_function_type_aliases: true + prefer_if_null_operators: true + prefer_initializing_formals: true + prefer_null_aware_operators: true + prefer_single_quotes: true + prefer_spread_collections: true + prefer_typing_uninitialized_variables: true + provide_deprecation_message: true + test_types_in_equals: true + unnecessary_await_in_return: true + unnecessary_brace_in_string_interps: true + unnecessary_getters_setters: true + unnecessary_lambdas: true + unnecessary_null_aware_assignments: true + unnecessary_overrides: true + unnecessary_statements: true + unsafe_html: true + use_function_type_syntax_for_parameters: true + use_to_and_as_if_applicable: true + void_checks: true diff --git a/app/over_react_redux/todo_client/build.yaml b/app/over_react_redux/todo_client/build.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/app/over_react_redux/todo_client/lib/sass/src/_constants.scss b/app/over_react_redux/todo_client/lib/sass/src/_constants.scss new file mode 100644 index 000000000..8178530d1 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/sass/src/_constants.scss @@ -0,0 +1 @@ +$max-app-width: 600px; diff --git a/app/over_react_redux/todo_client/lib/sass/src/_empty_view.scss b/app/over_react_redux/todo_client/lib/sass/src/_empty_view.scss new file mode 100644 index 000000000..5a4bdc12e --- /dev/null +++ b/app/over_react_redux/todo_client/lib/sass/src/_empty_view.scss @@ -0,0 +1,61 @@ +.empty-view { + margin-top: 10%; + padding: 1.5rem; + text-align: center; + color: #8a8a8a; +} + +.empty-view p:last-child { + margin-bottom: 0; +} + +.empty-view__message-heading { + padding: 0; + margin-top: 0; + margin-bottom: 1.6rem; + font-size: 1.8rem; + font-weight: normal; + color: inherit; +} + +.empty-view__icon { + width: 8rem; + height: 8rem; + font-size: 8rem; + margin-bottom: 2rem; + color: #cbcbcb; +} + +.empty-view-vblock { + display: flex; + flex: 1 1 0%; + flex-flow: column nowrap; + align-items: stretch; + justify-content: center; + height: 100%; +} + +.empty-view-vblock .empty-view { + margin-top: 0; +} + +.empty-view-page-frame { + height: 100vh; + overflow: hidden; + position: relative; + display: flex; + flex: 1 1 0%; + flex-direction: column; + flex-wrap: nowrap; + align-items: stretch; + justify-content: center; +} + +.empty-view-page-frame > .MuiGrid-item { + max-width: none; + min-height: 0; +} + +.empty-view-page-frame .empty-view { + margin-top: 0; +} diff --git a/app/over_react_redux/todo_client/lib/sass/src/_scaffolding.scss b/app/over_react_redux/todo_client/lib/sass/src/_scaffolding.scss new file mode 100644 index 000000000..58de83068 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/sass/src/_scaffolding.scss @@ -0,0 +1,13 @@ +body { + overflow: hidden; +} + +.app-shell, +#todo-container { + width: 100vw; + height: 100vh; +} + +.hide-using-aria[aria-hidden="true"] { + visibility: hidden; +} diff --git a/app/over_react_redux/todo_client/lib/sass/src/_todo_list.scss b/app/over_react_redux/todo_client/lib/sass/src/_todo_list.scss new file mode 100644 index 000000000..922eb34f1 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/sass/src/_todo_list.scss @@ -0,0 +1,16 @@ +.app-content { + overflow-y: auto; + padding-top: 32px; + height: calc(100% - 32px); +} + +@media screen and (min-width: 600px) { + .app-content { + overflow-y: hidden; + } + + .app-content__container, + .app-content__container-grid { + height: 100%; + } +} diff --git a/app/over_react_redux/todo_client/lib/sass/styles.css b/app/over_react_redux/todo_client/lib/sass/styles.css new file mode 100644 index 000000000..5889bbff6 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/sass/styles.css @@ -0,0 +1,93 @@ +body { + overflow: hidden; +} + +.app-shell, +#todo-container { + width: 100vw; + height: 100vh; +} + +.hide-using-aria[aria-hidden=true] { + visibility: hidden; +} + +.app-content { + overflow-y: auto; + padding-top: 32px; + height: calc(100% - 32px); +} + +@media screen and (min-width: 600px) { + .app-content { + overflow-y: hidden; + } + + .app-content__container, +.app-content__container-grid { + height: 100%; + } +} +.empty-view { + margin-top: 10%; + padding: 1.5rem; + text-align: center; + color: #8a8a8a; +} + +.empty-view p:last-child { + margin-bottom: 0; +} + +.empty-view__message-heading { + padding: 0; + margin-top: 0; + margin-bottom: 1.6rem; + font-size: 1.8rem; + font-weight: normal; + color: inherit; +} + +.empty-view__icon { + width: 8rem; + height: 8rem; + font-size: 8rem; + margin-bottom: 2rem; + color: #cbcbcb; +} + +.empty-view-vblock { + display: flex; + flex: 1 1 0%; + flex-flow: column nowrap; + align-items: stretch; + justify-content: center; + height: 100%; +} + +.empty-view-vblock .empty-view { + margin-top: 0; +} + +.empty-view-page-frame { + height: 100vh; + overflow: hidden; + position: relative; + display: flex; + flex: 1 1 0%; + flex-direction: column; + flex-wrap: nowrap; + align-items: stretch; + justify-content: center; +} + +.empty-view-page-frame > .MuiGrid-item { + max-width: none; + min-height: 0; +} + +.empty-view-page-frame .empty-view { + margin-top: 0; +} + +/*# sourceMappingURL=styles.css.map */ \ No newline at end of file diff --git a/app/over_react_redux/todo_client/lib/sass/styles.css.map b/app/over_react_redux/todo_client/lib/sass/styles.css.map new file mode 100644 index 000000000..a02664d57 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/sass/styles.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["file:///Users/aaronlademann/workspaces/wf/over_react/app/over_react_redux/todo_client/lib/sass/src/_scaffolding.scss","file:///Users/aaronlademann/workspaces/wf/over_react/app/over_react_redux/todo_client/lib/sass/src/_todo_list.scss","file:///Users/aaronlademann/workspaces/wf/over_react/app/over_react_redux/todo_client/lib/sass/src/_empty_view.scss"],"names":[],"mappings":"AAAA;EACE;;;AAGF;AAAA;EAEE;EACA;;;AAGF;EACE;;;ACXF;EACE;EACA;EACA;;;AAGF;EACE;IACE;;;EAGF;AAAA;IAEE;;;ACbJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE"} \ No newline at end of file diff --git a/app/over_react_redux/todo_client/lib/sass/styles.scss b/app/over_react_redux/todo_client/lib/sass/styles.scss new file mode 100644 index 000000000..edfffaa68 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/sass/styles.scss @@ -0,0 +1,4 @@ +@import 'src/constants'; +@import 'src/scaffolding'; +@import 'src/todo_list'; +@import 'src/empty_view'; diff --git a/app/over_react_redux/todo_client/lib/src/actions.dart b/app/over_react_redux/todo_client/lib/src/actions.dart new file mode 100644 index 000000000..6cecfb67f --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/actions.dart @@ -0,0 +1,112 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:todo_client/src/models/todo.dart'; +import 'package:todo_client/src/models/user.dart'; + +part 'actions.g.dart'; + +class Action { + Action({this.type, this.value}); + + final String type; + final T value; + + Map toJson() { + return {'value': this.value}; + } +} + +class LoadStateFromLocalStorageAction extends Action { + LoadStateFromLocalStorageAction([String value]) : super(type: 'LOAD_STATE_FROM_LOCAL_STORAGE', value: value); +} + +@JsonSerializable() +class SaveLocalStorageStateAsPayload { + final String name; + final String previousName; + + SaveLocalStorageStateAsPayload(this.name, {this.previousName}); + + factory SaveLocalStorageStateAsPayload.fromJson(Map json) => _$SaveLocalStorageStateAsPayloadFromJson(json); + Map toJson() => _$SaveLocalStorageStateAsPayloadToJson(this); +} + +class SaveLocalStorageStateAsAction extends Action { + SaveLocalStorageStateAsAction([SaveLocalStorageStateAsPayload value]) : super(type: 'SAVE_LOCAL_STORAGE_STATE_AS', value: value); +} + +// ------------ ITEM ACTIONS ------------------ + +class SelectTodoAction extends Action { + SelectTodoAction([String value]) : super(type: 'SELECT_TODO', value: value); +} + +class DeselectTodoAction extends Action { + DeselectTodoAction([String value]) : super(type: 'DESELECT_TODO', value: value); +} + +class BeginEditTodoAction extends Action { + BeginEditTodoAction([String value]) : super(type: 'EDIT_TODO_BEGIN', value: value); +} + +class FinishEditTodoAction extends Action { + FinishEditTodoAction([String value]) : super(type: 'EDIT_TODO_FINISH', value: value); +} + +class HighlightTodosAction extends Action> { + HighlightTodosAction([List value]) : super(type: 'HIGHLIGHT_TODOS', value: value); +} + +class UnHighlightTodosAction extends Action> { + UnHighlightTodosAction([List value]) : super(type: 'UNHIGHLIGHT_TODOS', value: value); +} + +class AddTodoAction extends Action { + AddTodoAction([Todo value]) : super(type: 'ADD_TODO', value: value); +} + +class RemoveTodoAction extends Action { + RemoveTodoAction([String value]) : super(type: 'REMOVE_TODO', value: value); +} + +class UpdateTodoAction extends Action { + UpdateTodoAction([Todo value]) : super(type: 'UPDATE_TODO', value: value); +} + +// ------------ USER ACTIONS ------------------ + +class SelectUserAction extends Action { + SelectUserAction([String value]) : super(type: 'SELECT_USER', value: value); +} + +class DeselectUserAction extends Action { + DeselectUserAction([String value]) : super(type: 'DESELECT_USER', value: value); +} + +class BeginEditUserAction extends Action { + BeginEditUserAction([String value]) : super(type: 'EDIT_USER_BEGIN', value: value); +} + +class FinishEditUserAction extends Action { + FinishEditUserAction([String value]) : super(type: 'EDIT_USER_FINISH', value: value); +} + +class HighlightUsersAction extends Action> { + HighlightUsersAction([List value]) : super(type: 'HIGHLIGHT_USERS', value: value); +} + +class UnHighlightUsersAction extends Action> { + UnHighlightUsersAction([List value]) : super(type: 'UNHIGHLIGHT_USERS', value: value); +} + +class AddUserAction extends Action { + AddUserAction([User value]) : super(type: 'ADD_USER', value: value); +} + +class RemoveUserAction extends Action { + RemoveUserAction([String value]) : super(type: 'REMOVE_USER', value: value); +} + +class UpdateUserAction extends Action { + UpdateUserAction([User value]) : super(type: 'UPDATE_USER', value: value); +} diff --git a/app/over_react_redux/todo_client/lib/src/actions.g.dart b/app/over_react_redux/todo_client/lib/src/actions.g.dart new file mode 100644 index 000000000..a243410d9 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/actions.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'actions.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SaveLocalStorageStateAsPayload _$SaveLocalStorageStateAsPayloadFromJson( + Map json) { + return SaveLocalStorageStateAsPayload(json['name'] as String, + previousName: json['previousName'] as String); +} + +Map _$SaveLocalStorageStateAsPayloadToJson( + SaveLocalStorageStateAsPayload instance) => + { + 'name': instance.name, + 'previousName': instance.previousName + }; diff --git a/app/over_react_redux/todo_client/lib/src/components/app.dart b/app/over_react_redux/todo_client/lib/src/components/app.dart new file mode 100644 index 000000000..73491fab8 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/app.dart @@ -0,0 +1,114 @@ +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; + +import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/store.dart'; +import 'package:todo_client/src/models/todo.dart'; +import 'package:todo_client/src/models/user.dart'; +import 'package:todo_client/src/components/app_bar/app_bar.dart'; +import 'package:todo_client/src/components/create_input.dart'; +import 'package:todo_client/src/components/shared/material_ui.dart'; +import 'package:todo_client/src/components/todo_list.dart'; +import 'package:todo_client/src/components/user_list.dart'; + +// ignore: uri_has_not_been_generated +part 'app.over_react.g.dart'; + +UiFactory ConnectedTodoApp = connect( + mapDispatchToProps: (dispatch) { + return (TodoApp() + ..createTodo = (description) { + dispatch(AddTodoAction(Todo(description: description))); + } + ..createUser = (name) { + dispatch(AddUserAction(User(name: name))); + } + ); + }, +)(TodoApp); + +@Factory() +UiFactory TodoApp = + // ignore: undefined_identifier + _$TodoApp; + +@Props() +class _$TodoAppProps extends UiProps with ConnectPropsMixin { + Function(String description) createTodo; + + Function(String name) createUser; +} + +@Component2() +class TodoAppComponent extends UiComponent2 { + @override + render() { + return Fragment()( + (TodoAppBar()..key = 'appBar')(), + Box({ + 'key': 'appContent', + 'className': 'app-content', + }, [ + CssBaseline({'key': 'cssBaseline'}), + Container({'key': 'container', 'maxWidth': 'lg', 'className': 'app-content__container'}, + Grid({'container': true, 'direction': 'row', 'spacing': 3, 'className': 'app-content__container-grid'}, [ + renderTodosColumn(), + renderUsersColumn(), + ]), + ), + ]) + ); + } + + ReactElement renderTodosColumn() { + return Grid( + { + 'key': 'todos', + 'container': true, + 'item': true, + 'sm': 8, + 'direction': 'column', + 'alignItems': 'stretch', + 'style': {'height': '100%'}, + }, + [ + (CreateInput() + ..key = 'todoInput' + ..autoFocus = true + ..label = 'New Todo' + ..placeholder = 'Create new Todo' + ..onCreate = props.createTodo + )(), + (ConnectedTodoList()..key = 'todoList')(), + ], + ); + } + + ReactElement renderUsersColumn() { + return Grid( + { + 'key': 'users', + 'container': true, + 'item': true, + 'sm': 4, + 'direction': 'column', + 'style': {'height': '100%'}, + }, + [ + (CreateInput() + ..key = 'userInput' + ..label = 'New User' + ..placeholder = 'Create new user' + ..onCreate = props.createUser + )(), + (ConnectedUserList()..key = 'userList')(), + ], + ); + } +} + +// ignore: mixin_of_non_class, undefined_class +class TodoAppProps extends _$TodoAppProps with _$TodoAppPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForTodoAppProps; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart new file mode 100644 index 000000000..83b15a60f --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart @@ -0,0 +1,39 @@ +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/components/app_bar/app_bar_local_storage_menu.dart'; +import 'package:todo_client/src/components/shared/material_ui.dart'; + +// ignore: uri_has_not_been_generated +part 'app_bar.over_react.g.dart'; + +@Factory() +UiFactory TodoAppBar = + // ignore: undefined_identifier + _$TodoAppBar; + +@Props() +class _$TodoAppBarProps extends UiProps {} + +@Component2() +class TodoAppBarComponent extends UiComponent2 { + @override + render() { + return Fragment()( + AppBar(props, + Toolbar({}, + Box({'flexGrow': 1}, + Typography({'variant': 'h6', 'className': classes['title']}, 'OverReact Todo Demo App'), + ), + ConnectedAppBarLocalStorageMenu()(), + ), + ), + Toolbar({'key': 'fakeAppBarToPushContentBelowFixedAppBar'}), + ); + } +} + +// ignore: mixin_of_non_class, undefined_class +class TodoAppBarProps extends _$TodoAppBarProps with _$TodoAppBarPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForTodoAppBarProps; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar_local_storage_menu.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar_local_storage_menu.dart new file mode 100644 index 000000000..080091e08 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar_local_storage_menu.dart @@ -0,0 +1,146 @@ +import 'dart:convert'; + +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; + +import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/local_storage.dart'; +import 'package:todo_client/src/store.dart'; +import 'package:todo_client/src/components/app_bar/save_as_menu_item.dart'; +import 'package:todo_client/src/components/app_bar/saved_data_menu_item.dart'; +import 'package:todo_client/src/components/shared/material_ui.dart'; +import 'package:todo_client/src/components/shared/menu_overlay.dart'; + +// ignore: uri_has_not_been_generated +part 'app_bar_local_storage_menu.over_react.g.dart'; + +UiFactory ConnectedAppBarLocalStorageMenu = connect( + mapStateToProps: (state) { + return (AppBarLocalStorageMenu() + ..currentDataSetName = state.name + ..currentDataHasBeenModified = json.encode(localTodoAppStorage[state.name]) != json.encode(state.toJson()) + ); + }, +)(AppBarLocalStorageMenu); + +@Factory() +UiFactory AppBarLocalStorageMenu = + // ignore: undefined_identifier + _$AppBarLocalStorageMenu; + +@Props() +class _$AppBarLocalStorageMenuProps extends MenuOverlayProps with ConnectPropsMixin { + String currentDataSetName; + List savedLocalStorageStateNames; + bool currentDataHasBeenModified; +} + +@Component2() +class AppBarLocalStorageMenuComponent extends UiComponent2 { + final _overlayRef = createRef(); + bool get _currentStateKeyIsReadOnly => TodoAppLocalStorage.reservedStateKeys.contains(props.currentDataSetName); + + @override + render() { + return (MenuOverlay() + ..modifyProps(addUnconsumedProps) + ..trigger = Button({'color': 'inherit'}, 'Data: ${props.currentDataSetName}') + ..useDerivedMaxWidth = true + ..ref = _overlayRef + )( + _renderSaveMenuItem(), + _renderSaveAsMenuItem(), + MenuItem({ + 'onClick': (_) { + _loadFromLocalStorage(TodoAppLocalStorage.defaultStateKey); + }, + }, 'Load Default Data'), + MenuItem({ + 'onClick': (_) { + _loadFromLocalStorage(TodoAppLocalStorage.emptyStateKey); + }, + }, 'Load Empty Data'), + _renderSavedStateMenuItems(), + ); + } + + ReactElement _renderSaveMenuItem() { + if (!props.currentDataHasBeenModified || _currentStateKeyIsReadOnly) return null; + + return MenuItem({ + 'onClick': (SyntheticMouseEvent event) { + event.stopPropagation(); // Don't close the menu + _handleCurrentLocalStorageStateSaveAs(newStateName: props.currentDataSetName); + } + }, + 'Save ${props.currentDataSetName}', + ); + } + + ReactElement _renderSaveAsMenuItem() { + return (SaveAsMenuItem() + ..initialValue = _currentStateKeyIsReadOnly ? 'Copy of ${props.currentDataSetName}' : props.currentDataSetName + ..onSave = (newStateName) { + _handleCurrentLocalStorageStateSaveAs(newStateName: newStateName); + } + )(); + } + + ReactElement _renderSavedStateMenuItem(String stateKey) { + final json = localTodoAppStorage.mapOfCustomStatesJson[stateKey]; + return (SavedDataMenuItem() + ..key = json['name'] + ..['selected'] = json['name'] == props.currentDataSetName + ..localStorageKey = json['name'] + ..onSelect = _loadFromLocalStorage + ..onRename = (newStateName) { + _handleLocalStorageStateRename(newStateName, renamedFrom: json['name']); + } + ..onDelete = (stateName) { + localTodoAppStorage.remove(stateName); + forceUpdate(); + } + )(); + } + + List _renderSavedStateMenuItems() { + if (localTodoAppStorage.mapOfCustomStatesJson.isEmpty) return null; + + return [ + Divider({'key': 'divider'}), + (Dom.li()..key = 'dividerLabel')( + Typography({ + 'color': 'textSecondary', + 'display': 'block', + 'variant': 'caption', + 'style': {'margin': '5px 0 0 16px'}, + }, + 'Custom Data Sets', + ), + ), + ...localTodoAppStorage.mapOfCustomStatesJson.keys.map(_renderSavedStateMenuItem).toList(), + ]; + } + + void _handleLocalStorageStateRename(String newStateName, {String renamedFrom}) { + props.dispatch(SaveLocalStorageStateAsAction( + SaveLocalStorageStateAsPayload(newStateName, previousName: renamedFrom))); + } + + void _handleCurrentLocalStorageStateSaveAs({String newStateName}) { + props.dispatch(SaveLocalStorageStateAsAction(SaveLocalStorageStateAsPayload(newStateName))); + _loadFromLocalStorage(newStateName); + _overlayRef.current.close(); + } + + void _loadFromLocalStorage(String stateName) { + props.dispatch(LoadStateFromLocalStorageAction(stateName)); + _overlayRef.current.close(); + } +} + +// ignore: mixin_of_non_class, undefined_class +class AppBarLocalStorageMenuProps extends _$AppBarLocalStorageMenuProps with _$AppBarLocalStorageMenuPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForAppBarLocalStorageMenuProps; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/local_storage_menu_item_input.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/local_storage_menu_item_input.dart new file mode 100644 index 000000000..9ca073e13 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/local_storage_menu_item_input.dart @@ -0,0 +1,104 @@ +import 'dart:html'; + +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/local_storage.dart'; +import 'package:todo_client/src/components/shared/material_ui.dart'; + +// ignore: uri_has_not_been_generated +part 'local_storage_menu_item_input.over_react.g.dart'; + +@Factory() +UiFactory LocalStorageMenuItemInput = + // ignore: undefined_identifier + _$LocalStorageMenuItemInput; + +@Props(keyNamespace: '') +class _$LocalStorageMenuItemInputProps extends UiProps { + String initialValue; + bool error; + dynamic helperText; + @requiredProp + Function(String value) onSave; + Function(SyntheticKeyboardEvent event) onCancel; +} + +@State() +class _$LocalStorageMenuItemInputState extends UiState { + String currentValue; +} + +@Component2() +class LocalStorageMenuItemInputComponent + extends UiStatefulComponent2 { + @override + get defaultProps => (newProps()..error = false); + + @override + get initialState => (newState()..currentValue = props.initialValue); + + @override + render() { + final propsToForward = Map.of(props) + ..remove('initialValue') + ..remove('onSave') + ..remove('onCancel'); + + return TextField({ + 'autoFocus': true, + 'variant': 'outlined', + 'size': 'small', + 'fullWidth': true, + 'inputProps': { + 'size': props.initialValue.length, + }, + ...propsToForward, + 'value': state.currentValue, + 'error': props.error || !_isValidValue, + 'helperText': _inputHelperText, + 'onKeyDown': _handleInputKeyDown, + 'onClick': (SyntheticMouseEvent event) { + // Prevent the menu from closing when clicking inside the input + event.stopPropagation(); + }, + 'onChange': (SyntheticFormEvent event) { + setState(newState()..currentValue = event.target.value); + } + }); + } + + String get _trimmedValue => state.currentValue.trim(); + + bool get _isValidValue { + return _trimmedValue.isNotEmpty && !TodoAppLocalStorage.reservedStateKeys.contains(_trimmedValue); + } + + dynamic get _inputHelperText { + if (props.helperText != null) return props.helperText; + if (_isValidValue || _trimmedValue.isEmpty) return 'Press ENTER to save or ESC to cancel'; + + return '$_trimmedValue is a reserved value'; + } + + void _handleInputKeyDown(SyntheticKeyboardEvent event) { + // Prevent menu item autofocus based on the key entered matching the first letter of a menu item. + event.stopPropagation(); + if (event.keyCode == KeyCode.ENTER && _isValidValue) { + props.onSave(_trimmedValue); + } else if (event.keyCode == KeyCode.ESC && props.onCancel != null) { + props.onCancel(event); + } + } +} + +// ignore: mixin_of_non_class, undefined_class +class LocalStorageMenuItemInputProps extends _$LocalStorageMenuItemInputProps with _$LocalStorageMenuItemInputPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForLocalStorageMenuItemInputProps; +} + +// ignore: mixin_of_non_class, undefined_class +class LocalStorageMenuItemInputState extends _$LocalStorageMenuItemInputState with _$LocalStorageMenuItemInputStateAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const StateMeta meta = _$metaForLocalStorageMenuItemInputState; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/save_as_menu_item.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/save_as_menu_item.dart new file mode 100644 index 000000000..f990f0abd --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/save_as_menu_item.dart @@ -0,0 +1,73 @@ +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/components/app_bar/local_storage_menu_item_input.dart'; +import 'package:todo_client/src/components/shared/material_ui.dart'; + +// ignore: uri_has_not_been_generated +part 'save_as_menu_item.over_react.g.dart'; + +@Factory() +UiFactory SaveAsMenuItem = + // ignore: undefined_identifier + _$SaveAsMenuItem; + +@Props() +class _$SaveAsMenuItemProps extends UiProps { + String initialValue; + @requiredProp + Function(String newName) onSave; +} + +@State() +class _$SaveAsMenuItemState extends UiState { + bool isEditable; +} + +@Component2() +class SaveAsMenuItemComponent extends UiStatefulComponent2 { + @override + get defaultProps => (newProps()..initialValue = ''); + + @override + get initialState => (newState()..isEditable = false); + + @override + render() { + return MenuItem({ + 'onClick': _handleMenuItemClick, + 'style': {'minWidth': '300px'} + }, + state.isEditable ? _renderSaveAsTextInput() : 'Save As...' + ); + } + + ReactElement _renderSaveAsTextInput() { + return (LocalStorageMenuItemInput() + ..initialValue = props.initialValue + ..onSave = (newValue) { + if (newValue != props.initialValue) { + props.onSave(newValue); + } + setState(newState()..isEditable = false); + } + )(); + } + + void _handleMenuItemClick(_) { + if (!state.isEditable) { + setState(newState()..isEditable = true); + } + } +} + +// ignore: mixin_of_non_class, undefined_class +class SaveAsMenuItemProps extends _$SaveAsMenuItemProps with _$SaveAsMenuItemPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForSaveAsMenuItemProps; +} + +// ignore: mixin_of_non_class, undefined_class +class SaveAsMenuItemState extends _$SaveAsMenuItemState with _$SaveAsMenuItemStateAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const StateMeta meta = _$metaForSaveAsMenuItemState; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart new file mode 100644 index 000000000..52a22cda8 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart @@ -0,0 +1,163 @@ +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/local_storage.dart'; +import 'package:todo_client/src/components/app_bar/local_storage_menu_item_input.dart'; +import 'package:todo_client/src/components/shared/hoverable_item_mixin.dart'; +import 'package:todo_client/src/components/shared/material_ui.dart'; +import 'package:todo_client/src/components/shared/menu_overlay.dart'; + +// ignore: uri_has_not_been_generated +part 'saved_data_menu_item.over_react.g.dart'; + +@Factory() +UiFactory SavedDataMenuItem = + // ignore: undefined_identifier + _$SavedDataMenuItem; + +@Props(keyNamespace: '') +class _$SavedDataMenuItemProps extends UiProps { + @requiredProp + String localStorageKey; + @requiredProp + Function(String localStorageKey) onSelect; + @requiredProp + Function(String localStorageKey) onDelete; + @requiredProp + Function(String localStorageKey) onRename; +} + +@State() +class _$SavedDataMenuItemState extends MenuOverlayState + with HoverableItemStateMixin, + // ignore: mixin_of_non_class, undefined_class + $HoverableItemStateMixin { + bool isEditable; +} + +@Component2() +class SavedDataMenuItemComponent extends UiStatefulComponent2 + with HoverableItemMixin { + @override + get initialState => (newState() + ..addAll(super.initialState) + ..isEditable = false + ); + + @override + render() { + final propsToForward = Map.of(props) + ..remove('localStorageKey') + ..remove('onSelect') + ..remove('onDelete') + ..remove('onRename'); + + return MenuItem({ + ...propsToForward, + 'onClick': _handleMenuItemClick, + 'onMouseEnter': handleItemMouseEnter, + 'onMouseLeave': handleItemMouseLeave, + 'onFocus': handleChildFocus, + 'onBlur': handleChildBlur, + }, + state.isEditable ? _renderRenameTextInput() : _renderNameWithOptions(), + ); + } + + ReactElement _renderNameWithOptions() { + return Grid({'container': true, 'direction': 'row', 'style': {'flexWrap': false}}, + Box({...grow, 'pr': 1, 'justifyContent': 'center', 'display': 'flex'}, + Box({'pr': 1, 'display': 'flex', 'justifyContent': 'center'}, + StorageIcon({'style': {'alignSelf': 'center'}}), + ), + Typography({ + 'noWrap': true, + 'style': { + 'alignSelf': 'center', + 'flexGrow': 1, + } + }, + props.localStorageKey, + ), + ), + IconButton({ + 'size': 'small', + 'color': 'inherit', + 'aria-label': 'Rename', + 'aria-hidden': !isHovered, + 'className': 'hide-using-aria', + 'onClick': (SyntheticMouseEvent event) { + event.stopPropagation(); // Do not trigger the onClick handler of the parent MenuItem + setState(newState()..isEditable = true); + } + }, + EditPencilIcon(), + ), + _renderDeleteButton(), + ); + } + + ReactElement _renderDeleteButton() { + final isDisabled = localTodoAppStorage.currentStateJson['name'] == props.localStorageKey; + + return Tooltip({ + 'title': isDisabled + ? 'Cannot delete the currently loaded data set.' + : 'Delete the ${props.localStorageKey} data set.', + 'enterDelay': 500, + }, + Box({ + ...shrinkToFit, + 'color': 'error.main', + 'aria-hidden': !isHovered, + 'className': 'hide-using-aria', + }, + IconButton({ + 'size': 'small', + 'color': 'inherit', + 'aria-label': 'Delete', + 'disabled': isDisabled, + 'onClick': (SyntheticMouseEvent event) { + event.stopPropagation(); // Do not trigger the onClick handler of the parent MenuItem + props.onDelete(props.localStorageKey); + } + }, + TrashIcon(), + ), + ), + ); + } + + ReactElement _renderRenameTextInput() { + return (LocalStorageMenuItemInput() + ..initialValue = props.localStorageKey + ..onSave = (String newValue) { + setState(newState()..isEditable = false, () { + if (newValue != props.localStorageKey) { + props.onRename(newValue); + } + }); + } + ..onCancel = (SyntheticKeyboardEvent event) { + // Prevent the entire menu from closing when the ESC key is pressed + event.stopPropagation(); + setState(newState()..isEditable = false); + } + )(); + } + + void _handleMenuItemClick(_) { + props.onSelect(props.localStorageKey); + } +} + +// ignore: mixin_of_non_class, undefined_class +class SavedDataMenuItemProps extends _$SavedDataMenuItemProps with _$SavedDataMenuItemPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForSavedDataMenuItemProps; +} + +// ignore: mixin_of_non_class, undefined_class +class SavedDataMenuItemState extends _$SavedDataMenuItemState with _$SavedDataMenuItemStateAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const StateMeta meta = _$metaForSavedDataMenuItemState; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/create_input.dart b/app/over_react_redux/todo_client/lib/src/components/create_input.dart new file mode 100644 index 000000000..fcfeb64b9 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/create_input.dart @@ -0,0 +1,55 @@ +import 'dart:html'; + +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/components/shared/material_ui.dart'; + +// ignore: uri_has_not_been_generated +part 'create_input.over_react.g.dart'; + +@Factory() +UiFactory CreateInput = + // ignore: undefined_identifier + _$CreateInput; + +@Props(keyNamespace: '') // No namespace so prop forwarding works when passing to the JS TextField component. +class _$CreateInputProps extends UiProps { + void Function(String s) onCreate; + bool autoFocus; + String label; + String placeholder; +} + +@Component2() +class CreateInputComponent extends UiComponent2 { + @override + get defaultProps => (newProps()..autoFocus = false); + + @override + render() { + final propsToForward = {...props}..remove('onCreate'); + + return Box({...shrinkToFit}, + TextField({ + 'fullWidth': true, + 'variant': 'outlined', + ...propsToForward, + 'onKeyDown': (SyntheticKeyboardEvent event) { + if (props.onKeyDown != null) props.onKeyDown(event); + InputElement target = event.target; + final trimmedValue = target.value.trim(); + if (event.keyCode == KeyCode.ENTER && trimmedValue.isNotEmpty) { + props.onCreate(trimmedValue); + target.value = ''; + } + }, + }), + ); + } +} + +// ignore: mixin_of_non_class, undefined_class +class CreateInputProps extends _$CreateInputProps with _$CreateInputPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForCreateInputProps; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart b/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart new file mode 100644 index 000000000..872c0e353 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart @@ -0,0 +1,137 @@ +import 'dart:math'; + +import 'package:color/color.dart'; +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/components/shared/material_ui.dart'; + +// ignore: uri_has_not_been_generated +part 'avatar_with_colors.over_react.g.dart'; + +@Factory() +UiFactory AvatarWithColors = + // ignore: undefined_identifier + _$AvatarWithColors; + +@Props() +class _$AvatarWithColorsProps extends UiProps { + String fullName; +} + +@State() +class _$AvatarWithColorsState extends UiState { + String fullName; + String backgroundColor; + String textColor; +} + +@Component2() +class AvatarWithColorsComponent extends UiStatefulComponent2 { + @override + get initialState => (newState() + ..fullName = props.fullName + ..addAll(_getDerivedColorsFromName()) + ); + + @override + Map getDerivedStateFromProps(Map nextProps, Map prevState) { + if (prevState == null) return null; // Initial mount is handled by initialState getter + final tNextProps = typedPropsFactory(nextProps); + if (typedStateFactory(prevState).fullName == tNextProps.fullName) return null; // Nothing is going to change. Short-circuit a bunch of color calc logic + return (newState() + ..fullName = tNextProps.fullName + ..addAll(_getDerivedColorsFromName(tNextProps)) + ); + } + + @override + render() { + return Avatar({ + 'style': { + 'backgroundColor': state.backgroundColor, + 'color': state.textColor, + } + }, _renderAvatarContent()); + } + + dynamic _renderAvatarContent() { + if (props.fullName != null) { + return _getUserInitials(state.fullName); + } + + return GroupIcon({'color': 'inherit'}); + } + + Map _getDerivedColorsFromName([AvatarWithColorsProps propsMap]) { + propsMap ??= props; + final stateToSet = newState(); + + final newBackgroundColor = _getRandomColorBasedOnUserName(propsMap.fullName); + if (newBackgroundColor != state?.backgroundColor) { + stateToSet.backgroundColor = newBackgroundColor; + + String newTextColor; + if (newBackgroundColor == 'transparent') { + newTextColor = 'inherit'; + } else { + final lightness = Color.hex(newBackgroundColor).toHslColor().l; + newTextColor = lightness < 70 ? '#fff' : '#595959'; + } + + if (newTextColor != state?.textColor) { + stateToSet.textColor = newTextColor; + } + } + + if (stateToSet.isNotEmpty) { + return stateToSet; + } + + return null; + } + + static String _getUserInitials(String fullName) { + if (fullName == null) return ' '; + + String initials; + + var names = splitSpaceDelimitedString(fullName); + if (names.length > 1) { + initials = names.first[0].toUpperCase() + names.last[0].toUpperCase(); + } else if (names.length == 1) { + initials = names.first[0].toUpperCase(); + } else { + initials = '?'; + } + + return initials; + } + + static String _getRandomColorBasedOnUserName(String fullName) { + if (fullName == null) return 'transparent'; + + final randomColorPaletteKeyIndex = Random(fullName?.hashCode).nextInt(colors.keys.length - 1); + final JsBackedMap colorMap = colors[colors.keys.elementAt(randomColorPaletteKeyIndex)]; + final randomColorHueKeyIndex = Random(fullName?.hashCode).nextInt(colorMap.keys.length - 1); + final String color = colorMap[colorMap.keys.elementAt(randomColorHueKeyIndex)]; + + if (color.length == 4) { + final v = color.split(''); + return '#${v[1]}${v[1]}${v[2]}${v[2]}${v[3]}${v[3]}'; + } + + return color; + } +} + +// ignore: mixin_of_non_class, undefined_class +class AvatarWithColorsProps extends _$AvatarWithColorsProps with _$AvatarWithColorsPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForAvatarWithColorsProps; +} + +// ignore: mixin_of_non_class, undefined_class +class AvatarWithColorsState extends _$AvatarWithColorsState with _$AvatarWithColorsStateAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const StateMeta meta = _$metaForAvatarWithColorsState; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/display_list.dart b/app/over_react_redux/todo_client/lib/src/components/shared/display_list.dart new file mode 100644 index 000000000..3d98f0b3d --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/shared/display_list.dart @@ -0,0 +1,51 @@ +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/components/shared/material_ui.dart'; +import 'package:todo_client/src/components/shared/empty_view.dart'; + +// ignore: uri_has_not_been_generated +part 'display_list.over_react.g.dart'; + +@Factory() +UiFactory DisplayList = + // ignore: undefined_identifier + _$DisplayList; + +@Props(keyNamespace: '') // No namespace so prop forwarding works when passing to the JS TextField component. +class _$DisplayListProps extends UiProps { + @requiredProp + String listItemTypeDescription; +} + +@Component2() +class DisplayListComponent extends UiComponent2 { + @override + render() { + if (props.children.isEmpty) { + return (EmptyView() + ..type = EmptyViewType.VBLOCK + ..header = 'No ${props.listItemTypeDescription} to show' + ..glyph = InfoIcon({'color': 'disabled'}) + )( + 'You should totally create one!', + ); + } + + final propsToForward = {...props}..remove('listItemTypeDescription'); + return Box({ + 'key': 'scrollableList', + 'flexGrow': 1, + 'flexShrink': 1, + 'flexBasis': '0%', + 'paddingTop': 2, + 'style': {...props.style ?? {}, 'overflowY': 'auto'}, + ...propsToForward + }, props.children); + } +} + +// ignore: mixin_of_non_class, undefined_class +class DisplayListProps extends _$DisplayListProps with _$DisplayListPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForDisplayListProps; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/empty_view.dart b/app/over_react_redux/todo_client/lib/src/components/shared/empty_view.dart new file mode 100644 index 000000000..e815c807b --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/shared/empty_view.dart @@ -0,0 +1,147 @@ +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/components/shared/material_ui.dart'; + +// ignore: uri_has_not_been_generated +part 'empty_view.over_react.g.dart'; + +/// Use the `EmptyView` component to provide messaging to users about an empty set of results, +/// or an empty view such as a 404 error page. +@Factory() +UiFactory EmptyView = + // ignore: undefined_identifier + _$EmptyView; + +@Props() +class _$EmptyViewProps extends UiProps { + /// The layout of the [EmptyView]. + /// + /// Default: [EmptyViewType.DEFAULT] + EmptyViewType type; + + /// Optional rendered icon glyph for the [EmptyView]. + /// + /// [glyph] and [content] cannot both be set simultaneously. + ReactElement glyph; + + /// Optional content to be displayed above the [header] in the [EmptyView]. + /// + /// [glyph] and [content] cannot both be set simultaneously. + dynamic content; + + /// Header for the [EmptyView]. + /// + /// If [type] is [EmptyViewType.PAGE_FRAME], the enclosing element is a [Dom.h1]. Otherwise, the enclosing + /// element is a [Dom.h4]; + @requiredProp + dynamic header; +} + +@Component2() +class EmptyViewComponent extends UiComponent2 { + @override + get defaultProps => (newProps()..type = EmptyViewType.DEFAULT); + + @override + get consumedProps => const [ + EmptyViewProps.meta, + ]; + + @override + get propTypes => { + keyForProp((p) => p.glyph): (props, info) { + if (props.glyph != null && props.content != null) { + return PropError.combination(info.propName, 'EmptyViewProps.content', + 'EmptyView does not support `props.glyph` and `props.content` being set simultaneously.'); + } + + return null; + }, + }; + + @override + render() { + return _renderInContainer(_renderBody()); + } + + ReactElement _renderBody() { + return (Dom.div() + ..modifyProps(addUnconsumedDomProps) + ..role = Role.status + ..className = (forwardingClassNameBuilder() // + ..add('empty-view')) + .toClassName() + )( + _renderAboveHeaderContent(), + _renderHeader(), + Typography({'variant': 'body1'}, props.children), + ); + } + + ReactElement _renderInContainer(dynamic content) { + if (props.type == EmptyViewType.DEFAULT) return content; + + return (Dom.div() + ..className = props.type.className + )(content); + } + + ReactElement _renderHeader() { + return Typography({ + 'component': props.type.headerFactory, + 'variant': props.type.headerFactory, + 'gutterBottom': true, + 'className': 'empty-view__message-heading', + }, props.header); + } + + ReactElement _renderAboveHeaderContent() { + if (props.glyph != null) { + return (Dom.span()..className = 'empty-view__icon')( + cloneElement(props.glyph, {'fontSize': 'inherit'}), + ); + } + + if (props.content != null) { + return Dom.div()(props.content); + } + + return null; + } +} + +/// Available layout options for an `EmptyView`. +/// +/// If you need to vertically center the messaging within the height of your layout, use `PAGE_FRAME` or `VBLOCK`. +class EmptyViewType extends ClassNameConstant { + /// The "heading level" that will be used as the parent node of `EmptyViewProps.header`. + final String headerFactory; + + /// Private constructor, for use by generated code only. + const EmptyViewType._(String name, String className, this.headerFactory) : super(name, className); + + /// Used as the root node of the within ``. + /// + /// * [className] value: 'empty-view-page-frame' + /// * [headerFactory] value: ['h1'] + static const EmptyViewType PAGE_FRAME = EmptyViewType._('PAGE_FRAME', 'empty-view-page-frame', 'h1'); + + /// An element nested within an element that has relative positioning, and a defined height either via an existing + /// flexbox axis surrounding it, or a fixed CSS height value. + /// + /// * [className] value: 'empty-view-vblock' + /// * [headerFactory] value: ['h4'] + static const EmptyViewType VBLOCK = EmptyViewType._('VBLOCK', 'empty-view-vblock', 'h4'); + + /// By default, the `EmptyView` element has some top margin applied to vertically space it within its container. + /// + /// * [className] value: '' + /// * [headerFactory] value: ['h4'] + static const EmptyViewType DEFAULT = EmptyViewType._('DEFAULT', null, 'h4'); +} + +// ignore: mixin_of_non_class, undefined_class +class EmptyViewProps extends _$EmptyViewProps with _$EmptyViewPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForEmptyViewProps; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/hoverable_item_component_mixin.dart b/app/over_react_redux/todo_client/lib/src/components/shared/hoverable_item_component_mixin.dart new file mode 100644 index 000000000..acbd9cc72 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/shared/hoverable_item_component_mixin.dart @@ -0,0 +1,52 @@ +import 'dart:html'; + +import 'package:meta/meta.dart'; +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/components/shared/hoverable_item_mixin.dart'; + +mixin HoverableItemMixin on UiStatefulComponent2 { + @mustCallSuper + @override + get initialState => (newState() + ..isHovered = false + ..isChildFocused = false + ); + + @protected + bool get isHovered => state.isHovered || state.isChildFocused; + + @protected + void handleChildBlur(SyntheticFocusEvent event) { + var newlyFocusedTarget = event.relatedTarget; + // newlyFocusedTarget could be null or a window, so check if it's an Element first. + if (newlyFocusedTarget is Element && findDomNode(this).contains(newlyFocusedTarget)) { + // Don't do anything if we're moving from one item to another + return; + } + + setState(newState()..isChildFocused = false); + } + + @protected + void handleChildFocus(SyntheticFocusEvent event) { + setState(newState()..isChildFocused = true); + } + + @protected + void handleItemMouseEnter(SyntheticMouseEvent event) { + setState(newState()..isHovered = true); + } + + @protected + void handleItemMouseOver(SyntheticMouseEvent event) { + if (!state.isHovered) { + setState(newState()..isHovered = true); + } + } + + @protected + void handleItemMouseLeave(SyntheticMouseEvent event) { + setState(newState()..isHovered = false); + } +} diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/hoverable_item_mixin.dart b/app/over_react_redux/todo_client/lib/src/components/shared/hoverable_item_mixin.dart new file mode 100644 index 000000000..3555af9b1 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/shared/hoverable_item_mixin.dart @@ -0,0 +1,19 @@ +import 'package:over_react/over_react.dart'; + +export 'package:todo_client/src/components/shared/hoverable_item_component_mixin.dart'; + +// ignore: uri_has_not_been_generated +part 'hoverable_item_mixin.over_react.g.dart'; + +@StateMixin() +abstract class HoverableItemStateMixin implements UiState { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const StateMeta meta = _$metaForHoverableItemStateMixin; + + @override + Map get state; + + bool isChildFocused; + + bool isHovered; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_component_mixin.dart b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_component_mixin.dart new file mode 100644 index 000000000..67f1581de --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_component_mixin.dart @@ -0,0 +1,121 @@ +import 'package:meta/meta.dart'; +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/models/base_model.dart'; +import 'package:todo_client/src/components/shared/list_item_mixin.dart'; +import 'package:todo_client/src/components/shared/material_ui.dart'; + +mixin ListItemMixin + on UiStatefulComponent2 { + @override + @mustCallSuper + get defaultProps => (newProps() + ..isEditable = false + ..isSelected = false + ); + + @override + @mustCallSuper + Map getDerivedStateFromProps(Map nextProps, Map prevState) { + final tNextProps = typedPropsFactory(nextProps); + final tPrevState = typedStateFactory(prevState); + final alreadyEditing = tPrevState?.localModel != null; + + if (!alreadyEditing && tNextProps.isEditable) { + return (newState()..localModel = tNextProps.model); + } else if (alreadyEditing && !tNextProps.isEditable) { + return (newState()..localModel = null); + } + + return null; + } + + Map get sharedExpansionPanelProps => { + 'onChange': handleExpansionPanelExpandedStateChange, + 'expanded': props.isSelected, + 'style': highlightedItemStyle, + }; + + @protected + Map get highlightedItemStyle => { + if (props.isHighlighted) 'backgroundColor': colors['yellow']['50'], + }; + + bool get hasDetails; + + bool get allowExpansion => hasDetails || props.isEditable; + + @protected + void handleExpansionPanelExpandedStateChange([SyntheticEvent event]) { + if (!allowExpansion) return; + event?.stopPropagation(); + toggleSelect(); + } + + M get model => props.isEditable ? state.localModel : props.model; + + @protected + void remove() { + props.onRemove(model.id); + } + + @protected + void updateModel(M newModelValue) { + if (props.isEditable) { + // When user is doing a bulk edit, store it locally first so they can cancel their changes if they want to. + setState(newState()..localModel = newModelValue); + } else { + props.onModelUpdate(newModelValue); + } + } + + @protected + void enterEditable() { + if (!props.isEditable) { + props.onBeginEdit(model.id); + } + } + + @protected + void exitEditable({bool saveChanges = true}) { + if (saveChanges) { + props.onModelUpdate(state.localModel); + } + + if (props.isEditable) { + props.onFinishEdit(model.id); + } + } + + @protected + void toggleEditable() { + if (props.isEditable) { + exitEditable(); + } else { + enterEditable(); + } + } + + @protected + void select() { + props.onSelect(model.id); + } + + @protected + void deselect() { + if (props.isEditable) { + exitEditable(saveChanges: true); + } + + props.onDeselect(model.id); + } + + @protected + void toggleSelect() { + if (props.isSelected) { + deselect(); + } else { + select(); + } + } +} diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart new file mode 100644 index 000000000..f69a6fda6 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart @@ -0,0 +1,95 @@ +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/components/shared/hoverable_item_mixin.dart'; +import 'package:todo_client/src/components/shared/material_ui.dart'; + +// ignore: uri_has_not_been_generated +part 'list_item_expansion_panel_summary.over_react.g.dart'; + +@Factory() +UiFactory ListItemExpansionPanelSummary = + // ignore: undefined_identifier + _$ListItemExpansionPanelSummary; + +@Props() +class _$ListItemExpansionPanelSummaryProps extends UiProps { + @requiredProp + String modelId; + @requiredProp + bool allowExpansion; + @requiredProp + bool isEditable; + @requiredProp + Function() onToggleEditable; +} + +@State() +class _$ListItemExpansionPanelSummaryState extends UiState + with HoverableItemStateMixin, + // ignore: mixin_of_non_class, undefined_class + $HoverableItemStateMixin {} + +@Component2() +class ListItemExpansionPanelSummaryComponent + extends UiStatefulComponent2 + with HoverableItemMixin { + @override + render() { + return ExpansionPanelSummary({ + 'aria-controls': 'details_${props.modelId}', + 'id': 'summary_${props.modelId}', + 'expandIcon': ExpandMoreIcon(), + 'IconButtonProps': { + 'disabled': !props.allowExpansion, + 'style': props.allowExpansion ? null : {'color': 'transparent'}, + }, + 'style': props.allowExpansion ? null : {'cursor': 'default'}, + 'onMouseEnter': handleItemMouseEnter, + 'onMouseLeave': handleItemMouseLeave, + 'onMouseOver': handleItemMouseOver, + 'onFocus': handleChildFocus, + 'onBlur': handleChildBlur, + }, + Grid({'container': true, 'direction': 'row'}, [ + ...props.children, + _renderEditButton(), + ]), + ); + } + + ReactElement _renderEditButton() { + return Box({...shrinkToFit, + 'key': 'editButton', + 'mr': -1, + 'alignSelf': 'center', + 'aria-hidden': !isHovered, + 'className': 'hide-using-aria', + }, + Tooltip({'enterDelay': 500, 'title': props.isEditable ? 'Save Changes' : 'Make Changes'}, + IconButton({ + 'aria-label': props.isEditable ? 'Save Changes' : 'Make Changes', + 'className': 'todo-list__item__edit-btn', + 'onClick': (SyntheticMouseEvent event) { + event.stopPropagation(); + props.onToggleEditable(); + }, + 'color': props.isEditable ? 'primary' : 'default', + }, + EditPencilIcon(), + ), + ), + ); + } +} + +// ignore: mixin_of_non_class, undefined_class +class ListItemExpansionPanelSummaryProps extends _$ListItemExpansionPanelSummaryProps with _$ListItemExpansionPanelSummaryPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForListItemExpansionPanelSummaryProps; +} + +// ignore: mixin_of_non_class, undefined_class +class ListItemExpansionPanelSummaryState extends _$ListItemExpansionPanelSummaryState with _$ListItemExpansionPanelSummaryStateAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const StateMeta meta = _$metaForListItemExpansionPanelSummaryState; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_mixin.dart b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_mixin.dart new file mode 100644 index 000000000..140024fb6 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_mixin.dart @@ -0,0 +1,39 @@ +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/models/base_model.dart'; + +export 'package:todo_client/src/components/shared/list_item_component_mixin.dart'; + +// ignore: uri_has_not_been_generated +part 'list_item_mixin.over_react.g.dart'; + +@PropsMixin() +abstract class ListItemPropsMixin implements UiProps { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForListItemPropsMixin; + + @override + Map get props; + + covariant BaseModel model; + bool isSelected; + bool isEditable; + bool isHighlighted; + Function(String id) onSelect; + Function(String id) onDeselect; + Function (String id) onBeginEdit; + Function (String id) onFinishEdit; + Function(BaseModel updatedModel) onModelUpdate; + Function(String id) onRemove; +} + +@StateMixin() +abstract class ListItemStateMixin implements UiState { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const StateMeta meta = _$metaForListItemStateMixin; + + @override + Map get state; + + covariant BaseModel localModel; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart new file mode 100644 index 000000000..c52da62eb --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart @@ -0,0 +1,209 @@ +// ignore_for_file: avoid_classes_with_only_static_members +@JS() +library material_ui; + +import 'package:js/js.dart'; +import 'package:over_react/over_react.dart'; +import 'package:react/react_client/js_backed_map.dart'; +import 'package:react/react_client.dart'; +import 'package:react/react_client/react_interop.dart'; +import 'package:todo_client/src/utils.dart'; + +/// JS interop wrapper class for [Material UI components](https://material-ui.com/). +/// +/// Made available in the app via the following script tag within our app's `index.html`: +/// +/// ``` +/// +/// ``` +@JS() +class MaterialUI { + external static JsMap get colors; + external static JsMap get classes; + + external static ReactClass get AppBar; + external static ReactClass get Avatar; + external static ReactClass get Badge; + external static ReactClass get Box; + external static ReactClass get Button; + external static ReactClass get Checkbox; + external static ReactClass get Container; + external static ReactClass get CssBaseline; + external static ReactClass get Divider; + external static ReactClass get ExpansionPanel; + external static ReactClass get ExpansionPanelActions; + external static ReactClass get ExpansionPanelDetails; + external static ReactClass get ExpansionPanelSummary; + external static ReactClass get Grid; + external static ReactClass get IconButton; + external static ReactClass get InputBase; + external static ReactClass get List; + external static ReactClass get ListItem; + external static ReactClass get Menu; + external static ReactClass get MenuItem; + external static ReactClass get Popover; + external static ReactClass get SvgIcon; + external static ReactClass get TextField; + external static ReactClass get Toolbar; + external static ReactClass get Tooltip; + external static ReactClass get Typography; +} + +final colors = createJsBackedMapRecursively(MaterialUI.colors); +final classes = createJsBackedMapRecursively(MaterialUI.classes); + +// ----------------------------------------------------------------------- +// Below, you'll find the top level JS component factories +// that we can use just like any other react-dart component in our app! +// ----------------------------------------------------------------------- + +/// See: +final AppBar = ReactJsComponentFactoryProxy(MaterialUI.AppBar); + +/// See: +final Avatar = ReactJsComponentFactoryProxy(MaterialUI.Avatar); + +/// See: +final Badge = ReactJsComponentFactoryProxy(MaterialUI.Badge); + +/// See: +final Box = ReactJsComponentFactoryProxy(MaterialUI.Box); + +/// See: +final Button = ReactJsComponentFactoryProxy(MaterialUI.Button); + +/// See: +final Checkbox = ReactJsComponentFactoryProxy(MaterialUI.Checkbox); + +/// See: +final Container = ReactJsComponentFactoryProxy(MaterialUI.Container); + +/// See: +final CssBaseline = ReactJsComponentFactoryProxy(MaterialUI.CssBaseline); + +/// See: +final Divider = ReactJsComponentFactoryProxy(MaterialUI.Divider); + +/// See: +final ExpansionPanel = ReactJsComponentFactoryProxy(MaterialUI.ExpansionPanel); + +/// See: +final ExpansionPanelActions = ReactJsComponentFactoryProxy(MaterialUI.ExpansionPanelActions); + +/// See: +final ExpansionPanelDetails = ReactJsComponentFactoryProxy(MaterialUI.ExpansionPanelDetails); + +/// See: +final ExpansionPanelSummary = ReactJsComponentFactoryProxy(MaterialUI.ExpansionPanelSummary); + +/// See: +final Grid = ReactJsComponentFactoryProxy(MaterialUI.Grid); + +/// See: +final IconButton = ReactJsComponentFactoryProxy(MaterialUI.IconButton); + +/// See: +final InputBase = ReactJsComponentFactoryProxy(MaterialUI.InputBase); + +/// See: +final ListUi = ReactJsComponentFactoryProxy(MaterialUI.List); + +/// See: +final ListItem = ReactJsComponentFactoryProxy(MaterialUI.ListItem); + +/// See: +final Menu = ReactJsComponentFactoryProxy(MaterialUI.Menu); + +/// See: +final MenuItem = ReactJsComponentFactoryProxy(MaterialUI.MenuItem); + +/// See: +final Popover = ReactJsComponentFactoryProxy(MaterialUI.Popover); + +/// See: +final SvgIcon = ReactJsComponentFactoryProxy(MaterialUI.SvgIcon); + +/// See: +final TextField = ReactJsComponentFactoryProxy(MaterialUI.TextField); + +/// See: +final Toolbar = ReactJsComponentFactoryProxy(MaterialUI.Toolbar); + +/// See: +final Tooltip = ReactJsComponentFactoryProxy(MaterialUI.Tooltip); + +/// See: +final Typography = ReactJsComponentFactoryProxy(MaterialUI.Typography); + +ReactElement TrashIcon([Map props = const {}]) { + return SvgIcon({'aria-label': 'Delete', ...props}, (Dom.path() + ..d = 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z' + )()); +} + +ReactElement ExpandLessIcon([Map props = const {}]) { + return SvgIcon({'aria-label': 'Collapse', ...props}, (Dom.path() + ..d = 'M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z6z' + )()); +} + +ReactElement ExpandMoreIcon([Map props = const {}]) { + return SvgIcon({'aria-label': 'Expand', ...props}, (Dom.path() + ..d = 'M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z' + )()); +} + +ReactElement GroupIcon([Map props = const {}]) { + return SvgIcon({'aria-label': 'Users', ...props}, (Dom.path() + ..d = 'M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z' + )()); +} + +ReactElement InfoIcon([Map props = const {}]) { + return SvgIcon({'aria-label': 'Information', ...props}, (Dom.path() + ..d = 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z' + )()); +} + +ReactElement VisibilityIcon([Map props = const {}]) { + return SvgIcon({'aria-label': 'Visible', ...props}, (Dom.path() + ..d = 'M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z' + )()); +} + +ReactElement VisibilityOffIcon([Map props = const {}]) { + return SvgIcon({'aria-label': 'Invisible', ...props}, (Dom.path() + ..d = 'M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z' + )()); +} + +/// a.k.a "Create" +ReactElement EditPencilIcon([Map props = const {}]) { + return SvgIcon({'aria-label': 'Edit', ...props}, (Dom.path() + ..d = 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z' + )()); +} + +ReactElement MenuIcon([Map props = const {}]) { + return SvgIcon({'aria-label': 'Menu', ...props}, (Dom.path() + ..d = 'M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z' + )()); +} + +ReactElement StorageIcon([Map props = const {}]) { + return SvgIcon({'aria-label': 'Menu', ...props}, (Dom.path() + ..d = 'M2 20h20v-4H2v4zm2-3h2v2H4v-2zM2 4v4h20V4H2zm4 3H4V5h2v2zm-4 7h20v-4H2v4zm2-3h2v2H4v-2z' + )()); +} + +const shrinkToFit = { + 'flexGrow': 0, + 'flexShrink': 0, + 'flexBasis': 'auto', +}; + +const grow = { + 'flexGrow': 1, + 'flexShrink': 1, + 'flexBasis': '0%', +}; diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/menu_overlay.dart b/app/over_react_redux/todo_client/lib/src/components/shared/menu_overlay.dart new file mode 100644 index 000000000..bd5a055c3 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/shared/menu_overlay.dart @@ -0,0 +1,99 @@ +import 'dart:html'; + +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/components/shared/material_ui.dart'; + +// ignore: uri_has_not_been_generated +part 'menu_overlay.over_react.g.dart'; + +/// Use the `MenuOverlay` component to provide messaging to users about an empty set of results, +/// or an empty view such as a 404 error page. +@Factory() +UiFactory MenuOverlay = + // ignore: undefined_identifier + _$MenuOverlay; + +@Props(keyNamespace: '') +class _$MenuOverlayProps extends UiProps { + ReactElement trigger; + bool useDerivedMaxWidth; +} + +@State() +class _$MenuOverlayState extends UiState { + Element anchorEl; + dynamic menuMaxWidth; +} + +@Component2() +class MenuOverlayComponent extends UiStatefulComponent2 { + bool get open => state.anchorEl != null; + String get id => open ? props.id ?? 'popover' : null; + + @override + get defaultProps => (newProps()..useDerivedMaxWidth = false); + + @override + get initialState => (newState()..menuMaxWidth = 'none'); + + @override + render() { + final propsToForward = Map.of(props) + ..remove('trigger') + ..remove('dispatch') + ..remove('useDerivedMaxWidth'); + + return Dom.div()( + cloneElement(props.trigger, { + 'onClick': _handleTriggerClick, + 'aria-describedby': id, + }), + Menu({ + ...propsToForward, + 'MenuListProps': { + 'id': id, + 'style': { + 'maxWidth': state.menuMaxWidth, + }, + }, + 'open': open, + 'anchorEl': state.anchorEl, + 'onClose': close, + 'onEntered': (_, __) { + if (!props.useDerivedMaxWidth) return; + final currentMenuWidth = querySelector('#$id')?.getBoundingClientRect()?.width?.ceil(); + if (currentMenuWidth != state.menuMaxWidth) { + setState(newState()..menuMaxWidth = currentMenuWidth ?? 'none'); + } + }, + }, props.children), + ); + } + + // TODO: Look into the cloneElement issue with the missing/problematic SyntheticEvent wrapper that forces us to use dynamic here + void _handleTriggerClick(dynamic event) { + event.stopPropagation(); + setState(newState()..anchorEl = event.target); + } + + void close([event, __]) { + if (!open) return; + if (event != null) { + event.stopPropagation(); + } + setState(newState()..anchorEl = null); + } +} + +// ignore: mixin_of_non_class, undefined_class +class MenuOverlayProps extends _$MenuOverlayProps with _$MenuOverlayPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForMenuOverlayProps; +} + +// ignore: mixin_of_non_class, undefined_class +class MenuOverlayState extends _$MenuOverlayState with _$MenuOverlayStateAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const StateMeta meta = _$metaForMenuOverlayState; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/todo_item_text_field.dart b/app/over_react_redux/todo_client/lib/src/components/shared/todo_item_text_field.dart new file mode 100644 index 000000000..b8e834a42 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/shared/todo_item_text_field.dart @@ -0,0 +1,82 @@ +import 'package:over_react/over_react.dart'; + +import 'package:todo_client/src/components/shared/material_ui.dart'; + +// ignore: uri_has_not_been_generated +part 'todo_item_text_field.over_react.g.dart'; + +/// Use the `TodoItemTextField` component to provide messaging to users about an empty set of results, +/// or an empty view such as a 404 error page. +@Factory() +UiFactory TodoItemTextField = + // ignore: undefined_identifier + _$TodoItemTextField; + +@Props(keyNamespace: '') // No namespace so prop forwarding works when passing to the JS TextField / InputBase components. +class _$TodoItemTextFieldProps extends UiProps { + bool readOnly; + bool fullWidth; + bool multiline; + bool autoFocus; + String variant; + String label; + String placeholder; + String defaultValue; + String value; + MouseEventCallback onClickWhenEditable; +} + +@State() +class _$TodoItemTextFieldState extends UiState {} + +@Component2() +class TodoItemTextFieldComponent extends UiStatefulComponent2 { + @override + get defaultProps => (newProps() + ..fullWidth = true + ..readOnly = true + ..multiline = false + ..variant = 'outlined' + ); + + @override + render() { + if (props.readOnly) return _renderReadonlyBaseInput(); + + return _renderEditableInput(); + } + + ReactElement _renderReadonlyBaseInput() { + final propsToForward = Map.of(props) + ..remove('variant') + ..remove('label') + ..remove('autoFocus') + ..remove('onClickWhenEditable'); + + return InputBase({ + ...propsToForward, + 'inputProps': {'style': {'whiteSpace': 'nowrap', 'textOverflow': 'ellipsis'}}, + }); + } + + ReactElement _renderEditableInput() { + final propsToForward = Map.of(props)..remove('onClickWhenEditable'); + + return TextField({ + ...propsToForward, + 'onClick': props.onClickWhenEditable, + }); + } +} + +// ignore: mixin_of_non_class, undefined_class +class TodoItemTextFieldProps extends _$TodoItemTextFieldProps with _$TodoItemTextFieldPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForTodoItemTextFieldProps; +} + +// ignore: mixin_of_non_class, undefined_class +class TodoItemTextFieldState extends _$TodoItemTextFieldState with _$TodoItemTextFieldStateAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const StateMeta meta = _$metaForTodoItemTextFieldState; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/task_count.dart b/app/over_react_redux/todo_client/lib/src/components/task_count.dart new file mode 100644 index 000000000..8ca3277df --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/task_count.dart @@ -0,0 +1,83 @@ +import 'package:collection/collection.dart'; +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; + +import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/store.dart'; +import 'package:todo_client/src/models/user.dart'; +import 'package:todo_client/src/components/shared/material_ui.dart'; + +// ignore: uri_has_not_been_generated +part 'task_count.over_react.g.dart'; + +UiFactory ConnectedTaskCountBadge = connect( + mapStateToPropsWithOwnProps: (state, ownProps) { + return (TaskCountBadge() + ..assignedTodoIds = state.todos.where((todo) => todo.assignedUserId == ownProps.user.id) + .map((todo) => todo.id).toList() + ); + }, + areStatePropsEqual: (nextProps, prevProps) { + return ListEquality().equals(nextProps.assignedTodoIds, prevProps.assignedTodoIds); + }, +)(TaskCountBadge); + +@Factory() +UiFactory TaskCountBadge = + // ignore: undefined_identifier + _$TaskCountBadge; + +@Props() +class _$TaskCountBadgeProps extends UiProps with ConnectPropsMixin { + @requiredProp + User user; + + @requiredProp + List assignedTodoIds; +} + +@Component2() +class TaskCountBadgeComponent extends UiComponent2 { + @override + render() { + return Box({ + 'onMouseEnter': (_) { + props.dispatch(HighlightTodosAction(props.assignedTodoIds)); + }, + 'onMouseLeave': (_) { + props.dispatch(UnHighlightTodosAction(props.assignedTodoIds)); + }, + }, + Tooltip({ + 'title': _tooltipContent, + 'arrow': true, + 'enterDelay': 500, + }, + Badge({ + 'badgeContent': props.assignedTodoIds.length, + 'color': 'secondary', + 'overlap': 'circle', + 'anchorOrigin': { + 'vertical': 'bottom', + 'horizontal': 'right', + }, + }, props.children), + ), + ); + } + + String get _tooltipContent { + final taskCount = props.assignedTodoIds.length; + if (taskCount == 1) { + return 'There is $taskCount task assigned to ${props.user.name}'; + } + + return 'There are $taskCount tasks assigned to ${props.user.name}'; + } +} + +// ignore: mixin_of_non_class, undefined_class +class TaskCountBadgeProps extends _$TaskCountBadgeProps with _$TaskCountBadgePropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForTaskCountBadgeProps; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/todo_list.dart b/app/over_react_redux/todo_client/lib/src/components/todo_list.dart new file mode 100644 index 000000000..800aa7185 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/todo_list.dart @@ -0,0 +1,53 @@ +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; + +import 'package:todo_client/src/store.dart'; +import 'package:todo_client/src/models/todo.dart'; +import 'package:todo_client/src/components/shared/display_list.dart'; +import 'package:todo_client/src/components/todo_list_item.dart'; + +// ignore: uri_has_not_been_generated +part 'todo_list.over_react.g.dart'; + +UiFactory ConnectedTodoList = connect( + mapStateToProps: (state) { + return (TodoList() + ..todos = state.todos + ); + }, +)(TodoList); + +@Factory() +UiFactory TodoList = + // ignore: undefined_identifier + _$TodoList; + +@Props() +class _$TodoListProps extends UiProps with ConnectPropsMixin { + @requiredProp + List todos; +} + +@Component2() +class TodoListComponent extends UiComponent2 { + @override + render() { + return (DisplayList()..listItemTypeDescription = 'todos')( + props.todos.map(_renderItem).toList() + ); + } + + ReactElement _renderItem(Todo todo) { + return (ConnectedTodoListItem() + ..key = todo.id + ..model = todo + ..assignedUserId = todo.assignedUserId + )(); + } +} + +// ignore: mixin_of_non_class, undefined_class +class TodoListProps extends _$TodoListProps with _$TodoListPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForTodoListProps; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart b/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart new file mode 100644 index 000000000..d21f5a52c --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart @@ -0,0 +1,258 @@ +import 'dart:html'; + +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; +import 'package:todo_client/src/actions.dart'; + +import 'package:todo_client/src/models/todo.dart'; +import 'package:todo_client/src/components/shared/list_item_expansion_panel_summary.dart'; +import 'package:todo_client/src/components/shared/list_item_mixin.dart'; +import 'package:todo_client/src/components/shared/material_ui.dart'; +import 'package:todo_client/src/components/shared/todo_item_text_field.dart'; +import 'package:todo_client/src/components/user_selector.dart'; +import 'package:todo_client/src/store.dart'; + +// ignore: uri_has_not_been_generated +part 'todo_list_item.over_react.g.dart'; + +UiFactory ConnectedTodoListItem = connect( + mapDispatchToProps: (dispatch) { + return (TodoListItem() + ..onSelect = (id) { dispatch(SelectTodoAction(id)); } + ..onDeselect = (id) { dispatch(DeselectTodoAction(id)); } + ..onBeginEdit = (id) { dispatch(BeginEditTodoAction(id)); } + ..onFinishEdit = (id) { dispatch(FinishEditTodoAction(id)); } + ..onModelUpdate = (todo) { dispatch(UpdateTodoAction(todo)); } + ..onRemove = (id) { dispatch(RemoveTodoAction(id)); } + ); + }, + mapStateToPropsWithOwnProps: (state, ownProps) { + final isEditable = Set.of(state.editableTodoIds).contains(ownProps.model.id); + final isSelected = isEditable || Set.of(state.selectedTodoIds).contains(ownProps.model.id); + final isHighlighted = Set.of(state.highlightedTodoIds).contains(ownProps.model.id); + + return (TodoListItem() + ..isSelected = isSelected + ..isEditable = isEditable + ..isHighlighted = isHighlighted + ); + }, +)(TodoListItem); + +@Factory() +UiFactory TodoListItem = + // ignore: undefined_identifier + _$TodoListItem; + +@Props() +class _$TodoListItemProps extends UiProps + with ListItemPropsMixin, + // ignore: mixin_of_non_class, undefined_class + $ListItemPropsMixin { + @requiredProp + @override + Todo model; + + String assignedUserId; +} + +@State() +class _$TodoListItemState extends UiState + with ListItemStateMixin, + // ignore: mixin_of_non_class, undefined_class + $ListItemStateMixin { + @override + Todo localModel; +} + +@Component2() +class TodoListItemComponent extends UiStatefulComponent2 + with ListItemMixin { + @override + bool get hasDetails => model.notes != null && model.notes.isNotEmpty; + + @override + render() { + return ExpansionPanel({ + ...sharedExpansionPanelProps, + 'className': model.isCompleted ? 'Mui-disabled' : null, + }, [ + (ListItemExpansionPanelSummary() + ..key = 'summary' + ..modelId = model.id + ..allowExpansion = allowExpansion + ..isEditable = props.isEditable + ..onToggleEditable = toggleEditable + )( + _renderTaskCheckbox(), + _renderTaskHeader(), + _renderUserSelector(), + ), + ExpansionPanelDetails({'key': 'details'}, + _renderTaskNotes(), + ), + _renderEditableTaskActions(), + ]); + } + + ReactElement _renderTaskCheckbox() { + return Box({...shrinkToFit, + 'key': 'taskCheckbox', + 'ml': -2, + 'mr': 1, + 'alignSelf': 'center', + }, + Tooltip({ + 'enterDelay': 500, + 'title': model.isCompleted ? 'Mark as not completed' : 'Mark as completed', + }, + Checkbox({ + 'checked': model.isCompleted, + 'inputProps': { + 'aria-label': 'Complete Task', + }, + 'value': 'isCompleted', + 'onChange': (_) { updateModel(Todo.from(model)..isCompleted = !model.isCompleted); }, + 'onClick': (SyntheticEvent e) { e.stopPropagation(); }, + 'onFocus': (SyntheticEvent e) { e.stopPropagation(); }, + }), + ), + ); + } + + ReactElement _renderTaskHeader() { + return Box({...grow, + 'key': 'taskHeader', + 'mr': 1, + 'alignSelf': 'center', + }, + (TodoItemTextField() + ..readOnly = !props.isEditable + ..autoFocus = props.isEditable + ..label = 'Description' + ..onChange = _updateDescriptionValue + ..placeholder = 'Describe the task...' + ..value = model.description + ..onClickWhenEditable = (event) { event.stopPropagation(); } + ..['inputProps'] = { + 'style': props.isEditable ? null : { + 'cursor': allowExpansion ? 'pointer' : 'default', + }, + } + )(), + ); + } + + void _updateDescriptionValue(SyntheticFormEvent event) { + InputElement target = event.target; + updateModel(Todo.from(model)..description = target.value); + } + + ReactElement _renderUserSelector() { + return Box({...shrinkToFit, + 'key': 'userSelector', + 'alignSelf': 'center', + }, + (ConnectedUserSelector() + ..selectedUserId = props.assignedUserId + ..onUserSelect = (userId) { updateModel(Todo.from(model)..assignedUserId = userId); } + )(), + ); + } + + ReactElement _renderTaskNotes() { + return (TodoItemTextField() + ..readOnly = !props.isEditable + ..label = 'Notes' + ..multiline = true + ..['rows'] = 3 + ..onChange = _updateNoteValue + ..placeholder = 'Add some notes about the task' + ..value = model.notes + )(); + } + + void _updateNoteValue(SyntheticFormEvent event) { + TextAreaElement target = event.target; + updateModel(Todo.from(model)..notes = target.value); + } + + ReactElement _renderEditableTaskActions() { + if (!props.isEditable) return null; + + return (Fragment()..key = 'editableTaskActions')( + Divider({}), + ExpansionPanelActions({}, + Grid({'container': true, 'direction': 'row'}, [ + Box({'key': 'leftButtons', 'flexGrow': 1, 'display': 'flex'}, [ + _renderEditableTaskDeleteButton(), + _renderEditableTaskPrivacyToggleButton(), + ]), + Box({'key': 'rightButtons', ...shrinkToFit}, [ + _renderEditableTaskCancelButton(), + _renderEditableTaskSaveButton(), + ]), + ]), + ), + ); + } + + ReactElement _renderEditableTaskDeleteButton() { + return Tooltip({'key': 'deleteButton', 'enterDelay': 500, 'title': 'Delete Todo'}, + Box({'color': 'error.main'}, + IconButton({ + 'size': 'small', + 'aria-label': 'delete todo', + 'color': 'inherit', + 'disabled': model.isCompleted, + 'onClick': (_) { remove(); }, + }, + TrashIcon(), + ), + ), + ); + } + + ReactElement _renderEditableTaskPrivacyToggleButton() { + final tooltipTitle = model.isPublic ? 'Make Private' : 'Make Public'; + + return Tooltip({'key': 'privacyButton', 'enterDelay': 500, 'title': tooltipTitle}, + IconButton({ + 'size': 'small', + 'aria-label': model.isPublic ? 'Make Private' : 'Make Public', + 'onClick': (_) { updateModel(Todo.from(model)..isPublic = !model.isPublic); }, + }, + model.isPublic ? VisibilityIcon() : VisibilityOffIcon(), + ), + ); + } + + ReactElement _renderEditableTaskCancelButton() { + return Button({ + 'key': 'cancel', + 'size': 'small', + 'onClick': (_) { exitEditable(saveChanges: false); }, + }, 'Cancel'); + } + + ReactElement _renderEditableTaskSaveButton() { + return Button({ + 'key': 'save', + 'size': 'small', + 'color': 'primary', + 'onClick': (_) { exitEditable(saveChanges: true); }, + }, 'Save'); + } +} + +// ignore: mixin_of_non_class, undefined_class +class TodoListItemProps extends _$TodoListItemProps with _$TodoListItemPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForTodoListItemProps; +} + +// ignore: mixin_of_non_class, undefined_class +class TodoListItemState extends _$TodoListItemState with _$TodoListItemStateAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const StateMeta meta = _$metaForTodoListItemState; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/user_list.dart b/app/over_react_redux/todo_client/lib/src/components/user_list.dart new file mode 100644 index 000000000..fdc33f47c --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/user_list.dart @@ -0,0 +1,52 @@ +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; + +import 'package:todo_client/src/store.dart'; +import 'package:todo_client/src/models/user.dart'; +import 'package:todo_client/src/components/shared/display_list.dart'; +import 'package:todo_client/src/components/user_list_item.dart'; + +// ignore: uri_has_not_been_generated +part 'user_list.over_react.g.dart'; + +UiFactory ConnectedUserList = connect( + mapStateToProps: (state) { + return (UserList() + ..users = state.users + ); + }, +)(UserList); + +@Factory() +UiFactory UserList = + // ignore: undefined_identifier + _$UserList; + +@Props() +class _$UserListProps extends UiProps with ConnectPropsMixin { + @requiredProp + List users; +} + +@Component2() +class UserListComponent extends UiComponent2 { + @override + render() { + return (DisplayList()..listItemTypeDescription = 'users')( + props.users.map(_renderUser).toList() + ); + } + + ReactElement _renderUser(User user) { + return (ConnectedUserListItem() + ..key = user.id + ..model = user + )(); + } +} + +// ignore: mixin_of_non_class, undefined_class +class UserListProps extends _$UserListProps with _$UserListPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForUserListProps; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart b/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart new file mode 100644 index 000000000..abf5b1d38 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart @@ -0,0 +1,216 @@ +import 'dart:html'; + +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; +import 'package:todo_client/src/actions.dart'; + +import 'package:todo_client/src/models/user.dart'; +import 'package:todo_client/src/components/shared/avatar_with_colors.dart'; +import 'package:todo_client/src/components/shared/list_item_expansion_panel_summary.dart'; +import 'package:todo_client/src/components/shared/list_item_mixin.dart'; +import 'package:todo_client/src/components/shared/material_ui.dart'; +import 'package:todo_client/src/components/shared/todo_item_text_field.dart'; +import 'package:todo_client/src/components/task_count.dart'; +import 'package:todo_client/src/store.dart'; + +// ignore: uri_has_not_been_generated +part 'user_list_item.over_react.g.dart'; + +UiFactory ConnectedUserListItem = connect( + mapDispatchToProps: (dispatch) { + return (UserListItem() + ..onSelect = (id) { dispatch(SelectUserAction(id)); } + ..onDeselect = (id) { dispatch(DeselectUserAction(id)); } + ..onBeginEdit = (id) { dispatch(BeginEditUserAction(id)); } + ..onFinishEdit = (id) { dispatch(FinishEditUserAction(id)); } + ..onModelUpdate = (user) { dispatch(UpdateUserAction(user)); } + ..onRemove = (id) { dispatch(RemoveUserAction(id)); } + ); + }, + mapStateToPropsWithOwnProps: (state, ownProps) { + final isEditable = Set.of(state.editableUserIds).contains(ownProps.model.id); + final isSelected = isEditable || Set.of(state.selectedUserIds).contains(ownProps.model.id); + final isHighlighted = Set.of(state.highlightedUserIds).contains(ownProps.model.id); + + return (UserListItem() + ..isSelected = isSelected + ..isEditable = isEditable + ..isHighlighted = isHighlighted + ); + }, +)(UserListItem); + +@Factory() +UiFactory UserListItem = + // ignore: undefined_identifier + _$UserListItem; + +@Props() +class _$UserListItemProps extends UiProps + with ListItemPropsMixin, + // ignore: mixin_of_non_class, undefined_class + $ListItemPropsMixin { + @requiredProp + @override + User model; +} + +@State() +class _$UserListItemState extends UiState + with ListItemStateMixin, + // ignore: mixin_of_non_class, undefined_class + $ListItemStateMixin { + @override + User localModel; +} + +@Component2() +class UserListItemComponent extends UiStatefulComponent2 + with ListItemMixin { + @override + bool get hasDetails => model.bio != null && model.bio.isNotEmpty; + + @override + render() { + return ExpansionPanel(sharedExpansionPanelProps, [ + (ListItemExpansionPanelSummary() + ..key = 'summary' + ..modelId = model.id + ..allowExpansion = allowExpansion + ..isEditable = props.isEditable + ..onToggleEditable = toggleEditable + )( + _renderUserAvatar(), + _renderUserNameHeader(), + ), + ExpansionPanelDetails({'key': 'details'}, + _renderUserBio(), + ), + _renderEditableUserActions(), + ]); + } + + ReactElement _renderUserAvatar() { + return Box({...shrinkToFit, + 'key': 'userAvatar', + 'ml': -1, + 'mr': 2, + 'alignSelf': 'center', + }, + (ConnectedTaskCountBadge()..user = model)( + (AvatarWithColors() + ..key = 'avatar' + ..fullName = props.model.name + )(), + ), + ); + } + + ReactElement _renderUserNameHeader() { + return Box({...grow, + 'key': 'userNameHeader', + 'mr': 1, + 'alignSelf': 'center', + }, + (TodoItemTextField() + ..readOnly = !props.isEditable + ..autoFocus = props.isEditable + ..label = 'Name' + ..onChange = _updateNameValue + ..placeholder = 'Enter a name...' + ..value = model.name + ..onClickWhenEditable = (event) { event.stopPropagation(); } + ..['inputProps'] = { + 'style': props.isEditable ? null : { + 'cursor': allowExpansion ? 'pointer' : 'default', + }, + } + )(), + ); + } + + void _updateNameValue(SyntheticFormEvent event) { + InputElement target = event.target; + updateModel(User.from(model)..name = target.value); + } + + ReactElement _renderUserBio() { + return (TodoItemTextField() + ..readOnly = !props.isEditable + ..label = 'Biography' + ..multiline = true + ..['rows'] = 3 + ..onChange = _updateBioValue + ..placeholder = 'Tell us a little bit about ${model.name}' + ..value = model.bio + )(); + } + + void _updateBioValue(SyntheticFormEvent event) { + TextAreaElement target = event.target; + updateModel(User.from(model)..bio = target.value); + } + + ReactElement _renderEditableUserActions() { + if (!props.isEditable) return null; + + return (Fragment()..key = 'editableUserActions')( + Divider({}), + ExpansionPanelActions({}, + Grid({'container': true, 'direction': 'row'}, [ + Box({'key': 'leftButtons', 'flexGrow': 1, 'display': 'flex'}, [ + _renderEditableUserDeleteButton(), + ]), + Box({'key': 'rightButtons', ...shrinkToFit}, [ + _renderEditableUserCancelButton(), + _renderEditableUserSaveButton(), + ]), + ]), + ), + ); + } + + ReactElement _renderEditableUserDeleteButton() { + return Tooltip({'key': 'deleteButton', 'enterDelay': 500, 'title': 'Delete Todo'}, + Box({'color': 'error.main'}, + IconButton({ + 'size': 'small', + 'aria-label': 'delete todo', + 'color': 'inherit', + 'onClick': (_) { remove(); }, + }, + TrashIcon(), + ), + ), + ); + } + + ReactElement _renderEditableUserCancelButton() { + return Button({ + 'key': 'cancel', + 'size': 'small', + 'onClick': (_) { exitEditable(saveChanges: false); } + }, 'Cancel'); + } + + ReactElement _renderEditableUserSaveButton() { + return Button({ + 'key': 'save', + 'size': 'small', + 'color': 'primary', + 'onClick': (_) { exitEditable(saveChanges: true); } + }, 'Save'); + } +} + +// ignore: mixin_of_non_class, undefined_class +class UserListItemProps extends _$UserListItemProps with _$UserListItemPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForUserListItemProps; +} + +// ignore: mixin_of_non_class, undefined_class +class UserListItemState extends _$UserListItemState with _$UserListItemStateAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const StateMeta meta = _$metaForUserListItemState; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/user_selector.dart b/app/over_react_redux/todo_client/lib/src/components/user_selector.dart new file mode 100644 index 000000000..d785068f9 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/user_selector.dart @@ -0,0 +1,99 @@ +library todo_client.src.components.user_selector; + +import 'package:collection/collection.dart'; +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; + +import 'package:todo_client/src/store.dart'; +import 'package:todo_client/src/models/user.dart'; +import 'package:todo_client/src/components/shared/avatar_with_colors.dart'; +import 'package:todo_client/src/components/shared/material_ui.dart'; +import 'package:todo_client/src/components/shared/menu_overlay.dart'; + +part 'user_selector_trigger.dart'; +// ignore: uri_has_not_been_generated +part 'user_selector.over_react.g.dart'; + +UiFactory ConnectedUserSelector = connect( + mapStateToPropsWithOwnProps: (state, ownProps) { + return (UserSelector() + ..selectedUser = ownProps.selectedUserId.isNotEmpty + ? state.users.firstWhere((user) => user.id == ownProps.selectedUserId) + : null + ..users = state.users + ); + }, +)(UserSelector); + +@Factory() +UiFactory UserSelector = + // ignore: undefined_identifier + _$UserSelector; + +@Props() +class _$UserSelectorProps extends UiProps { + String selectedUserId; + User selectedUser; + List users; + Function(String userId) onUserSelect; +} + +@Component2() +class UserSelectorComponent extends UiComponent2 { + final _overlayRef = createRef(); + + @override + bool shouldComponentUpdate(Map nextProps, Map nextState) { + final tNextProps = typedPropsFactory(nextProps); + if (_overlayRef.current?.open != true) { + return tNextProps.selectedUserId != props.selectedUserId; + } + + return !ListEquality().equals(tNextProps.users, props.users); + } + + @override + render() { + return (MenuOverlay() + ..modifyProps(addUnconsumedProps) + ..trigger = _renderMenuTrigger() + ..ref = _overlayRef + )( + props.users.map(_renderMenuItem).toList(), + ); + } + + ReactElement _renderMenuTrigger() { + return (UserSelectorTrigger() + ..selectedUserName = props.selectedUser?.name + ..disabled = props.users.isEmpty + )(); + } + + ReactElement _renderMenuItem(User user) { + return MenuItem({ + 'key': user.id, + 'onClick': (SyntheticMouseEvent event) { + // Don't expand / collapse the user list item + event.stopPropagation(); + _handleUserSelect(user); + }, + }, [ + Box({'key': 'avatarBox', 'mr': 1}, + (AvatarWithColors()..fullName = user.name)(), + ), + user.name, + ]); + } + + void _handleUserSelect(User user) { + props.onUserSelect(user.id); + _overlayRef.current.close(); + } +} + +// ignore: mixin_of_non_class, undefined_class +class UserSelectorProps extends _$UserSelectorProps with _$UserSelectorPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForUserSelectorProps; +} diff --git a/app/over_react_redux/todo_client/lib/src/components/user_selector_trigger.dart b/app/over_react_redux/todo_client/lib/src/components/user_selector_trigger.dart new file mode 100644 index 000000000..81e86490c --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/user_selector_trigger.dart @@ -0,0 +1,42 @@ +part of todo_client.src.components.user_selector; + +@Factory() +UiFactory UserSelectorTrigger = + // ignore: undefined_identifier + _$UserSelectorTrigger; + +@Props(keyNamespace: '') +class _$UserSelectorTriggerProps extends UiProps { + String selectedUserName; + bool disabled; +} + +@Component2() +class UserSelectorTriggerComponent extends UiComponent2 { + @override + bool shouldComponentUpdate(Map nextProps, Map nextState) { + final tNextProps = typedPropsFactory(nextProps); + return tNextProps.selectedUserName != props.selectedUserName || tNextProps.disabled != props.disabled; + } + + @override + get defaultProps => (newProps()..disabled = false); + + @override + render() { + final propsToForward = Map.of(props)..remove('selectedUserName'); + return IconButton({ + ...propsToForward, + // Avatar height is 40px, so 4px on top and bottom will make it match the default height of adjacent IconButtons (48px) + 'style': { 'padding': '4px' }, + }, + (AvatarWithColors()..fullName = props.selectedUserName)(), + ); + } +} + +// ignore: mixin_of_non_class, undefined_class +class UserSelectorTriggerProps extends _$UserSelectorTriggerProps with _$UserSelectorTriggerPropsAccessorsMixin { + // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value + static const PropsMeta meta = _$metaForUserSelectorTriggerProps; +} diff --git a/app/over_react_redux/todo_client/lib/src/local_storage.dart b/app/over_react_redux/todo_client/lib/src/local_storage.dart new file mode 100644 index 000000000..d0b7d7040 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/local_storage.dart @@ -0,0 +1,120 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'dart:html'; + +import 'package:todo_client/src/store.dart'; + +/// The `window.localStorage` interface for our application. +TodoAppLocalStorage localTodoAppStorage; + +/// A map interface for mutating `window.localStorage` values +/// used to persist [AppState] values across browser refreshes. +class TodoAppLocalStorage extends MapBase { + final AppState initialState; + + TodoAppLocalStorage([this.initialState]) { + if (isInitialized) return; + + final emptyState = AppState(TodoAppLocalStorage.emptyStateKey, + todos: [], + users: [], + selectedTodoIds: [], + editableTodoIds: [], + highlightedTodoIds: [], + selectedUserIds: [], + editableUserIds: [], + highlightedUserIds: [], + ).toJson(); + + window.localStorage[localStorageKey] = json.encode({ + currentStateKey: this.initialState?.toJson() ?? {}, + defaultStateKey: this.initialState?.toJson() ?? {}, + emptyStateKey: emptyState, + }); + } + + // -------------------- Utilities -------------------- + + static bool isInitialized = window.localStorage[localStorageKey] != null; + + Map get _proxiedMap => json.decode(window.localStorage[localStorageKey]); + + void updateCurrentState(AppState newValue) { + this[currentStateKey] = newValue.toJson(); + } + + void reset() { + this[currentStateKey] = defaultStateJson; + } + + Map get currentStateJson => this[currentStateKey]; + Map get defaultStateJson => this[defaultStateKey]; + Map get emptyStateJson => this[emptyStateKey]; + Map> get mapOfCustomStatesJson => { + ...Map.from(this)..removeWhere((key, value) => reservedStateKeys.contains(key)) + }; + + // -------------------- MapBase implementations -------------------- + + @override + operator [](Object key) { + return _proxiedMap[key]; + } + + @override + void operator []=(key, value) { + final updatedJson = Map.of(_proxiedMap); + updatedJson[key] = value; + window.localStorage[localStorageKey] = json.encode(updatedJson); + } + + @override + Iterable get keys => _proxiedMap.keys; + + @override + bool containsKey(Object key) => keys.contains(key); + + @override + Iterable get values => _proxiedMap.values; + + @override + bool containsValue(Object value) => values.contains(value); + + @override + void addAll(Map other) { + final updatedJson = Map.of(_proxiedMap)..addAll(other); + window.localStorage[localStorageKey] = json.encode(updatedJson); + } + + @override + void clear() { + this[currentStateKey] = emptyStateJson; + } + + @override + Object remove(Object key) { + assert(!reservedStateKeys.contains(key)); + + final updatedJson = Map.of(_proxiedMap); + var valueRemoved = updatedJson.remove(key); + + window.localStorage[localStorageKey] = json.encode(updatedJson); + return valueRemoved; + } + + @override + String toString() => window.localStorage[localStorageKey]; + + static const String localStorageKey = 'todo_client.AppState'; + static const String currentStateKey = 'current'; + static const String emptyStateKey = 'empty'; + static const String defaultStateKey = 'default'; + static const List reservedStateKeys = [ + currentStateKey, + emptyStateKey, + defaultStateKey, + ]; +} + +// ignore: prefer_single_quotes +const defaultAppState = {"name":"default","todos":[{"id":"1e6c70ae-69c5-49a9-a605-68b4900e0e55","description":"Own Joe in Super Smash Bros","assignedUserId":"77dd23d7-19ce-4ba4-9075-cabb4572c83d","isCompleted":true,"isPublic":true,"notes": "6/25/2019"},{"id":"8f487d61-927f-436e-b794-f9749d520854","description":"Trim your beard","isCompleted":false,"isPublic":false,"notes":"Here's some notes about when it should be trimmed, how short, and so much more!","assignedUserId":"1b948c58-b0f3-4a9d-87d8-32c6862a1e18"},{"id":"81caf55e-c0c0-4e13-a271-98dca3d67d9e","description":"Get a haircut","isCompleted":false,"isPublic":false,"notes":"Self explanatory. Clean it up hippie.","assignedUserId":"c42ef2c2-aa37-42c1-bd2a-bb5d4935846e"},{"id":"23987b56-1e1a-4c52-963b-44cfeb35d368","description":"Take the kids to school","isCompleted":false,"isPublic":false,"notes":"","assignedUserId":"77dd23d7-19ce-4ba4-9075-cabb4572c83d"},{"id":"0009face-8b12-45aa-b2c4-251db7e46bec","description":"Explain that the platform is important","isCompleted":false,"isPublic":false,"notes":"Again.","assignedUserId":"5c261eca-7aca-47b8-a223-db83ff868444"},{"id":"eee66b2b-0028-411d-97af-91fdfe3ee89f","description":"Go to Disneyland","isCompleted":true,"isPublic":false,"notes":"","assignedUserId":"3859c1ca-a254-4522-85c5-8f6af8490eb4"},{"id":"f6542e94-c12e-4e29-bd0a-af838363686b","description":"Study for that one last final!","isCompleted":false,"isPublic":false,"notes":"","assignedUserId":"56590b19-c40d-4e76-9bfd-79e2f5fa57ee"},{"id":"0e5653a7-5287-4058-b1d2-0187198f1953","description":"Learn more of Behdad's sayings","isCompleted":false,"isPublic":false,"notes":"","assignedUserId":"b9186b2b-7606-4b9b-9cdc-96be45e6ef07"},{"id":"8d4686a4-0b2f-4a34-8756-2af328354653","description":"Finish Context","isCompleted":true,"isPublic":false,"notes":"","assignedUserId":"1b514273-a2d3-4e0c-a4fa-7b2dbdd89e7f"},{"id":"d28fb4f6-3336-4d7f-84b3-010b7a68d149","description":"Go to Disneyland again","isCompleted":false,"isPublic":false,"notes":"","assignedUserId":"3859c1ca-a254-4522-85c5-8f6af8490eb4"},{"id":"01362064-9b83-4103-83db-94a69a04e5e8","description":"Check unread messages in platform support chats","isCompleted":false,"isPublic":false,"notes":"","assignedUserId":"cfbe3200-dc71-48d2-a5f9-c2404f3a23b5"},{"id":"85501e1e-8433-4896-959a-30a289ed68f0","description":"Write tests for w_router","isCompleted":false,"isPublic":false,"notes":"","assignedUserId":"d487282b-20d9-480f-ae4f-5b93168b6717"},{"id":"67b1fa43-4eb1-4c27-8519-8886f379dc58","description":"Walk the dog","isCompleted":false,"isPublic":false,"notes":"","assignedUserId":"77dd23d7-19ce-4ba4-9075-cabb4572c83d"},{"id":"7a34cfb4-4368-4570-ae19-39732b8b3c96","description":"Find more \"retro\" pictures","isCompleted":false,"isPublic":false,"notes":"","assignedUserId":"5c261eca-7aca-47b8-a223-db83ff868444"}],"users":[{"id":"77dd23d7-19ce-4ba4-9075-cabb4572c83d","name":"Aaron Lademann","bio":""},{"id":"1b514273-a2d3-4e0c-a4fa-7b2dbdd89e7f","name":"Greg Littlefield","bio":""},{"id":"b9186b2b-7606-4b9b-9cdc-96be45e6ef07","name":"Joe Bingham","bio":""},{"id":"3859c1ca-a254-4522-85c5-8f6af8490eb4","name":"Keal Jones","bio":""},{"id":"56590b19-c40d-4e76-9bfd-79e2f5fa57ee","name":"Sydney Jodon","bio":""},{"id":"c42ef2c2-aa37-42c1-bd2a-bb5d4935846e","name":"Evan Weible","bio":""},{"id":"cfbe3200-dc71-48d2-a5f9-c2404f3a23b5","name":"Corwin Sheahan","bio":""},{"id":"ab0683c1-9bc5-4af2-be4d-43ec9d8516d0","name":"Smai Fullerton","bio":""},{"id":"3d035e0c-0dbc-4d80-bf8b-8613a0bd9b43","name":"Tod Bachman","bio":""},{"id":"bd4b321e-c216-4b51-ad83-cff92e72dabf","name":"Rob Becker","bio":""},{"id":"5c261eca-7aca-47b8-a223-db83ff868444","name":"Rob Duff","bio":""},{"id":"d487282b-20d9-480f-ae4f-5b93168b6717","name":"Trent Grover","bio":""},{"id":"ef803556-410a-4b96-ac49-f05642594a61","name":"Joel Leibow","bio":""},{"id":"f05227ed-ef5f-45c1-becd-84d39c04315c","name":"Aaron St. George","bio":""},{"id":"b9a45ddc-0ecc-4dba-90db-968e95c99ffe","name":"Olesia Thoms","bio":""},{"id":"06935491-fd09-4aa7-8994-d2725b9f0378","name":"Will Drach","bio":""},{"id":"1b948c58-b0f3-4a9d-87d8-32c6862a1e18","name":"Behdad Shayegan","bio":""}],"selectedTodoIds":[],"editableTodoIds":[],"highlightedTodoIds":[],"selectedUserIds":[],"editableUserIds":[],"highlightedUserIds":[]}; diff --git a/app/over_react_redux/todo_client/lib/src/models/base_model.dart b/app/over_react_redux/todo_client/lib/src/models/base_model.dart new file mode 100644 index 000000000..f8205c92c --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/models/base_model.dart @@ -0,0 +1,6 @@ +abstract class BaseModel { + /// Unique identifier. Assigned by server. + String get id; + + Map toJson(); +} diff --git a/app/over_react_redux/todo_client/lib/src/models/todo.dart b/app/over_react_redux/todo_client/lib/src/models/todo.dart new file mode 100644 index 000000000..caf03baad --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/models/todo.dart @@ -0,0 +1,51 @@ +import 'package:uuid/uuid.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'package:todo_client/src/models/base_model.dart'; + +part 'todo.g.dart'; + +@JsonSerializable() +class Todo implements BaseModel { + /// Unique identifier. Assigned by server. + @override + final String id; + + /// Short description of item. Serves as the title. + String description; + + /// Whether or not this item has been marked as completed. + bool isCompleted; + + /// Whether or not this item is public. Public means anyone in the application + /// can see it. Private means only the creator can see it. + bool isPublic; + + /// Notes + String notes; + + /// The id of the User object assigned to this instance. + String assignedUserId; + + Todo({ + this.description = '', + String id, + this.isCompleted = false, + this.isPublic = false, + this.notes = '', + this.assignedUserId = '', + }) : id = id ?? Uuid().v4(); + + factory Todo.fromJson(Map json) => _$TodoFromJson(json); + factory Todo.from(Todo todo) => Todo( + description: todo.description, + id: todo.id, + isCompleted: todo.isCompleted, + isPublic: todo.isPublic, + notes: todo.notes, + assignedUserId: todo.assignedUserId, + ); + + @override + Map toJson() => _$TodoToJson(this); +} diff --git a/app/over_react_redux/todo_client/lib/src/models/todo.g.dart b/app/over_react_redux/todo_client/lib/src/models/todo.g.dart new file mode 100644 index 000000000..f211a2fac --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/models/todo.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'todo.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Todo _$TodoFromJson(Map json) { + return Todo( + description: json['description'] as String, + id: json['id'] as String, + isCompleted: json['isCompleted'] as bool, + isPublic: json['isPublic'] as bool, + notes: json['notes'] as String, + assignedUserId: json['assignedUserId'] as String); +} + +Map _$TodoToJson(Todo instance) => { + 'id': instance.id, + 'description': instance.description, + 'isCompleted': instance.isCompleted, + 'isPublic': instance.isPublic, + 'notes': instance.notes, + 'assignedUserId': instance.assignedUserId + }; diff --git a/app/over_react_redux/todo_client/lib/src/models/user.dart b/app/over_react_redux/todo_client/lib/src/models/user.dart new file mode 100644 index 000000000..1ad2bc48b --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/models/user.dart @@ -0,0 +1,35 @@ +import 'package:uuid/uuid.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'package:todo_client/src/models/base_model.dart'; + +part 'user.g.dart'; + +@JsonSerializable() +class User implements BaseModel { + /// Unique identifier. Assigned by server. + @override + final String id; + + /// First and last name. Used as a display name. + String name; + + /// Short description of the user. + String bio; + + User({ + this.name = '?', + String id, + this.bio = '', + }) : id = id ?? Uuid().v4(); + + factory User.fromJson(Map json) => _$UserFromJson(json); + factory User.from(User user) => User( + name: user.name, + id: user.id, + bio: user.bio, + ); + + @override + Map toJson() => _$UserToJson(this); +} diff --git a/app/over_react_redux/todo_client/lib/src/models/user.g.dart b/app/over_react_redux/todo_client/lib/src/models/user.g.dart new file mode 100644 index 000000000..8e0fdf674 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/models/user.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +User _$UserFromJson(Map json) { + return User( + name: json['name'] as String, + id: json['id'] as String, + bio: json['bio'] as String); +} + +Map _$UserToJson(User instance) => { + 'id': instance.id, + 'name': instance.name, + 'bio': instance.bio + }; diff --git a/app/over_react_redux/todo_client/lib/src/store.dart b/app/over_react_redux/todo_client/lib/src/store.dart new file mode 100644 index 000000000..c71a797ea --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/store.dart @@ -0,0 +1,190 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:over_react/over_react_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:redux_dev_tools/redux_dev_tools.dart'; + +import 'package:todo_client/src/models/todo.dart'; +import 'package:todo_client/src/models/user.dart'; +import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/local_storage.dart'; + +part 'store.g.dart'; + +AppState _initializeState() { + AppState initialState; + if (!TodoAppLocalStorage.isInitialized) { + // First load - give the user some data to work with, and set up our default / empty states. + initialState = AppState.fromJson(defaultAppState); + localTodoAppStorage = TodoAppLocalStorage(initialState); + } else { + localTodoAppStorage ??= TodoAppLocalStorage(); + initialState = AppState.fromJson(localTodoAppStorage.currentStateJson); + } + + return initialState; +} + +var store = DevToolsStore( + appStateReducer, + initialState: _initializeState(), + middleware: [overReactReduxDevToolsMiddleware], +); + +@JsonSerializable(explicitToJson: true) +class AppState { + String name; + List todos; + List users; + List selectedTodoIds; + List editableTodoIds; + List highlightedTodoIds; + List selectedUserIds; + List editableUserIds; + List highlightedUserIds; + + AppState(this.name, { + this.todos, + this.users, + this.selectedTodoIds, + this.editableTodoIds, + this.highlightedTodoIds, + this.selectedUserIds, + this.editableUserIds, + this.highlightedUserIds, + }) { + assert(name != null); + localTodoAppStorage?.updateCurrentState(this); + } + + factory AppState.fromJson(Map json) => _$AppStateFromJson(json); + Map toJson() => _$AppStateToJson(this); +} + +AppState appStateReducer(AppState state, dynamic action) { + if (action is SaveLocalStorageStateAsAction) { + Map previousValue; + if (action.value.previousName != null) { + previousValue = localTodoAppStorage.remove(action.value.previousName); + } else { + previousValue = localTodoAppStorage.currentStateJson; + } + + localTodoAppStorage[action.value.name] = (AppState.fromJson(previousValue)..name = action.value.name).toJson(); + } + + if (action is LoadStateFromLocalStorageAction) { + return AppState.fromJson(localTodoAppStorage[action.value]); + } + + return AppState(localTodoAppStorage.currentStateJson['name'], + todos: todosReducer(state.todos, action), + users: usersReducer(state.users, action), + editableTodoIds: editableTodosReducer(state.editableTodoIds, action), + selectedTodoIds: selectedTodosReducer(state.selectedTodoIds, action), + highlightedTodoIds: highlightedTodosReducer(state.highlightedTodoIds, action), + editableUserIds: editableUsersReducer(state.editableUserIds, action), + selectedUserIds: selectedUsersReducer(state.selectedUserIds, action), + highlightedUserIds: highlightedUsersReducer(state.highlightedUserIds, action), + ); +} + +// ------------ ITEM REDUCERS ------------------ + +final todosReducer = combineReducers>([ + TypedReducer, AddTodoAction>((todos, action) { + return [action.value, ...todos]; + }), + TypedReducer, RemoveTodoAction>((todos, action) { + return List.of(todos)..removeWhere((todo) => todo.id == action.value); + }), + TypedReducer, UpdateTodoAction>((todos, action) { + return todos.map((todo) { + final updatedTodo = action.value; + if (todo.id == updatedTodo.id) { + return updatedTodo; + } + return todo; + }).toList(); + }), +]); + +final selectedTodosReducer = combineReducers>([ + TypedReducer, SelectTodoAction>((selectedTodoIds, action) { + return [...selectedTodoIds, action.value]; + }), + TypedReducer, DeselectTodoAction>((selectedTodoIds, action) { + return List.of(selectedTodoIds)..removeWhere((id) => id == action.value); + }), +]); + +final editableTodosReducer = combineReducers>([ + TypedReducer, BeginEditTodoAction>((editableTodoIds, action) { + return [...editableTodoIds, action.value]; + }), + TypedReducer, FinishEditTodoAction>((editableTodoIds, action) { + return List.of(editableTodoIds)..removeWhere((id) => id == action.value); + }), +]); + +final highlightedTodosReducer = combineReducers>([ + TypedReducer, HighlightTodosAction>((highlightedTodoIds, action) { + return [...highlightedTodoIds, ...action.value]; + }), + TypedReducer, UnHighlightTodosAction>((highlightedTodoIds, action) { + return List.of(highlightedTodoIds)..removeWhere((id) => action.value.contains(id)); + }), +]); + +List highlightTodosReducer(List highlightedTodoIds, dynamic action) { + if (action is HighlightTodosAction) { + return action.value; + } + return highlightedTodoIds; +} + +// ------------ USER REDUCERS ------------------ + +final usersReducer = combineReducers>([ + TypedReducer, AddUserAction>((users, action) { + return [action.value, ...users]; + }), + TypedReducer, RemoveUserAction>((users, action) { + return List.of(users)..removeWhere((user) => user.id == action.value); + }), + TypedReducer, UpdateUserAction>((users, action) { + return users.map((user) { + final updatedUser = action.value; + if (user.id == updatedUser.id) { + return updatedUser; + } + return user; + }).toList(); + }), +]); + +Reducer> selectedUsersReducer = combineReducers>([ + TypedReducer, SelectUserAction>((selectedUserIds, action) { + return [...selectedUserIds, action.value]; + }), + TypedReducer, DeselectUserAction>((selectedUserIds, action) { + return List.of(selectedUserIds)..removeWhere((id) => id == action.value); + }), +]); + +Reducer> editableUsersReducer = combineReducers>([ + TypedReducer, BeginEditUserAction>((editableUserIds, action) { + return [...editableUserIds, action.value]; + }), + TypedReducer, FinishEditUserAction>((editableUserIds, action) { + return List.of(editableUserIds)..removeWhere((id) => id == action.value); + }), +]); + +final highlightedUsersReducer = combineReducers>([ + TypedReducer, HighlightUsersAction>((highlightedUserIds, action) { + return [...highlightedUserIds, ...action.value]; + }), + TypedReducer, UnHighlightUsersAction>((highlightedUserIds, action) { + return List.of(highlightedUserIds)..removeWhere((id) => action.value.contains(id)); + }), +]); diff --git a/app/over_react_redux/todo_client/lib/src/store.g.dart b/app/over_react_redux/todo_client/lib/src/store.g.dart new file mode 100644 index 000000000..1f895784a --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/store.g.dart @@ -0,0 +1,45 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'store.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AppState _$AppStateFromJson(Map json) { + return AppState(json['name'] as String, + todos: (json['todos'] as List) + ?.map((e) => + e == null ? null : Todo.fromJson(e as Map)) + ?.toList(), + users: (json['users'] as List) + ?.map((e) => + e == null ? null : User.fromJson(e as Map)) + ?.toList(), + selectedTodoIds: + (json['selectedTodoIds'] as List)?.map((e) => e as String)?.toList(), + editableTodoIds: + (json['editableTodoIds'] as List)?.map((e) => e as String)?.toList(), + highlightedTodoIds: (json['highlightedTodoIds'] as List) + ?.map((e) => e as String) + ?.toList(), + selectedUserIds: + (json['selectedUserIds'] as List)?.map((e) => e as String)?.toList(), + editableUserIds: + (json['editableUserIds'] as List)?.map((e) => e as String)?.toList(), + highlightedUserIds: (json['highlightedUserIds'] as List) + ?.map((e) => e as String) + ?.toList()); +} + +Map _$AppStateToJson(AppState instance) => { + 'name': instance.name, + 'todos': instance.todos?.map((e) => e?.toJson())?.toList(), + 'users': instance.users?.map((e) => e?.toJson())?.toList(), + 'selectedTodoIds': instance.selectedTodoIds, + 'editableTodoIds': instance.editableTodoIds, + 'highlightedTodoIds': instance.highlightedTodoIds, + 'selectedUserIds': instance.selectedUserIds, + 'editableUserIds': instance.editableUserIds, + 'highlightedUserIds': instance.highlightedUserIds + }; diff --git a/app/over_react_redux/todo_client/lib/src/utils.dart b/app/over_react_redux/todo_client/lib/src/utils.dart new file mode 100644 index 000000000..261b127bc --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/utils.dart @@ -0,0 +1,23 @@ +import 'package:react/react_client/js_backed_map.dart'; + +JsBackedMap createJsBackedMapRecursively(JsMap jsMap) { + JsBackedMap _createJsBackedMap(JsMap nestedJsMap) { + final map = JsBackedMap.fromJs(nestedJsMap); + map.forEach((key, value) { + if (value is JsMap) { + map[key] = _createJsBackedMap(value); + } + }); + + return map; + } + + final topLevelJsBackedMap = JsBackedMap.fromJs(jsMap); + topLevelJsBackedMap.forEach((key, value) { + if (value is JsMap) { + topLevelJsBackedMap[key] = _createJsBackedMap(value); + } + }); + + return topLevelJsBackedMap; +} diff --git a/app/over_react_redux/todo_client/lib/todo_client.dart b/app/over_react_redux/todo_client/lib/todo_client.dart new file mode 100644 index 000000000..fc9116743 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/todo_client.dart @@ -0,0 +1,4 @@ +library todo_client; + +export 'package:todo_client/src/components/app.dart' show ConnectedTodoApp; +export 'package:todo_client/src/store.dart' show store; diff --git a/app/over_react_redux/todo_client/pubspec.yaml b/app/over_react_redux/todo_client/pubspec.yaml new file mode 100644 index 000000000..0b808bc8f --- /dev/null +++ b/app/over_react_redux/todo_client/pubspec.yaml @@ -0,0 +1,20 @@ +name: todo_client +version: 1.0.0 + +environment: + sdk: ">=2.4.0 <3.0.0" + +dependencies: + color: any + over_react: ^3.1.5 + redux: ^3.0.0 + redux_dev_tools: 0.4.0 + uuid: ^1.0.3 + +dev_dependencies: + w_common: ^1.20.0 + over_react_test: ^2.0.0 + build_runner: ^1.7.1 + build_web_compilers: ^2.5.1 + json_serializable: ^2.0.0 + pedantic: ^1.8.0 diff --git a/app/over_react_redux/todo_client/smithy.yml b/app/over_react_redux/todo_client/smithy.yml new file mode 100644 index 000000000..7fbb3b3f9 --- /dev/null +++ b/app/over_react_redux/todo_client/smithy.yml @@ -0,0 +1,19 @@ +project: dart +language: dart + +# Dart 1.23.0 image from https://github.com/Workiva/smithy-runner-generator +runner_image: drydock-prod.workiva.net/workiva/smithy-runner-generator:153818 + +before_script: + - dart --version + - pub get --packages-dir + - pub run over_react_format:bootstrap --check + - pub run dart_dev format --check + - pub run dart_dev analyze + +script: + - DART_FLAGS=--checked xvfb-run -s '-screen 0 1024x768x24' pub run dart_dev test + +after_script: + +artifacts: diff --git a/app/over_react_redux/todo_client/web/index.html b/app/over_react_redux/todo_client/web/index.html new file mode 100644 index 000000000..0ba091c73 --- /dev/null +++ b/app/over_react_redux/todo_client/web/index.html @@ -0,0 +1,24 @@ + + + + + + Todo Example + + + + + + + +
+ + + + + + + + + + diff --git a/app/over_react_redux/todo_client/web/js/material-ui-config.js b/app/over_react_redux/todo_client/web/js/material-ui-config.js new file mode 100644 index 000000000..16fd5e772 --- /dev/null +++ b/app/over_react_redux/todo_client/web/js/material-ui-config.js @@ -0,0 +1,24 @@ +// Lets use some of material ui's stuff in JS world +const { + createMuiTheme, + colors, +} = window.MaterialUI; + +// Assigned to the window so that dart can get access to it. +// window.theme = createMuiTheme({ +// palette: { +// primary: { +// light: colors.purple[300], +// main: colors.purple[500], +// dark: colors.purple[700] +// }, +// secondary: { +// light: colors.green[300], +// main: colors.green[500], +// dark: colors.green[700] +// } +// }, +// typography: { +// useNextVariants: true +// } +// }); diff --git a/app/over_react_redux/todo_client/web/main.dart b/app/over_react_redux/todo_client/web/main.dart new file mode 100644 index 000000000..34d4b72cf --- /dev/null +++ b/app/over_react_redux/todo_client/web/main.dart @@ -0,0 +1,18 @@ +import 'dart:html'; + +import 'package:over_react/over_react.dart'; +import 'package:over_react/react_dom.dart' as react_dom; +import 'package:over_react/over_react_redux.dart'; + +import 'package:todo_client/todo_client.dart'; + +main() { + setClientConfiguration(); + + final container = querySelector('#todo-container'); + react_dom.render( + (ReduxProvider()..store = store)( + ConnectedTodoApp()(), + ), + container); +} diff --git a/doc/over_react_redux_documentation.md b/doc/over_react_redux_documentation.md index f23520e03..0c83eb033 100644 --- a/doc/over_react_redux_documentation.md +++ b/doc/over_react_redux_documentation.md @@ -37,19 +37,35 @@ behavior with React). By utilizing the `connect()` function in conjunction with only update when a piece of information it uses is updated. ## Examples -Examples are available in the `web` directory. Each example illustrates a different variation or use case of OverReact - Redux. Additionally, the store files contain comments that call out specifics pertaining to that example and - provides further explanation. -### Running the Examples +### Individual component examples +There are some individual component examples within the `web/over_react_redux` directory. +Each example illustrates a different variation or use case of OverReact Redux. Additionally, the store files contain +comments that call out specifics pertaining to that example and provides further explanation. + +#### Running the component examples To run and experiment with the demo: 1. `pub get` -1. `pub run build_runner serve web` +1. `webdev serve` 1. Navigate to `localhost:8080/over_react_redux/` 1. If you have the [React DevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en), you can view the isolated state updates based on the `mapStateToProps` when you turn on ["Highlight updates when components render."](https://github.com/facebook/react/pull/16989) +### Application example +There is a "Todo" example app built with OverReact Redux within the `app/over_react_redux` directory. +This app illustrates a full-scale implementation of an application that handles all of the data flow using redux. + +#### Running the application +To run and experiment with the "Todo" app: +1. `cd app/over_react_redux/todo_client` +1. `pub get` +1. `webdev serve` +1. Navigate to `localhost:8080` +1. If you have the [React DevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en), +you can view the isolated state updates based on the `mapStateToProps` when you turn on +["Highlight updates when components render."](https://github.com/facebook/react/pull/16989) + ## Using it in your project 1. Add the `redux` package as a dependency in your `pubspec.yaml`. diff --git a/example/index.html b/example/index.html index c0bd086f6..1d0ce685b 100644 --- a/example/index.html +++ b/example/index.html @@ -17,6 +17,12 @@

OverReact Examples

diff --git a/pubspec.yaml b/pubspec.yaml index ec44455a0..c1d41b568 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: logging: ">=0.11.3+2 <1.0.0" meta: ^1.1.6 path: ^1.5.1 - react: ^5.1.0 + react: ^5.2.1 redux: ^3.0.0 source_span: ^1.4.1 transformer_utils: ^0.2.0 From 5b2f7363823f88b8a01354afe27bb172704a2231 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 18 Dec 2019 13:19:02 -0700 Subject: [PATCH 04/34] Refine recursive JsBackedMap stuff in todo_client example + Use extension methods! + Fix some problematic comparison logic + Simplify top level traversal --- .../src/components/shared/material_ui.dart | 4 +- .../todo_client/lib/src/utils.dart | 50 +++++++++++++------ app/over_react_redux/todo_client/pubspec.yaml | 2 +- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart index c52da62eb..a453e0434 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart @@ -49,8 +49,8 @@ class MaterialUI { external static ReactClass get Typography; } -final colors = createJsBackedMapRecursively(MaterialUI.colors); -final classes = createJsBackedMapRecursively(MaterialUI.classes); +final colors = JsBackedMap().deepConvertFromJs(MaterialUI.colors); +final classes = JsBackedMap().deepConvertFromJs(MaterialUI.classes); // ----------------------------------------------------------------------- // Below, you'll find the top level JS component factories diff --git a/app/over_react_redux/todo_client/lib/src/utils.dart b/app/over_react_redux/todo_client/lib/src/utils.dart index 261b127bc..31e080d0d 100644 --- a/app/over_react_redux/todo_client/lib/src/utils.dart +++ b/app/over_react_redux/todo_client/lib/src/utils.dart @@ -1,23 +1,43 @@ import 'package:react/react_client/js_backed_map.dart'; -JsBackedMap createJsBackedMapRecursively(JsMap jsMap) { - JsBackedMap _createJsBackedMap(JsMap nestedJsMap) { - final map = JsBackedMap.fromJs(nestedJsMap); - map.forEach((key, value) { - if (value is JsMap) { - map[key] = _createJsBackedMap(value); - } - }); +extension DeepConvert on JsBackedMap { + bool _isJsMap(dynamic value) => value is JsMap && value is! Function; - return map; + /// Traverses the provided [jsMap] and recursively + /// converts any nested [JsMap]s to [JsBackedMap]s. + /// + /// Inverse of [deepJsObject]. + JsBackedMap deepConvertFromJs(JsMap jsMap) { + JsBackedMap _createJsBackedMap(JsMap nestedJsMap) { + final map = JsBackedMap.fromJs(nestedJsMap); + map.forEach((key, value) { + if (_isJsMap(value)) { + map[key] = _createJsBackedMap(value); + } + }); + + return map; + } + + return _createJsBackedMap(jsMap); } - final topLevelJsBackedMap = JsBackedMap.fromJs(jsMap); - topLevelJsBackedMap.forEach((key, value) { - if (value is JsMap) { - topLevelJsBackedMap[key] = _createJsBackedMap(value); + /// Traverses the [jsObject] of the current instance and recursively + /// converts any nested [Map]s to [JsMap]s. + /// + /// Inverse of [deepConvertFromJs]. + JsMap get deepJsObject { + JsMap _createJsMap(Map nestedDartMap) { + final jsBackedMap = JsBackedMap.from(nestedDartMap); + jsBackedMap.forEach((key, value) { + if (value is Map) { + jsBackedMap[key] = _createJsMap(value); + } + }); + + return jsBackedMap.jsObject; } - }); - return topLevelJsBackedMap; + return _createJsMap(JsBackedMap.fromJs(jsObject)); + } } diff --git a/app/over_react_redux/todo_client/pubspec.yaml b/app/over_react_redux/todo_client/pubspec.yaml index 0b808bc8f..37c4d9efe 100644 --- a/app/over_react_redux/todo_client/pubspec.yaml +++ b/app/over_react_redux/todo_client/pubspec.yaml @@ -2,7 +2,7 @@ name: todo_client version: 1.0.0 environment: - sdk: ">=2.4.0 <3.0.0" + sdk: ">=2.6.0 <3.0.0" dependencies: color: any From 7a8b09babea68e2f713b00f87fad72fb75aacd25 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 18 Dec 2019 13:21:24 -0700 Subject: [PATCH 05/34] Improve variable names --- .../todo_client/lib/src/components/app_bar/app_bar.dart | 2 +- .../lib/src/components/shared/avatar_with_colors.dart | 4 ++-- .../lib/src/components/shared/list_item_component_mixin.dart | 2 +- .../todo_client/lib/src/components/shared/material_ui.dart | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart index 83b15a60f..61f849632 100644 --- a/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart @@ -22,7 +22,7 @@ class TodoAppBarComponent extends UiComponent2 { AppBar(props, Toolbar({}, Box({'flexGrow': 1}, - Typography({'variant': 'h6', 'className': classes['title']}, 'OverReact Todo Demo App'), + Typography({'variant': 'h6', 'className': muiClasses['title']}, 'OverReact Todo Demo App'), ), ConnectedAppBarLocalStorageMenu()(), ), diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart b/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart index 872c0e353..ba5478e9a 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart @@ -110,8 +110,8 @@ class AvatarWithColorsComponent extends UiStatefulComponent2 get highlightedItemStyle => { - if (props.isHighlighted) 'backgroundColor': colors['yellow']['50'], + if (props.isHighlighted) 'backgroundColor': muiColors['yellow']['50'], }; bool get hasDetails; diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart index a453e0434..7939859b7 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart @@ -49,8 +49,8 @@ class MaterialUI { external static ReactClass get Typography; } -final colors = JsBackedMap().deepConvertFromJs(MaterialUI.colors); -final classes = JsBackedMap().deepConvertFromJs(MaterialUI.classes); +final muiColors = JsBackedMap().deepConvertFromJs(MaterialUI.colors); +final muiClasses = JsBackedMap().deepConvertFromJs(MaterialUI.classes); // ----------------------------------------------------------------------- // Below, you'll find the top level JS component factories From 3a3288780b73800c03306c83fc57a7503b627365 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 18 Dec 2019 13:21:45 -0700 Subject: [PATCH 06/34] Remove unnecessary build tooling config --- app/over_react_redux/todo_client/smithy.yml | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 app/over_react_redux/todo_client/smithy.yml diff --git a/app/over_react_redux/todo_client/smithy.yml b/app/over_react_redux/todo_client/smithy.yml deleted file mode 100644 index 7fbb3b3f9..000000000 --- a/app/over_react_redux/todo_client/smithy.yml +++ /dev/null @@ -1,19 +0,0 @@ -project: dart -language: dart - -# Dart 1.23.0 image from https://github.com/Workiva/smithy-runner-generator -runner_image: drydock-prod.workiva.net/workiva/smithy-runner-generator:153818 - -before_script: - - dart --version - - pub get --packages-dir - - pub run over_react_format:bootstrap --check - - pub run dart_dev format --check - - pub run dart_dev analyze - -script: - - DART_FLAGS=--checked xvfb-run -s '-screen 0 1024x768x24' pub run dart_dev test - -after_script: - -artifacts: From 39d44ab1cd222e3158836fd2268c194730a4cf2a Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 18 Dec 2019 13:23:53 -0700 Subject: [PATCH 07/34] Remove unused js config --- .../todo_client/web/index.html | 1 - .../todo_client/web/js/material-ui-config.js | 24 ------------------- 2 files changed, 25 deletions(-) delete mode 100644 app/over_react_redux/todo_client/web/js/material-ui-config.js diff --git a/app/over_react_redux/todo_client/web/index.html b/app/over_react_redux/todo_client/web/index.html index 0ba091c73..6cad81980 100644 --- a/app/over_react_redux/todo_client/web/index.html +++ b/app/over_react_redux/todo_client/web/index.html @@ -16,7 +16,6 @@ - diff --git a/app/over_react_redux/todo_client/web/js/material-ui-config.js b/app/over_react_redux/todo_client/web/js/material-ui-config.js deleted file mode 100644 index 16fd5e772..000000000 --- a/app/over_react_redux/todo_client/web/js/material-ui-config.js +++ /dev/null @@ -1,24 +0,0 @@ -// Lets use some of material ui's stuff in JS world -const { - createMuiTheme, - colors, -} = window.MaterialUI; - -// Assigned to the window so that dart can get access to it. -// window.theme = createMuiTheme({ -// palette: { -// primary: { -// light: colors.purple[300], -// main: colors.purple[500], -// dark: colors.purple[700] -// }, -// secondary: { -// light: colors.green[300], -// main: colors.green[500], -// dark: colors.green[700] -// } -// }, -// typography: { -// useNextVariants: true -// } -// }); From 9525ea807ef0df309eafaa8e2b41ca4e128e67e0 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 18 Dec 2019 13:29:13 -0700 Subject: [PATCH 08/34] Specify requiredProps --- .../components/shared/list_item_mixin.dart | 19 ++++++++++--------- .../lib/src/components/user_selector.dart | 4 ++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_mixin.dart b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_mixin.dart index 140024fb6..b9ee2650e 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_mixin.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_mixin.dart @@ -16,15 +16,16 @@ abstract class ListItemPropsMixin implements UiProps { Map get props; covariant BaseModel model; - bool isSelected; - bool isEditable; - bool isHighlighted; - Function(String id) onSelect; - Function(String id) onDeselect; - Function (String id) onBeginEdit; - Function (String id) onFinishEdit; - Function(BaseModel updatedModel) onModelUpdate; - Function(String id) onRemove; + + @requiredProp bool isSelected; + @requiredProp bool isEditable; + @requiredProp bool isHighlighted; + @requiredProp Function(String id) onSelect; + @requiredProp Function(String id) onDeselect; + @requiredProp Function (String id) onBeginEdit; + @requiredProp Function (String id) onFinishEdit; + @requiredProp Function(BaseModel updatedModel) onModelUpdate; + @requiredProp Function(String id) onRemove; } @StateMixin() diff --git a/app/over_react_redux/todo_client/lib/src/components/user_selector.dart b/app/over_react_redux/todo_client/lib/src/components/user_selector.dart index d785068f9..e205db6cd 100644 --- a/app/over_react_redux/todo_client/lib/src/components/user_selector.dart +++ b/app/over_react_redux/todo_client/lib/src/components/user_selector.dart @@ -34,8 +34,8 @@ UiFactory UserSelector = class _$UserSelectorProps extends UiProps { String selectedUserId; User selectedUser; - List users; - Function(String userId) onUserSelect; + @requiredProp List users; + @requiredProp Function(String userId) onUserSelect; } @Component2() From e39071a6aaae3d4cebab13fa93dd7ae79ca0d0dd Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Thu, 19 Dec 2019 13:16:52 -0700 Subject: [PATCH 09/34] Bump SDK constraint for example app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + Since we’re using extension methods --- app/over_react_redux/todo_client/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/over_react_redux/todo_client/pubspec.yaml b/app/over_react_redux/todo_client/pubspec.yaml index 37c4d9efe..083696dda 100644 --- a/app/over_react_redux/todo_client/pubspec.yaml +++ b/app/over_react_redux/todo_client/pubspec.yaml @@ -2,7 +2,7 @@ name: todo_client version: 1.0.0 environment: - sdk: ">=2.6.0 <3.0.0" + sdk: ">=2.7.0 <3.0.0" dependencies: color: any From 372fc5d78203079aadbd2deddc9f151609731af9 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Thu, 19 Dec 2019 13:17:02 -0700 Subject: [PATCH 10/34] Remove unnecessary white space --- .../lib/src/components/shared/list_item_mixin.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_mixin.dart b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_mixin.dart index b9ee2650e..35c0451ca 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_mixin.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_mixin.dart @@ -22,8 +22,8 @@ abstract class ListItemPropsMixin implements UiProps { @requiredProp bool isHighlighted; @requiredProp Function(String id) onSelect; @requiredProp Function(String id) onDeselect; - @requiredProp Function (String id) onBeginEdit; - @requiredProp Function (String id) onFinishEdit; + @requiredProp Function(String id) onBeginEdit; + @requiredProp Function(String id) onFinishEdit; @requiredProp Function(BaseModel updatedModel) onModelUpdate; @requiredProp Function(String id) onRemove; } From c083a32618e90029d8012e68ba0b3239aef37355 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Thu, 19 Dec 2019 13:19:33 -0700 Subject: [PATCH 11/34] Update .gitignore for example app --- app/over_react_redux/todo_client/.gitignore | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/over_react_redux/todo_client/.gitignore b/app/over_react_redux/todo_client/.gitignore index 6646773d0..fde767b57 100644 --- a/app/over_react_redux/todo_client/.gitignore +++ b/app/over_react_redux/todo_client/.gitignore @@ -1,15 +1,10 @@ .packages .pub packages +build/ +.dart_tool *.over_react.g.dart -# Files created by dart2js -*.dart.js -*.part.js -*.js.deps -*.js.map -*.info.json - # Directory created by dartdoc doc/api/ @@ -19,5 +14,4 @@ doc/api/ *.ipr *.iws .arcconfig -.dart_tool .vscode From 56b39f27f3ee7ad8490c37369e569fd273fa3977 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Thu, 19 Dec 2019 13:52:55 -0700 Subject: [PATCH 12/34] Use variadic children consistently in the example app --- .../todo_client/lib/src/components/app.dart | 91 +++++++++---------- .../lib/src/components/app_bar/app_bar.dart | 7 +- .../app_bar/saved_data_menu_item.dart | 29 ++++-- .../components/shared/avatar_with_colors.dart | 6 +- .../src/components/shared/display_list.dart | 4 +- .../lib/src/components/shared/empty_view.dart | 4 +- .../list_item_expansion_panel_summary.dart | 18 ++-- .../src/components/shared/menu_overlay.dart | 4 +- .../shared/todo_item_text_field.dart | 7 +- .../lib/src/components/task_count.dart | 4 +- .../lib/src/components/todo_list.dart | 2 +- .../lib/src/components/todo_list_item.dart | 50 ++++++---- .../lib/src/components/user_list.dart | 2 +- .../lib/src/components/user_list_item.dart | 46 ++++++---- .../lib/src/components/user_selector.dart | 6 +- .../src/components/user_selector_trigger.dart | 4 +- .../todo_client/web/main.dart | 11 ++- 17 files changed, 172 insertions(+), 123 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/components/app.dart b/app/over_react_redux/todo_client/lib/src/components/app.dart index 73491fab8..a15423e01 100644 --- a/app/over_react_redux/todo_client/lib/src/components/app.dart +++ b/app/over_react_redux/todo_client/lib/src/components/app.dart @@ -44,65 +44,60 @@ class TodoAppComponent extends UiComponent2 { @override render() { return Fragment()( - (TodoAppBar()..key = 'appBar')(), - Box({ - 'key': 'appContent', - 'className': 'app-content', - }, [ - CssBaseline({'key': 'cssBaseline'}), - Container({'key': 'container', 'maxWidth': 'lg', 'className': 'app-content__container'}, - Grid({'container': true, 'direction': 'row', 'spacing': 3, 'className': 'app-content__container-grid'}, [ + TodoAppBar()(), + Box({'className': 'app-content'}, + CssBaseline({}), + Container({ + 'maxWidth': 'lg', + 'className': 'app-content__container' + }, + Grid({ + 'container': true, + 'direction': 'row', + 'spacing': 3, + 'className': 'app-content__container-grid' + }, renderTodosColumn(), renderUsersColumn(), - ]), + ), ), - ]) + ), ); } ReactElement renderTodosColumn() { - return Grid( - { - 'key': 'todos', - 'container': true, - 'item': true, - 'sm': 8, - 'direction': 'column', - 'alignItems': 'stretch', - 'style': {'height': '100%'}, - }, - [ - (CreateInput() - ..key = 'todoInput' - ..autoFocus = true - ..label = 'New Todo' - ..placeholder = 'Create new Todo' - ..onCreate = props.createTodo - )(), - (ConnectedTodoList()..key = 'todoList')(), - ], + return Grid({ + 'container': true, + 'item': true, + 'sm': 8, + 'direction': 'column', + 'alignItems': 'stretch', + 'style': {'height': '100%'}, + }, + (CreateInput() + ..autoFocus = true + ..label = 'New Todo' + ..placeholder = 'Create new Todo' + ..onCreate = props.createTodo + )(), + ConnectedTodoList()(), ); } ReactElement renderUsersColumn() { - return Grid( - { - 'key': 'users', - 'container': true, - 'item': true, - 'sm': 4, - 'direction': 'column', - 'style': {'height': '100%'}, - }, - [ - (CreateInput() - ..key = 'userInput' - ..label = 'New User' - ..placeholder = 'Create new user' - ..onCreate = props.createUser - )(), - (ConnectedUserList()..key = 'userList')(), - ], + return Grid({ + 'container': true, + 'item': true, + 'sm': 4, + 'direction': 'column', + 'style': {'height': '100%'}, + }, + (CreateInput() + ..label = 'New User' + ..placeholder = 'Create new user' + ..onCreate = props.createUser + )(), + ConnectedUserList()(), ); } } diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart index 61f849632..a7866c230 100644 --- a/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart @@ -22,12 +22,15 @@ class TodoAppBarComponent extends UiComponent2 { AppBar(props, Toolbar({}, Box({'flexGrow': 1}, - Typography({'variant': 'h6', 'className': muiClasses['title']}, 'OverReact Todo Demo App'), + Typography({ + 'variant': 'h6', + 'className': muiClasses['title'] + }, 'OverReact Redux Todo Demo App'), ), ConnectedAppBarLocalStorageMenu()(), ), ), - Toolbar({'key': 'fakeAppBarToPushContentBelowFixedAppBar'}), + Toolbar({}), ); } } diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart index 52a22cda8..2aaa49972 100644 --- a/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart @@ -64,17 +64,32 @@ class SavedDataMenuItemComponent extends UiStatefulComponent2 { 'paddingTop': 2, 'style': {...props.style ?? {}, 'overflowY': 'auto'}, ...propsToForward - }, props.children); + }, + props.children, + ); } } diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/empty_view.dart b/app/over_react_redux/todo_client/lib/src/components/shared/empty_view.dart index e815c807b..41c918df4 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/empty_view.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/empty_view.dart @@ -81,9 +81,7 @@ class EmptyViewComponent extends UiComponent2 { ReactElement _renderInContainer(dynamic content) { if (props.type == EmptyViewType.DEFAULT) return content; - return (Dom.div() - ..className = props.type.className - )(content); + return (Dom.div()..className = props.type.className)(content); } ReactElement _renderHeader() { diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart index f69a6fda6..65124dde2 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart @@ -50,22 +50,28 @@ class ListItemExpansionPanelSummaryComponent 'onFocus': handleChildFocus, 'onBlur': handleChildBlur, }, - Grid({'container': true, 'direction': 'row'}, [ - ...props.children, + Grid({ + 'container': true, + 'direction': 'row', + }, + props.children, _renderEditButton(), - ]), + ), ); } ReactElement _renderEditButton() { - return Box({...shrinkToFit, - 'key': 'editButton', + return Box({ + ...shrinkToFit, 'mr': -1, 'alignSelf': 'center', 'aria-hidden': !isHovered, 'className': 'hide-using-aria', }, - Tooltip({'enterDelay': 500, 'title': props.isEditable ? 'Save Changes' : 'Make Changes'}, + Tooltip({ + 'enterDelay': 500, + 'title': props.isEditable ? 'Save Changes' : 'Make Changes', + }, IconButton({ 'aria-label': props.isEditable ? 'Save Changes' : 'Make Changes', 'className': 'todo-list__item__edit-btn', diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/menu_overlay.dart b/app/over_react_redux/todo_client/lib/src/components/shared/menu_overlay.dart index bd5a055c3..819517e4d 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/menu_overlay.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/menu_overlay.dart @@ -67,7 +67,9 @@ class MenuOverlayComponent extends UiStatefulComponent2 { 'vertical': 'bottom', 'horizontal': 'right', }, - }, props.children), + }, + props.children, + ), ), ); } diff --git a/app/over_react_redux/todo_client/lib/src/components/todo_list.dart b/app/over_react_redux/todo_client/lib/src/components/todo_list.dart index 800aa7185..35e7f1efa 100644 --- a/app/over_react_redux/todo_client/lib/src/components/todo_list.dart +++ b/app/over_react_redux/todo_client/lib/src/components/todo_list.dart @@ -33,7 +33,7 @@ class TodoListComponent extends UiComponent2 { @override render() { return (DisplayList()..listItemTypeDescription = 'todos')( - props.todos.map(_renderItem).toList() + props.todos.map(_renderItem).toList(), ); } diff --git a/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart b/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart index d21f5a52c..1bf06081f 100644 --- a/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart +++ b/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart @@ -76,9 +76,8 @@ class TodoListItemComponent extends UiStatefulComponent2 { @override render() { return (DisplayList()..listItemTypeDescription = 'users')( - props.users.map(_renderUser).toList() + props.users.map(_renderUser).toList(), ); } diff --git a/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart b/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart index abf5b1d38..59b98911f 100644 --- a/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart +++ b/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart @@ -72,9 +72,8 @@ class UserListItemComponent extends UiStatefulComponent2 { event.stopPropagation(); _handleUserSelect(user); }, - }, [ - Box({'key': 'avatarBox', 'mr': 1}, + }, + Box({'mr': 1}, (AvatarWithColors()..fullName = user.name)(), ), user.name, - ]); + ); } void _handleUserSelect(User user) { diff --git a/app/over_react_redux/todo_client/lib/src/components/user_selector_trigger.dart b/app/over_react_redux/todo_client/lib/src/components/user_selector_trigger.dart index 81e86490c..0073e21bf 100644 --- a/app/over_react_redux/todo_client/lib/src/components/user_selector_trigger.dart +++ b/app/over_react_redux/todo_client/lib/src/components/user_selector_trigger.dart @@ -28,7 +28,9 @@ class UserSelectorTriggerComponent extends UiComponent2 Date: Thu, 19 Dec 2019 13:57:25 -0700 Subject: [PATCH 13/34] =?UTF-8?q?Don=E2=80=99t=20use=20instance=20state=20?= =?UTF-8?q?here?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/src/components/shared/avatar_with_colors.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart b/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart index 8f94bb6d3..caab78d87 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart @@ -37,10 +37,11 @@ class AvatarWithColorsComponent extends UiStatefulComponent2 Date: Thu, 19 Dec 2019 14:44:47 -0700 Subject: [PATCH 14/34] =?UTF-8?q?Don=E2=80=99t=20use=20getDerivedStateFrom?= =?UTF-8?q?Props?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/shared/avatar_with_colors.dart | 72 +++---------------- .../shared/list_item_component_mixin.dart | 26 ++++--- .../lib/src/components/user_selector.dart | 11 --- app/over_react_redux/todo_client/pubspec.yaml | 1 + 4 files changed, 24 insertions(+), 86 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart b/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart index caab78d87..94e0788ec 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/avatar_with_colors.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:color/color.dart'; +import 'package:memoize/memoize.dart'; import 'package:over_react/over_react.dart'; import 'package:todo_client/src/components/shared/material_ui.dart'; @@ -18,39 +19,14 @@ class _$AvatarWithColorsProps extends UiProps { String fullName; } -@State() -class _$AvatarWithColorsState extends UiState { - String fullName; - String backgroundColor; - String textColor; -} - @Component2() -class AvatarWithColorsComponent extends UiStatefulComponent2 { - @override - get initialState => (newState() - ..fullName = props.fullName - ..addAll(_getDerivedColorsFromName()) - ); - - @override - Map getDerivedStateFromProps(Map nextProps, Map prevState) { - if (prevState == null) return null; // Initial mount is handled by initialState getter - final tNextProps = typedPropsFactory(nextProps); - final tPrevState = typedStateFactory(prevState); - if (tPrevState.fullName == tNextProps.fullName) return null; // Nothing is going to change. Short-circuit a bunch of color calc logic - return (newState() - ..fullName = tNextProps.fullName - ..addAll(_getDerivedColorsFromName(tNextProps, tPrevState)) - ); - } - +class AvatarWithColorsComponent extends UiComponent2 { @override render() { return Avatar({ 'style': { - 'backgroundColor': state.backgroundColor, - 'color': state.textColor, + 'backgroundColor': _backgroundColorMemo(props.fullName), + 'color': _textColorMemo(_backgroundColorMemo(props.fullName)), }, }, _renderAvatarContent(), @@ -59,39 +35,19 @@ class AvatarWithColorsComponent extends UiStatefulComponent2((backgroundColor) { + if (backgroundColor == 'transparent') return 'inherit'; - return null; - } + final lightness = Color.hex(backgroundColor).toHslColor().l; + return lightness < 70 ? '#fff' : '#595959'; + }); static String _getUserInitials(String fullName) { if (fullName == null) return ' '; @@ -132,9 +88,3 @@ class AvatarWithColorsProps extends _$AvatarWithColorsProps with _$AvatarWithCol // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value static const PropsMeta meta = _$metaForAvatarWithColorsProps; } - -// ignore: mixin_of_non_class, undefined_class -class AvatarWithColorsState extends _$AvatarWithColorsState with _$AvatarWithColorsStateAccessorsMixin { - // ignore: undefined_identifier, undefined_class, const_initialized_with_non_constant_value - static const StateMeta meta = _$metaForAvatarWithColorsState; -} diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_component_mixin.dart b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_component_mixin.dart index 74162ae56..d405dac22 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_component_mixin.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_component_mixin.dart @@ -16,19 +16,9 @@ mixin ListItemMixin (newState() + ..localModel = props.model + ); Map get sharedExpansionPanelProps => { 'onChange': handleExpansionPanelExpandedStateChange, @@ -54,6 +44,10 @@ mixin ListItemMixin props.isEditable ? state.localModel : props.model; + void _resetLocalModelToPersistedModel([Function() afterReset]) { + setState(newState()..localModel = props.model, afterReset); + } + @protected void remove() { props.onRemove(model.id); @@ -72,7 +66,9 @@ mixin ListItemMixin { final _overlayRef = createRef(); - @override - bool shouldComponentUpdate(Map nextProps, Map nextState) { - final tNextProps = typedPropsFactory(nextProps); - if (_overlayRef.current?.open != true) { - return tNextProps.selectedUserId != props.selectedUserId; - } - - return !ListEquality().equals(tNextProps.users, props.users); - } - @override render() { return (MenuOverlay() diff --git a/app/over_react_redux/todo_client/pubspec.yaml b/app/over_react_redux/todo_client/pubspec.yaml index 083696dda..794bbd369 100644 --- a/app/over_react_redux/todo_client/pubspec.yaml +++ b/app/over_react_redux/todo_client/pubspec.yaml @@ -6,6 +6,7 @@ environment: dependencies: color: any + memoize: ^2.0.0 over_react: ^3.1.5 redux: ^3.0.0 redux_dev_tools: 0.4.0 From acd8949bd830f0f29c83958d7e648c92c3eac5db Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Thu, 19 Dec 2019 14:56:42 -0700 Subject: [PATCH 15/34] Improve variable names --- .../lib/src/components/app_bar/saved_data_menu_item.dart | 4 ++-- .../todo_client/lib/src/components/create_input.dart | 2 +- .../shared/list_item_expansion_panel_summary.dart | 2 +- .../lib/src/components/shared/material_ui.dart | 4 ++-- .../todo_client/lib/src/components/todo_list_item.dart | 8 ++++---- .../todo_client/lib/src/components/user_list_item.dart | 6 +++--- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart index 2aaa49972..215b3fcaf 100644 --- a/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart @@ -70,7 +70,7 @@ class SavedDataMenuItemComponent extends UiStatefulComponent2 { render() { final propsToForward = {...props}..remove('onCreate'); - return Box({...shrinkToFit}, + return Box({...shrinkToFitProps}, TextField({ 'fullWidth': true, 'variant': 'outlined', diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart index 65124dde2..20e9b2906 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart @@ -62,7 +62,7 @@ class ListItemExpansionPanelSummaryComponent ReactElement _renderEditButton() { return Box({ - ...shrinkToFit, + ...shrinkToFitProps, 'mr': -1, 'alignSelf': 'center', 'aria-hidden': !isHovered, diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart index 7939859b7..76ca2e306 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart @@ -196,13 +196,13 @@ ReactElement StorageIcon([Map props = const {}]) { )()); } -const shrinkToFit = { +const shrinkToFitProps = { 'flexGrow': 0, 'flexShrink': 0, 'flexBasis': 'auto', }; -const grow = { +const growProps = { 'flexGrow': 1, 'flexShrink': 1, 'flexBasis': '0%', diff --git a/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart b/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart index 1bf06081f..4de7e4388 100644 --- a/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart +++ b/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart @@ -96,7 +96,7 @@ class TodoListItemComponent extends UiStatefulComponent2 Date: Thu, 19 Dec 2019 14:56:57 -0700 Subject: [PATCH 16/34] =?UTF-8?q?Don=E2=80=99t=20waste=20time=20creating?= =?UTF-8?q?=20sets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../todo_client/lib/src/components/todo_list_item.dart | 6 +++--- .../todo_client/lib/src/components/user_list_item.dart | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart b/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart index 4de7e4388..70b51f7c6 100644 --- a/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart +++ b/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart @@ -27,9 +27,9 @@ UiFactory ConnectedTodoListItem = connect ConnectedUserListItem = connect Date: Thu, 19 Dec 2019 14:57:51 -0700 Subject: [PATCH 17/34] Simplify JsBackedMap utility --- .../src/components/shared/material_ui.dart | 4 +- .../todo_client/lib/src/utils.dart | 47 +++++-------------- 2 files changed, 14 insertions(+), 37 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart index 76ca2e306..c126284bb 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart @@ -49,8 +49,8 @@ class MaterialUI { external static ReactClass get Typography; } -final muiColors = JsBackedMap().deepConvertFromJs(MaterialUI.colors); -final muiClasses = JsBackedMap().deepConvertFromJs(MaterialUI.classes); +final muiColors = jsBackedMapDeepFromJs(MaterialUI.colors); +final muiClasses = jsBackedMapDeepFromJs(MaterialUI.classes); // ----------------------------------------------------------------------- // Below, you'll find the top level JS component factories diff --git a/app/over_react_redux/todo_client/lib/src/utils.dart b/app/over_react_redux/todo_client/lib/src/utils.dart index 31e080d0d..046c6660b 100644 --- a/app/over_react_redux/todo_client/lib/src/utils.dart +++ b/app/over_react_redux/todo_client/lib/src/utils.dart @@ -1,43 +1,20 @@ import 'package:react/react_client/js_backed_map.dart'; -extension DeepConvert on JsBackedMap { +/// Traverses the provided [jsMap] and recursively +/// converts any nested [JsMap]s to [JsBackedMap]s. +JsBackedMap jsBackedMapDeepFromJs(JsMap jsMap) { bool _isJsMap(dynamic value) => value is JsMap && value is! Function; - /// Traverses the provided [jsMap] and recursively - /// converts any nested [JsMap]s to [JsBackedMap]s. - /// - /// Inverse of [deepJsObject]. - JsBackedMap deepConvertFromJs(JsMap jsMap) { - JsBackedMap _createJsBackedMap(JsMap nestedJsMap) { - final map = JsBackedMap.fromJs(nestedJsMap); - map.forEach((key, value) { - if (_isJsMap(value)) { - map[key] = _createJsBackedMap(value); - } - }); + JsBackedMap _createJsBackedMap(JsMap nestedJsMap) { + final map = JsBackedMap.fromJs(nestedJsMap); + map.forEach((key, value) { + if (_isJsMap(value)) { + map[key] = _createJsBackedMap(value); + } + }); - return map; - } - - return _createJsBackedMap(jsMap); + return map; } - /// Traverses the [jsObject] of the current instance and recursively - /// converts any nested [Map]s to [JsMap]s. - /// - /// Inverse of [deepConvertFromJs]. - JsMap get deepJsObject { - JsMap _createJsMap(Map nestedDartMap) { - final jsBackedMap = JsBackedMap.from(nestedDartMap); - jsBackedMap.forEach((key, value) { - if (value is Map) { - jsBackedMap[key] = _createJsMap(value); - } - }); - - return jsBackedMap.jsObject; - } - - return _createJsMap(JsBackedMap.fromJs(jsObject)); - } + return _createJsBackedMap(jsMap); } From 4dce6c183b9d515bf048dba19eaf96fab5dffdc1 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Thu, 19 Dec 2019 14:58:38 -0700 Subject: [PATCH 18/34] Remove unnecessary toList() --- .../lib/src/components/app_bar/app_bar_local_storage_menu.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar_local_storage_menu.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar_local_storage_menu.dart index 080091e08..b10617fe6 100644 --- a/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar_local_storage_menu.dart +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar_local_storage_menu.dart @@ -118,7 +118,7 @@ class AppBarLocalStorageMenuComponent extends UiComponent2 Date: Thu, 19 Dec 2019 17:29:50 -0700 Subject: [PATCH 19/34] Update json_serializable dependency --- .../todo_client/lib/src/actions.g.dart | 8 ++-- .../todo_client/lib/src/models/todo.g.dart | 15 +++--- .../todo_client/lib/src/models/user.g.dart | 9 ++-- .../todo_client/lib/src/store.g.dart | 48 +++++++++---------- app/over_react_redux/todo_client/pubspec.yaml | 2 +- 5 files changed, 43 insertions(+), 39 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/actions.g.dart b/app/over_react_redux/todo_client/lib/src/actions.g.dart index a243410d9..c8bc45a03 100644 --- a/app/over_react_redux/todo_client/lib/src/actions.g.dart +++ b/app/over_react_redux/todo_client/lib/src/actions.g.dart @@ -8,13 +8,15 @@ part of 'actions.dart'; SaveLocalStorageStateAsPayload _$SaveLocalStorageStateAsPayloadFromJson( Map json) { - return SaveLocalStorageStateAsPayload(json['name'] as String, - previousName: json['previousName'] as String); + return SaveLocalStorageStateAsPayload( + json['name'] as String, + previousName: json['previousName'] as String, + ); } Map _$SaveLocalStorageStateAsPayloadToJson( SaveLocalStorageStateAsPayload instance) => { 'name': instance.name, - 'previousName': instance.previousName + 'previousName': instance.previousName, }; diff --git a/app/over_react_redux/todo_client/lib/src/models/todo.g.dart b/app/over_react_redux/todo_client/lib/src/models/todo.g.dart index f211a2fac..5d9d8945b 100644 --- a/app/over_react_redux/todo_client/lib/src/models/todo.g.dart +++ b/app/over_react_redux/todo_client/lib/src/models/todo.g.dart @@ -8,12 +8,13 @@ part of 'todo.dart'; Todo _$TodoFromJson(Map json) { return Todo( - description: json['description'] as String, - id: json['id'] as String, - isCompleted: json['isCompleted'] as bool, - isPublic: json['isPublic'] as bool, - notes: json['notes'] as String, - assignedUserId: json['assignedUserId'] as String); + description: json['description'] as String, + id: json['id'] as String, + isCompleted: json['isCompleted'] as bool, + isPublic: json['isPublic'] as bool, + notes: json['notes'] as String, + assignedUserId: json['assignedUserId'] as String, + ); } Map _$TodoToJson(Todo instance) => { @@ -22,5 +23,5 @@ Map _$TodoToJson(Todo instance) => { 'isCompleted': instance.isCompleted, 'isPublic': instance.isPublic, 'notes': instance.notes, - 'assignedUserId': instance.assignedUserId + 'assignedUserId': instance.assignedUserId, }; diff --git a/app/over_react_redux/todo_client/lib/src/models/user.g.dart b/app/over_react_redux/todo_client/lib/src/models/user.g.dart index 8e0fdf674..027a88117 100644 --- a/app/over_react_redux/todo_client/lib/src/models/user.g.dart +++ b/app/over_react_redux/todo_client/lib/src/models/user.g.dart @@ -8,13 +8,14 @@ part of 'user.dart'; User _$UserFromJson(Map json) { return User( - name: json['name'] as String, - id: json['id'] as String, - bio: json['bio'] as String); + name: json['name'] as String, + id: json['id'] as String, + bio: json['bio'] as String, + ); } Map _$UserToJson(User instance) => { 'id': instance.id, 'name': instance.name, - 'bio': instance.bio + 'bio': instance.bio, }; diff --git a/app/over_react_redux/todo_client/lib/src/store.g.dart b/app/over_react_redux/todo_client/lib/src/store.g.dart index 1f895784a..5f115c6bf 100644 --- a/app/over_react_redux/todo_client/lib/src/store.g.dart +++ b/app/over_react_redux/todo_client/lib/src/store.g.dart @@ -7,29 +7,29 @@ part of 'store.dart'; // ************************************************************************** AppState _$AppStateFromJson(Map json) { - return AppState(json['name'] as String, - todos: (json['todos'] as List) - ?.map((e) => - e == null ? null : Todo.fromJson(e as Map)) - ?.toList(), - users: (json['users'] as List) - ?.map((e) => - e == null ? null : User.fromJson(e as Map)) - ?.toList(), - selectedTodoIds: - (json['selectedTodoIds'] as List)?.map((e) => e as String)?.toList(), - editableTodoIds: - (json['editableTodoIds'] as List)?.map((e) => e as String)?.toList(), - highlightedTodoIds: (json['highlightedTodoIds'] as List) - ?.map((e) => e as String) - ?.toList(), - selectedUserIds: - (json['selectedUserIds'] as List)?.map((e) => e as String)?.toList(), - editableUserIds: - (json['editableUserIds'] as List)?.map((e) => e as String)?.toList(), - highlightedUserIds: (json['highlightedUserIds'] as List) - ?.map((e) => e as String) - ?.toList()); + return AppState( + json['name'] as String, + todos: (json['todos'] as List) + ?.map( + (e) => e == null ? null : Todo.fromJson(e as Map)) + ?.toList(), + users: (json['users'] as List) + ?.map( + (e) => e == null ? null : User.fromJson(e as Map)) + ?.toList(), + selectedTodoIds: + (json['selectedTodoIds'] as List)?.map((e) => e as String)?.toList(), + editableTodoIds: + (json['editableTodoIds'] as List)?.map((e) => e as String)?.toList(), + highlightedTodoIds: + (json['highlightedTodoIds'] as List)?.map((e) => e as String)?.toList(), + selectedUserIds: + (json['selectedUserIds'] as List)?.map((e) => e as String)?.toList(), + editableUserIds: + (json['editableUserIds'] as List)?.map((e) => e as String)?.toList(), + highlightedUserIds: + (json['highlightedUserIds'] as List)?.map((e) => e as String)?.toList(), + ); } Map _$AppStateToJson(AppState instance) => { @@ -41,5 +41,5 @@ Map _$AppStateToJson(AppState instance) => { 'highlightedTodoIds': instance.highlightedTodoIds, 'selectedUserIds': instance.selectedUserIds, 'editableUserIds': instance.editableUserIds, - 'highlightedUserIds': instance.highlightedUserIds + 'highlightedUserIds': instance.highlightedUserIds, }; diff --git a/app/over_react_redux/todo_client/pubspec.yaml b/app/over_react_redux/todo_client/pubspec.yaml index 794bbd369..53e259bd7 100644 --- a/app/over_react_redux/todo_client/pubspec.yaml +++ b/app/over_react_redux/todo_client/pubspec.yaml @@ -17,5 +17,5 @@ dev_dependencies: over_react_test: ^2.0.0 build_runner: ^1.7.1 build_web_compilers: ^2.5.1 - json_serializable: ^2.0.0 + json_serializable: ^3.2.2 pedantic: ^1.8.0 From 59b8dea4e0cb865013ceae3e13d09e86cbe608ed Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Thu, 19 Dec 2019 17:30:55 -0700 Subject: [PATCH 20/34] Set up testing for todo example app --- app/over_react_redux/todo_client/build.yaml | 10 ++++++++++ app/over_react_redux/todo_client/dart_test.yaml | 11 +++++++++++ app/over_react_redux/todo_client/pubspec.yaml | 9 +++++++-- .../_templates/react_components_test_template.html | 10 ++++++++++ .../test/unit/_templates/react_test_template.html | 9 +++++++++ .../todo_client/tool/dart_dev/config.dart | 14 ++++++++++++++ 6 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 app/over_react_redux/todo_client/dart_test.yaml create mode 100644 app/over_react_redux/todo_client/test/unit/_templates/react_components_test_template.html create mode 100644 app/over_react_redux/todo_client/test/unit/_templates/react_test_template.html create mode 100644 app/over_react_redux/todo_client/tool/dart_dev/config.dart diff --git a/app/over_react_redux/todo_client/build.yaml b/app/over_react_redux/todo_client/build.yaml index e69de29bb..46b586be3 100644 --- a/app/over_react_redux/todo_client/build.yaml +++ b/app/over_react_redux/todo_client/build.yaml @@ -0,0 +1,10 @@ +targets: + $default: + builders: + test_html_builder: + options: + templates: + "test/unit/_templates/react_test_template.html": + - "test/unit/browser/redux/**_test.dart" + "test/unit/_templates/react_components_test_template.html": + - "test/unit/browser/components/**_test.dart" diff --git a/app/over_react_redux/todo_client/dart_test.yaml b/app/over_react_redux/todo_client/dart_test.yaml new file mode 100644 index 000000000..1df6b6de5 --- /dev/null +++ b/app/over_react_redux/todo_client/dart_test.yaml @@ -0,0 +1,11 @@ +platforms: + - chrome +concurrency: 4 +filename: "*_test.dart" + +presets: + # Pass "-P debug" to enable debugging configuration + debug: + pause_after_load: true + exclude_tags: undebuggable + reporter: expanded diff --git a/app/over_react_redux/todo_client/pubspec.yaml b/app/over_react_redux/todo_client/pubspec.yaml index 53e259bd7..eee6df5f2 100644 --- a/app/over_react_redux/todo_client/pubspec.yaml +++ b/app/over_react_redux/todo_client/pubspec.yaml @@ -13,9 +13,14 @@ dependencies: uuid: ^1.0.3 dev_dependencies: - w_common: ^1.20.0 - over_react_test: ^2.0.0 build_runner: ^1.7.1 build_web_compilers: ^2.5.1 + build_test: ^0.10.9 + dart_dev: ^3.0.0 + glob: ^1.2.0 json_serializable: ^3.2.2 + over_react_test: ^2.0.0 pedantic: ^1.8.0 + test: ^1.9.1 + test_html_builder: ^1.0.0 + w_common: ^1.20.0 diff --git a/app/over_react_redux/todo_client/test/unit/_templates/react_components_test_template.html b/app/over_react_redux/todo_client/test/unit/_templates/react_components_test_template.html new file mode 100644 index 000000000..1c9bbe652 --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/_templates/react_components_test_template.html @@ -0,0 +1,10 @@ + + + + + + + + {test} + + diff --git a/app/over_react_redux/todo_client/test/unit/_templates/react_test_template.html b/app/over_react_redux/todo_client/test/unit/_templates/react_test_template.html new file mode 100644 index 000000000..5bfa623ad --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/_templates/react_test_template.html @@ -0,0 +1,9 @@ + + + + + + + {test} + + diff --git a/app/over_react_redux/todo_client/tool/dart_dev/config.dart b/app/over_react_redux/todo_client/tool/dart_dev/config.dart new file mode 100644 index 000000000..ae212f1aa --- /dev/null +++ b/app/over_react_redux/todo_client/tool/dart_dev/config.dart @@ -0,0 +1,14 @@ +import 'package:dart_dev/dart_dev.dart'; +import 'package:glob/glob.dart'; + +final config = { + 'analyze': AnalyzeTool(), + 'format': FormatTool() + ..exclude = [ + Glob('**/*.dart'), // We don't format this repo with dartfmt at this time. + ], + 'test': TestTool() + ..buildArgs = [ + '--delete-conflicting-outputs', + ], +}; From 949570417feceff6258b4a026226a4e91fd8dbb5 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Thu, 19 Dec 2019 17:31:31 -0700 Subject: [PATCH 21/34] Add tests for example todo app store --- .../todo_client/lib/src/local_storage.dart | 30 +- .../todo_client/lib/src/store.dart | 20 +- .../todo_client/lib/todo_client.dart | 2 +- app/over_react_redux/todo_client/pubspec.yaml | 1 + .../test/unit/browser/redux/store_test.dart | 294 ++++++++++++++++++ .../todo_client/web/main.dart | 2 +- 6 files changed, 321 insertions(+), 28 deletions(-) create mode 100644 app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart diff --git a/app/over_react_redux/todo_client/lib/src/local_storage.dart b/app/over_react_redux/todo_client/lib/src/local_storage.dart index d0b7d7040..811e13c6e 100644 --- a/app/over_react_redux/todo_client/lib/src/local_storage.dart +++ b/app/over_react_redux/todo_client/lib/src/local_storage.dart @@ -2,6 +2,7 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:html'; +import 'package:meta/meta.dart'; import 'package:todo_client/src/store.dart'; /// The `window.localStorage` interface for our application. @@ -13,29 +14,30 @@ class TodoAppLocalStorage extends MapBase { final AppState initialState; TodoAppLocalStorage([this.initialState]) { - if (isInitialized) return; - - final emptyState = AppState(TodoAppLocalStorage.emptyStateKey, - todos: [], - users: [], - selectedTodoIds: [], - editableTodoIds: [], - highlightedTodoIds: [], - selectedUserIds: [], - editableUserIds: [], - highlightedUserIds: [], - ).toJson(); + if (isInitialized()) return; window.localStorage[localStorageKey] = json.encode({ currentStateKey: this.initialState?.toJson() ?? {}, defaultStateKey: this.initialState?.toJson() ?? {}, - emptyStateKey: emptyState, + emptyStateKey: emptyState.toJson(), }); } // -------------------- Utilities -------------------- - static bool isInitialized = window.localStorage[localStorageKey] != null; + static AppState get emptyState => AppState(TodoAppLocalStorage.emptyStateKey, + todos: [], + users: [], + selectedTodoIds: [], + editableTodoIds: [], + highlightedTodoIds: [], + selectedUserIds: [], + editableUserIds: [], + highlightedUserIds: [], + ); + + static bool isInitialized() => window.localStorage[localStorageKey] != null + && window.localStorage[localStorageKey].isNotEmpty; Map get _proxiedMap => json.decode(window.localStorage[localStorageKey]); diff --git a/app/over_react_redux/todo_client/lib/src/store.dart b/app/over_react_redux/todo_client/lib/src/store.dart index c71a797ea..fef2aa0b5 100644 --- a/app/over_react_redux/todo_client/lib/src/store.dart +++ b/app/over_react_redux/todo_client/lib/src/store.dart @@ -1,4 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; import 'package:over_react/over_react_redux.dart'; import 'package:redux/redux.dart'; import 'package:redux_dev_tools/redux_dev_tools.dart'; @@ -10,9 +11,10 @@ import 'package:todo_client/src/local_storage.dart'; part 'store.g.dart'; -AppState _initializeState() { +@visibleForTesting +AppState initializeState() { AppState initialState; - if (!TodoAppLocalStorage.isInitialized) { + if (!TodoAppLocalStorage.isInitialized()) { // First load - give the user some data to work with, and set up our default / empty states. initialState = AppState.fromJson(defaultAppState); localTodoAppStorage = TodoAppLocalStorage(initialState); @@ -24,9 +26,9 @@ AppState _initializeState() { return initialState; } -var store = DevToolsStore( +DevToolsStore getStore() => DevToolsStore( appStateReducer, - initialState: _initializeState(), + initialState: initializeState(), middleware: [overReactReduxDevToolsMiddleware], ); @@ -52,7 +54,7 @@ class AppState { this.editableUserIds, this.highlightedUserIds, }) { - assert(name != null); + assert(name != null && name.isNotEmpty); localTodoAppStorage?.updateCurrentState(this); } @@ -60,6 +62,7 @@ class AppState { Map toJson() => _$AppStateToJson(this); } +@visibleForTesting AppState appStateReducer(AppState state, dynamic action) { if (action is SaveLocalStorageStateAsAction) { Map previousValue; @@ -135,13 +138,6 @@ final highlightedTodosReducer = combineReducers>([ }), ]); -List highlightTodosReducer(List highlightedTodoIds, dynamic action) { - if (action is HighlightTodosAction) { - return action.value; - } - return highlightedTodoIds; -} - // ------------ USER REDUCERS ------------------ final usersReducer = combineReducers>([ diff --git a/app/over_react_redux/todo_client/lib/todo_client.dart b/app/over_react_redux/todo_client/lib/todo_client.dart index fc9116743..93de3465d 100644 --- a/app/over_react_redux/todo_client/lib/todo_client.dart +++ b/app/over_react_redux/todo_client/lib/todo_client.dart @@ -1,4 +1,4 @@ library todo_client; export 'package:todo_client/src/components/app.dart' show ConnectedTodoApp; -export 'package:todo_client/src/store.dart' show store; +export 'package:todo_client/src/store.dart' show getStore; diff --git a/app/over_react_redux/todo_client/pubspec.yaml b/app/over_react_redux/todo_client/pubspec.yaml index eee6df5f2..ae3893e31 100644 --- a/app/over_react_redux/todo_client/pubspec.yaml +++ b/app/over_react_redux/todo_client/pubspec.yaml @@ -7,6 +7,7 @@ environment: dependencies: color: any memoize: ^2.0.0 + meta: ^1.0.0 over_react: ^3.1.5 redux: ^3.0.0 redux_dev_tools: 0.4.0 diff --git a/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart b/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart new file mode 100644 index 000000000..0c212ce88 --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart @@ -0,0 +1,294 @@ +@TestOn('browser') +import 'dart:convert'; +import 'dart:html'; + +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:test/test.dart'; + +import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/components/app.dart'; +import 'package:todo_client/src/local_storage.dart'; +import 'package:todo_client/src/models/base_model.dart'; +import 'package:todo_client/src/models/todo.dart'; +import 'package:todo_client/src/models/user.dart'; +import 'package:todo_client/src/store.dart'; + +import '../../fixtures/mock_app_state_data.dart'; + +main() { + setClientConfiguration(); + Store testStore; + + Store initializeTestStore() { + return testStore = Store( + appStateReducer, + initialState: initializeState(), + ); + } + + String getLocalStorage() => window.localStorage[TodoAppLocalStorage.localStorageKey]; + + Iterable> getSerializedListOfModels(List models) { + models.map((model) => model.toJson()); + } + + group('AppState', () { + setUp(() { + expect(TodoAppLocalStorage.isInitialized(), isFalse, reason: 'test setup sanity check'); + initializeTestStore(); + }); + + tearDown(() { + testStore = null; + localTodoAppStorage = null; + window.localStorage[TodoAppLocalStorage.localStorageKey] = ''; + }); + + test('requires a name', () { + expect(() => AppState(null), throwsA(TypeMatcher())); + expect(() => AppState(''), throwsA(TypeMatcher())); + }); + + group('fromJson()', () { + test('does not throw', () { + expect(() => AppState.fromJson(defaultAppState), returnsNormally); + }); + }); + + group('initializes via initialState with data', () { + group('from defaultAppState when window.localStorage[${TodoAppLocalStorage.localStorageKey}] is unset', () { + test('', () { + final mockDefaultAppState = AppState.fromJson(defaultAppState); + + expect(getSerializedListOfModels(testStore.state.todos), + getSerializedListOfModels(mockDefaultAppState.todos)); + expect(getSerializedListOfModels(testStore.state.users), + getSerializedListOfModels(mockDefaultAppState.users)); + expect(testStore.state.selectedTodoIds, mockDefaultAppState.selectedTodoIds); + expect(testStore.state.editableTodoIds, mockDefaultAppState.editableTodoIds); + expect(testStore.state.highlightedTodoIds, mockDefaultAppState.highlightedTodoIds); + expect(testStore.state.selectedUserIds, mockDefaultAppState.selectedUserIds); + expect(testStore.state.editableUserIds, mockDefaultAppState.editableUserIds); + expect(testStore.state.highlightedUserIds, mockDefaultAppState.highlightedUserIds); + }); + + test('and then initializes / persists the data to window.localStorage', () { + expect(getLocalStorage(), isNotNull); + + final localStorageJson = json.decode(getLocalStorage()); + expect(localStorageJson['current'], defaultAppState); + expect(localStorageJson['default'], defaultAppState); + expect(localStorageJson['empty'], TodoAppLocalStorage.emptyState.toJson()); + }); + }); + + group('from the "current" localStorage data set', () { + setUp(() { + expect(json.decode(getLocalStorage())['current'], defaultAppState, + reason: 'test setup sanity check'); + + localTodoAppStorage = TodoAppLocalStorage(TodoAppLocalStorage.emptyState); + expect(json.decode(getLocalStorage())['current'], TodoAppLocalStorage.emptyState.toJson(), + reason: 'test setup sanity check'); + + initializeTestStore(); + }); + + test('', () { + expect(testStore.state.todos, isEmpty); + expect(testStore.state.users, isEmpty); + expect(testStore.state.selectedTodoIds, isEmpty); + expect(testStore.state.editableTodoIds, isEmpty); + expect(testStore.state.highlightedTodoIds, isEmpty); + expect(testStore.state.selectedUserIds, isEmpty); + expect(testStore.state.editableUserIds, isEmpty); + expect(testStore.state.highlightedUserIds, isEmpty); + }); + }); + }); + + group('todosReducer updates state', () { + test('when an AddTodoAction is dispatched', () { + final initialTodos = testStore.state.todos; + final newTodo = Todo(description: 'yo'); + testStore.dispatch(AddTodoAction(newTodo)); + expect(getSerializedListOfModels(testStore.state.todos), + getSerializedListOfModels([newTodo, ...initialTodos])); + }); + + test('when a RemoveTodoAction is dispatched', () { + final initialTodos = testStore.state.todos; + final idOfTodoToRemove = testStore.state.todos.first.id; + testStore.dispatch(RemoveTodoAction(idOfTodoToRemove)); + expect(getSerializedListOfModels(testStore.state.todos), + getSerializedListOfModels([...initialTodos]..removeWhere((todo) => todo.id == idOfTodoToRemove))); + }); + + test('when an UpdateTodoAction is dispatched', () { + final initialTodos = testStore.state.todos; + final updatedTodo = Todo.from(initialTodos.first)..description += 'foooo'; + expect(testStore.state.todos.first.toJson(), isNot(updatedTodo.toJson())); + + testStore.dispatch(UpdateTodoAction(updatedTodo)); + expect(testStore.state.todos.first.toJson(), updatedTodo.toJson()); + }); + }); + + group('editableTodosReducer updates state', () { + void beginEdits() { + final initialEditableTodoIds = testStore.state.editableTodoIds; + expect(initialEditableTodoIds, isEmpty); + + final newEditableTodoId = testStore.state.todos.first.id; + testStore.dispatch(BeginEditTodoAction(newEditableTodoId)); + expect(testStore.state.editableTodoIds, [newEditableTodoId]); + + final anotherNewEditableTodoId = testStore.state.todos[1].id; + testStore.dispatch(BeginEditTodoAction(anotherNewEditableTodoId)); + expect(testStore.state.editableTodoIds, [newEditableTodoId, anotherNewEditableTodoId]); + } + + test('when an BeginEditTodoAction is dispatched', beginEdits); + + test('when a FinishEditTodoAction is dispatched', () { + beginEdits(); + + final noLongerEditableTodoId = testStore.state.todos.first.id; + testStore.dispatch(FinishEditTodoAction(noLongerEditableTodoId)); + expect(testStore.state.editableTodoIds, isNot(contains(noLongerEditableTodoId))); + }); + }); + + group('selectedTodosReducer updates state', () { + void select() { + final initialSelectedTodoIds = testStore.state.selectedTodoIds; + expect(initialSelectedTodoIds, isEmpty); + + final newSelectedTodoId = testStore.state.todos.first.id; + testStore.dispatch(SelectTodoAction(newSelectedTodoId)); + expect(testStore.state.selectedTodoIds, [newSelectedTodoId]); + + final anotherNewSelectedTodoId = testStore.state.todos[1].id; + testStore.dispatch(SelectTodoAction(anotherNewSelectedTodoId)); + expect(testStore.state.selectedTodoIds, [newSelectedTodoId, anotherNewSelectedTodoId]); + } + + test('when an SelectTodoAction is dispatched', select); + + test('when a DeselectTodoAction is dispatched', () { + select(); + + final noLongerSelectedTodoId = testStore.state.todos.first.id; + testStore.dispatch(DeselectTodoAction(noLongerSelectedTodoId)); + expect(testStore.state.selectedTodoIds, isNot(contains(noLongerSelectedTodoId))); + }); + }); + + group('highlightedTodosReducer updates state', () { + void highlight() { + final initialHighlightedTodoIds = testStore.state.highlightedTodoIds; + expect(initialHighlightedTodoIds, isEmpty); + + final newHighlightedTodoIds = [ + testStore.state.todos[0].id, + testStore.state.todos[1].id, + ]; + testStore.dispatch(HighlightTodosAction(newHighlightedTodoIds)); + expect(testStore.state.highlightedTodoIds, newHighlightedTodoIds); + + final anotherNewHighlightedTodoId = testStore.state.todos[2].id; + testStore.dispatch(HighlightTodosAction([anotherNewHighlightedTodoId])); + expect(testStore.state.highlightedTodoIds, [...newHighlightedTodoIds, anotherNewHighlightedTodoId]); + } + + test('when an HighlightTodosAction is dispatched', highlight); + + test('when a UnHighlightTodosAction is dispatched', () { + highlight(); + + final noLongerHighlightedTodoId = testStore.state.todos.first.id; + testStore.dispatch(UnHighlightTodosAction([noLongerHighlightedTodoId])); + expect(testStore.state.highlightedTodoIds, isNot(contains(noLongerHighlightedTodoId))); + }); + }); + + group('usersReducer updates state', () { + test('when an AddUserAction is dispatched', () { + final initialUsers = testStore.state.users; + final newUser = User(name: 'yo'); + testStore.dispatch(AddUserAction(newUser)); + expect(getSerializedListOfModels(testStore.state.users), + getSerializedListOfModels([newUser, ...initialUsers])); + }); + + test('when a RemoveUserAction is dispatched', () { + final initialUsers = testStore.state.users; + final idOfUserToRemove = testStore.state.users.first.id; + testStore.dispatch(RemoveUserAction(idOfUserToRemove)); + expect(getSerializedListOfModels(testStore.state.users), + getSerializedListOfModels([...initialUsers]..removeWhere((todo) => todo.id == idOfUserToRemove))); + }); + + test('when an UpdateUserAction is dispatched', () { + final initialUsers = testStore.state.users; + final updatedUser = User.from(initialUsers.first)..name += 'foooo'; + expect(testStore.state.users.first.toJson(), isNot(updatedUser.toJson())); + + testStore.dispatch(UpdateUserAction(updatedUser)); + expect(testStore.state.users.first.toJson(), updatedUser.toJson()); + }); + }); + + group('editableUsersReducer updates state', () { + void beginEdits() { + final initialEditableUserIds = testStore.state.editableUserIds; + expect(initialEditableUserIds, isEmpty); + + final newEditableUserId = testStore.state.users.first.id; + testStore.dispatch(BeginEditUserAction(newEditableUserId)); + expect(testStore.state.editableUserIds, [newEditableUserId]); + + final anotherNewEditableUserId = testStore.state.users[1].id; + testStore.dispatch(BeginEditUserAction(anotherNewEditableUserId)); + expect(testStore.state.editableUserIds, [newEditableUserId, anotherNewEditableUserId]); + } + + test('when an BeginEditUserAction is dispatched', beginEdits); + + test('when a FinishEditUserAction is dispatched', () { + beginEdits(); + + final noLongerEditableUserId = testStore.state.users.first.id; + testStore.dispatch(FinishEditUserAction(noLongerEditableUserId)); + expect(testStore.state.editableUserIds, isNot(contains(noLongerEditableUserId))); + }); + }); + + group('selectedUsersReducer updates state', () { + void select() { + final initialSelectedUserIds = testStore.state.selectedUserIds; + expect(initialSelectedUserIds, isEmpty); + + final newSelectedUserId = testStore.state.users.first.id; + testStore.dispatch(SelectUserAction(newSelectedUserId)); + expect(testStore.state.selectedUserIds, [newSelectedUserId]); + + final anotherNewSelectedUserId = testStore.state.users[1].id; + testStore.dispatch(SelectUserAction(anotherNewSelectedUserId)); + expect(testStore.state.selectedUserIds, [newSelectedUserId, anotherNewSelectedUserId]); + } + + test('when an SelectUserAction is dispatched', select); + + test('when a DeselectUserAction is dispatched', () { + select(); + + final noLongerSelectedUserId = testStore.state.users.first.id; + testStore.dispatch(DeselectUserAction(noLongerSelectedUserId)); + expect(testStore.state.selectedUserIds, isNot(contains(noLongerSelectedUserId))); + }); + }); + }); +} diff --git a/app/over_react_redux/todo_client/web/main.dart b/app/over_react_redux/todo_client/web/main.dart index 46fc15c3a..07e497485 100644 --- a/app/over_react_redux/todo_client/web/main.dart +++ b/app/over_react_redux/todo_client/web/main.dart @@ -11,7 +11,7 @@ main() { final container = querySelector('#todo-container'); - final app = (ReduxProvider()..store = store)( + final app = (ReduxProvider()..store = getStore())( ConnectedTodoApp()(), ); From 52a007c80400628579ad17568daf3096be0888fb Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Fri, 20 Dec 2019 07:18:54 -0700 Subject: [PATCH 22/34] Add more tests for example todo app store --- .../todo_client/lib/src/local_storage.dart | 1 - .../todo_client/lib/src/store.dart | 16 +- .../test/unit/browser/redux/store_test.dart | 273 +++++++++++++++--- 3 files changed, 239 insertions(+), 51 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/local_storage.dart b/app/over_react_redux/todo_client/lib/src/local_storage.dart index 811e13c6e..51841a128 100644 --- a/app/over_react_redux/todo_client/lib/src/local_storage.dart +++ b/app/over_react_redux/todo_client/lib/src/local_storage.dart @@ -2,7 +2,6 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:html'; -import 'package:meta/meta.dart'; import 'package:todo_client/src/store.dart'; /// The `window.localStorage` interface for our application. diff --git a/app/over_react_redux/todo_client/lib/src/store.dart b/app/over_react_redux/todo_client/lib/src/store.dart index fef2aa0b5..691c8619d 100644 --- a/app/over_react_redux/todo_client/lib/src/store.dart +++ b/app/over_react_redux/todo_client/lib/src/store.dart @@ -64,22 +64,24 @@ class AppState { @visibleForTesting AppState appStateReducer(AppState state, dynamic action) { + var stateName = localTodoAppStorage.currentStateJson['name']; + if (action is SaveLocalStorageStateAsAction) { - Map previousValue; - if (action.value.previousName != null) { - previousValue = localTodoAppStorage.remove(action.value.previousName); - } else { - previousValue = localTodoAppStorage.currentStateJson; + if (action.value.previousName != null && action.value.previousName == action.value.name) { + // Overwrite + localTodoAppStorage.remove(action.value.previousName); } - localTodoAppStorage[action.value.name] = (AppState.fromJson(previousValue)..name = action.value.name).toJson(); + stateName = action.value.name; + localTodoAppStorage[stateName] = + (AppState.fromJson(localTodoAppStorage.currentStateJson)..name = stateName).toJson(); } if (action is LoadStateFromLocalStorageAction) { return AppState.fromJson(localTodoAppStorage[action.value]); } - return AppState(localTodoAppStorage.currentStateJson['name'], + return AppState(stateName, todos: todosReducer(state.todos, action), users: usersReducer(state.users, action), editableTodoIds: editableTodosReducer(state.editableTodoIds, action), diff --git a/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart b/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart index 0c212ce88..c8b2da4ea 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart @@ -3,23 +3,21 @@ import 'dart:convert'; import 'dart:html'; import 'package:over_react/over_react.dart'; -import 'package:over_react/over_react_redux.dart'; import 'package:redux/redux.dart'; import 'package:test/test.dart'; import 'package:todo_client/src/actions.dart'; -import 'package:todo_client/src/components/app.dart'; import 'package:todo_client/src/local_storage.dart'; import 'package:todo_client/src/models/base_model.dart'; import 'package:todo_client/src/models/todo.dart'; import 'package:todo_client/src/models/user.dart'; import 'package:todo_client/src/store.dart'; -import '../../fixtures/mock_app_state_data.dart'; - main() { setClientConfiguration(); Store testStore; + const reasonCurrentSetShouldBePersisted = + 'The state update should be persisted as the "current" set in window.localStorage'; Store initializeTestStore() { return testStore = Store( @@ -30,8 +28,12 @@ main() { String getLocalStorage() => window.localStorage[TodoAppLocalStorage.localStorageKey]; + Map getLocalStorageSetByKey(String key) => json.decode(getLocalStorage())[key]; + + Map getCurrentLocalStorageSet() => getLocalStorageSetByKey('current'); + Iterable> getSerializedListOfModels(List models) { - models.map((model) => model.toJson()); + return models.map((model) => model.toJson()); } group('AppState', () { @@ -86,11 +88,10 @@ main() { group('from the "current" localStorage data set', () { setUp(() { - expect(json.decode(getLocalStorage())['current'], defaultAppState, - reason: 'test setup sanity check'); + expect(getCurrentLocalStorageSet(), defaultAppState, reason: 'test setup sanity check'); localTodoAppStorage = TodoAppLocalStorage(TodoAppLocalStorage.emptyState); - expect(json.decode(getLocalStorage())['current'], TodoAppLocalStorage.emptyState.toJson(), + expect(getCurrentLocalStorageSet(), TodoAppLocalStorage.emptyState.toJson(), reason: 'test setup sanity check'); initializeTestStore(); @@ -113,41 +114,58 @@ main() { test('when an AddTodoAction is dispatched', () { final initialTodos = testStore.state.todos; final newTodo = Todo(description: 'yo'); + final expectedNewState = getSerializedListOfModels([newTodo, ...initialTodos]); testStore.dispatch(AddTodoAction(newTodo)); - expect(getSerializedListOfModels(testStore.state.todos), - getSerializedListOfModels([newTodo, ...initialTodos])); + + expect(getSerializedListOfModels(testStore.state.todos), expectedNewState); + expect(getCurrentLocalStorageSet()['todos'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); }); test('when a RemoveTodoAction is dispatched', () { final initialTodos = testStore.state.todos; final idOfTodoToRemove = testStore.state.todos.first.id; + final expectedNewState = getSerializedListOfModels( + [...initialTodos]..removeWhere((todo) => todo.id == idOfTodoToRemove)); testStore.dispatch(RemoveTodoAction(idOfTodoToRemove)); - expect(getSerializedListOfModels(testStore.state.todos), - getSerializedListOfModels([...initialTodos]..removeWhere((todo) => todo.id == idOfTodoToRemove))); + + expect(getSerializedListOfModels(testStore.state.todos), expectedNewState); + expect(getCurrentLocalStorageSet()['todos'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); }); test('when an UpdateTodoAction is dispatched', () { final initialTodos = testStore.state.todos; final updatedTodo = Todo.from(initialTodos.first)..description += 'foooo'; - expect(testStore.state.todos.first.toJson(), isNot(updatedTodo.toJson())); - + final expectedNewState = updatedTodo.toJson(); + expect(testStore.state.todos.first.toJson(), isNot(expectedNewState), reason: 'test setup sanity check'); testStore.dispatch(UpdateTodoAction(updatedTodo)); - expect(testStore.state.todos.first.toJson(), updatedTodo.toJson()); + + expect(testStore.state.todos.first.toJson(), expectedNewState); + expect(getCurrentLocalStorageSet()['todos'][0], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); }); }); group('editableTodosReducer updates state', () { void beginEdits() { final initialEditableTodoIds = testStore.state.editableTodoIds; - expect(initialEditableTodoIds, isEmpty); - + expect(initialEditableTodoIds, isEmpty, reason: 'test setup sanity check'); final newEditableTodoId = testStore.state.todos.first.id; + var expectedNewState = [newEditableTodoId]; testStore.dispatch(BeginEditTodoAction(newEditableTodoId)); - expect(testStore.state.editableTodoIds, [newEditableTodoId]); + + expect(testStore.state.editableTodoIds, expectedNewState); + expect(getCurrentLocalStorageSet()['editableTodoIds'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); final anotherNewEditableTodoId = testStore.state.todos[1].id; + expectedNewState = [newEditableTodoId, anotherNewEditableTodoId]; testStore.dispatch(BeginEditTodoAction(anotherNewEditableTodoId)); - expect(testStore.state.editableTodoIds, [newEditableTodoId, anotherNewEditableTodoId]); + + expect(testStore.state.editableTodoIds, expectedNewState); + expect(getCurrentLocalStorageSet()['editableTodoIds'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); } test('when an BeginEditTodoAction is dispatched', beginEdits); @@ -157,22 +175,32 @@ main() { final noLongerEditableTodoId = testStore.state.todos.first.id; testStore.dispatch(FinishEditTodoAction(noLongerEditableTodoId)); + expect(testStore.state.editableTodoIds, isNot(contains(noLongerEditableTodoId))); + expect(getCurrentLocalStorageSet()['editableTodoIds'], isNot(contains(noLongerEditableTodoId)), + reason: reasonCurrentSetShouldBePersisted); }); }); group('selectedTodosReducer updates state', () { void select() { final initialSelectedTodoIds = testStore.state.selectedTodoIds; - expect(initialSelectedTodoIds, isEmpty); - + expect(initialSelectedTodoIds, isEmpty, reason: 'test setup sanity check'); final newSelectedTodoId = testStore.state.todos.first.id; + var expectedNewState = [newSelectedTodoId]; testStore.dispatch(SelectTodoAction(newSelectedTodoId)); - expect(testStore.state.selectedTodoIds, [newSelectedTodoId]); + + expect(testStore.state.selectedTodoIds, expectedNewState); + expect(getCurrentLocalStorageSet()['selectedTodoIds'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); final anotherNewSelectedTodoId = testStore.state.todos[1].id; + expectedNewState = [newSelectedTodoId, anotherNewSelectedTodoId]; testStore.dispatch(SelectTodoAction(anotherNewSelectedTodoId)); - expect(testStore.state.selectedTodoIds, [newSelectedTodoId, anotherNewSelectedTodoId]); + + expect(testStore.state.selectedTodoIds, expectedNewState); + expect(getCurrentLocalStorageSet()['selectedTodoIds'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); } test('when an SelectTodoAction is dispatched', select); @@ -182,25 +210,34 @@ main() { final noLongerSelectedTodoId = testStore.state.todos.first.id; testStore.dispatch(DeselectTodoAction(noLongerSelectedTodoId)); + expect(testStore.state.selectedTodoIds, isNot(contains(noLongerSelectedTodoId))); + expect(getCurrentLocalStorageSet()['selectedTodoIds'], isNot(contains(noLongerSelectedTodoId)), + reason: reasonCurrentSetShouldBePersisted); }); }); group('highlightedTodosReducer updates state', () { void highlight() { final initialHighlightedTodoIds = testStore.state.highlightedTodoIds; - expect(initialHighlightedTodoIds, isEmpty); - - final newHighlightedTodoIds = [ + expect(initialHighlightedTodoIds, isEmpty, reason: 'test setup sanity check'); + var expectedNewState = [ testStore.state.todos[0].id, testStore.state.todos[1].id, ]; - testStore.dispatch(HighlightTodosAction(newHighlightedTodoIds)); - expect(testStore.state.highlightedTodoIds, newHighlightedTodoIds); + testStore.dispatch(HighlightTodosAction(expectedNewState)); + + expect(testStore.state.highlightedTodoIds, expectedNewState); + expect(getCurrentLocalStorageSet()['highlightedTodoIds'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); final anotherNewHighlightedTodoId = testStore.state.todos[2].id; + expectedNewState.add(anotherNewHighlightedTodoId); testStore.dispatch(HighlightTodosAction([anotherNewHighlightedTodoId])); - expect(testStore.state.highlightedTodoIds, [...newHighlightedTodoIds, anotherNewHighlightedTodoId]); + + expect(testStore.state.highlightedTodoIds, expectedNewState); + expect(getCurrentLocalStorageSet()['highlightedTodoIds'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); } test('when an HighlightTodosAction is dispatched', highlight); @@ -210,7 +247,11 @@ main() { final noLongerHighlightedTodoId = testStore.state.todos.first.id; testStore.dispatch(UnHighlightTodosAction([noLongerHighlightedTodoId])); + expect(testStore.state.highlightedTodoIds, isNot(contains(noLongerHighlightedTodoId))); + expect(getCurrentLocalStorageSet()['highlightedTodoIds'], + isNot(contains(noLongerHighlightedTodoId)), + reason: reasonCurrentSetShouldBePersisted); }); }); @@ -218,41 +259,58 @@ main() { test('when an AddUserAction is dispatched', () { final initialUsers = testStore.state.users; final newUser = User(name: 'yo'); + final expectedNewState = getSerializedListOfModels([newUser, ...initialUsers]); testStore.dispatch(AddUserAction(newUser)); - expect(getSerializedListOfModels(testStore.state.users), - getSerializedListOfModels([newUser, ...initialUsers])); + + expect(getSerializedListOfModels(testStore.state.users), expectedNewState); + expect(getCurrentLocalStorageSet()['users'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); }); test('when a RemoveUserAction is dispatched', () { final initialUsers = testStore.state.users; final idOfUserToRemove = testStore.state.users.first.id; + final expectedNewState = getSerializedListOfModels( + [...initialUsers]..removeWhere((todo) => todo.id == idOfUserToRemove)); testStore.dispatch(RemoveUserAction(idOfUserToRemove)); - expect(getSerializedListOfModels(testStore.state.users), - getSerializedListOfModels([...initialUsers]..removeWhere((todo) => todo.id == idOfUserToRemove))); + + expect(getSerializedListOfModels(testStore.state.users), expectedNewState); + expect(getCurrentLocalStorageSet()['users'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); }); test('when an UpdateUserAction is dispatched', () { final initialUsers = testStore.state.users; final updatedUser = User.from(initialUsers.first)..name += 'foooo'; - expect(testStore.state.users.first.toJson(), isNot(updatedUser.toJson())); - + final expectedNewState = updatedUser.toJson(); + expect(testStore.state.users.first.toJson(), isNot(expectedNewState), reason: 'test setup sanity check'); testStore.dispatch(UpdateUserAction(updatedUser)); + expect(testStore.state.users.first.toJson(), updatedUser.toJson()); + expect(getCurrentLocalStorageSet()['users'][0], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); }); }); group('editableUsersReducer updates state', () { void beginEdits() { final initialEditableUserIds = testStore.state.editableUserIds; - expect(initialEditableUserIds, isEmpty); - + expect(initialEditableUserIds, isEmpty, reason: 'test setup sanity check'); final newEditableUserId = testStore.state.users.first.id; + var expectedNewState = [newEditableUserId]; testStore.dispatch(BeginEditUserAction(newEditableUserId)); - expect(testStore.state.editableUserIds, [newEditableUserId]); + + expect(testStore.state.editableUserIds, expectedNewState); + expect(getCurrentLocalStorageSet()['editableUserIds'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); final anotherNewEditableUserId = testStore.state.users[1].id; + expectedNewState = [newEditableUserId, anotherNewEditableUserId]; testStore.dispatch(BeginEditUserAction(anotherNewEditableUserId)); - expect(testStore.state.editableUserIds, [newEditableUserId, anotherNewEditableUserId]); + + expect(testStore.state.editableUserIds, expectedNewState); + expect(getCurrentLocalStorageSet()['editableUserIds'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); } test('when an BeginEditUserAction is dispatched', beginEdits); @@ -262,22 +320,32 @@ main() { final noLongerEditableUserId = testStore.state.users.first.id; testStore.dispatch(FinishEditUserAction(noLongerEditableUserId)); + expect(testStore.state.editableUserIds, isNot(contains(noLongerEditableUserId))); + expect(getCurrentLocalStorageSet()['editableUserIds'], isNot(contains(noLongerEditableUserId)), + reason: reasonCurrentSetShouldBePersisted); }); }); group('selectedUsersReducer updates state', () { void select() { final initialSelectedUserIds = testStore.state.selectedUserIds; - expect(initialSelectedUserIds, isEmpty); - + expect(initialSelectedUserIds, isEmpty, reason: 'test setup sanity check'); final newSelectedUserId = testStore.state.users.first.id; + var expectedNewState = [newSelectedUserId]; testStore.dispatch(SelectUserAction(newSelectedUserId)); - expect(testStore.state.selectedUserIds, [newSelectedUserId]); + + expect(testStore.state.selectedUserIds, expectedNewState); + expect(getCurrentLocalStorageSet()['selectedUserIds'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); final anotherNewSelectedUserId = testStore.state.users[1].id; + expectedNewState = [newSelectedUserId, anotherNewSelectedUserId]; testStore.dispatch(SelectUserAction(anotherNewSelectedUserId)); - expect(testStore.state.selectedUserIds, [newSelectedUserId, anotherNewSelectedUserId]); + + expect(testStore.state.selectedUserIds, expectedNewState); + expect(getCurrentLocalStorageSet()['selectedUserIds'], expectedNewState, + reason: reasonCurrentSetShouldBePersisted); } test('when an SelectUserAction is dispatched', select); @@ -287,7 +355,126 @@ main() { final noLongerSelectedUserId = testStore.state.users.first.id; testStore.dispatch(DeselectUserAction(noLongerSelectedUserId)); + expect(testStore.state.selectedUserIds, isNot(contains(noLongerSelectedUserId))); + expect(getCurrentLocalStorageSet()['selectedUserIds'], isNot(contains(noLongerSelectedUserId)), + reason: reasonCurrentSetShouldBePersisted); + }); + }); + + group('appStateReducer updates state', () { + group('when a LoadStateFromLocalStorageAction action is dispatched', () { + setUp(() { + expect(getCurrentLocalStorageSet(), defaultAppState, reason: 'test setup sanity check'); + testStore.dispatch(LoadStateFromLocalStorageAction('empty')); + }); + + test('', () { + expect(testStore.state.todos, isEmpty); + expect(testStore.state.users, isEmpty); + expect(testStore.state.selectedTodoIds, isEmpty); + expect(testStore.state.editableTodoIds, isEmpty); + expect(testStore.state.highlightedTodoIds, isEmpty); + expect(testStore.state.selectedUserIds, isEmpty); + expect(testStore.state.editableUserIds, isEmpty); + expect(testStore.state.highlightedUserIds, isEmpty); + }); + + test('and persists the state as the "current" set in window.localStorage', () { + expect(getCurrentLocalStorageSet(), TodoAppLocalStorage.emptyState.toJson()); + }); + }); + + group('when a SaveLocalStorageStateAsAction action is dispatched', () { + const customPersistedDataSetName = 'custom'; + void addCustomPersistedDataSet() { + expect(getCurrentLocalStorageSet(), defaultAppState, reason: 'test setup sanity check'); + final newTodo = Todo(description: 'yo'); + // Update the "current" persisted data set so that it differs from the default + testStore.dispatch(AddTodoAction(newTodo)); + + final addCustomPersistedSetPayload = SaveLocalStorageStateAsPayload(customPersistedDataSetName); + // Save a custom set based on the "current" persisted data set + testStore.dispatch(SaveLocalStorageStateAsAction(addCustomPersistedSetPayload)); + + expect(getLocalStorageSetByKey(customPersistedDataSetName), testStore.state.toJson()); + expect(getCurrentLocalStorageSet(), getLocalStorageSetByKey(customPersistedDataSetName), + reason: reasonCurrentSetShouldBePersisted); + } + + setUp(() { + expect(getCurrentLocalStorageSet(), defaultAppState, reason: 'test setup sanity check'); + }); + + test('as a new custom persisted set when a previousName is not specified (save)', addCustomPersistedDataSet); + + test('as an updated custom persisted set when a previousName is specified (overwrite)', () { + addCustomPersistedDataSet(); + final initialCustomPersistedDataSetValue = getLocalStorageSetByKey(customPersistedDataSetName); + final newTodo = Todo(description: 'yo again'); + // Update the "current" persisted data set so that it differs from the custom persisted set + testStore.dispatch(AddTodoAction(newTodo)); + + final overwriteCustomPersistedSetPayload = + SaveLocalStorageStateAsPayload(customPersistedDataSetName, previousName: customPersistedDataSetName); + // Save the existing custom set based on the "current" persisted data set + testStore.dispatch(SaveLocalStorageStateAsAction(overwriteCustomPersistedSetPayload)); + + expect(getLocalStorageSetByKey(customPersistedDataSetName), isNot(initialCustomPersistedDataSetValue)); + expect(getLocalStorageSetByKey(customPersistedDataSetName), testStore.state.toJson()); + expect(getCurrentLocalStorageSet(), getLocalStorageSetByKey(customPersistedDataSetName), + reason: reasonCurrentSetShouldBePersisted); + }); + + test('as a new custom persisted set with identical data when a previousName is specified (copy)', () { + addCustomPersistedDataSet(); + const newCustomPersistedDataSetName = 'Copy of $customPersistedDataSetName'; + final initialCustomPersistedDataSetValue = getLocalStorageSetByKey(customPersistedDataSetName); + + final overwriteCustomPersistedSetPayload = + SaveLocalStorageStateAsPayload(newCustomPersistedDataSetName, previousName: customPersistedDataSetName); + // Save a copy of the existing custom set based on the "current" persisted data set + testStore.dispatch(SaveLocalStorageStateAsAction(overwriteCustomPersistedSetPayload)); + + expect(getLocalStorageSetByKey(newCustomPersistedDataSetName), testStore.state.toJson()); + expect(getCurrentLocalStorageSet(), getLocalStorageSetByKey(newCustomPersistedDataSetName), + reason: reasonCurrentSetShouldBePersisted); + expect(getLocalStorageSetByKey(customPersistedDataSetName), initialCustomPersistedDataSetValue, + reason: 'The persisted "$customPersistedDataSetName" set should remain unchanged'); + expect(getLocalStorageSetByKey(customPersistedDataSetName)['name'], + isNot(getLocalStorageSetByKey(newCustomPersistedDataSetName)['name']), + reason: 'The two sets should have different names'); + expect(getLocalStorageSetByKey(customPersistedDataSetName)..remove('name'), + getLocalStorageSetByKey(newCustomPersistedDataSetName)..remove('name'), + reason: 'The "$newCustomPersistedDataSetName" set should be an ' + 'exact copy of "$customPersistedDataSetName" except for the name'); + }); + + test('as a new custom persisted set with different data when a previousName is specified (save as)', () { + addCustomPersistedDataSet(); + const newCustomPersistedDataSetName = 'Copy of $customPersistedDataSetName'; + final initialCustomPersistedDataSetValue = getLocalStorageSetByKey(customPersistedDataSetName); + final newTodo = Todo(description: 'yo again'); + // Update the "current" persisted data set so that it differs from the custom persisted set + testStore.dispatch(AddTodoAction(newTodo)); + + final overwriteCustomPersistedSetPayload = + SaveLocalStorageStateAsPayload(newCustomPersistedDataSetName, previousName: customPersistedDataSetName); + // Save a copy of the existing custom set based on the "current" persisted data set + testStore.dispatch(SaveLocalStorageStateAsAction(overwriteCustomPersistedSetPayload)); + + expect(getLocalStorageSetByKey(newCustomPersistedDataSetName), testStore.state.toJson()); + expect(getCurrentLocalStorageSet(), getLocalStorageSetByKey(newCustomPersistedDataSetName), + reason: reasonCurrentSetShouldBePersisted); + expect(getLocalStorageSetByKey(customPersistedDataSetName), initialCustomPersistedDataSetValue, + reason: 'The persisted "$customPersistedDataSetName" set should remain unchanged'); + expect(getLocalStorageSetByKey(customPersistedDataSetName)['name'], + isNot(getLocalStorageSetByKey(newCustomPersistedDataSetName)['name']), + reason: 'The two sets should have different names'); + expect(getLocalStorageSetByKey(customPersistedDataSetName), + isNot(getLocalStorageSetByKey(newCustomPersistedDataSetName)), + reason: 'The "$newCustomPersistedDataSetName" set should differ from "$customPersistedDataSetName"'); + }); }); }); }); From 6a889efa4e7828472ffaa318b244f5d14b81d69a Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Fri, 20 Dec 2019 08:24:11 -0700 Subject: [PATCH 23/34] Add tests for example todo app models --- .../unit/browser/redux/models/todo_test.dart | 80 +++++++++++++++++++ .../unit/browser/redux/models/user_test.dart | 44 ++++++++++ 2 files changed, 124 insertions(+) create mode 100644 app/over_react_redux/todo_client/test/unit/browser/redux/models/todo_test.dart create mode 100644 app/over_react_redux/todo_client/test/unit/browser/redux/models/user_test.dart diff --git a/app/over_react_redux/todo_client/test/unit/browser/redux/models/todo_test.dart b/app/over_react_redux/todo_client/test/unit/browser/redux/models/todo_test.dart new file mode 100644 index 000000000..b47293532 --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/redux/models/todo_test.dart @@ -0,0 +1,80 @@ +@TestOn('browser') +import 'package:test/test.dart'; + +import 'package:todo_client/src/models/todo.dart'; + +main() { + group('Todo', () { + group('is constructed as expected using the default constructor', () { + test('when there are no arguments', () { + final model = Todo(); + expect(model.id, hasLength(36), reason: 'A Uuid().v4() should be used when id is not specified'); + expect(model.description, isEmpty); + expect(model.notes, isEmpty); + expect(model.assignedUserId, isEmpty); + expect(model.isCompleted, isFalse); + expect(model.isPublic, isFalse); + }); + + test('when id is specified', () { + final model = Todo(id: 'some-unique-id'); + expect(model.id, 'some-unique-id'); + }); + + test('when description is specified', () { + final model = Todo(description: 'Do this'); + expect(model.description, 'Do this'); + }); + + test('when notes is specified', () { + final model = Todo(notes: 'Something about this'); + expect(model.notes, 'Something about this'); + }); + + test('when assignedUserId is specified', () { + final model = Todo(assignedUserId: 'some-user-id'); + expect(model.assignedUserId, 'some-user-id'); + }); + + test('when isCompleted is true', () { + final model = Todo(isCompleted: true); + expect(model.isCompleted, isTrue); + }); + + test('when isPublic is true', () { + final model = Todo(isPublic: true); + expect(model.isPublic, isTrue); + }); + + }); + + test('is constructed as expected using the fromJson() factory constructor', () { + final model = Todo.fromJson({ + 'id': 'some-unique-id', + 'description': 'Do this', + 'notes': 'Something about this', + 'assignedUserId': 'some-user-id', + 'isCompleted': true, + 'isPublic': true, + }); + expect(model.id, 'some-unique-id'); + expect(model.description, 'Do this'); + expect(model.notes, 'Something about this'); + expect(model.assignedUserId, 'some-user-id'); + expect(model.isCompleted, isTrue); + expect(model.isPublic, isTrue); + }); + + test('has a toJson() method that returns the expected value', () { + final model = Todo(); + expect(model.toJson(), { + 'id': model.id, + 'description': model.description, + 'notes': model.notes, + 'assignedUserId': model.assignedUserId, + 'isCompleted': model.isCompleted, + 'isPublic': model.isPublic, + }); + }); + }); +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/redux/models/user_test.dart b/app/over_react_redux/todo_client/test/unit/browser/redux/models/user_test.dart new file mode 100644 index 000000000..798fe5cc3 --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/redux/models/user_test.dart @@ -0,0 +1,44 @@ +@TestOn('browser') +import 'package:test/test.dart'; + +import 'package:todo_client/src/models/user.dart'; + +main() { + group('User', () { + group('is constructed as expected using the default constructor', () { + test('when there are no arguments', () { + final model = User(); + expect(model.id, hasLength(36), reason: 'A Uuid().v4() should be used when id is not specified'); + expect(model.name, '?'); + expect(model.bio, isEmpty); + }); + + test('when id is specified', () { + final model = User(id: 'some-unique-id'); + expect(model.id, 'some-unique-id'); + }); + + test('when name is specified', () { + final model = User(name: 'Joe'); + expect(model.name, 'Joe'); + }); + + test('when bio is specified', () { + final model = User(bio: 'Something really interesting'); + expect(model.bio, 'Something really interesting'); + }); + }); + + test('is constructed as expected using the fromJson() factory constructor', () { + final model = User.fromJson({'id': 'some-unique-id', 'name': 'Joe', 'bio': 'Something really interesting'}); + expect(model.name, 'Joe'); + expect(model.id, 'some-unique-id'); + expect(model.bio, 'Something really interesting'); + }); + + test('has a toJson() method that returns the expected value', () { + final model = User(); + expect(model.toJson(), {'id': model.id, 'name': model.name, 'bio': model.bio}); + }); + }); +} From a9941148b08627fe765154e9c13caff52cb1e3e8 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Fri, 20 Dec 2019 09:18:52 -0700 Subject: [PATCH 24/34] Add tests for example todo app material-ui js interop --- .../lib/src/components/app_bar/app_bar.dart | 1 - .../src/components/shared/material_ui.dart | 2 -- .../lib/src/test_fixtures/mock_js_objects.js | 10 ++++++ .../react_components_test_template.html | 1 + .../browser/components/js_interop_test.dart | 36 +++++++++++++++++++ 5 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 app/over_react_redux/todo_client/lib/src/test_fixtures/mock_js_objects.js create mode 100644 app/over_react_redux/todo_client/test/unit/browser/components/js_interop_test.dart diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart index a7866c230..12d8a2472 100644 --- a/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar.dart @@ -24,7 +24,6 @@ class TodoAppBarComponent extends UiComponent2 { Box({'flexGrow': 1}, Typography({ 'variant': 'h6', - 'className': muiClasses['title'] }, 'OverReact Redux Todo Demo App'), ), ConnectedAppBarLocalStorageMenu()(), diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart index c126284bb..ce7428aca 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart @@ -19,7 +19,6 @@ import 'package:todo_client/src/utils.dart'; @JS() class MaterialUI { external static JsMap get colors; - external static JsMap get classes; external static ReactClass get AppBar; external static ReactClass get Avatar; @@ -50,7 +49,6 @@ class MaterialUI { } final muiColors = jsBackedMapDeepFromJs(MaterialUI.colors); -final muiClasses = jsBackedMapDeepFromJs(MaterialUI.classes); // ----------------------------------------------------------------------- // Below, you'll find the top level JS component factories diff --git a/app/over_react_redux/todo_client/lib/src/test_fixtures/mock_js_objects.js b/app/over_react_redux/todo_client/lib/src/test_fixtures/mock_js_objects.js new file mode 100644 index 000000000..6e29cadf6 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/test_fixtures/mock_js_objects.js @@ -0,0 +1,10 @@ +const mockJsMap = { + 'foo': 'bar', + 'nested': { + 'bizzle': 'foobaz', + 'nested': { + 'bazzle': 'foo', + }, + }, +}; +window['mockJsMap'] = mockJsMap; diff --git a/app/over_react_redux/todo_client/test/unit/_templates/react_components_test_template.html b/app/over_react_redux/todo_client/test/unit/_templates/react_components_test_template.html index 1c9bbe652..63bb3ca21 100644 --- a/app/over_react_redux/todo_client/test/unit/_templates/react_components_test_template.html +++ b/app/over_react_redux/todo_client/test/unit/_templates/react_components_test_template.html @@ -3,6 +3,7 @@ + {test} diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/js_interop_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/js_interop_test.dart new file mode 100644 index 000000000..144e30cb0 --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/components/js_interop_test.dart @@ -0,0 +1,36 @@ +@TestOn('browser') +@JS() +library js_interop_test; + +import 'package:js/js.dart'; +import 'package:over_react/over_react.dart'; +import 'package:react/react_client/js_backed_map.dart'; +import 'package:test/test.dart'; + +import 'package:todo_client/src/components/shared/material_ui.dart'; +import 'package:todo_client/src/utils.dart'; + +@JS() +external JsMap get mockJsMap; + +main() { + setClientConfiguration(); + JsBackedMap mockJsBackedMap; + + setUpAll(() async { + expect(mockJsMap, isNotNull, reason: 'test setup sanity check'); + mockJsBackedMap = JsBackedMap.backedBy(mockJsMap); + }); + + test('jsBackedMapDeepFromJs() converts deeply nested JS objects into JsBackedMaps', () { + expect(jsBackedMapDeepFromJs(mockJsBackedMap.jsObject), isA()); + expect(jsBackedMapDeepFromJs(mockJsBackedMap['nested']), isA()); + expect(jsBackedMapDeepFromJs(mockJsBackedMap['nested'])['nested'], isA()); + }); + + test('muiColors is a JsBackedMap backed by the JS MaterialUI.colors object', () { + expect(muiColors, jsBackedMapDeepFromJs(MaterialUI.colors)); + expect(muiColors['blue']['500'], isA()); + expect(muiColors['blue']['500'], isNotEmpty); + }); +} From 1e62fba0ec5bf04b38a6622c7543dbfffb0e1387 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Fri, 20 Dec 2019 09:40:19 -0700 Subject: [PATCH 25/34] =?UTF-8?q?Add=20tests=20for=20example=20todo=20app?= =?UTF-8?q?=20mui=20interop=E2=80=99d=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../browser/components/fixtures/utils.dart | 13 ++ .../browser/components/material_ui_test.dart | 196 ++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 app/over_react_redux/todo_client/test/unit/browser/components/fixtures/utils.dart create mode 100644 app/over_react_redux/todo_client/test/unit/browser/components/material_ui_test.dart diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/fixtures/utils.dart b/app/over_react_redux/todo_client/test/unit/browser/components/fixtures/utils.dart new file mode 100644 index 000000000..380338c67 --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/components/fixtures/utils.dart @@ -0,0 +1,13 @@ +import 'dart:js'; + +import 'package:test/test.dart'; + +bool muiJsIsAvailable() { + // Skip these tests if the JS file isn't loaded (e.g. if you're running tests without an internet connection, etc) + if (context['MaterialUI'] == null) { + test('MaterialUI', () {}, + skip: 'Skipping MaterialUI tests because the required JS file from unpkg.com did not load'); + return false; + } + return true; +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/material_ui_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/material_ui_test.dart new file mode 100644 index 000000000..40d854519 --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/components/material_ui_test.dart @@ -0,0 +1,196 @@ +@TestOn('browser') +import 'package:over_react/over_react.dart'; +import 'package:test/test.dart'; + +import 'package:todo_client/src/components/shared/material_ui.dart'; + +import 'fixtures/utils.dart'; + +main() { + setClientConfiguration(); + if (!muiJsIsAvailable()) return; + + group('MaterialUI', () { + group('AppBar', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.AppBar component', () { + expect(AppBar.type.displayName, MaterialUI.AppBar.displayName); + expect(AppBar({}), isA()); + }); + }); + + group('Avatar', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Avatar component', () { + expect(Avatar.type.displayName, MaterialUI.Avatar.displayName); + expect(Avatar({}), isA()); + }); + }); + + group('Badge', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Badge component', () { + expect(Badge.type.displayName, MaterialUI.Badge.displayName); + expect(Badge({}), isA()); + }); + }); + + group('Box', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Box component', () { + expect(Box.type.displayName, MaterialUI.Box.displayName); + expect(Box({}), isA()); + }); + }); + + group('Button', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Button component', () { + expect(Button.type.displayName, MaterialUI.Button.displayName); + expect(Button({}), isA()); + }); + }); + + group('Checkbox', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Checkbox component', () { + expect(Checkbox.type.displayName, MaterialUI.Checkbox.displayName); + expect(Checkbox({}), isA()); + }); + }); + + group('Container', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Container component', () { + expect(Container.type.displayName, MaterialUI.Container.displayName); + expect(Container({}), isA()); + }); + }); + + group('CssBaseline', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.CssBaseline component', () { + expect(CssBaseline.type.displayName, MaterialUI.CssBaseline.displayName); + expect(CssBaseline({}), isA()); + }); + }); + + group('Divider', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Divider component', () { + expect(Divider.type.displayName, MaterialUI.Divider.displayName); + expect(Divider({}), isA()); + }); + }); + + group('ExpansionPanel', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.ExpansionPanel component', () { + expect(ExpansionPanel.type.displayName, MaterialUI.ExpansionPanel.displayName); + expect(ExpansionPanel({}), isA()); + }); + }); + + group('ExpansionPanelActions', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.ExpansionPanelActions component', () { + expect(ExpansionPanelActions.type.displayName, MaterialUI.ExpansionPanelActions.displayName); + expect(ExpansionPanelActions({}), isA()); + }); + }); + + group('ExpansionPanelDetails', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.ExpansionPanelDetails component', () { + expect(ExpansionPanelDetails.type.displayName, MaterialUI.ExpansionPanelDetails.displayName); + expect(ExpansionPanelDetails({}), isA()); + }); + }); + + group('ExpansionPanelSummary', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.ExpansionPanelSummary component', () { + expect(ExpansionPanelSummary.type.displayName, MaterialUI.ExpansionPanelSummary.displayName); + expect(ExpansionPanelSummary({}), isA()); + }); + }); + + group('Grid', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Grid component', () { + expect(Grid.type.displayName, MaterialUI.Grid.displayName); + expect(Grid({}), isA()); + }); + }); + + group('IconButton', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.IconButton component', () { + expect(IconButton.type.displayName, MaterialUI.IconButton.displayName); + expect(IconButton({}), isA()); + }); + }); + + group('InputBase', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.InputBase component', () { + expect(InputBase.type.displayName, MaterialUI.InputBase.displayName); + expect(InputBase({}), isA()); + }); + }); + + group('ListUi', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.List component', () { + expect(ListUi.type.displayName, MaterialUI.List.displayName); + expect(ListUi({}), isA()); + }); + }); + + group('ListItem', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.ListItem component', () { + expect(ListItem.type.displayName, MaterialUI.ListItem.displayName); + expect(ListItem({}), isA()); + }); + }); + + group('Menu', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Menu component', () { + expect(Menu.type.displayName, MaterialUI.Menu.displayName); + expect(Menu({}), isA()); + }); + }); + + group('MenuItem', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.MenuItem component', () { + expect(MenuItem.type.displayName, MaterialUI.MenuItem.displayName); + expect(MenuItem({}), isA()); + }); + }); + + group('Popover', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Popover component', () { + expect(Popover.type.displayName, MaterialUI.Popover.displayName); + expect(Popover({}), isA()); + }); + }); + + group('SvgIcon', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.SvgIcon component', () { + expect(SvgIcon.type.displayName, MaterialUI.SvgIcon.displayName); + expect(SvgIcon({}), isA()); + }); + }); + + group('TextField', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.TextField component', () { + expect(TextField.type.displayName, MaterialUI.TextField.displayName); + expect(TextField({}), isA()); + }); + }); + + group('Toolbar', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Toolbar component', () { + expect(Toolbar.type.displayName, MaterialUI.Toolbar.displayName); + expect(Toolbar({}), isA()); + }); + }); + + group('Tooltip', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Tooltip component', () { + expect(Tooltip.type.displayName, MaterialUI.Tooltip.displayName); + expect(Tooltip({}), isA()); + }); + }); + + group('Typography', () { + test('is a ReactJsComponentFactoryProxy of the JS MaterialUI.Typography component', () { + expect(Typography.type.displayName, MaterialUI.Typography.displayName); + expect(Typography({}), isA()); + }); + }); + }); +} From bfc5ceb6b2e6e0dcc731280480928c6fce456ce0 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Fri, 20 Dec 2019 09:47:54 -0700 Subject: [PATCH 26/34] Move test store into a shared utils file --- .../test/unit/browser/fixtures/utils.dart | 20 +++++++++++++++++++ .../test/unit/browser/redux/store_test.dart | 17 ++-------------- 2 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 app/over_react_redux/todo_client/test/unit/browser/fixtures/utils.dart diff --git a/app/over_react_redux/todo_client/test/unit/browser/fixtures/utils.dart b/app/over_react_redux/todo_client/test/unit/browser/fixtures/utils.dart new file mode 100644 index 000000000..2cda72642 --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/fixtures/utils.dart @@ -0,0 +1,20 @@ +import 'dart:html'; + +import 'package:redux/redux.dart'; +import 'package:test/test.dart'; +import 'package:todo_client/src/local_storage.dart'; +import 'package:todo_client/src/store.dart'; + +Store testStore; +Store initializeTestStore() { + addTearDown(() { + testStore = null; + localTodoAppStorage = null; + window.localStorage[TodoAppLocalStorage.localStorageKey] = ''; + }); + + return testStore = Store( + appStateReducer, + initialState: initializeState(), + ); +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart b/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart index c8b2da4ea..741d1f1c0 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:html'; import 'package:over_react/over_react.dart'; -import 'package:redux/redux.dart'; import 'package:test/test.dart'; import 'package:todo_client/src/actions.dart'; @@ -13,19 +12,13 @@ import 'package:todo_client/src/models/todo.dart'; import 'package:todo_client/src/models/user.dart'; import 'package:todo_client/src/store.dart'; +import '../fixtures/utils.dart'; + main() { setClientConfiguration(); - Store testStore; const reasonCurrentSetShouldBePersisted = 'The state update should be persisted as the "current" set in window.localStorage'; - Store initializeTestStore() { - return testStore = Store( - appStateReducer, - initialState: initializeState(), - ); - } - String getLocalStorage() => window.localStorage[TodoAppLocalStorage.localStorageKey]; Map getLocalStorageSetByKey(String key) => json.decode(getLocalStorage())[key]; @@ -42,12 +35,6 @@ main() { initializeTestStore(); }); - tearDown(() { - testStore = null; - localTodoAppStorage = null; - window.localStorage[TodoAppLocalStorage.localStorageKey] = ''; - }); - test('requires a name', () { expect(() => AppState(null), throwsA(TypeMatcher())); expect(() => AppState(''), throwsA(TypeMatcher())); From 1ade26b7ae52c964f828927a18463f00c1985dc0 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Fri, 20 Dec 2019 17:13:01 -0700 Subject: [PATCH 27/34] Begin adding tests for example todo app custom components --- .../todo_client/lib/src/components/app.dart | 12 +- .../lib/src/components/create_input.dart | 8 +- .../src/components/shared/display_list.dart | 8 +- .../src/components/shared/material_ui.dart | 2 +- .../redraw_counter_component_mixin.dart | 29 ++ .../shared/todo_item_text_field.dart | 6 + .../lib/src/components/todo_list.dart | 11 +- .../lib/src/components/todo_list_item.dart | 10 +- .../lib/src/components/user_list.dart | 10 +- .../lib/src/components/user_list_item.dart | 3 +- .../lib/src/components/user_selector.dart | 6 +- app/over_react_redux/todo_client/pubspec.yaml | 1 + .../unit/browser/components/app_test.dart | 132 ++++++++ .../connected_todo_list_item_test.dart | 175 +++++++++++ .../components/connected_todo_list_test.dart | 95 ++++++ .../connected_user_list_item_test.dart | 175 +++++++++++ .../components/connected_user_list_test.dart | 95 ++++++ .../browser/components/create_input_test.dart | 57 ++++ .../browser/components/display_list_test.dart | 52 ++++ .../browser/components/fixtures/utils.dart | 25 ++ .../components/todo_list_item_test.dart | 292 ++++++++++++++++++ .../test/unit/browser/fixtures/utils.dart | 23 +- .../test/unit/browser/redux/store_test.dart | 18 +- 23 files changed, 1208 insertions(+), 37 deletions(-) create mode 100644 app/over_react_redux/todo_client/lib/src/components/shared/redraw_counter_component_mixin.dart create mode 100644 app/over_react_redux/todo_client/test/unit/browser/components/app_test.dart create mode 100644 app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_item_test.dart create mode 100644 app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_test.dart create mode 100644 app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_item_test.dart create mode 100644 app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_test.dart create mode 100644 app/over_react_redux/todo_client/test/unit/browser/components/create_input_test.dart create mode 100644 app/over_react_redux/todo_client/test/unit/browser/components/display_list_test.dart create mode 100644 app/over_react_redux/todo_client/test/unit/browser/components/todo_list_item_test.dart diff --git a/app/over_react_redux/todo_client/lib/src/components/app.dart b/app/over_react_redux/todo_client/lib/src/components/app.dart index a15423e01..6a48e66b8 100644 --- a/app/over_react_redux/todo_client/lib/src/components/app.dart +++ b/app/over_react_redux/todo_client/lib/src/components/app.dart @@ -2,6 +2,7 @@ import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; import 'package:todo_client/src/store.dart'; import 'package:todo_client/src/models/todo.dart'; import 'package:todo_client/src/models/user.dart'; @@ -25,6 +26,7 @@ UiFactory ConnectedTodoApp = connect( } ); }, + forwardRef: true, )(TodoApp); @Factory() @@ -40,7 +42,7 @@ class _$TodoAppProps extends UiProps with ConnectPropsMixin { } @Component2() -class TodoAppComponent extends UiComponent2 { +class TodoAppComponent extends UiComponent2 with RedrawCounterMixin { @override render() { return Fragment()( @@ -79,8 +81,9 @@ class TodoAppComponent extends UiComponent2 { ..label = 'New Todo' ..placeholder = 'Create new Todo' ..onCreate = props.createTodo + ..addTestId('todo_client.createTodoInput') )(), - ConnectedTodoList()(), + (ConnectedTodoList()..addTestId('todo_client.ConnectedTodoList'))(), ); } @@ -94,10 +97,11 @@ class TodoAppComponent extends UiComponent2 { }, (CreateInput() ..label = 'New User' - ..placeholder = 'Create new user' + ..placeholder = 'Create new User' ..onCreate = props.createUser + ..addTestId('todo_client.createUserInput') )(), - ConnectedUserList()(), + (ConnectedUserList()..addTestId('todo_client.ConnectedUserList'))(), ); } } diff --git a/app/over_react_redux/todo_client/lib/src/components/create_input.dart b/app/over_react_redux/todo_client/lib/src/components/create_input.dart index d87efcd75..d147b3bf8 100644 --- a/app/over_react_redux/todo_client/lib/src/components/create_input.dart +++ b/app/over_react_redux/todo_client/lib/src/components/create_input.dart @@ -1,5 +1,6 @@ import 'dart:html'; +import 'package:meta/meta.dart'; import 'package:over_react/over_react.dart'; import 'package:todo_client/src/components/shared/material_ui.dart'; @@ -14,7 +15,7 @@ UiFactory CreateInput = @Props(keyNamespace: '') // No namespace so prop forwarding works when passing to the JS TextField component. class _$CreateInputProps extends UiProps { - void Function(String s) onCreate; + @requiredProp void Function(String s) onCreate; bool autoFocus; String label; String placeholder; @@ -22,6 +23,9 @@ class _$CreateInputProps extends UiProps { @Component2() class CreateInputComponent extends UiComponent2 { + @visibleForTesting + final textFieldRef = createRef().jsRef; + @override get defaultProps => (newProps()..autoFocus = false); @@ -34,6 +38,8 @@ class CreateInputComponent extends UiComponent2 { 'fullWidth': true, 'variant': 'outlined', ...propsToForward, + 'inputRef': textFieldRef, + // TODO: How do we get this to play nice with something like forwardRef instead of storing a ref inside the parent component? 'onKeyDown': (SyntheticKeyboardEvent event) { if (props.onKeyDown != null) props.onKeyDown(event); InputElement target = event.target; diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/display_list.dart b/app/over_react_redux/todo_client/lib/src/components/shared/display_list.dart index 80a240e1d..6f8ac5064 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/display_list.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/display_list.dart @@ -1,3 +1,4 @@ +import 'package:meta/meta.dart'; import 'package:over_react/over_react.dart'; import 'package:todo_client/src/components/shared/material_ui.dart'; @@ -19,6 +20,9 @@ class _$DisplayListProps extends UiProps { @Component2() class DisplayListComponent extends UiComponent2 { + @visibleForTesting + final scrollingBoxRef = createRef().jsRef; + @override render() { if (props.children.isEmpty) { @@ -26,6 +30,7 @@ class DisplayListComponent extends UiComponent2 { ..type = EmptyViewType.VBLOCK ..header = 'No ${props.listItemTypeDescription} to show' ..glyph = InfoIcon({'color': 'disabled'}) + ..addTestId('todo_client.DisplayList.EmptyView') )( 'You should totally create one!', ); @@ -39,7 +44,8 @@ class DisplayListComponent extends UiComponent2 { 'flexBasis': '0%', 'paddingTop': 2, 'style': {...props.style ?? {}, 'overflowY': 'auto'}, - ...propsToForward + ...propsToForward, + 'ref': scrollingBoxRef, }, props.children, ); diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart index ce7428aca..22bc2dc64 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/material_ui.dart @@ -3,7 +3,7 @@ library material_ui; import 'package:js/js.dart'; -import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react.dart' hide forwardRef; import 'package:react/react_client/js_backed_map.dart'; import 'package:react/react_client.dart'; import 'package:react/react_client/react_interop.dart'; diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/redraw_counter_component_mixin.dart b/app/over_react_redux/todo_client/lib/src/components/shared/redraw_counter_component_mixin.dart new file mode 100644 index 000000000..f3f7ae2d7 --- /dev/null +++ b/app/over_react_redux/todo_client/lib/src/components/shared/redraw_counter_component_mixin.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:over_react/over_react.dart'; + +mixin RedrawCounterMixin on UiComponent2 { + int _desiredRedrawCount = 1; + Completer _didRedraw = Completer(); + + @visibleForTesting + Completer didRedraw([int desiredRedrawCount = 1]) { + _desiredRedrawCount = desiredRedrawCount; + return _didRedraw; + } + + @visibleForTesting + int redrawCount = 0; + + @override + @mustCallSuper + void componentDidUpdate(_, __, [___]) { + redrawCount++; + if (redrawCount < _desiredRedrawCount) { + return; + } + _didRedraw.complete(redrawCount); + _didRedraw = Completer(); + } +} diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/todo_item_text_field.dart b/app/over_react_redux/todo_client/lib/src/components/shared/todo_item_text_field.dart index 7b465a41f..892768049 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/todo_item_text_field.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/todo_item_text_field.dart @@ -1,3 +1,5 @@ +import 'dart:html'; + import 'package:over_react/over_react.dart'; import 'package:todo_client/src/components/shared/material_ui.dart'; @@ -31,6 +33,8 @@ class _$TodoItemTextFieldState extends UiState {} @Component2() class TodoItemTextFieldComponent extends UiStatefulComponent2 { + final textFieldRef = createRef(); + @override get defaultProps => (newProps() ..fullWidth = true @@ -55,6 +59,7 @@ class TodoItemTextFieldComponent extends UiStatefulComponent2 ConnectedTodoList = connect( ..todos = state.todos ); }, + forwardRef: true, )(TodoList); @Factory() @@ -29,10 +31,13 @@ class _$TodoListProps extends UiProps with ConnectPropsMixin { } @Component2() -class TodoListComponent extends UiComponent2 { +class TodoListComponent extends UiComponent2 with RedrawCounterMixin { @override render() { - return (DisplayList()..listItemTypeDescription = 'todos')( + return (DisplayList() + ..listItemTypeDescription = 'todos' + ..addTestId('todo_client.TodoList.DisplayList') + )( props.todos.map(_renderItem).toList(), ); } @@ -41,7 +46,7 @@ class TodoListComponent extends UiComponent2 { return (ConnectedTodoListItem() ..key = todo.id ..model = todo - ..assignedUserId = todo.assignedUserId + ..addTestId('todo_client.TodoListItem.${todo.id}') )(); } } diff --git a/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart b/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart index 70b51f7c6..7f987c672 100644 --- a/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart +++ b/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart @@ -3,6 +3,7 @@ import 'dart:html'; import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; import 'package:todo_client/src/models/todo.dart'; import 'package:todo_client/src/components/shared/list_item_expansion_panel_summary.dart'; @@ -52,8 +53,6 @@ class _$TodoListItemProps extends UiProps @requiredProp @override Todo model; - - String assignedUserId; } @State() @@ -67,7 +66,7 @@ class _$TodoListItemState extends UiState @Component2() class TodoListItemComponent extends UiStatefulComponent2 - with ListItemMixin { + with ListItemMixin, RedrawCounterMixin { @override bool get hasDetails => model.notes != null && model.notes.isNotEmpty; @@ -134,6 +133,7 @@ class TodoListItemComponent extends UiStatefulComponent2 ConnectedUserList = connect( ..users = state.users ); }, + forwardRef: true, )(UserList); @Factory() @@ -29,10 +31,13 @@ class _$UserListProps extends UiProps with ConnectPropsMixin { } @Component2() -class UserListComponent extends UiComponent2 { +class UserListComponent extends UiComponent2 with RedrawCounterMixin { @override render() { - return (DisplayList()..listItemTypeDescription = 'users')( + return (DisplayList() + ..listItemTypeDescription = 'users' + ..addTestId('todo_client.UserList.DisplayList') + )( props.users.map(_renderUser).toList(), ); } @@ -41,6 +46,7 @@ class UserListComponent extends UiComponent2 { return (ConnectedUserListItem() ..key = user.id ..model = user + ..addTestId('todo_client.UserListItem.${user.id}') )(); } } diff --git a/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart b/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart index 8e9831f34..6d965e1cc 100644 --- a/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart +++ b/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart @@ -3,6 +3,7 @@ import 'dart:html'; import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; import 'package:todo_client/src/models/user.dart'; import 'package:todo_client/src/components/shared/avatar_with_colors.dart'; @@ -66,7 +67,7 @@ class _$UserListItemState extends UiState @Component2() class UserListItemComponent extends UiStatefulComponent2 - with ListItemMixin { + with ListItemMixin, RedrawCounterMixin { @override bool get hasDetails => model.bio != null && model.bio.isNotEmpty; diff --git a/app/over_react_redux/todo_client/lib/src/components/user_selector.dart b/app/over_react_redux/todo_client/lib/src/components/user_selector.dart index cfd73896a..5c483d497 100644 --- a/app/over_react_redux/todo_client/lib/src/components/user_selector.dart +++ b/app/over_react_redux/todo_client/lib/src/components/user_selector.dart @@ -2,6 +2,7 @@ library todo_client.src.components.user_selector; import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; +import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; import 'package:todo_client/src/store.dart'; import 'package:todo_client/src/models/user.dart'; @@ -17,11 +18,12 @@ UiFactory ConnectedUserSelector = connect user.id == ownProps.selectedUserId) + ? state.users.singleWhere((user) => user.id == ownProps.selectedUserId, orElse: () => null) : null ..users = state.users ); }, + forwardRef: true )(UserSelector); @Factory() @@ -38,7 +40,7 @@ class _$UserSelectorProps extends UiProps { } @Component2() -class UserSelectorComponent extends UiComponent2 { +class UserSelectorComponent extends UiComponent2 with RedrawCounterMixin { final _overlayRef = createRef(); @override diff --git a/app/over_react_redux/todo_client/pubspec.yaml b/app/over_react_redux/todo_client/pubspec.yaml index ae3893e31..8b74d6489 100644 --- a/app/over_react_redux/todo_client/pubspec.yaml +++ b/app/over_react_redux/todo_client/pubspec.yaml @@ -24,4 +24,5 @@ dev_dependencies: pedantic: ^1.8.0 test: ^1.9.1 test_html_builder: ^1.0.0 + time: ^1.2.0 w_common: ^1.20.0 diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/app_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/app_test.dart new file mode 100644 index 000000000..347fd88bd --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/components/app_test.dart @@ -0,0 +1,132 @@ +@TestOn('browser') +import 'dart:async'; + +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; +import 'package:over_react_test/over_react_test.dart'; +import 'package:test/test.dart'; +import 'package:time/time.dart'; + +import 'package:todo_client/src/components/app.dart'; +import 'package:todo_client/src/components/create_input.dart'; +import 'package:todo_client/src/components/todo_list.dart'; +import 'package:todo_client/src/components/user_list.dart'; +import 'package:todo_client/src/models/todo.dart'; +import 'package:todo_client/src/models/user.dart'; + +import 'fixtures/utils.dart'; + +main() { + initializeComponentTests(); + + group('ConnectedTodoApp', () { + Ref componentRef; + TodoAppComponent component; + + setUp(() { + initializeTestStore(); + componentRef = createRef(); + mount((ReduxProvider()..store = testStore)( + (ConnectedTodoApp()..ref = componentRef)(), + )); + component = componentRef.current; + expect(component, isNotNull, reason: 'ConnectedTodoApp should forward refs to the child TodoApp'); + }); + + tearDown(() { + componentRef = null; + component = null; + }); + + group('renders a TodoApp', () { + group('with prop callbacks mapped to the expected Redux action:', () { + test('props.createTodo => AddTodoAction', () async { + final todoAddedCompleter = Completer(); + testStore.onChange.listen((newState) { + todoAddedCompleter.complete(newState.todos.first); + }); + component.props.createTodo('some description'); + final todoAdded = await todoAddedCompleter.future.timeout(10.milliseconds); + expect(todoAdded.description, 'some description'); + }); + + test('props.createUser => AddUserAction', () async { + final userAddedCompleter = Completer(); + testStore.onChange.listen((newState) { + userAddedCompleter.complete(newState.users.first); + }); + component.props.createUser('Joe Smith'); + final userAdded = await userAddedCompleter.future.timeout(10.milliseconds); + expect(userAdded.name, 'Joe Smith'); + }); + }); + + test('that does not rerender when AppState is updated', () async { + component.props.createTodo('some description'); + await expectNoRedraws(component); + }); + + group('that renders a CreateInput component for creating Todos', () { + test('', () { + final createInputInstance = getByTestId(component, 'todo_client.createTodoInput'); + expect(createInputInstance, isNotNull); + expect(getDartComponent(createInputInstance), isA()); + + final createInputProps = getDartComponent(createInputInstance).props; + expect(createInputProps.autoFocus, isTrue); + expect(createInputProps.label, 'New Todo'); + expect(createInputProps.placeholder, 'Create new Todo'); + }); + + test('with a props.onCreate value that proxies props.createTodo', () async { + CreateInputComponent createInputComponent = getComponentByTestId(component, 'todo_client.createTodoInput'); + final todoAddedCompleter = Completer(); + testStore.onChange.listen((newState) { + todoAddedCompleter.complete(newState.todos.first); + }); + createInputComponent.props.onCreate('some description'); + final todoAdded = await todoAddedCompleter.future.timeout(10.milliseconds); + expect(todoAdded.description, 'some description'); + }); + }); + + test('that renders a ConnectedTodoList', () { + final connectedTodoListInstance = getByTestId(component, 'todo_client.ConnectedTodoList'); + expect(connectedTodoListInstance, isNotNull); + expect(getDartComponent(connectedTodoListInstance), isA(), + reason: 'ConnectedTodoList should forward refs to the child TodoList'); + }); + + group('that renders a CreateInput component for creating Users', () { + test('', () { + final createInputInstance = getByTestId(component, 'todo_client.createUserInput'); + expect(createInputInstance, isNotNull); + expect(getDartComponent(createInputInstance), isA()); + + final createInputProps = getDartComponent(createInputInstance).props; + expect(createInputProps.autoFocus, isFalse); + expect(createInputProps.label, 'New User'); + expect(createInputProps.placeholder, 'Create new User'); + }); + + test('with a props.onCreate value that proxies props.createUser', () async { + CreateInputComponent createInputComponent = getComponentByTestId(component, 'todo_client.createUserInput'); + final userAddedCompleter = Completer(); + testStore.onChange.listen((newState) { + userAddedCompleter.complete(newState.users.first); + }); + createInputComponent.props.onCreate('Joe Smith'); + final userAdded = await userAddedCompleter.future.timeout(10.milliseconds); + expect(userAdded.name, 'Joe Smith'); + }); + }); + + test('that renders a ConnectedUserList', () { + final connectedUserListInstance = getByTestId(component, 'todo_client.ConnectedUserList'); + expect(connectedUserListInstance, isNotNull); + expect(getDartComponent(connectedUserListInstance), isA(), + reason: 'ConnectedUserList should forward refs to the child UserList'); + }); + }); + }); +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_item_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_item_test.dart new file mode 100644 index 000000000..960eba015 --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_item_test.dart @@ -0,0 +1,175 @@ +import 'dart:async'; + +@TestOn('browser') +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; +import 'package:over_react_test/over_react_test.dart'; +import 'package:test/test.dart'; +import 'package:time/time.dart'; +import 'package:todo_client/src/actions.dart'; + +import 'package:todo_client/src/components/app.dart'; +import 'package:todo_client/src/components/todo_list_item.dart'; +import 'package:todo_client/src/models/todo.dart'; + +import 'fixtures/utils.dart'; + +main() { + initializeComponentTests(); + + group('ConnectedTodoListItem', () { + Ref appRef; + TodoListItemComponent component; + Todo model; + + setUp(() { + initializeTestStore(); + appRef = createRef(); + mount((ReduxProvider()..store = testStore)( + (ConnectedTodoApp()..ref = appRef)(), + )); + final appComponent = appRef.current; + model = testStore.state.todos.first; + component = getComponentByTestId(appComponent, 'todo_client.TodoListItem.${model.id}'); + expect(component, isA(), reason: 'test setup sanity check'); + }); + + tearDown(() { + appRef = null; + component = null; + model = null; + }); + + bool todoShouldAppearSelected() => testStore.state.selectedTodoIds.contains(model.id); + bool todoShouldAppearHighlighted() => testStore.state.highlightedTodoIds.contains(model.id); + bool todoShouldBeEditable() => testStore.state.editableTodoIds.contains(model.id); + + group('renders a TodoListItem', () { + group('with prop callbacks mapped to the expected Redux action', () { + group('involving selection:', () { + Future selectTodo() async { + expect(component.props.isSelected, isFalse, reason: 'test setup sanity check'); + component.props.onSelect(model.id); + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(component.props.isSelected, isTrue); + } + + test('props.onSelect => SelectTodoAction', selectTodo); + + test('props.onDeselect => DeselectTodoAction', () async { + await selectTodo(); + component.redrawCount = 0; + + component.props.onDeselect(model.id); + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(component.props.isSelected, isFalse); + }); + }); + + group('involving editability:', () { + Future makeTodoEditable() async { + expect(component.props.isEditable, isFalse, reason: 'test setup sanity check'); + component.props.onBeginEdit(model.id); + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(component.props.isEditable, isTrue); + } + + test('props.onBeginEdit => BeginEditTodoAction', makeTodoEditable); + + test('props.onFinishEdit => FinishEditTodoAction', () async { + await makeTodoEditable(); + component.redrawCount = 0; + + component.props.onFinishEdit(model.id); + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(component.props.isEditable, isFalse); + }); + }); + + group('involving model CRUD:', () { + test('props.onModelUpdate => UpdateTodoAction', () async { + const newDescription = 'this was not the original description'; + component.props.onModelUpdate(Todo.from(model)..description = newDescription); + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(component.props.model.description, newDescription); + }); + + test('props.onRemove => RemoveTodoAction', () async { + final todoRemovedCompleter = Completer(); + testStore.onChange.listen((newState) { + todoRemovedCompleter.complete(); + }); + component.props.onRemove(model.id); + await todoRemovedCompleter.future.timeout(10.milliseconds); + expect(getByTestId(appRef.current, 'todo_client.TodoListItem.${model.id}'), isNull); + }); + }); + }); + + group('with props.isSelected mapped to AppState.selectedTodoIds', () { + test('initially', () { + expect(component.props.isSelected, todoShouldAppearSelected()); + }); + + test('when AppState.selectedTodoIds is updated', () async { + final wasSelected = todoShouldAppearSelected(); + if (!wasSelected) { + testStore.dispatch(SelectTodoAction(model.id)); + } else { + testStore.dispatch(DeselectTodoAction(model.id)); + } + + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(wasSelected, !todoShouldAppearSelected()); + expect(component.props.isSelected, todoShouldAppearSelected()); + }); + }); + + group('with props.isEditable mapped to AppState.editableTodoIds', () { + test('initially', () { + expect(component.props.isEditable, todoShouldBeEditable()); + }); + + test('when AppState.selectedTodoIds is updated', () async { + final wasEditable = todoShouldBeEditable(); + if (!wasEditable) { + testStore.dispatch(BeginEditTodoAction(model.id)); + } else { + testStore.dispatch(FinishEditTodoAction(model.id)); + } + + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(wasEditable, !todoShouldBeEditable()); + expect(component.props.isEditable, todoShouldBeEditable()); + }); + }); + + group('with props.isHighlighted mapped to AppState.editableTodoIds', () { + test('initially', () { + expect(component.props.isHighlighted, todoShouldAppearHighlighted()); + }); + + test('when AppState.selectedTodoIds is updated', () async { + final wasHighlighted = todoShouldAppearHighlighted(); + if (!wasHighlighted) { + testStore.dispatch(HighlightTodosAction([model.id])); + } else { + testStore.dispatch(UnHighlightTodosAction([model.id])); + } + + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(wasHighlighted, !todoShouldAppearHighlighted()); + expect(component.props.isHighlighted, todoShouldAppearHighlighted()); + }); + }); + }); + }); +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_test.dart new file mode 100644 index 000000000..3f8f765a4 --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_test.dart @@ -0,0 +1,95 @@ +@TestOn('browser') +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; +import 'package:over_react_test/over_react_test.dart'; +import 'package:test/test.dart'; +import 'package:time/time.dart'; +import 'package:todo_client/src/actions.dart'; + +import 'package:todo_client/src/components/app.dart'; +import 'package:todo_client/src/components/shared/display_list.dart'; +import 'package:todo_client/src/components/shared/empty_view.dart'; +import 'package:todo_client/src/components/todo_list.dart'; +import 'package:todo_client/src/components/todo_list_item.dart'; +import 'package:todo_client/src/local_storage.dart'; +import 'package:todo_client/src/models/todo.dart'; + +import 'fixtures/utils.dart'; + +main() { + initializeComponentTests(); + + group('ConnectedTodoList', () { + Ref appRef; + TodoListComponent component; + TestJacket jacket; + + setUp(() { + initializeTestStore(); + appRef = createRef(); + jacket = mount((ReduxProvider()..store = testStore)( + (ConnectedTodoApp()..ref = appRef)(), + )); + final appComponent = appRef.current; + component = getComponentByTestId(appComponent, 'todo_client.ConnectedTodoList'); + expect(component, isA(), reason: 'test setup sanity check'); + }); + + tearDown(() { + appRef = null; + component = null; + jacket = null; + }); + + group('renders a TodoList', () { + group('with props.todos mapped to AppState.todos', () { + test('initially', () { + expect(component.props.todos, testStore.state.todos); + }); + + test('when AppState.todos is updated', () async { + final newTodo = Todo(description: 'foo'); + testStore.dispatch(AddTodoAction(newTodo)); + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(component.props.todos.first.toJson(), newTodo.toJson()); + expect(redrawCount, 1); + }); + }); + + group('with ConnectedTodoListItems for each todo in the model within a DisplayList', () { + dynamic displayListInstance; + setUp(() { + displayListInstance = getByTestId(component, 'todo_client.TodoList.DisplayList'); + expect(displayListInstance, isNotNull); + expect(getDartComponent(displayListInstance), isA()); + expect(component.props.todos, isNotEmpty, reason: 'test setup sanity check'); + }); + + tearDown(() { + displayListInstance = null; + }); + + test('', () { + for (var todo in component.props.todos) { + final listItemComponent = getComponentByTestId(displayListInstance, 'todo_client.TodoListItem.${todo.id}'); + expect(listItemComponent, isA()); + TodoListItemProps props = listItemComponent.props; + expect(props.model, todo); + } + }); + + test('unless props.todos is empty', () { + initializeTestStore(TodoAppLocalStorage.emptyState); + jacket.rerender((ReduxProvider()..store = testStore)( + (ConnectedTodoApp()..ref = appRef)(), + )); + expect(testStore.state.todos, isEmpty, reason: 'test setup sanity check'); + + expect(findDomNode(displayListInstance).children, hasLength(1)); + expect(getComponentByTestId(displayListInstance, 'todo_client.DisplayList.EmptyView'), + isA()); + }); + }); + }); + }); +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_item_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_item_test.dart new file mode 100644 index 000000000..f6271e38a --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_item_test.dart @@ -0,0 +1,175 @@ +import 'dart:async'; + +@TestOn('browser') +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; +import 'package:over_react_test/over_react_test.dart'; +import 'package:test/test.dart'; +import 'package:time/time.dart'; +import 'package:todo_client/src/actions.dart'; + +import 'package:todo_client/src/components/app.dart'; +import 'package:todo_client/src/components/user_list_item.dart'; +import 'package:todo_client/src/models/user.dart'; + +import 'fixtures/utils.dart'; + +main() { + initializeComponentTests(); + + group('ConnectedUserListItem', () { + Ref appRef; + UserListItemComponent component; + User model; + + setUp(() { + initializeTestStore(); + appRef = createRef(); + mount((ReduxProvider()..store = testStore)( + (ConnectedTodoApp()..ref = appRef)(), + )); + final appComponent = appRef.current; + model = testStore.state.users.first; + component = getComponentByTestId(appComponent, 'todo_client.UserListItem.${model.id}'); + expect(component, isA(), reason: 'test setup sanity check'); + }); + + tearDown(() { + appRef = null; + component = null; + model = null; + }); + + bool userShouldAppearSelected() => testStore.state.selectedUserIds.contains(model.id); + bool userShouldAppearHighlighted() => testStore.state.highlightedUserIds.contains(model.id); + bool userShouldBeEditable() => testStore.state.editableUserIds.contains(model.id); + + group('renders a UserListItem', () { + group('with prop callbacks mapped to the expected Redux action', () { + group('involving selection:', () { + Future selectUser() async { + expect(component.props.isSelected, isFalse, reason: 'test setup sanity check'); + component.props.onSelect(model.id); + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(component.props.isSelected, isTrue); + } + + test('props.onSelect => SelectUserAction', selectUser); + + test('props.onDeselect => DeselectUserAction', () async { + await selectUser(); + component.redrawCount = 0; + + component.props.onDeselect(model.id); + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(component.props.isSelected, isFalse); + }); + }); + + group('involving editability:', () { + Future makeUserEditable() async { + expect(component.props.isEditable, isFalse, reason: 'test setup sanity check'); + component.props.onBeginEdit(model.id); + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(component.props.isEditable, isTrue); + } + + test('props.onBeginEdit => BeginEditUserAction', makeUserEditable); + + test('props.onFinishEdit => FinishEditUserAction', () async { + await makeUserEditable(); + component.redrawCount = 0; + + component.props.onFinishEdit(model.id); + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(component.props.isEditable, isFalse); + }); + }); + + group('involving model CRUD:', () { + test('props.onModelUpdate => UpdateUserAction', () async { + const newName = 'Something Else'; + component.props.onModelUpdate(User.from(model)..name = newName); + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(component.props.model.name, newName); + }); + + test('props.onRemove => RemoveUserAction', () async { + final todoRemovedCompleter = Completer(); + testStore.onChange.listen((newState) { + todoRemovedCompleter.complete(); + }); + component.props.onRemove(model.id); + await todoRemovedCompleter.future.timeout(10.milliseconds); + expect(getByTestId(appRef.current, 'todo_client.UserListItem.${model.id}'), isNull); + }); + }); + }); + + group('with props.isSelected mapped to AppState.selectedUserIds', () { + test('initially', () { + expect(component.props.isSelected, userShouldAppearSelected()); + }); + + test('when AppState.selectedUserIds is updated', () async { + final wasSelected = userShouldAppearSelected(); + if (!wasSelected) { + testStore.dispatch(SelectUserAction(model.id)); + } else { + testStore.dispatch(DeselectUserAction(model.id)); + } + + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(wasSelected, !userShouldAppearSelected()); + expect(component.props.isSelected, userShouldAppearSelected()); + }); + }); + + group('with props.isEditable mapped to AppState.editableUserIds', () { + test('initially', () { + expect(component.props.isEditable, userShouldBeEditable()); + }); + + test('when AppState.selectedUserIds is updated', () async { + final wasEditable = userShouldBeEditable(); + if (!wasEditable) { + testStore.dispatch(BeginEditUserAction(model.id)); + } else { + testStore.dispatch(FinishEditUserAction(model.id)); + } + + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(wasEditable, !userShouldBeEditable()); + expect(component.props.isEditable, userShouldBeEditable()); + }); + }); + + group('with props.isHighlighted mapped to AppState.editableUserIds', () { + test('initially', () { + expect(component.props.isHighlighted, userShouldAppearHighlighted()); + }); + + test('when AppState.selectedUserIds is updated', () async { + final wasHighlighted = userShouldAppearHighlighted(); + if (!wasHighlighted) { + testStore.dispatch(HighlightUsersAction([model.id])); + } else { + testStore.dispatch(UnHighlightUsersAction([model.id])); + } + + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(redrawCount, 1); + expect(wasHighlighted, !userShouldAppearHighlighted()); + expect(component.props.isHighlighted, userShouldAppearHighlighted()); + }); + }); + }); + }); +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_test.dart new file mode 100644 index 000000000..37c9fc549 --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_test.dart @@ -0,0 +1,95 @@ +@TestOn('browser') +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; +import 'package:over_react_test/over_react_test.dart'; +import 'package:test/test.dart'; +import 'package:time/time.dart'; +import 'package:todo_client/src/actions.dart'; + +import 'package:todo_client/src/components/app.dart'; +import 'package:todo_client/src/components/shared/display_list.dart'; +import 'package:todo_client/src/components/shared/empty_view.dart'; +import 'package:todo_client/src/components/user_list.dart'; +import 'package:todo_client/src/components/user_list_item.dart'; +import 'package:todo_client/src/local_storage.dart'; +import 'package:todo_client/src/models/user.dart'; + +import 'fixtures/utils.dart'; + +main() { + initializeComponentTests(); + + group('ConnectedUserList', () { + Ref appRef; + UserListComponent component; + TestJacket jacket; + + setUp(() { + initializeTestStore(); + appRef = createRef(); + jacket = mount((ReduxProvider()..store = testStore)( + (ConnectedTodoApp()..ref = appRef)(), + )); + final appComponent = appRef.current; + component = getComponentByTestId(appComponent, 'todo_client.ConnectedUserList'); + expect(component, isA(), reason: 'test setup sanity check'); + }); + + tearDown(() { + appRef = null; + component = null; + jacket = null; + }); + + group('renders a UserList', () { + group('with props.users mapped to AppState.users', () { + test('initially', () { + expect(component.props.users, testStore.state.users); + }); + + test('when AppState.users is updated', () async { + final newUser = User(name: 'foo'); + testStore.dispatch(AddUserAction(newUser)); + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds); + expect(component.props.users.first.toJson(), newUser.toJson()); + expect(redrawCount, 1); + }); + }); + + group('with ConnectedUserListItems for each todo in the model within a DisplayList', () { + dynamic displayListInstance; + setUp(() { + displayListInstance = getByTestId(component, 'todo_client.UserList.DisplayList'); + expect(displayListInstance, isNotNull); + expect(getDartComponent(displayListInstance), isA()); + expect(component.props.users, isNotEmpty, reason: 'test setup sanity check'); + }); + + tearDown(() { + displayListInstance = null; + }); + + test('', () { + for (var user in component.props.users) { + final listItemComponent = getComponentByTestId(displayListInstance, 'todo_client.UserListItem.${user.id}'); + expect(listItemComponent, isA()); + UserListItemProps props = listItemComponent.props; + expect(props.model, user); + } + }); + + test('unless props.users is empty', () { + initializeTestStore(TodoAppLocalStorage.emptyState); + jacket.rerender((ReduxProvider()..store = testStore)( + (ConnectedTodoApp()..ref = appRef)(), + )); + expect(testStore.state.users, isEmpty, reason: 'test setup sanity check'); + + expect(findDomNode(displayListInstance).children, hasLength(1)); + expect(getComponentByTestId(displayListInstance, 'todo_client.DisplayList.EmptyView'), + isA()); + }); + }); + }); + }); +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/create_input_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/create_input_test.dart new file mode 100644 index 000000000..66513560e --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/components/create_input_test.dart @@ -0,0 +1,57 @@ +@TestOn('browser') +import 'dart:async'; +import 'dart:html'; + +import 'package:over_react/over_react.dart'; +import 'package:over_react_test/over_react_test.dart'; +import 'package:test/test.dart'; + +import 'package:todo_client/src/components/create_input.dart'; + +import 'fixtures/utils.dart'; + +main() { + initializeComponentTests(); + + group('CreateInput', () { + Ref textFieldComponentRef; + TestJacket jacket; + Completer onCreateCompleter; + + setUp(() { + textFieldComponentRef = createRef(); + onCreateCompleter = Completer(); + }); + + tearDown(() { + jacket = null; + textFieldComponentRef = null; + onCreateCompleter = null; + }); + + group('renders a Mui TextField component', () { + setUp(() { + jacket = mount((CreateInput() + ..ref = textFieldComponentRef + ..onCreate = (value) { + onCreateCompleter.complete(value); + } + )()); + }); + + test('', () { + expect(jacket.getDartInstance().textFieldRef, isNotNull); + expect(jacket.getDartInstance().textFieldRef.current, isA()); + }); + + test('that calls props.onCreate when the ENTER key is pressed', () async { + InputElement inputElement = jacket.getDartInstance().textFieldRef.current; + inputElement.focus(); + inputElement.value = ' and something else '; + keyDown(inputElement, {'keyCode': KeyCode.ENTER}); + final onCreateValue = await onCreateCompleter.future; + expect(onCreateValue, 'and something else'); + }); + }); + }); +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/display_list_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/display_list_test.dart new file mode 100644 index 000000000..a4df53186 --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/components/display_list_test.dart @@ -0,0 +1,52 @@ +@TestOn('browser') +import 'dart:html'; + +import 'package:over_react/over_react.dart'; +import 'package:over_react_test/over_react_test.dart'; +import 'package:test/test.dart'; + +import 'package:todo_client/src/components/shared/display_list.dart'; +import 'package:todo_client/src/components/shared/empty_view.dart'; + +import 'fixtures/utils.dart'; + +main() { + initializeComponentTests(); + + group('DisplayList', () { + TestJacket jacket; + + tearDown(() { + jacket = null; + }); + + test('renders an EmptyView when props.children is empty', () { + jacket = mount((DisplayList() + ..listItemTypeDescription = 'Foo' + )()); + + final emptyListInstance = getByTestId(jacket.getInstance(), 'todo_client.DisplayList.EmptyView'); + expect(emptyListInstance, isNotNull); + expect(getDartComponent(emptyListInstance), isA()); + + EmptyViewProps emptyViewProps = getDartComponent(emptyListInstance).props; + expect(emptyViewProps.type, EmptyViewType.VBLOCK); + expect(emptyViewProps.header, 'No ${jacket.getDartInstance().props.listItemTypeDescription} to show'); + expect(emptyViewProps.glyph, isNotNull); + expect(emptyViewProps.children, ['You should totally create one!']); + }); + + test('renders a scrolling Box containing children when props.children is not empty', () { + jacket = mount((DisplayList() + ..listItemTypeDescription = 'Foo' + )('not empty!')); + + final emptyListInstance = getByTestId(jacket.getInstance(), 'todo_client.DisplayList.EmptyView'); + expect(emptyListInstance, isNull); + + Element box = jacket.getDartInstance().scrollingBoxRef.current; + expect(box.style.overflowY, 'auto'); + expect(box.text, 'not empty!'); + }); + }); +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/fixtures/utils.dart b/app/over_react_redux/todo_client/test/unit/browser/components/fixtures/utils.dart index 380338c67..a09f373de 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/components/fixtures/utils.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/components/fixtures/utils.dart @@ -1,6 +1,12 @@ import 'dart:js'; +import 'package:over_react/over_react.dart'; +import 'package:react/react_client/react_interop.dart'; import 'package:test/test.dart'; +import 'package:time/time.dart'; +import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; + +export '../../fixtures/utils.dart'; bool muiJsIsAvailable() { // Skip these tests if the JS file isn't loaded (e.g. if you're running tests without an internet connection, etc) @@ -11,3 +17,22 @@ bool muiJsIsAvailable() { } return true; } + +void initializeComponentTests() { + setClientConfiguration(); + enableTestMode(); + if (!muiJsIsAvailable()) return; +} + +JsBackedMap getJsProps(Ref ref) { + if (ref.jsRef == null) { + throw ArgumentError('There is no js component found within this Ref'); + } + + return JsBackedMap.fromJs(ref.jsRef.current.props); +} + +Future expectNoRedraws(RedrawCounterMixin component) async { + final redrawCount = await component.didRedraw().future.timeout(20.milliseconds, onTimeout: () => 0); + expect(redrawCount, 0); +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/todo_list_item_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/todo_list_item_test.dart new file mode 100644 index 000000000..087344edc --- /dev/null +++ b/app/over_react_redux/todo_client/test/unit/browser/components/todo_list_item_test.dart @@ -0,0 +1,292 @@ +import 'dart:async'; +import 'dart:html'; + +@TestOn('browser') +import 'package:over_react/over_react.dart'; +import 'package:over_react/over_react_redux.dart'; +import 'package:over_react_test/over_react_test.dart'; +import 'package:test/test.dart'; +import 'package:time/time.dart'; +import 'package:todo_client/src/actions.dart'; + +import 'package:todo_client/src/components/app.dart'; +import 'package:todo_client/src/components/shared/todo_item_text_field.dart'; +import 'package:todo_client/src/components/todo_list_item.dart'; +import 'package:todo_client/src/components/user_selector.dart'; +import 'package:todo_client/src/models/todo.dart'; +import 'package:todo_client/src/models/user.dart'; + +import 'fixtures/utils.dart'; + +main() { + initializeComponentTests(); + + group('TodoListItem', () { + Ref appRef; + TodoListItemComponent component; + Todo model; + const localModelSynchronizationTimingReason = + 'state.localModel should only be synchronized with persisted state ' + 'as a result of the component entering an editable state'; + const modelGetterReturnsPersistedStateReason = + 'When props.isEditable is false, the component\'s model getter should ' + 'return the value of props.model instead of state.localModel.'; + + setUp(() { + initializeTestStore(); + appRef = createRef(); + mount((ReduxProvider()..store = testStore)( + (ConnectedTodoApp()..ref = appRef)(), + )); + final appComponent = appRef.current; + model = testStore.state.todos.first; + component = getComponentByTestId(appComponent, 'todo_client.TodoListItem.${model.id}'); + expect(component, isA(), reason: 'test setup sanity check'); + }); + + tearDown(() { + appRef = null; + component = null; + model = null; + }); + + Future enterEditable() async { + expect(component.props.isEditable, isFalse, reason: 'test setup sanity check'); + component.redrawCount = 0; + // ignore: invalid_use_of_protected_member + component.enterEditable(); + await component.didRedraw().future.timeout(10.milliseconds); + expect(component.props.isEditable, isTrue, reason: 'test setup sanity check'); + } + + Future exitEditable({bool saveChanges = true}) async { + expect(component.props.isEditable, isTrue, reason: 'test setup sanity check'); + component.redrawCount = 0; + // ignore: invalid_use_of_protected_member + component.exitEditable(saveChanges: saveChanges); + await component.didRedraw().future.timeout(10.milliseconds); + expect(component.props.isEditable, isFalse, reason: 'test setup sanity check'); + } + + group('renders', () { + group('a description text field', () { + TodoItemTextFieldComponent descriptionTextFieldComponent() { + final textFieldComponent = getComponentByTestId(component, 'todo_client.TodoListItem.descriptionTextField'); + expect(textFieldComponent, isA(), reason: 'test setup sanity check'); + return textFieldComponent; + } + InputElement descriptionTextFieldNode() => descriptionTextFieldComponent().textFieldRef.current; + + group('with its props set as expected', () { + test('', () { + expect(descriptionTextFieldComponent().props.label, 'Description'); + expect(descriptionTextFieldComponent().props.placeholder, 'Describe the task...'); + expect(descriptionTextFieldComponent().props.value, model.description); + }); + + group('based on props.isEditable', () { + setUp(() { + expect(component.props.isEditable, isFalse, reason: 'test setup sanity check'); + }); + + test('initially', () { + expect(descriptionTextFieldComponent().props.readOnly, isTrue); + expect(descriptionTextFieldComponent().props.autoFocus, isFalse); + expect(descriptionTextFieldComponent().props['inputProps']['style'], isNotNull); + }); + + test('when props.isEditable is updated', () async { + await enterEditable(); + expect(descriptionTextFieldComponent().props.readOnly, isFalse); + expect(descriptionTextFieldComponent().props.autoFocus, isTrue); + expect(descriptionTextFieldComponent().props['inputProps']['style'], isNull); + }); + }); + }); + + group('with a props.onChange callback that updates', () { + const String newDescription = 'Something different entirely that was not the description before'; + + setUp(() { + expect(component.model.description, isNot(newDescription), reason: 'test setup sanity check'); + }); + + group('state.localModel when props.isEditable is true', () { + Future storeDescriptionUpdateLocally() async { + await enterEditable(); + descriptionTextFieldNode().value = newDescription; + change(descriptionTextFieldNode()); + } + + test('', () async { + await storeDescriptionUpdateLocally(); + + expect(descriptionTextFieldComponent().props.value, newDescription); + expect(component.state.localModel.description, newDescription); + expect(component.props.model.description, isNot(newDescription), + reason: 'The change should not have been persisted yet'); + }); + + test('- and then synchronizes props.model with state.localModel' + 'when exitEditable() is called', () async { + await storeDescriptionUpdateLocally(); + await exitEditable(); + expect(descriptionTextFieldComponent().props.value, newDescription); + expect(component.props.model.description, newDescription); + }); + }); + }); + }); + + group('a notes text field', () { + TodoItemTextFieldComponent notesTextFieldComponent() { + final textFieldComponent = getComponentByTestId(component, 'todo_client.TodoListItem.notesTextField'); + expect(textFieldComponent, isA(), reason: 'test setup sanity check'); + return textFieldComponent; + } + TextAreaElement notesTextFieldNode() => notesTextFieldComponent().textFieldRef.current; + + group('with its props set as expected', () { + test('', () { + expect(notesTextFieldComponent().props.label, 'Notes'); + expect(notesTextFieldComponent().props.placeholder, 'Add some notes about the task'); + expect(notesTextFieldComponent().props.value, model.notes); + expect(notesTextFieldComponent().props.multiline, isTrue); + expect(notesTextFieldComponent().props['rows'], 3); + }); + + group('based on props.isEditable', () { + setUp(() { + expect(component.props.isEditable, isFalse, reason: 'test setup sanity check'); + }); + + test('initially', () { + expect(notesTextFieldComponent().props.readOnly, isTrue); + }); + + test('when props.isEditable is updated', () async { + await enterEditable(); + expect(notesTextFieldComponent().props.readOnly, isFalse); + }); + }); + }); + + group('with a props.onChange callback that updates', () { + const String newNotes = 'Something different entirely that was not the notes before'; + + setUp(() { + expect(component.model.notes, isNot(newNotes), reason: 'test setup sanity check'); + }); + + group('state.localModel when props.isEditable is true', () { + Future storeDescriptionUpdateLocally() async { + await enterEditable(); + notesTextFieldNode().value = newNotes; + change(notesTextFieldNode()); + } + + test('', () async { + await storeDescriptionUpdateLocally(); + + expect(notesTextFieldComponent().props.value, newNotes); + expect(component.state.localModel.notes, newNotes); + expect(component.props.model.notes, isNot(newNotes), + reason: 'The change should not have been persisted yet'); + }); + + test('- and then synchronizes props.model with state.localModel' + 'when exitEditable() is called', () async { + await storeDescriptionUpdateLocally(); + await exitEditable(); + expect(notesTextFieldComponent().props.value, newNotes); + expect(component.props.model.notes, newNotes); + }); + }); + }); + }); + + group('a ConnectedUserSelector', () { + UserSelectorProps connectedUserSelectorProps() { + return UserSelector(getProps(getByTestId(component, 'todo_client.TodoListItem.ConnectedUserSelector'))); + } + + group('with props.model.assignedUserId forwarded to props.selectedUserId', () { + test('initially', () { + expect(connectedUserSelectorProps().selectedUserId, component.props.model.assignedUserId); + }); + + test('when props.assignedUserId is updated', () async { + final newAssignedUser = testStore.state.users[1]; + expect(component.props.model.assignedUserId, isNot(newAssignedUser.id), reason: 'test setup sanity check'); + + testStore.dispatch(UpdateTodoAction(Todo.from(component.props.model)..assignedUserId = newAssignedUser.id)); + await component.didRedraw().future.timeout(10.milliseconds); + expect(component.props.model.assignedUserId, newAssignedUser.id); + expect(connectedUserSelectorProps().selectedUserId, component.props.model.assignedUserId); + }); + }); + + group('with a props.onUserSelect callback that updates', () { + User newAssignedUser; + + group('the persisted model data immediately when props.isEditable is false', () { + Future persistSelectedUpdate() async { + newAssignedUser = testStore.state.users[1]; + expect(component.model.assignedUserId, isNot(newAssignedUser.id), reason: 'test setup sanity check'); + expect(component.props.isEditable, isFalse, reason: 'test setup sanity check'); + + connectedUserSelectorProps().onUserSelect(newAssignedUser.id); + await component.didRedraw().future.timeout(10.milliseconds); + } + + test('', () async { + await persistSelectedUpdate(); + + expect(connectedUserSelectorProps().selectedUserId, newAssignedUser.id); + expect(component.state.localModel.assignedUserId, isNot(component.props.model.assignedUserId), + reason: localModelSynchronizationTimingReason); + expect(component.model.assignedUserId, component.props.model.assignedUserId, + reason: modelGetterReturnsPersistedStateReason); + }); + + test('- and then synchronizes props.model with state.localModel' + 'when enterEditable() is called', () async { + await persistSelectedUpdate(); + await enterEditable(); + expect(connectedUserSelectorProps().selectedUserId, newAssignedUser.id); + expect(component.state.localModel.assignedUserId, component.props.model.assignedUserId); + }); + }); + + group('state.localModel when props.isEditable is true', () { + Future storeSelectedUpdateLocally() async { + newAssignedUser = testStore.state.users[1]; + await enterEditable(); + expect(component.model.assignedUserId, isNot(newAssignedUser.id), reason: 'test setup sanity check'); + connectedUserSelectorProps().onUserSelect(newAssignedUser.id); + } + + test('', () async { + await storeSelectedUpdateLocally(); + + expect(connectedUserSelectorProps().selectedUserId, newAssignedUser.id); + expect(component.state.localModel.assignedUserId, newAssignedUser.id); + expect(component.props.model.assignedUserId, isNot(newAssignedUser.id), + reason: 'The change should not have been persisted yet'); + }); + + test('- and then synchronizes props.model with state.localModel' + 'when exitEditable() is called', () async { + await storeSelectedUpdateLocally(); + await exitEditable(); + expect(connectedUserSelectorProps().selectedUserId, newAssignedUser.id); + expect(component.props.model.assignedUserId, newAssignedUser.id); + }); + }); + }); + }); + + // TODO: Add tests for cancel / save button, and the footer area itself that contains them that is only visible when the item is editable + }); + }); +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/fixtures/utils.dart b/app/over_react_redux/todo_client/test/unit/browser/fixtures/utils.dart index 2cda72642..4f37ea1a4 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/fixtures/utils.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/fixtures/utils.dart @@ -1,20 +1,41 @@ +import 'dart:convert'; import 'dart:html'; import 'package:redux/redux.dart'; import 'package:test/test.dart'; import 'package:todo_client/src/local_storage.dart'; +import 'package:todo_client/src/models/base_model.dart'; import 'package:todo_client/src/store.dart'; Store testStore; -Store initializeTestStore() { +Store initializeTestStore([AppState initialState]) { addTearDown(() { testStore = null; localTodoAppStorage = null; window.localStorage[TodoAppLocalStorage.localStorageKey] = ''; }); + if (initialState != null) { + localTodoAppStorage = TodoAppLocalStorage(initialState); + expect(getCurrentLocalStorageSet(), initialState.toJson(), reason: 'test setup sanity check'); + } + return testStore = Store( appStateReducer, initialState: initializeState(), ); } + +String getLocalStorage() => window.localStorage[TodoAppLocalStorage.localStorageKey]; + +Map getLocalStorageSetByKey(String key) => json.decode(getLocalStorage())[key]; + +Map getCurrentLocalStorageSet() => getLocalStorageSetByKey('current'); + +Iterable> getSerializedListOfModels(List models) { + return models.map((model) => model.toJson()); +} + +bool itemShouldAppearSelected(BaseModel model, List listOfIds) { + return listOfIds.contains(model.id); +} diff --git a/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart b/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart index 741d1f1c0..b51463dad 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart @@ -1,13 +1,11 @@ @TestOn('browser') import 'dart:convert'; -import 'dart:html'; import 'package:over_react/over_react.dart'; import 'package:test/test.dart'; import 'package:todo_client/src/actions.dart'; import 'package:todo_client/src/local_storage.dart'; -import 'package:todo_client/src/models/base_model.dart'; import 'package:todo_client/src/models/todo.dart'; import 'package:todo_client/src/models/user.dart'; import 'package:todo_client/src/store.dart'; @@ -19,16 +17,6 @@ main() { const reasonCurrentSetShouldBePersisted = 'The state update should be persisted as the "current" set in window.localStorage'; - String getLocalStorage() => window.localStorage[TodoAppLocalStorage.localStorageKey]; - - Map getLocalStorageSetByKey(String key) => json.decode(getLocalStorage())[key]; - - Map getCurrentLocalStorageSet() => getLocalStorageSetByKey('current'); - - Iterable> getSerializedListOfModels(List models) { - return models.map((model) => model.toJson()); - } - group('AppState', () { setUp(() { expect(TodoAppLocalStorage.isInitialized(), isFalse, reason: 'test setup sanity check'); @@ -77,11 +65,7 @@ main() { setUp(() { expect(getCurrentLocalStorageSet(), defaultAppState, reason: 'test setup sanity check'); - localTodoAppStorage = TodoAppLocalStorage(TodoAppLocalStorage.emptyState); - expect(getCurrentLocalStorageSet(), TodoAppLocalStorage.emptyState.toJson(), - reason: 'test setup sanity check'); - - initializeTestStore(); + initializeTestStore(TodoAppLocalStorage.emptyState); }); test('', () { From fd5b04bc6d4f12c9cd1166732ddb15be11c29f04 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Wed, 15 Jan 2020 06:29:03 -0700 Subject: [PATCH 28/34] Update generated file + Apparently this was added in built_redux 7.5.9 --- .../redux_component_test/test_reducer.g.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/test/over_react/component_declaration/redux_component_test/test_reducer.g.dart b/test/over_react/component_declaration/redux_component_test/test_reducer.g.dart index 30a785985..1813d772f 100644 --- a/test/over_react/component_declaration/redux_component_test/test_reducer.g.dart +++ b/test/over_react/component_declaration/redux_component_test/test_reducer.g.dart @@ -9,6 +9,7 @@ part of over_react.component_declaration.redux_component.reducer; // ignore_for_file: avoid_classes_with_only_static_members // ignore_for_file: annotate_overrides // ignore_for_file: overridden_fields +// ignore_for_file: type_annotate_public_apis class _$BaseActions extends BaseActions { factory _$BaseActions() => new _$BaseActions._(); From 00a1cd009b7e9257ec174b70dcbbeec7aaa09a47 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Thu, 16 Jan 2020 13:47:48 -0700 Subject: [PATCH 29/34] Move local storage side manipulation into middleware to make reducer pure --- .../todo_client/lib/src/actions.dart | 5 ++ .../todo_client/lib/src/store.dart | 59 ++++++++++++------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/actions.dart b/app/over_react_redux/todo_client/lib/src/actions.dart index 6cecfb67f..db53e1e0e 100644 --- a/app/over_react_redux/todo_client/lib/src/actions.dart +++ b/app/over_react_redux/todo_client/lib/src/actions.dart @@ -2,6 +2,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:todo_client/src/models/todo.dart'; import 'package:todo_client/src/models/user.dart'; +import 'package:todo_client/src/store.dart'; part 'actions.g.dart'; @@ -35,6 +36,10 @@ class SaveLocalStorageStateAsAction extends Action { + LocalStorageStateLoadedAction(AppState value) : super(type: 'LOCAL_STORAGE_STATE_LOADED', value: value); +} + // ------------ ITEM ACTIONS ------------------ class SelectTodoAction extends Action { diff --git a/app/over_react_redux/todo_client/lib/src/store.dart b/app/over_react_redux/todo_client/lib/src/store.dart index 691c8619d..4d1fac114 100644 --- a/app/over_react_redux/todo_client/lib/src/store.dart +++ b/app/over_react_redux/todo_client/lib/src/store.dart @@ -29,7 +29,10 @@ AppState initializeState() { DevToolsStore getStore() => DevToolsStore( appStateReducer, initialState: initializeState(), - middleware: [overReactReduxDevToolsMiddleware], + middleware: [ + overReactReduxDevToolsMiddleware, + localStorageMiddleware(), + ], ); @JsonSerializable(explicitToJson: true) @@ -53,10 +56,7 @@ class AppState { this.selectedUserIds, this.editableUserIds, this.highlightedUserIds, - }) { - assert(name != null && name.isNotEmpty); - localTodoAppStorage?.updateCurrentState(this); - } + }) : assert(name != null && name.isNotEmpty); factory AppState.fromJson(Map json) => _$AppStateFromJson(json); Map toJson() => _$AppStateToJson(this); @@ -64,24 +64,11 @@ class AppState { @visibleForTesting AppState appStateReducer(AppState state, dynamic action) { - var stateName = localTodoAppStorage.currentStateJson['name']; - - if (action is SaveLocalStorageStateAsAction) { - if (action.value.previousName != null && action.value.previousName == action.value.name) { - // Overwrite - localTodoAppStorage.remove(action.value.previousName); - } - - stateName = action.value.name; - localTodoAppStorage[stateName] = - (AppState.fromJson(localTodoAppStorage.currentStateJson)..name = stateName).toJson(); + if (action is LocalStorageStateLoadedAction) { + return action.value; } - if (action is LoadStateFromLocalStorageAction) { - return AppState.fromJson(localTodoAppStorage[action.value]); - } - - return AppState(stateName, + return AppState(stateNameReducer(state.name, action), todos: todosReducer(state.todos, action), users: usersReducer(state.users, action), editableTodoIds: editableTodosReducer(state.editableTodoIds, action), @@ -93,8 +80,38 @@ AppState appStateReducer(AppState state, dynamic action) { ); } + +// todo inject localTodoAppStorage as an arg instead of using a global variable +Middleware localStorageMiddleware() { + return (store, action, next) { + next(action); + + if (action is LoadStateFromLocalStorageAction) { + final localStorageState = AppState.fromJson(localTodoAppStorage[action.value]); + store.dispatch(LocalStorageStateLoadedAction(localStorageState)); + } else if (action is SaveLocalStorageStateAsAction) { + if (action.value.previousName != null) { + // This is a rename; remove the old entry + localTodoAppStorage.remove(action.value.previousName); + } + + final stateName = action.value.name; + // Run the reducer here so that the name is updated in response to the + // current action before saving. + final stateWithUpdatedName = store.reducer(store.state, action); + localTodoAppStorage[stateName] = stateWithUpdatedName.toJson(); + } else { + localTodoAppStorage?.updateCurrentState(store.state); + } + }; +} + // ------------ ITEM REDUCERS ------------------ +final stateNameReducer = TypedReducer((name, action) { + return action.value.name; +}); + final todosReducer = combineReducers>([ TypedReducer, AddTodoAction>((todos, action) { return [action.value, ...todos]; From 73f25385173e50a07b1d80eb6d5ab7baef944947 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Thu, 16 Jan 2020 14:45:28 -0700 Subject: [PATCH 30/34] Remove unused type from actions, simplify JSON representation and payloads --- .../todo_client/lib/src/actions.dart | 113 +++++++++--------- .../todo_client/lib/src/actions.g.dart | 8 +- .../app_bar/app_bar_local_storage_menu.dart | 5 +- .../todo_client/lib/src/store.dart | 11 +- .../test/unit/browser/redux/store_test.dart | 18 ++- 5 files changed, 77 insertions(+), 78 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/actions.dart b/app/over_react_redux/todo_client/lib/src/actions.dart index db53e1e0e..7b3daa680 100644 --- a/app/over_react_redux/todo_client/lib/src/actions.dart +++ b/app/over_react_redux/todo_client/lib/src/actions.dart @@ -6,112 +6,115 @@ import 'package:todo_client/src/store.dart'; part 'actions.g.dart'; -class Action { - Action({this.type, this.value}); +class _Action { + _Action(this.value); - final String type; final T value; - Map toJson() { - return {'value': this.value}; + dynamic toJson() { + // JSON-encodable objects need to be either nested in a JSON primitive (map/list) + // or returned as a primitive. + // Try calling `toJson`. + try { + return (value as dynamic).toJson(); + } catch (_) {} + + // Otherwise, assume it's a JSON primitive + return value; } } -class LoadStateFromLocalStorageAction extends Action { - LoadStateFromLocalStorageAction([String value]) : super(type: 'LOAD_STATE_FROM_LOCAL_STORAGE', value: value); +class LoadStateFromLocalStorageAction extends _Action { + LoadStateFromLocalStorageAction(String value) : super(value); +} + +class LocalStorageStateLoadedAction extends _Action { + LocalStorageStateLoadedAction(AppState value) : super(value); } @JsonSerializable() -class SaveLocalStorageStateAsPayload { +class SaveLocalStorageStateAsAction { final String name; final String previousName; - SaveLocalStorageStateAsPayload(this.name, {this.previousName}); - - factory SaveLocalStorageStateAsPayload.fromJson(Map json) => _$SaveLocalStorageStateAsPayloadFromJson(json); - Map toJson() => _$SaveLocalStorageStateAsPayloadToJson(this); -} - -class SaveLocalStorageStateAsAction extends Action { - SaveLocalStorageStateAsAction([SaveLocalStorageStateAsPayload value]) : super(type: 'SAVE_LOCAL_STORAGE_STATE_AS', value: value); -} + SaveLocalStorageStateAsAction(this.name, {this.previousName}); -class LocalStorageStateLoadedAction extends Action { - LocalStorageStateLoadedAction(AppState value) : super(type: 'LOCAL_STORAGE_STATE_LOADED', value: value); + factory SaveLocalStorageStateAsAction.fromJson(Map json) => _$SaveLocalStorageStateAsActionFromJson(json); + Map toJson() => _$SaveLocalStorageStateAsActionToJson(this); } // ------------ ITEM ACTIONS ------------------ -class SelectTodoAction extends Action { - SelectTodoAction([String value]) : super(type: 'SELECT_TODO', value: value); +class SelectTodoAction extends _Action { + SelectTodoAction(String value) : super(value); } -class DeselectTodoAction extends Action { - DeselectTodoAction([String value]) : super(type: 'DESELECT_TODO', value: value); +class DeselectTodoAction extends _Action { + DeselectTodoAction(String value) : super(value); } -class BeginEditTodoAction extends Action { - BeginEditTodoAction([String value]) : super(type: 'EDIT_TODO_BEGIN', value: value); +class BeginEditTodoAction extends _Action { + BeginEditTodoAction(String value) : super(value); } -class FinishEditTodoAction extends Action { - FinishEditTodoAction([String value]) : super(type: 'EDIT_TODO_FINISH', value: value); +class FinishEditTodoAction extends _Action { + FinishEditTodoAction(String value) : super(value); } -class HighlightTodosAction extends Action> { - HighlightTodosAction([List value]) : super(type: 'HIGHLIGHT_TODOS', value: value); +class HighlightTodosAction extends _Action> { + HighlightTodosAction(List value) : super(value); } -class UnHighlightTodosAction extends Action> { - UnHighlightTodosAction([List value]) : super(type: 'UNHIGHLIGHT_TODOS', value: value); +class UnHighlightTodosAction extends _Action> { + UnHighlightTodosAction(List value) : super(value); } -class AddTodoAction extends Action { - AddTodoAction([Todo value]) : super(type: 'ADD_TODO', value: value); +class AddTodoAction extends _Action { + AddTodoAction(Todo value) : super(value); } -class RemoveTodoAction extends Action { - RemoveTodoAction([String value]) : super(type: 'REMOVE_TODO', value: value); +class RemoveTodoAction extends _Action { + RemoveTodoAction(String value) : super(value); } -class UpdateTodoAction extends Action { - UpdateTodoAction([Todo value]) : super(type: 'UPDATE_TODO', value: value); +class UpdateTodoAction extends _Action { + UpdateTodoAction(Todo value) : super(value); } // ------------ USER ACTIONS ------------------ -class SelectUserAction extends Action { - SelectUserAction([String value]) : super(type: 'SELECT_USER', value: value); +class SelectUserAction extends _Action { + SelectUserAction(String value) : super(value); } -class DeselectUserAction extends Action { - DeselectUserAction([String value]) : super(type: 'DESELECT_USER', value: value); +class DeselectUserAction extends _Action { + DeselectUserAction(String value) : super(value); } -class BeginEditUserAction extends Action { - BeginEditUserAction([String value]) : super(type: 'EDIT_USER_BEGIN', value: value); +class BeginEditUserAction extends _Action { + BeginEditUserAction(String value) : super(value); } -class FinishEditUserAction extends Action { - FinishEditUserAction([String value]) : super(type: 'EDIT_USER_FINISH', value: value); +class FinishEditUserAction extends _Action { + FinishEditUserAction(String value) : super(value); } -class HighlightUsersAction extends Action> { - HighlightUsersAction([List value]) : super(type: 'HIGHLIGHT_USERS', value: value); +class HighlightUsersAction extends _Action> { + HighlightUsersAction(List value) : super(value); } -class UnHighlightUsersAction extends Action> { - UnHighlightUsersAction([List value]) : super(type: 'UNHIGHLIGHT_USERS', value: value); +class UnHighlightUsersAction extends _Action> { + UnHighlightUsersAction(List value) : super(value); } -class AddUserAction extends Action { - AddUserAction([User value]) : super(type: 'ADD_USER', value: value); +class AddUserAction extends _Action { + AddUserAction(User value) : super(value); } -class RemoveUserAction extends Action { - RemoveUserAction([String value]) : super(type: 'REMOVE_USER', value: value); +class RemoveUserAction extends _Action { + RemoveUserAction(String value) : super(value); } -class UpdateUserAction extends Action { - UpdateUserAction([User value]) : super(type: 'UPDATE_USER', value: value); +class UpdateUserAction extends _Action { + UpdateUserAction(User value) : super(value); } diff --git a/app/over_react_redux/todo_client/lib/src/actions.g.dart b/app/over_react_redux/todo_client/lib/src/actions.g.dart index c8bc45a03..a48abb31b 100644 --- a/app/over_react_redux/todo_client/lib/src/actions.g.dart +++ b/app/over_react_redux/todo_client/lib/src/actions.g.dart @@ -6,16 +6,16 @@ part of 'actions.dart'; // JsonSerializableGenerator // ************************************************************************** -SaveLocalStorageStateAsPayload _$SaveLocalStorageStateAsPayloadFromJson( +SaveLocalStorageStateAsAction _$SaveLocalStorageStateAsActionFromJson( Map json) { - return SaveLocalStorageStateAsPayload( + return SaveLocalStorageStateAsAction( json['name'] as String, previousName: json['previousName'] as String, ); } -Map _$SaveLocalStorageStateAsPayloadToJson( - SaveLocalStorageStateAsPayload instance) => +Map _$SaveLocalStorageStateAsActionToJson( + SaveLocalStorageStateAsAction instance) => { 'name': instance.name, 'previousName': instance.previousName, diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar_local_storage_menu.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar_local_storage_menu.dart index b10617fe6..fca389f59 100644 --- a/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar_local_storage_menu.dart +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/app_bar_local_storage_menu.dart @@ -123,12 +123,11 @@ class AppBarLocalStorageMenuComponent extends UiComponent2 localStorageMiddleware() { final localStorageState = AppState.fromJson(localTodoAppStorage[action.value]); store.dispatch(LocalStorageStateLoadedAction(localStorageState)); } else if (action is SaveLocalStorageStateAsAction) { - if (action.value.previousName != null) { + if (action.previousName != null) { // This is a rename; remove the old entry - localTodoAppStorage.remove(action.value.previousName); + localTodoAppStorage.remove(action.previousName); } - final stateName = action.value.name; + final stateName = action.name; // Run the reducer here so that the name is updated in response to the // current action before saving. - final stateWithUpdatedName = store.reducer(store.state, action); + // TODO use store.reducer once null DevToolsStore.reducer bug is fixed + final stateWithUpdatedName = appStateReducer(store.state, action); localTodoAppStorage[stateName] = stateWithUpdatedName.toJson(); } else { localTodoAppStorage?.updateCurrentState(store.state); @@ -109,7 +110,7 @@ Middleware localStorageMiddleware() { // ------------ ITEM REDUCERS ------------------ final stateNameReducer = TypedReducer((name, action) { - return action.value.name; + return action.name; }); final todosReducer = combineReducers>([ diff --git a/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart b/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart index b51463dad..3ac0d9642 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart @@ -364,9 +364,8 @@ main() { // Update the "current" persisted data set so that it differs from the default testStore.dispatch(AddTodoAction(newTodo)); - final addCustomPersistedSetPayload = SaveLocalStorageStateAsPayload(customPersistedDataSetName); // Save a custom set based on the "current" persisted data set - testStore.dispatch(SaveLocalStorageStateAsAction(addCustomPersistedSetPayload)); + testStore.dispatch(SaveLocalStorageStateAsAction(customPersistedDataSetName)); expect(getLocalStorageSetByKey(customPersistedDataSetName), testStore.state.toJson()); expect(getCurrentLocalStorageSet(), getLocalStorageSetByKey(customPersistedDataSetName), @@ -386,10 +385,9 @@ main() { // Update the "current" persisted data set so that it differs from the custom persisted set testStore.dispatch(AddTodoAction(newTodo)); - final overwriteCustomPersistedSetPayload = - SaveLocalStorageStateAsPayload(customPersistedDataSetName, previousName: customPersistedDataSetName); // Save the existing custom set based on the "current" persisted data set - testStore.dispatch(SaveLocalStorageStateAsAction(overwriteCustomPersistedSetPayload)); + testStore.dispatch( + SaveLocalStorageStateAsAction(customPersistedDataSetName, previousName: customPersistedDataSetName)); expect(getLocalStorageSetByKey(customPersistedDataSetName), isNot(initialCustomPersistedDataSetValue)); expect(getLocalStorageSetByKey(customPersistedDataSetName), testStore.state.toJson()); @@ -402,10 +400,9 @@ main() { const newCustomPersistedDataSetName = 'Copy of $customPersistedDataSetName'; final initialCustomPersistedDataSetValue = getLocalStorageSetByKey(customPersistedDataSetName); - final overwriteCustomPersistedSetPayload = - SaveLocalStorageStateAsPayload(newCustomPersistedDataSetName, previousName: customPersistedDataSetName); // Save a copy of the existing custom set based on the "current" persisted data set - testStore.dispatch(SaveLocalStorageStateAsAction(overwriteCustomPersistedSetPayload)); + testStore.dispatch( + SaveLocalStorageStateAsAction(newCustomPersistedDataSetName, previousName: customPersistedDataSetName)); expect(getLocalStorageSetByKey(newCustomPersistedDataSetName), testStore.state.toJson()); expect(getCurrentLocalStorageSet(), getLocalStorageSetByKey(newCustomPersistedDataSetName), @@ -429,10 +426,9 @@ main() { // Update the "current" persisted data set so that it differs from the custom persisted set testStore.dispatch(AddTodoAction(newTodo)); - final overwriteCustomPersistedSetPayload = - SaveLocalStorageStateAsPayload(newCustomPersistedDataSetName, previousName: customPersistedDataSetName); // Save a copy of the existing custom set based on the "current" persisted data set - testStore.dispatch(SaveLocalStorageStateAsAction(overwriteCustomPersistedSetPayload)); + testStore.dispatch( + SaveLocalStorageStateAsAction(newCustomPersistedDataSetName, previousName: customPersistedDataSetName)); expect(getLocalStorageSetByKey(newCustomPersistedDataSetName), testStore.state.toJson()); expect(getCurrentLocalStorageSet(), getLocalStorageSetByKey(newCustomPersistedDataSetName), From c2ec75a56ea087af655e0bdcaaf285a36f697348 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Thu, 16 Jan 2020 15:14:25 -0700 Subject: [PATCH 31/34] Fix save menu not detecting changes --- app/over_react_redux/todo_client/lib/src/store.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/store.dart b/app/over_react_redux/todo_client/lib/src/store.dart index 48b3addbe..ab6e31649 100644 --- a/app/over_react_redux/todo_client/lib/src/store.dart +++ b/app/over_react_redux/todo_client/lib/src/store.dart @@ -101,9 +101,9 @@ Middleware localStorageMiddleware() { // TODO use store.reducer once null DevToolsStore.reducer bug is fixed final stateWithUpdatedName = appStateReducer(store.state, action); localTodoAppStorage[stateName] = stateWithUpdatedName.toJson(); - } else { - localTodoAppStorage?.updateCurrentState(store.state); } + + localTodoAppStorage?.updateCurrentState(store.state); }; } From dd01aecbcfeb097916f14b1641fd50bad788c24d Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Thu, 16 Jan 2020 15:14:55 -0700 Subject: [PATCH 32/34] Fix "Save As" not working for default value --- .../lib/src/components/app_bar/save_as_menu_item.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/save_as_menu_item.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/save_as_menu_item.dart index f990f0abd..e743c6d5e 100644 --- a/app/over_react_redux/todo_client/lib/src/components/app_bar/save_as_menu_item.dart +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/save_as_menu_item.dart @@ -45,9 +45,7 @@ class SaveAsMenuItemComponent extends UiStatefulComponent2 Date: Thu, 16 Jan 2020 15:16:12 -0700 Subject: [PATCH 33/34] Simplify LocalStorage initialization --- .../todo_client/lib/src/local_storage.dart | 8 +++----- .../todo_client/lib/src/store.dart | 14 +++----------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/app/over_react_redux/todo_client/lib/src/local_storage.dart b/app/over_react_redux/todo_client/lib/src/local_storage.dart index 51841a128..501cbe045 100644 --- a/app/over_react_redux/todo_client/lib/src/local_storage.dart +++ b/app/over_react_redux/todo_client/lib/src/local_storage.dart @@ -10,14 +10,12 @@ TodoAppLocalStorage localTodoAppStorage; /// A map interface for mutating `window.localStorage` values /// used to persist [AppState] values across browser refreshes. class TodoAppLocalStorage extends MapBase { - final AppState initialState; - - TodoAppLocalStorage([this.initialState]) { + TodoAppLocalStorage([AppState initialState]) { if (isInitialized()) return; window.localStorage[localStorageKey] = json.encode({ - currentStateKey: this.initialState?.toJson() ?? {}, - defaultStateKey: this.initialState?.toJson() ?? {}, + currentStateKey: initialState?.toJson() ?? {}, + defaultStateKey: initialState?.toJson() ?? {}, emptyStateKey: emptyState.toJson(), }); } diff --git a/app/over_react_redux/todo_client/lib/src/store.dart b/app/over_react_redux/todo_client/lib/src/store.dart index ab6e31649..262595ac5 100644 --- a/app/over_react_redux/todo_client/lib/src/store.dart +++ b/app/over_react_redux/todo_client/lib/src/store.dart @@ -13,17 +13,9 @@ part 'store.g.dart'; @visibleForTesting AppState initializeState() { - AppState initialState; - if (!TodoAppLocalStorage.isInitialized()) { - // First load - give the user some data to work with, and set up our default / empty states. - initialState = AppState.fromJson(defaultAppState); - localTodoAppStorage = TodoAppLocalStorage(initialState); - } else { - localTodoAppStorage ??= TodoAppLocalStorage(); - initialState = AppState.fromJson(localTodoAppStorage.currentStateJson); - } - - return initialState; + // Initialize local storage with the default state, unless state is already saved in local storage. + localTodoAppStorage = TodoAppLocalStorage(AppState.fromJson(defaultAppState)); + return AppState.fromJson(localTodoAppStorage.currentStateJson); } DevToolsStore getStore() => DevToolsStore( From 5a84e2a21bbdcc799b72a434adaa8a33777766f8 Mon Sep 17 00:00:00 2001 From: Aaron Lademann Date: Fri, 17 Jan 2020 15:33:54 -0700 Subject: [PATCH 34/34] Address CR feedback --- app/over_react_redux/todo_client/.gitignore | 1 - .../src/components/app_bar/saved_data_menu_item.dart | 6 ++++++ .../shared/hoverable_item_component_mixin.dart | 4 +++- .../shared/list_item_expansion_panel_summary.dart | 6 ++++++ .../shared/redraw_counter_component_mixin.dart | 2 ++ .../todo_client/lib/src/components/todo_list.dart | 2 +- .../lib/src/components/todo_list_item.dart | 4 ++-- .../todo_client/lib/src/components/user_list.dart | 2 +- .../lib/src/components/user_list_item.dart | 2 +- .../lib/src/components/user_selector.dart | 2 +- .../components/connected_todo_list_item_test.dart | 2 +- .../browser/components/connected_todo_list_test.dart | 2 +- .../components/connected_user_list_item_test.dart | 4 ++-- .../browser/components/connected_user_list_test.dart | 2 +- .../test/unit/browser/components/fixtures/utils.dart | 8 -------- .../unit/browser/components/todo_list_item_test.dart | 4 ++-- .../test/unit/browser/redux/store_test.dart | 12 ++++++------ 17 files changed, 36 insertions(+), 29 deletions(-) diff --git a/app/over_react_redux/todo_client/.gitignore b/app/over_react_redux/todo_client/.gitignore index fde767b57..9b1b2a5dc 100644 --- a/app/over_react_redux/todo_client/.gitignore +++ b/app/over_react_redux/todo_client/.gitignore @@ -3,7 +3,6 @@ packages build/ .dart_tool -*.over_react.g.dart # Directory created by dartdoc doc/api/ diff --git a/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart b/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart index 215b3fcaf..4d6831777 100644 --- a/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart +++ b/app/over_react_redux/todo_client/lib/src/components/app_bar/saved_data_menu_item.dart @@ -1,3 +1,5 @@ +import 'dart:html'; + import 'package:over_react/over_react.dart'; import 'package:todo_client/src/local_storage.dart'; @@ -37,6 +39,9 @@ class _$SavedDataMenuItemState extends MenuOverlayState @Component2() class SavedDataMenuItemComponent extends UiStatefulComponent2 with HoverableItemMixin { + @override + get itemNodeRef => createRef(); + @override get initialState => (newState() ..addAll(super.initialState) @@ -53,6 +58,7 @@ class SavedDataMenuItemComponent extends UiStatefulComponent2 on UiStatefulComponent2 { + Ref get itemNodeRef; + @mustCallSuper @override get initialState => (newState() @@ -20,7 +22,7 @@ mixin HoverableItemMixin o void handleChildBlur(SyntheticFocusEvent event) { var newlyFocusedTarget = event.relatedTarget; // newlyFocusedTarget could be null or a window, so check if it's an Element first. - if (newlyFocusedTarget is Element && findDomNode(this).contains(newlyFocusedTarget)) { + if (newlyFocusedTarget is Element && itemNodeRef.current?.contains(newlyFocusedTarget) == true) { // Don't do anything if we're moving from one item to another return; } diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart index 20e9b2906..bc243373b 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/list_item_expansion_panel_summary.dart @@ -1,3 +1,5 @@ +import 'dart:html'; + import 'package:over_react/over_react.dart'; import 'package:todo_client/src/components/shared/hoverable_item_mixin.dart'; @@ -33,9 +35,13 @@ class _$ListItemExpansionPanelSummaryState extends UiState class ListItemExpansionPanelSummaryComponent extends UiStatefulComponent2 with HoverableItemMixin { + @override + get itemNodeRef => createRef(); + @override render() { return ExpansionPanelSummary({ + 'ref': itemNodeRef, 'aria-controls': 'details_${props.modelId}', 'id': 'summary_${props.modelId}', 'expandIcon': ExpandMoreIcon(), diff --git a/app/over_react_redux/todo_client/lib/src/components/shared/redraw_counter_component_mixin.dart b/app/over_react_redux/todo_client/lib/src/components/shared/redraw_counter_component_mixin.dart index f3f7ae2d7..d5061ba10 100644 --- a/app/over_react_redux/todo_client/lib/src/components/shared/redraw_counter_component_mixin.dart +++ b/app/over_react_redux/todo_client/lib/src/components/shared/redraw_counter_component_mixin.dart @@ -19,6 +19,8 @@ mixin RedrawCounterMixin on UiComponent2 { @override @mustCallSuper void componentDidUpdate(_, __, [___]) { + super.componentDidUpdate(_, __, ___); + redrawCount++; if (redrawCount < _desiredRedrawCount) { return; diff --git a/app/over_react_redux/todo_client/lib/src/components/todo_list.dart b/app/over_react_redux/todo_client/lib/src/components/todo_list.dart index 562bc280f..6de4c2ce3 100644 --- a/app/over_react_redux/todo_client/lib/src/components/todo_list.dart +++ b/app/over_react_redux/todo_client/lib/src/components/todo_list.dart @@ -1,10 +1,10 @@ import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; -import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; import 'package:todo_client/src/store.dart'; import 'package:todo_client/src/models/todo.dart'; import 'package:todo_client/src/components/shared/display_list.dart'; +import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; import 'package:todo_client/src/components/todo_list_item.dart'; // ignore: uri_has_not_been_generated diff --git a/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart b/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart index 7f987c672..70badf9ec 100644 --- a/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart +++ b/app/over_react_redux/todo_client/lib/src/components/todo_list_item.dart @@ -2,13 +2,13 @@ import 'dart:html'; import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; -import 'package:todo_client/src/actions.dart'; -import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; +import 'package:todo_client/src/actions.dart'; import 'package:todo_client/src/models/todo.dart'; import 'package:todo_client/src/components/shared/list_item_expansion_panel_summary.dart'; import 'package:todo_client/src/components/shared/list_item_mixin.dart'; import 'package:todo_client/src/components/shared/material_ui.dart'; +import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; import 'package:todo_client/src/components/shared/todo_item_text_field.dart'; import 'package:todo_client/src/components/user_selector.dart'; import 'package:todo_client/src/store.dart'; diff --git a/app/over_react_redux/todo_client/lib/src/components/user_list.dart b/app/over_react_redux/todo_client/lib/src/components/user_list.dart index 19c9292bf..b0c491f54 100644 --- a/app/over_react_redux/todo_client/lib/src/components/user_list.dart +++ b/app/over_react_redux/todo_client/lib/src/components/user_list.dart @@ -1,10 +1,10 @@ import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; -import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; import 'package:todo_client/src/store.dart'; import 'package:todo_client/src/models/user.dart'; import 'package:todo_client/src/components/shared/display_list.dart'; +import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; import 'package:todo_client/src/components/user_list_item.dart'; // ignore: uri_has_not_been_generated diff --git a/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart b/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart index 6d965e1cc..3e5d6d693 100644 --- a/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart +++ b/app/over_react_redux/todo_client/lib/src/components/user_list_item.dart @@ -3,13 +3,13 @@ import 'dart:html'; import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; import 'package:todo_client/src/actions.dart'; -import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; import 'package:todo_client/src/models/user.dart'; import 'package:todo_client/src/components/shared/avatar_with_colors.dart'; import 'package:todo_client/src/components/shared/list_item_expansion_panel_summary.dart'; import 'package:todo_client/src/components/shared/list_item_mixin.dart'; import 'package:todo_client/src/components/shared/material_ui.dart'; +import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; import 'package:todo_client/src/components/shared/todo_item_text_field.dart'; import 'package:todo_client/src/components/task_count.dart'; import 'package:todo_client/src/store.dart'; diff --git a/app/over_react_redux/todo_client/lib/src/components/user_selector.dart b/app/over_react_redux/todo_client/lib/src/components/user_selector.dart index 5c483d497..a1e049a67 100644 --- a/app/over_react_redux/todo_client/lib/src/components/user_selector.dart +++ b/app/over_react_redux/todo_client/lib/src/components/user_selector.dart @@ -2,13 +2,13 @@ library todo_client.src.components.user_selector; import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; -import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; import 'package:todo_client/src/store.dart'; import 'package:todo_client/src/models/user.dart'; import 'package:todo_client/src/components/shared/avatar_with_colors.dart'; import 'package:todo_client/src/components/shared/material_ui.dart'; import 'package:todo_client/src/components/shared/menu_overlay.dart'; +import 'package:todo_client/src/components/shared/redraw_counter_component_mixin.dart'; part 'user_selector_trigger.dart'; // ignore: uri_has_not_been_generated diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_item_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_item_test.dart index 960eba015..315f69aec 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_item_test.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_item_test.dart @@ -6,8 +6,8 @@ import 'package:over_react/over_react_redux.dart'; import 'package:over_react_test/over_react_test.dart'; import 'package:test/test.dart'; import 'package:time/time.dart'; -import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/actions.dart'; import 'package:todo_client/src/components/app.dart'; import 'package:todo_client/src/components/todo_list_item.dart'; import 'package:todo_client/src/models/todo.dart'; diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_test.dart index 3f8f765a4..e77d4cd91 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_test.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/components/connected_todo_list_test.dart @@ -4,8 +4,8 @@ import 'package:over_react/over_react_redux.dart'; import 'package:over_react_test/over_react_test.dart'; import 'package:test/test.dart'; import 'package:time/time.dart'; -import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/actions.dart'; import 'package:todo_client/src/components/app.dart'; import 'package:todo_client/src/components/shared/display_list.dart'; import 'package:todo_client/src/components/shared/empty_view.dart'; diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_item_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_item_test.dart index f6271e38a..eaa1e5106 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_item_test.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_item_test.dart @@ -1,13 +1,13 @@ +@TestOn('browser') import 'dart:async'; -@TestOn('browser') import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; import 'package:over_react_test/over_react_test.dart'; import 'package:test/test.dart'; import 'package:time/time.dart'; -import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/actions.dart'; import 'package:todo_client/src/components/app.dart'; import 'package:todo_client/src/components/user_list_item.dart'; import 'package:todo_client/src/models/user.dart'; diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_test.dart index 37c9fc549..e4d965cf7 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_test.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/components/connected_user_list_test.dart @@ -4,8 +4,8 @@ import 'package:over_react/over_react_redux.dart'; import 'package:over_react_test/over_react_test.dart'; import 'package:test/test.dart'; import 'package:time/time.dart'; -import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/actions.dart'; import 'package:todo_client/src/components/app.dart'; import 'package:todo_client/src/components/shared/display_list.dart'; import 'package:todo_client/src/components/shared/empty_view.dart'; diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/fixtures/utils.dart b/app/over_react_redux/todo_client/test/unit/browser/components/fixtures/utils.dart index a09f373de..2e4c76503 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/components/fixtures/utils.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/components/fixtures/utils.dart @@ -24,14 +24,6 @@ void initializeComponentTests() { if (!muiJsIsAvailable()) return; } -JsBackedMap getJsProps(Ref ref) { - if (ref.jsRef == null) { - throw ArgumentError('There is no js component found within this Ref'); - } - - return JsBackedMap.fromJs(ref.jsRef.current.props); -} - Future expectNoRedraws(RedrawCounterMixin component) async { final redrawCount = await component.didRedraw().future.timeout(20.milliseconds, onTimeout: () => 0); expect(redrawCount, 0); diff --git a/app/over_react_redux/todo_client/test/unit/browser/components/todo_list_item_test.dart b/app/over_react_redux/todo_client/test/unit/browser/components/todo_list_item_test.dart index 087344edc..8bdc0aeff 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/components/todo_list_item_test.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/components/todo_list_item_test.dart @@ -1,14 +1,14 @@ +@TestOn('browser') import 'dart:async'; import 'dart:html'; -@TestOn('browser') import 'package:over_react/over_react.dart'; import 'package:over_react/over_react_redux.dart'; import 'package:over_react_test/over_react_test.dart'; import 'package:test/test.dart'; import 'package:time/time.dart'; -import 'package:todo_client/src/actions.dart'; +import 'package:todo_client/src/actions.dart'; import 'package:todo_client/src/components/app.dart'; import 'package:todo_client/src/components/shared/todo_item_text_field.dart'; import 'package:todo_client/src/components/todo_list_item.dart'; diff --git a/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart b/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart index 3ac0d9642..189dc0fa7 100644 --- a/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart +++ b/app/over_react_redux/todo_client/test/unit/browser/redux/store_test.dart @@ -139,7 +139,7 @@ main() { reason: reasonCurrentSetShouldBePersisted); } - test('when an BeginEditTodoAction is dispatched', beginEdits); + test('when a BeginEditTodoAction is dispatched', beginEdits); test('when a FinishEditTodoAction is dispatched', () { beginEdits(); @@ -174,7 +174,7 @@ main() { reason: reasonCurrentSetShouldBePersisted); } - test('when an SelectTodoAction is dispatched', select); + test('when a SelectTodoAction is dispatched', select); test('when a DeselectTodoAction is dispatched', () { select(); @@ -211,9 +211,9 @@ main() { reason: reasonCurrentSetShouldBePersisted); } - test('when an HighlightTodosAction is dispatched', highlight); + test('when a HighlightTodosAction is dispatched', highlight); - test('when a UnHighlightTodosAction is dispatched', () { + test('when an UnHighlightTodosAction is dispatched', () { highlight(); final noLongerHighlightedTodoId = testStore.state.todos.first.id; @@ -284,7 +284,7 @@ main() { reason: reasonCurrentSetShouldBePersisted); } - test('when an BeginEditUserAction is dispatched', beginEdits); + test('when a BeginEditUserAction is dispatched', beginEdits); test('when a FinishEditUserAction is dispatched', () { beginEdits(); @@ -319,7 +319,7 @@ main() { reason: reasonCurrentSetShouldBePersisted); } - test('when an SelectUserAction is dispatched', select); + test('when a SelectUserAction is dispatched', select); test('when a DeselectUserAction is dispatched', () { select();