From 31d820c4dd9a7159edee6718860b7dd933a2db5c Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Wed, 31 Mar 2021 14:37:03 +0300 Subject: [PATCH] Epic with sets and relations + new app flavors (stable, experimental) (#1048) --- .../analytics/base/EventsDictionary.kt | 1 + app/build.gradle | 37 +- app/gradle.properties | 2 +- .../features/auth/SetupSelectedAccountTest.kt | 7 +- .../features/editor/ClipboardTesting.kt | 29 +- .../features/editor/CreateBlockTesting.kt | 17 +- .../editor/CreateRelationBlockTesting.kt | 349 ++++ .../features/editor/DeleteBlockTesting.kt | 15 +- .../editor/EditorIntegrationTesting.kt | 22 +- .../features/editor/ListBlockTesting.kt | 19 +- .../features/editor/MergeBlockTesting.kt | 15 +- .../features/editor/RelationBlockUITesting.kt | 1101 +++++++++++++ .../features/editor/ScrollAndMoveTesting.kt | 2 +- .../features/editor/SplitBlockTesting.kt | 194 +-- .../features/editor/base/EditorTestSetup.kt | 157 +- .../emoji/DocumentEmojiPickerFragmentTest.kt | 9 +- .../relations/AddRelationStatusValueTest.kt | 606 +++++++ .../relations/AddRelationTagValueTest.kt | 532 ++++++ .../DisplayObjectRelationTextValueTest.kt | 447 +++++ .../DisplayRelationObjectValueTest.kt | 640 ++++++++ .../DisplayRelationStatusValueTest.kt | 508 ++++++ .../relations/DisplayRelationTagValueTest.kt | 518 ++++++ .../relations/EditRelationTagValueTest.kt | 328 ++++ .../relations/ObjectRelationDateValueTest.kt | 590 +++++++ .../relations/ObjectRelationListTest.kt | 631 ++++++++ ...AddObjectSetObjectRelationValueFragment.kt | 18 + .../TestObjectRelationDateValueFragment.kt | 18 + .../TestObjectRelationListFragment.kt | 16 + .../TestObjectRelationTextValueFragment.kt | 18 + ...estObjectSetObjectRelationValueFragment.kt | 17 + .../dv/ObjectSetGridColumnRenderingTest.kt | 172 ++ .../dv/ObjectSetGridFileCellRenderingTest.kt | 191 +++ .../ObjectSetGridObjectCellRenderingTest.kt | 296 ++++ .../dv/ObjectSetGridPrimitiveRelationTest.kt | 261 +++ .../dv/ObjectSetGridTagCellRenderingTest.kt | 202 +++ .../features/sets/dv/ObjectSetHeaderTest.kt | 93 ++ .../features/sets/dv/TestObjectSetFragment.kt | 17 + .../features/sets/dv/TestObjectSetSetup.kt | 176 ++ .../sets/filter/ModifyInputValueFilterTest.kt | 316 ++++ .../sets/filter/ModifyStatusFilterTest.kt | 266 +++ .../sets/filter/ModifyTagFilterTest.kt | 246 +++ ...ModifyFilterFromInputFieldValueFragment.kt | 18 + ...stModifyFilterFromSelectedValueFragment.kt | 18 + .../sets/sort/TestViewerSortFragment.kt | 18 + .../sets/sort/ViewerObjectSortTest.kt | 165 ++ .../anytypeio/anytype/mocking/MockUiTests.kt | 2 +- .../anytypeio/anytype/utils/EspressoExt.kt | 87 + .../utils/RecyclerViewItemCountAssertion.kt | 19 + .../anytype/utils/RecyclerViewMatcher.java | 30 +- .../anytypeio/anytype/utils/TestUtils.java | 1 - .../anytype/utils/espresso/ViewMatchers.kt | 116 ++ app/src/debug/google-services.json | 39 + .../debug/res/values/technical_strings.xml | 5 + .../ui/desktop/HomeDashboardFragment.kt | 191 +++ .../ui/page/sheets/DocMenuBottomSheet.kt | 159 ++ .../res/layout/fragment_desktop.xml | 31 + .../layout/fragment_doc_menu_bottom_sheet.xml | 218 +++ app/src/main/AndroidManifest.xml | 3 - .../anytype/app/AndroidApplication.kt | 9 - .../DefaultGradientCollectionProvider.kt | 2 +- .../anytype/di/common/ComponentManager.kt | 288 +++- .../feature/AddObjectRelationObjectValuDI.kt | 50 + .../di/feature/AddObjectRelationValueDI.kt | 102 ++ .../anytypeio/anytype/di/feature/ArchiveDI.kt | 4 +- .../di/feature/CreateDataViewRelationDI.kt | 31 + .../di/feature/CreateDataViewViewerDI.kt | 46 + .../anytype/di/feature/CreateObjectSetDI.kt | 70 + .../anytype/di/feature/CreateObjectTypeDI.kt | 33 + .../di/feature/DataViewViewerActionDI.kt | 68 + .../anytype/di/feature/DebugSettingsDi.kt | 6 + .../di/feature/DocumentAddNewBlockDi.kt | 40 + .../di/feature/DocumentEmojiIconPickerDI.kt | 19 +- .../di/feature/DocumentIconActionMenuDI.kt | 21 +- .../anytype/di/feature/EditDataViewViewer.kt | 50 + .../anytype/di/feature/EditGridCellDI.kt | 61 + .../anytype/di/feature/ManageViewerDI.kt | 44 + .../anytype/di/feature/ModifyViewerSort.kt | 44 + .../anytype/di/feature/ObjectRelationDI.kt | 60 + .../di/feature/ObjectRelationValueDi.kt | 130 ++ .../anytype/di/feature/ObjectSetDI.kt | 230 +++ .../anytype/di/feature/ObjectSetRecordDI.kt | 45 + .../anytypeio/anytype/di/feature/PageDI.kt | 116 +- .../di/feature/RelationFileValueAddDi.kt | 50 + .../anytype/di/feature/SelectDocCoverDI.kt | 15 +- .../di/feature/SelectSortRelationDI.kt | 52 + .../di/feature/UploadDocCoverImageDI.kt | 6 +- .../anytype/di/feature/ViewerCustomizeDI.kt | 41 + .../anytype/di/feature/ViewerFilter.kt | 52 + .../di/feature/ViewerRelationListDi.kt | 56 + .../anytype/di/feature/ViewerSort.kt | 44 + .../anytype/di/feature/ViewerSortByDI.kt | 38 + .../anytype/di/feature/sets/CreateFilter.kt | 62 + .../anytype/di/feature/sets/ModifyFilter.kt | 62 + .../di/feature/sets/PickConditionDi.kt | 33 + .../di/feature/sets/SelectFilterRelationDI.kt | 37 + .../anytypeio/anytype/di/main/BridgeModule.kt | 17 - .../anytypeio/anytype/di/main/ConfigModule.kt | 2 +- .../anytypeio/anytype/di/main/DataModule.kt | 11 +- .../anytype/di/main/MainComponent.kt | 9 +- .../com/anytypeio/anytype/ext/MarkupExt.kt | 6 +- .../anytypeio/anytype/navigation/Navigator.kt | 20 +- .../DefaultCoverImageHashProvider.kt | 4 +- .../anytype/ui/base/NavigationFragment.kt | 7 +- .../modals/ObjectRelationValueFragment.kt | 438 +++++ .../anytype/ui/desktop/DashboardAdapter.kt | 54 +- .../ui/linking/LinkToObjectFragment.kt | 2 +- .../anytypeio/anytype/ui/page/PageFragment.kt | 122 +- .../ui/page/cover/DocCoverGalleryFragment.kt | 2 +- .../ui/page/cover/DocCoverSliderFragment.kt | 2 +- .../ui/page/cover/UploadCoverImageFragment.kt | 2 +- .../ui/page/modals/AddBlockFragment.kt | 67 +- .../modals/DocumentEmojiIconPickerFragment.kt | 4 +- .../SelectProgrammingLanguageFragment.kt | 2 +- .../ui/page/modals/TurnIntoFragment.kt | 5 +- .../page/modals/actions/BlockActionToolbar.kt | 26 +- .../actions/BlockActionToolbarFactory.kt | 51 + .../actions/DocumentIconActionMenuFragment.kt | 15 +- .../actions/ProfileIconActionMenuFragment.kt | 18 +- .../actions/RelationDefaultActionToolbar.kt | 258 +++ .../RelationPlaceholderActionToolbar.kt | 36 + .../AddObjectRelationObjectValueFragment.kt | 148 ++ .../AddObjectRelationValueFragment.kt | 247 +++ .../CreateDataViewRelationFragment.kt | 98 ++ .../ObjectRelationDateValueFragment.kt | 166 ++ .../relations/ObjectRelationListFragment.kt | 201 +++ .../ObjectRelationTextValueFragment.kt | 204 +++ .../RelationFileValueActionsFragment.kt | 59 + .../relations/RelationFileValueAddFragment.kt | 148 ++ .../ui/sets/CreateObjectSetFragment.kt | 94 ++ .../ui/sets/CreateObjectTypeFragment.kt | 89 + .../anytype/ui/sets/ObjectSetFragment.kt | 443 +++++ .../ui/sets/SetObjectSetRecordNameFragment.kt | 70 + .../anytype/ui/sets/ViewerFilterFragment.kt | 154 ++ .../anytype/ui/sets/ViewerSortByFragment.kt | 134 ++ .../ui/sets/modals/BaseDialogListFragment.kt | 46 + .../modals/CreateDataViewViewerFragment.kt | 74 + .../modals/DataViewViewerActionFragment.kt | 98 ++ .../ui/sets/modals/DatePickerFragment.kt | 73 + .../sets/modals/EditDataViewViewerFragment.kt | 96 ++ .../ui/sets/modals/ManageViewerFragment.kt | 106 ++ .../modals/PickFilterConditionFragment.kt | 139 ++ .../ui/sets/modals/PickSortingKeyFragment.kt | 59 + .../ui/sets/modals/PickSortingTypeFragment.kt | 43 + .../modals/ViewerBottomSheetRootFragment.kt | 125 ++ .../ui/sets/modals/ViewerCustomizeFragment.kt | 97 ++ .../modals/ViewerRelationOptionFragment.kt | 84 + .../ui/sets/modals/ViewerRelationsFragment.kt | 147 ++ .../ui/sets/modals/filter/CreateFilterFlow.kt | 9 + .../filter/CreateFilterFlowRootFragment.kt | 97 ++ ...CreateFilterFromInputFieldValueFragment.kt | 115 ++ .../CreateFilterFromSelectedValueFragment.kt | 158 ++ ...ModifyFilterFromInputFieldValueFragment.kt | 140 ++ .../ModifyFilterFromSelectedValueFragment.kt | 162 ++ .../filter/SelectFilterRelationFragment.kt | 43 + .../filter/UpdateConditionActionReceiver.kt | 7 + .../modals/search/SearchRelationFragment.kt | 61 + .../modals/sort/ModifyViewerSortFragment.kt | 93 ++ .../modals/sort/SelectSortRelationFragment.kt | 86 + .../ui/sets/modals/sort/ViewerSortFragment.kt | 116 ++ .../ui/settings/DebugSettingsFragment.kt | 28 + app/src/main/res/anim/slide_bottom.xml | 7 + app/src/main/res/anim/slide_up.xml | 6 + .../divider_sort_or_filter_relation.xml | 6 + .../drawable/ic_arrow_expand_dv_viewer.xml | 10 + .../res/drawable/ic_create_viewer_gallery.xml | 22 + .../res/drawable/ic_create_viewer_grid.xml | 19 + .../res/drawable/ic_create_viewer_kanban.xml | 14 + .../res/drawable/ic_create_viewer_list.xml | 15 + .../drawable/ic_data_view_viewer_delete.xml | 25 + .../ic_data_view_viewer_duplicate.xml | 20 + .../res/drawable/ic_data_view_viewer_edit.xml | 14 + .../res/drawable/ic_dv_relation_search.xml | 14 + .../main/res/drawable/ic_viewer_chosen.xml | 10 + .../rect_create_viewer_icon_background.xml | 6 + .../res/drawable/rect_dv_search_relation.xml | 6 + .../res/drawable/rectangle_default_button.xml | 6 +- .../add_object_relation_value_fragment.xml | 110 ++ .../fragment_create_data_view_relation.xml | 62 + .../fragment_create_data_view_viewer.xml | 248 +++ .../layout/fragment_create_filter_flow.xml | 11 + .../layout/fragment_create_object_type.xml | 79 + .../fragment_create_or_update_filter.xml | 146 ++ ...ate_or_update_filter_input_field_value.xml | 102 ++ .../main/res/layout/fragment_create_set.xml | 10 + .../fragment_data_view_viewer_actions.xml | 182 +++ .../main/res/layout/fragment_date_picker.xml | 39 + .../res/layout/fragment_debug_settings.xml | 38 + .../layout/fragment_edit_data_view_viewer.xml | 249 +++ app/src/main/res/layout/fragment_filter.xml | 82 + app/src/main/res/layout/fragment_list.xml | 26 + .../res/layout/fragment_manage_viewer.xml | 60 + .../layout/fragment_modify_viewer_sort.xml | 86 + .../fragment_object_relation_date_value.xml | 227 +++ .../layout/fragment_object_relation_list.xml | 89 + .../fragment_object_relation_text_value.xml | 37 + .../main/res/layout/fragment_object_set.xml | 138 ++ app/src/main/res/layout/fragment_page.xml | 12 +- .../fragment_relation_file_value_action.xml | 61 + .../fragment_relation_value_file_add.xml | 110 ++ .../fragment_relation_value_object_add.xml | 110 ++ ...ragment_select_sort_or_filter_relation.xml | 62 + .../fragment_set_object_set_record_name.xml | 64 + app/src/main/res/layout/fragment_sorting.xml | 65 + .../fragment_viewer_bottom_sheet_root.xml | 11 + .../res/layout/fragment_viewer_customize.xml | 170 ++ .../fragment_viewer_relation_option.xml | 186 +++ .../layout/fragment_viewer_relations_list.xml | 88 + .../main/res/layout/fragment_viewer_sort.xml | 62 + .../res/layout/item_desktop_object_set.xml | 52 + app/src/main/res/layout/modal_customize.xml | 2 +- .../layout/object_relation_value_fragment.xml | 115 ++ app/src/main/res/mipmap-anydpi-v26/ic_app.xml | 2 +- .../res/mipmap-anydpi-v26/ic_app_round.xml | 2 +- app/src/main/res/navigation/graph.xml | 18 +- app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/ic_app_back.xml | 4 - app/src/main/res/values/strings.xml | 52 +- app/src/main/res/values/styles.xml | 79 + .../res/xml/fragment_object_set_scene.xml | 136 ++ app/{ => src/release}/google-services.json | 0 .../release/res/values/technical_strings.xml | 5 + .../ui/desktop/HomeDashboardFragment.kt | 11 +- .../ui/page/sheets/DocMenuBottomSheet.kt | 152 ++ .../res/layout/fragment_desktop.xml | 0 .../layout/fragment_doc_menu_bottom_sheet.xml | 195 +++ .../anytypeio/anytype/MarkupExtractTest.kt | 2 +- build.gradle | 2 +- clipboard/build.gradle | 1 + .../clipboard/AnytypeClipboardStorage.kt | 6 +- core-models/.gitignore | 1 + core-models/build.gradle | 13 + core-models/proguard-rules.pro | 21 + core-models/src/main/AndroidManifest.xml | 5 + .../anytypeio/anytype/core_models/Alias.kt | 24 + .../anytypeio/anytype/core_models}/Block.kt | 85 +- .../anytype/core_models}/BlockSplitMode.kt | 2 +- .../anytypeio/anytype/core_models}/Command.kt | 24 +- .../anytypeio/anytype/core_models}/Config.kt | 5 +- .../anytypeio/anytype/core_models/Event.kt | 279 ++++ .../anytype/core_models/ObjectType.kt | 34 + .../anytype/core_models}/PageLink.kt | 6 +- .../anytypeio/anytype/core_models}/Payload.kt | 4 +- .../anytype/core_models}/Position.kt | 2 +- .../anytypeio/anytype/core_models/Relation.kt | 49 + .../anytypeio/anytype/core_models/Response.kt | 47 + .../anytype/core_models}/ext/BlockExt.kt | 10 +- .../anytype/core_models/ext/DetailsExt.kt | 44 + .../anytype/core_models}/ext/MarkupExt.kt | 6 +- .../anytype/core_models/ext/RelationExt.kt | 29 + .../anytype/core_models}/misc/Overlap.kt | 4 +- core-ui/build.gradle | 30 + .../anytype/core_ui/common/AbstractAdapter.kt | 28 + .../core_ui/common/AbstractViewHolder.kt | 8 + .../core_ui/extensions/ResExtension.kt | 214 ++- .../features/dataview/ViewerGridAdapter.kt | 84 + .../dataview/ViewerGridCellsAdapter.kt | 203 +++ .../dataview/ViewerGridHeaderAdapter.kt | 73 + .../features/dataview/ViewerListAdapter.kt | 23 + .../dataview/ViewerModifyOrderAdapter.kt | 93 ++ .../dataview/ViewerRelationsAdapter.kt | 62 + .../features/dataview/ViewerTabItemAdapter.kt | 78 + .../features/dataview/ViewerTitleAdapter.kt | 21 + .../features/dataview/ViewerTypeAdapter.kt | 119 ++ .../dataview/diff/CellViewDiffUtil.kt | 25 + .../dataview/diff/ColumnViewDiffUtil.kt | 28 + .../features/dataview/diff/GridRowDiffUtil.kt | 24 + .../dataview/holders/DVGridCellDateHolder.kt | 14 + .../holders/DVGridCellDescriptionHolder.kt | 13 + .../dataview/holders/DVGridCellEmailHolder.kt | 13 + .../dataview/holders/DVGridCellFileHolder.kt | 37 + .../holders/DVGridCellNumberHolder.kt | 13 + .../holders/DVGridCellObjectHolder.kt | 42 + .../dataview/holders/DVGridCellPhoneHolder.kt | 13 + .../holders/DVGridCellStatusHolder.kt | 27 + .../dataview/holders/DVGridCellTagHolder.kt | 51 + .../dataview/holders/DVGridCellUrlHolder.kt | 13 + .../dataview/modals/FilterAddViewHolder.kt | 6 + .../dataview/modals/FilterByAdapter.kt | 158 ++ .../dataview/modals/FilterDateViewHolder.kt | 29 + .../dataview/modals/FilterNumberViewHolder.kt | 30 + .../dataview/modals/FilterObjectViewHolder.kt | 54 + .../dataview/modals/FilterStatusViewHolder.kt | 50 + .../dataview/modals/FilterTagViewHolder.kt | 65 + .../dataview/modals/FilterTextViewHolder.kt | 88 + .../dataview/modals/FilterViewHolder.kt | 33 + .../features/dataview/modals/SortByAdapter.kt | 139 ++ .../features/dataview/modals/TagAdapter.kt | 32 + .../holders/ext/EditorHolderExtensions.kt | 64 + .../holders/relations/RelationViewHolder.kt | 205 +++ .../core_ui/features/page/BlockAdapter.kt | 114 +- .../page/modal/AddBlockOrTurnIntoAdapter.kt | 138 +- .../relations/DocumentRelationAdapter.kt | 98 ++ .../ObjectRelationTextValueAdapter.kt | 104 ++ .../relations/RelationFileValueAdapter.kt | 38 + .../relations/RelationObjectValueAdapter.kt | 38 + .../relations/create/RelationFormatAdapter.kt | 84 + .../holders/ObjectRelationBaseHolder.kt | 6 + .../holders/ObjectRelationObjectHolder.kt | 33 + .../holders/ObjectRelationTextHolder.kt | 75 + .../relations/holders/RelationFileHolder.kt | 24 + .../features/sets/CreateFilterAdapter.kt | 159 ++ .../features/sets/CreateObjectTypeAdapter.kt | 47 + .../features/sets/CreateSetHeaderAdapter.kt | 42 + .../sets/CreateSetObjectTypeAdapter.kt | 66 + .../sets/FilterRelationPickerAdapter.kt | 12 + .../features/sets/ManageViewerAdapter.kt | 57 + .../sets/ObjectRelationValueAdapter.kt | 255 +++ .../features/sets/PickColumnAdapter.kt | 71 + .../features/sets/PickFilterColumnAdapter.kt | 14 + .../sets/PickFilterConditionAdapter.kt | 52 + .../sets/PickFilterOperatorAdapter.kt | 47 + .../features/sets/PickSortingTypeAdapter.kt | 54 + .../features/sets/RelationPickerAdapter.kt | 72 + .../features/sets/SearchRelationAdapter.kt | 28 + .../sets/SortingRelationPickerAdapter.kt | 14 + .../features/sets/ViewerSortAdapter.kt | 89 + .../layout/DividerVerticalItemDecoration.kt | 35 + .../anytype/core_ui/menu/DocumentPopUpMenu.kt | 4 +- .../core_ui/reactive/ViewClickedFlow.kt | 18 + .../tools/BottomSheetSharedTransition.kt | 163 ++ .../tools/DefaultDragAndDropBehavior.kt | 13 + .../anytype/core_ui/widgets/DoсIconWidget.kt | 54 + .../core_ui/widgets/GridCellFileItem.kt | 31 + .../core_ui/widgets/GridCellObjectItem.kt | 28 + .../anytype/core_ui/widgets/IconWidget.kt | 61 + .../core_ui/widgets/ObjectIconTextWidget.kt | 48 + .../core_ui/widgets/ObjectIconWidget.kt | 157 ++ .../core_ui/widgets/RelationObjectItem.kt | 28 + .../core_ui/widgets/text/StaticTextView.kt | 22 + .../core_ui/widgets/text/StatusWidget.kt | 24 + .../anytype/core_ui/widgets/text/TagWidget.kt | 46 + .../color/selector_viewer_tab_text_color.xml | 5 + .../drawable-xxxhdpi/ic_dv_filter_search.xml | 14 + .../rect_dv_filter_search.xml | 6 + ...heckbox_selected_relation_not_selected.xml | 6 + .../checkbox_selected_relation_selected.xml | 5 + .../checkbox_selected_relation_selector.xml | 5 + .../main/res/drawable/circle_cell_action.xml | 7 + .../res/drawable/circle_grid_cell_profile.xml | 5 + .../circle_object_icon_emoji_background.xml | 5 + core-ui/src/main/res/drawable/cursor.xml | 9 + .../res/drawable/decoration_viewer_sort.xml | 6 + .../main/res/drawable/divider_filter_edit.xml | 7 + .../main/res/drawable/divider_filter_list.xml | 7 + .../res/drawable/divider_relation_layer.xml | 7 + .../main/res/drawable/divider_relations.xml | 7 + core-ui/src/main/res/drawable/dragger.xml | 5 + core-ui/src/main/res/drawable/ic_add_24.xml | 12 + .../ic_add_block_or_turn_into_relation.xml | 42 + .../res/drawable/ic_add_new_object_to_set.xml | 12 + .../main/res/drawable/ic_add_new_relation.xml | 16 + .../src/main/res/drawable/ic_arrow_back.xml | 10 + .../main/res/drawable/ic_arrow_forward_24.xml | 10 + .../ic_block_relation_placeholder.xml | 36 + .../drawable/ic_cell_relation_call_with.xml | 10 + .../drawable/ic_cell_relation_go_to_link.xml | 14 + .../ic_cell_relation_go_to_mail_client.xml | 14 + ...lti_select_more.xml => ic_circle_more.xml} | 0 .../main/res/drawable/ic_delete_option.xml | 12 + core-ui/src/main/res/drawable/ic_dnd.xml | 15 + .../ic_dragger_modify_relation_order.xml | 12 + .../main/res/drawable/ic_dv_add_new_view.xml | 12 + .../drawable/ic_dv_customize_panel_filter.xml | 15 + .../drawable/ic_dv_customize_panel_group.xml | 26 + .../ic_dv_customize_panel_relations.xml | 24 + .../drawable/ic_dv_customize_panel_sort.xml | 18 + .../res/drawable/ic_dv_customize_view.xml | 22 + .../src/main/res/drawable/ic_dv_edit_view.xml | 12 + .../main/res/drawable/ic_dv_gallery_view.xml | 18 + .../main/res/drawable/ic_dv_kanban_view.xml | 15 + .../src/main/res/drawable/ic_dv_list_view.xml | 18 + .../ic_dv_manage_view_dnd_dragger.xml | 15 + .../main/res/drawable/ic_dv_modal_plus.xml | 12 + core-ui/src/main/res/drawable/ic_dv_plus.xml | 12 + .../main/res/drawable/ic_dv_table_view.xml | 20 + .../main/res/drawable/ic_ellipse_small.xml | 9 + .../src/main/res/drawable/ic_list_arrow.xml | 10 + .../drawable/ic_manage_dv_viewer_gallery.xml | 22 + .../res/drawable/ic_manage_dv_viewer_grid.xml | 19 + .../drawable/ic_manage_dv_viewer_kanban.xml | 14 + .../res/drawable/ic_manage_dv_viewer_list.xml | 15 + core-ui/src/main/res/drawable/ic_more.xml | 14 +- .../ic_object_relation_format_date.xml | 5 + .../ic_object_relation_format_emoji.xml | 5 + .../ic_object_relation_format_number.xml | 5 + .../ic_object_relation_format_object.xml | 5 + .../ic_object_relation_format_text.xml | 5 + .../ic_object_relation_format_title.xml | 5 + .../src/main/res/drawable/ic_open_to_edit.xml | 26 + .../src/main/res/drawable/ic_option_add.xml | 12 + .../main/res/drawable/ic_option_checked.xml | 10 + .../src/main/res/drawable/ic_plus_white.xml | 12 + .../drawable/ic_relation_attachment_32.xml | 12 + .../drawable/ic_relation_attachment_48.xml | 30 + .../res/drawable/ic_relation_checkbox_32.xml | 17 + .../res/drawable/ic_relation_checkbox_48.xml | 17 + .../main/res/drawable/ic_relation_date_32.xml | 19 + .../main/res/drawable/ic_relation_date_48.xml | 19 + .../res/drawable/ic_relation_email_32.xml | 17 + .../res/drawable/ic_relation_email_48.xml | 17 + .../main/res/drawable/ic_relation_name_32.xml | 18 + .../main/res/drawable/ic_relation_name_48.xml | 18 + .../res/drawable/ic_relation_number_32.xml | 21 + .../res/drawable/ic_relation_number_48.xml | 21 + .../res/drawable/ic_relation_object_32.xml | 17 + .../res/drawable/ic_relation_object_48.xml | 17 + .../drawable/ic_relation_phone_number_32.xml | 13 + .../drawable/ic_relation_phone_number_48.xml | 13 + .../res/drawable/ic_relation_status_32.xml | 21 + .../res/drawable/ic_relation_status_48.xml | 21 + .../main/res/drawable/ic_relation_tag_32.xml | 16 + .../main/res/drawable/ic_relation_tag_48.xml | 16 + .../main/res/drawable/ic_relation_text_32.xml | 18 + .../main/res/drawable/ic_relation_text_48.xml | 18 + .../main/res/drawable/ic_relation_url_32.xml | 15 + .../main/res/drawable/ic_relation_url_48.xml | 15 + core-ui/src/main/res/drawable/ic_remove.xml | 13 + .../src/main/res/drawable/ic_remove_red.xml | 12 + .../res/drawable/ic_remove_viewer_sort.xml | 12 + core-ui/src/main/res/drawable/ic_set_big.xml | 17 + .../main/res/drawable/ic_tag_not_selected.xml | 11 + .../src/main/res/drawable/ic_tag_selected.xml | 14 + .../res/drawable/ic_tag_selected_selector.xml | 5 + .../main/res/drawable/ic_viewer_chosen.xml | 10 + .../res/drawable/ic_viewer_sort_arrow.xml | 10 + .../item_viewer_item_selected_background.xml | 6 + .../drawable/item_viewer_list_selected.xml | 4 + .../rect_data_view_customize_view_button.xml | 17 + .../res/drawable/rect_dv_cell_tag_item.xml | 11 + ...ect_dv_manage_viewer_background_active.xml | 6 + ...t_dv_manage_viewer_background_selector.xml | 4 + .../res/drawable/rect_dv_manage_viewers.xml | 6 + .../drawable/rect_orange_action_button.xml | 6 + .../main/res/drawable/rect_relation_tag.xml | 6 + .../rectangle_avatar_initial_background.xml | 5 + ...ectangle_item_relation_format_selected.xml | 9 + ...ectangle_item_relation_format_selector.xml | 4 + ...modifier_viewer_relation_order_divider.xml | 6 + ...angle_modify_viewer_relation_order_dnd.xml | 6 + ...rectangle_object_icon_emoji_background.xml | 6 + ...gle_relation_icon_container_background.xml | 6 + .../rounded_rectangle_data_view_action.xml | 6 + .../src/main/res/drawable/sort_divider.xml | 7 + .../drawable/viewer_relation_switch_thumb.xml | 17 + .../drawable/viewer_relation_switch_track.xml | 17 + ...tem_add_block_or_turn_into_object_type.xml | 53 + .../layout/item_block_relation_default.xml | 14 + .../res/layout/item_block_relation_file.xml | 17 + .../res/layout/item_block_relation_object.xml | 17 + .../item_block_relation_placeholder.xml | 40 + .../res/layout/item_block_relation_status.xml | 17 + .../res/layout/item_block_relation_tag.xml | 17 + ...ate_data_view_relation_relation_format.xml | 32 + .../res/layout/item_create_filter_date.xml | 48 + .../res/layout/item_create_filter_object.xml | 66 + .../res/layout/item_create_filter_status.xml | 25 + .../res/layout/item_create_filter_tag.xml | 27 + .../res/layout/item_create_set_header.xml | 92 ++ .../layout/item_create_set_object_type.xml | 45 + .../layout/item_document_relation_default.xml | 31 + .../layout/item_document_relation_file.xml | 62 + .../layout/item_document_relation_object.xml | 73 + .../layout/item_document_relation_status.xml | 31 + .../res/layout/item_document_relation_tag.xml | 101 ++ .../main/res/layout/item_dv_manage_viewer.xml | 80 + .../res/layout/item_dv_viewer_filter_add.xml | 28 + .../item_dv_viewer_filter_condition.xml | 28 + .../res/layout/item_dv_viewer_filter_date.xml | 73 + .../layout/item_dv_viewer_filter_number.xml | 73 + .../layout/item_dv_viewer_filter_object.xml | 105 ++ .../layout/item_dv_viewer_filter_status.xml | 73 + .../res/layout/item_dv_viewer_filter_tag.xml | 72 + .../item_dv_viewer_filter_tag_value.xml | 58 + .../res/layout/item_dv_viewer_filter_text.xml | 75 + .../main/res/layout/item_dv_viewer_tab.xml | 17 + .../res/layout/item_dv_viewer_tab_group.xml | 11 + .../res/layout/item_dv_viewer_tab_plus.xml | 11 + .../main/res/layout/item_edit_cell_file.xml | 85 + .../main/res/layout/item_edit_cell_object.xml | 102 ++ .../layout/item_edit_cell_option_create.xml | 27 + .../main/res/layout/item_edit_cell_status.xml | 43 + .../main/res/layout/item_edit_cell_tag.xml | 45 + .../item_edit_cell_tag_or_status_empty.xml | 14 + .../main/res/layout/item_filter_condition.xml | 18 + .../main/res/layout/item_filter_operator.xml | 18 + .../res/layout/item_grid_column_header.xml | 21 + .../layout/item_grid_column_header_plus.xml | 15 + .../src/main/res/layout/item_list_base.xml | 34 + .../item_modify_viewer_relation_order.xml | 49 + .../res/layout/item_object_relation_text.xml | 51 + .../src/main/res/layout/item_object_type.xml | 44 + .../main/res/layout/item_search_relation.xml | 28 + .../src/main/res/layout/item_sorting_key.xml | 18 + .../main/res/layout/item_viewer_container.xml | 40 + .../src/main/res/layout/item_viewer_grid.xml | 37 + .../item_viewer_grid_cell_description.xml | 18 + .../res/layout/item_viewer_grid_cell_file.xml | 43 + .../layout/item_viewer_grid_cell_object.xml | 54 + .../res/layout/item_viewer_grid_cell_tag.xml | 75 + .../res/layout/item_viewer_grid_column.xml | 21 + .../main/res/layout/item_viewer_grid_row.xml | 51 + .../res/layout/item_viewer_grid_row_title.xml | 14 + .../res/layout/item_viewer_relation_list.xml | 46 + .../src/main/res/layout/item_viewer_sort.xml | 70 + .../main/res/layout/item_viewer_sort_add.xml | 35 + .../res/layout/item_viewer_sort_apply.xml | 19 + .../main/res/layout/item_viewer_sort_set.xml | 80 + .../src/main/res/layout/item_viewer_title.xml | 19 + .../res/layout/layout_object_set_header.xml | 41 + .../src/main/res/layout/modal_item_filter.xml | 9 +- .../layout/modal_item_filter_tag_value.xml | 17 + .../main/res/layout/modal_item_gallery.xml | 2 +- .../src/main/res/layout/modal_item_group.xml | 2 +- .../src/main/res/layout/modal_item_kanban.xml | 2 +- .../src/main/res/layout/modal_item_list.xml | 2 +- ...roperties.xml => modal_item_relations.xml} | 20 +- .../src/main/res/layout/modal_item_sort.xml | 17 +- core-ui/src/main/res/layout/viewer_cell.xml | 18 + .../widget_data_view_customize_view.xml | 154 ++ .../main/res/layout/widget_dv_grid_file.xml | 23 + .../main/res/layout/widget_dv_grid_object.xml | 29 + core-ui/src/main/res/layout/widget_icon.xml | 12 + .../main/res/layout/widget_object_icon.xml | 46 + .../res/layout/widget_object_icon_text.xml | 27 + .../res/layout/widget_relation_object.xml | 27 + .../res/menu/menu_edit_grid_cell_email.xml | 9 + .../res/menu/menu_edit_grid_cell_phone.xml | 9 + .../main/res/menu/menu_edit_grid_cell_url.xml | 9 + core-ui/src/main/res/menu/menu_page.xml | 3 + core-ui/src/main/res/values/attrs.xml | 21 +- core-ui/src/main/res/values/colors.xml | 11 + core-ui/src/main/res/values/dimens.xml | 21 + core-ui/src/main/res/values/strings.xml | 55 + core-ui/src/main/res/values/styles.xml | 242 +++ .../features/dv/CellViewDiffUtilTest.kt | 190 +++ .../features/dv/ColumnViewDiffUtilTest.kt | 62 + core-utils/build.gradle | 2 + .../anytype/core_utils/OnSwipeListener.kt | 121 ++ .../anytype/core_utils/const/DetailsKeys.kt | 8 + .../anytype/core_utils/di/scope/PerDialog.kt | 7 + .../core_utils/diff/DefaultDiffUtil.kt | 27 + .../diff/DefaultObjectDiffIdentifier.kt | 10 + .../core_utils/ext/AndroidExtension.kt | 24 + .../anytype/core_utils/ext/DateUtils.kt | 79 + .../anytype/core_utils/ext/Extensions.kt | 12 +- .../core_utils/ext/FragmentExtensions.kt | 32 +- .../anytype/core_utils/ext/ViewExtensions.kt | 1 + .../core_utils/text/ActionDoneListener.kt | 16 + .../anytype/core_utils/tools/Tools.kt | 12 + .../core_utils/ui/BaseBottomSheetFragment.kt | 24 +- .../core_utils/ui/BaseDialogFragment.kt | 20 + .../anytype/core_utils/ui/BaseFragment.kt | 11 + .../core_utils/ui/DragAndDropViewHolder.kt | 3 + .../ui/ItemTouchHelperViewHolder.kt | 23 + .../ui/NonScrollLinearLayoutManager.kt | 8 + .../core_utils/ui/OnStartDragListener.kt | 12 + data/build.gradle | 1 + .../data/auth/event/EventDataChannel.kt | 11 +- .../data/auth/event/EventRemoteChannel.kt | 4 +- .../data/auth/mapper/MapperExtension.kt | 630 +------- .../anytype/data/auth/mapper/Serializer.kt | 6 +- .../anytype/data/auth/model/BlockEntity.kt | 133 -- .../anytype/data/auth/model/CommandEntity.kt | 176 -- .../anytype/data/auth/model/ConfigEntity.kt | 7 - .../anytype/data/auth/model/EventEntity.kt | 99 -- .../anytype/data/auth/model/Navigation.kt | 23 - .../anytype/data/auth/model/PayloadEntity.kt | 6 - .../anytype/data/auth/model/PositionEntity.kt | 3 - .../anytype/data/auth/model/Response.kt | 24 - .../anytype/data/auth/other/DataDownloader.kt | 2 +- .../auth/repo/block/BlockDataRepository.kt | 302 +++- .../data/auth/repo/block/BlockDataStore.kt | 184 ++- .../data/auth/repo/block/BlockRemote.kt | 184 ++- .../auth/repo/block/BlockRemoteDataStore.kt | 285 +++- .../repo/clipboard/ClipboardDataRepository.kt | 8 +- .../auth/repo/clipboard/ClipboardDataStore.kt | 6 +- .../data/auth/repo/config/Configuration.kt | 4 +- .../data/auth/repo/config/Configurator.kt | 5 +- .../auth/status/ThreadStatusRemoteChannel.kt | 2 +- dependencies.gradle | 11 +- domain/build.gradle | 3 + .../auth/interactor/GetCurrentAccount.kt | 2 +- .../domain/auth/interactor/GetProfile.kt | 2 +- .../anytype/domain/auth/model/Account.kt | 2 +- .../anytypeio/anytype/domain/base/Either.kt | 8 + .../anytype/domain/block/UpdateDivider.kt | 8 +- .../domain/block/interactor/CreateBlock.kt | 10 +- .../block/interactor/CreateLinkToObject.kt | 6 +- .../domain/block/interactor/DuplicateBlock.kt | 6 +- .../domain/block/interactor/MergeBlocks.kt | 6 +- .../anytype/domain/block/interactor/Move.kt | 6 +- .../domain/block/interactor/RemoveLinkMark.kt | 4 +- .../domain/block/interactor/ReplaceBlock.kt | 8 +- .../domain/block/interactor/SplitBlock.kt | 12 +- .../block/interactor/TurnIntoDocument.kt | 4 +- .../domain/block/interactor/TurnIntoStyle.kt | 8 +- .../domain/block/interactor/UnlinkBlocks.kt | 6 +- .../block/interactor/UpdateAlignment.kt | 8 +- .../block/interactor/UpdateBackgroundColor.kt | 6 +- .../domain/block/interactor/UpdateCheckbox.kt | 4 +- .../domain/block/interactor/UpdateFields.kt | 8 +- .../block/interactor/UpdateLinkMarks.kt | 4 +- .../domain/block/interactor/UpdateText.kt | 6 +- .../block/interactor/UpdateTextColor.kt | 6 +- .../block/interactor/UpdateTextStyle.kt | 8 +- .../domain/block/interactor/UploadBlock.kt | 4 +- .../block/interactor/sets/AddObjectType.kt | 4 + .../interactor/sets/AddObjectTypeRelation.kt | 18 + .../block/interactor/sets/CreateObjectSet.kt | 34 + .../block/interactor/sets/CreateObjectType.kt | 39 + .../block/interactor/sets/GetObjectTypes.kt | 13 + .../block/interactor/sets/RemoveObjectType.kt | 2 + .../sets/RemoveObjectTypeRelation.kt | 18 + .../domain/block/repo/BlockRepository.kt | 125 +- .../anytype/domain/clipboard/Clipboard.kt | 2 +- .../anytype/domain/clipboard/Copy.kt | 17 +- .../anytype/domain/clipboard/Paste.kt | 20 +- .../anytypeio/anytype/domain/common/Alias.kt | 9 - .../anytype/domain/config/GetConfig.kt | 1 + .../cover/GradientCollectionProvider.kt | 3 +- .../anytype/domain/cover/RemoveDocCover.kt | 4 +- .../anytype/domain/cover/SetDocCoverColor.kt | 4 +- .../domain/cover/SetDocCoverGradient.kt | 4 +- .../anytype/domain/cover/SetDocCoverImage.kt | 8 +- .../dashboard/interactor/OpenDashboard.kt | 2 +- .../domain/dashboard/model/HomeDashboard.kt | 2 +- .../anytype/domain/database/DatabaseMock.kt | 4 - .../domain/database/model/DatabaseView.kt | 15 +- .../interactor/AddDataViewRelation.kt | 35 + .../interactor/AddDataViewRelationOption.kt | 40 + .../dataview/interactor/AddDataViewViewer.kt | 37 + .../interactor/AddDataViewViewerSort.kt | 32 + .../interactor/AddStatusToDataViewRecord.kt | 45 + .../interactor/AddTagToDataViewRecord.kt | 54 + .../interactor/CreateDataViewRecord.kt | 30 + .../domain/dataview/interactor/DebugSync.kt | 12 + .../interactor/DeleteDataViewViewer.kt | 36 + .../interactor/DuplicateDataViewViewer.kt | 36 + .../ModifyDataViewViewerRelationOrder.kt | 36 + .../dataview/interactor/ObjectRelationList.kt | 22 + .../interactor/RemoveTagFromDataViewRecord.kt | 50 + .../interactor/RenameDataViewViewer.kt | 36 + .../dataview/interactor/SearchObjects.kt | 36 + .../dataview/interactor/SetActiveViewer.kt | 39 + .../dataview/interactor/SetRelationKey.kt | 29 + .../interactor/UpdateDataViewRecord.kt | 36 + .../interactor/UpdateDataViewViewer.kt | 34 + .../anytype/domain/download/DownloadFile.kt | 2 +- .../anytype/domain/download/Downloader.kt | 2 +- .../anytypeio/anytype/domain/editor/Editor.kt | 2 +- .../domain/event/interactor/EventChannel.kt | 4 +- .../event/interactor/InterceptEvents.kt | 4 +- .../anytype/domain/event/model/Event.kt | 151 -- .../domain/icon/SetDocumentEmojiIcon.kt | 6 +- .../domain/icon/SetDocumentImageIcon.kt | 12 +- .../anytype/domain/misc/UrlBuilder.kt | 2 +- .../anytype/domain/object/UpdateDetail.kt | 23 + .../anytype/domain/page/ArchiveDocument.kt | 4 +- .../page/{ClosePage.kt => CloseBlock.kt} | 6 +- .../anytype/domain/page/CreateDocument.kt | 10 +- .../anytype/domain/page/CreateNewDocument.kt | 2 +- .../anytype/domain/page/CreateObject.kt | 62 + .../anytype/domain/page/CreatePage.kt | 2 +- .../anytypeio/anytype/domain/page/OpenPage.kt | 2 +- .../com/anytypeio/anytype/domain/page/Redo.kt | 6 +- .../com/anytypeio/anytype/domain/page/Undo.kt | 6 +- .../anytype/domain/page/UpdateTitle.kt | 4 +- .../domain/page/bookmark/SetupBookmark.kt | 6 +- .../domain/page/navigation/GetListPages.kt | 7 +- .../page/navigation/GetPageInfoWithLinks.kt | 12 +- .../relations/AddObjectRelationOption.kt | 35 + .../anytype/domain/sets/OpenObjectSet.kt | 10 + .../domain/status/InterceptThreadStatus.kt | 5 +- .../anytype/domain/status/SyncAccount.kt | 4 +- .../auth/CheckAuthorizationStatusTest.kt | 4 +- .../anytype/domain/auth/CreateAccountTest.kt | 4 +- .../domain/auth/GetCurrentAccountTest.kt | 10 +- .../domain/auth/ObserveAccountsTest.kt | 4 +- .../anytype/domain/auth/SetupWalletTest.kt | 2 +- .../anytype/domain/auth/StartAccountTest.kt | 4 +- .../block/interactor/RemoveLinkMarkTest.kt | 2 +- .../domain/block/interactor/SplitBlockTest.kt | 10 +- .../block/interactor/UpdateLinkMarksTest.kt | 2 +- .../anytype/domain/block/model/FieldsTest.kt | 2 +- .../domain/common/CoroutineTestRule.kt | 2 +- .../anytype/domain/common/MockDataFactory.kt | 2 +- .../domain/dashboard/OpenDashboardTest.kt | 6 +- .../database/interactor/DeleteDetailTest.kt | 2 +- .../database/interactor/HideDetailTest.kt | 2 +- .../anytype/domain/ext/BlockExtensionTest.kt | 5 +- .../domain/ext/BlockMentionUpdateTest.kt | 6 +- .../anytype/domain/markup/MarkupExtTest.kt | 8 +- .../anytype/domain/page/UnlinkTextTest.kt | 2 +- .../page/navigation/GetListPagesTest.kt | 5 +- .../navigation/GetPageInfoWithLinksTest.kt | 9 +- library-page-icon-picker-widget/build.gradle | 29 + middleware/build.gradle | 2 + .../middleware/block/BlockMiddleware.kt | 303 +++- .../middleware/config/DefaultConfigurator.kt | 8 +- .../converters/ClipboardSerializer.kt | 12 +- .../middleware/converters/MapperExtension.kt | 317 +--- .../middleware/converters/ToMiddleware.kt | 203 --- .../middleware/interactor/EventHandler.kt | 2 - .../middleware/interactor/Middleware.kt | 565 +++++-- .../interactor/MiddlewareEventChannel.kt | 27 +- .../interactor/MiddlewareEventMapper.kt | 215 ++- .../interactor/MiddlewareFactory.kt | 52 +- .../middleware/interactor/MiddlewareMapper.kt | 54 - .../anytype/middleware/mappers/Alias.kt | 41 + .../middleware/mappers/ToCoreModelMappers.kt | 484 ++++++ .../mappers/ToMiddlewareModelMappers.kt | 329 ++++ .../middleware/service/MiddlewareService.kt | 52 +- .../MiddlewareServiceImplementation.kt | 219 ++- .../anytype/MiddlewareEventChannelTest.kt | 76 +- .../com/anytypeio/anytype/MiddlewareTest.kt | 84 +- presentation/build.gradle | 32 + .../presentation/page/TurnIntoConstants.kt | 10 +- .../presentation/page/picker/AddBlockView.kt | 69 + .../page/picker/DocumentAddBlockViewModel.kt | 72 + .../auth/model/SelectAccountView.kt | 2 +- .../presentation/common/BaseListViewModel.kt | 9 + .../presentation/common/BaseViewModel.kt | 10 + .../presentation/desktop/DashboardView.kt | 13 +- .../desktop/HomeDashboardEventConverter.kt | 8 +- .../desktop/HomeDashboardStateMachine.kt | 13 +- .../desktop/HomeDashboardViewModel.kt | 43 +- .../presentation/extension/AnalyticsExt.kt | 5 +- .../extension/DashboardViewExtension.kt | 20 +- .../extension/FilterConditionExtension.kt | 142 ++ .../presentation/extension/FilterExtension.kt | 28 + .../presentation/extension/MapExtension.kt | 12 + .../presentation/extension/MarkupExtension.kt | 4 +- .../linking/LinkToObjectViewModel.kt | 4 +- .../presentation/mapper/MapperExtension.kt | 245 ++- .../presentation/moving/MoveToViewModel.kt | 4 +- .../presentation/navigation/AppNavigation.kt | 13 +- .../presentation/page/ControlPanelMachine.kt | 55 +- .../page/DocumentExternalEventReducer.kt | 53 +- .../anytype/presentation/page/Editor.kt | 2 + .../presentation/page/PageViewModel.kt | 242 ++- .../presentation/page/PageViewModelFactory.kt | 22 +- .../page/archive/ArchiveViewModel.kt | 23 +- .../page/archive/ArchiveViewModelFactory.kt | 9 +- .../page/cover/CoverImageHashProvider.kt | 5 +- .../page/cover/DocCoverGalleryView.kt | 4 +- .../page/cover/SelectDocCoverViewModel.kt | 40 +- .../cover/UploadDocCoverImageViewModel.kt | 8 +- .../presentation/page/editor/Command.kt | 16 +- .../page/editor/DetailModificator.kt | 185 +++ .../presentation/page/editor/Intent.kt | 12 +- .../presentation/page/editor/Interactor.kt | 4 +- .../presentation/page/editor/Orchestrator.kt | 18 +- .../anytype/presentation/page/editor/Proxy.kt | 4 +- .../anytype/presentation/page/editor/Store.kt | 10 +- .../page/editor/Transformation.kt | 8 +- .../presentation/page/editor/ViewState.kt | 2 +- .../page/editor/ext/BlockExtension.kt | 2 + .../page/editor/listener/ListenerType.kt | 6 + .../page/editor/model/BlockView.kt | 38 +- .../presentation/page/editor/model/Types.kt | 7 + .../presentation/page/editor/model/UiBlock.kt | 5 +- .../page/editor/styling/StyleConfig.kt | 9 +- .../presentation/page/model/TextUpdate.kt | 4 +- .../DocumentEmojiIconPickerViewModel.kt | 16 +- ...DocumentEmojiIconPickerViewModelFactory.kt | 11 +- .../picker/DocumentIconActionMenuViewModel.kt | 21 +- .../DocumentIconActionMenuViewModelFactory.kt | 11 +- .../page/render/BlockViewRenderer.kt | 8 +- .../page/render/DefaultBlockViewRenderer.kt | 95 +- .../page/selection/SelectionStateHolder.kt | 2 +- .../page/toggle/ToggleStateHolder.kt | 2 +- .../presentation/profile/ProfileView.kt | 2 +- .../AddObjectRelationObjectValueViewModel.kt | 195 +++ .../AddObjectRelationValueViewModel.kt | 544 +++++++ .../CreateDataViewRelationViewModel.kt | 30 + .../CreateDataViewRelationViewModelFactory.kt | 12 + .../relations/DocumentRelationView.kt | 51 + .../relations/ObjectRelationListViewModel.kt | 190 +++ .../ObjectRelationListViewModelFactory.kt | 33 + .../presentation/relations/ObjectSetConfig.kt | 37 + .../relations/ObjectSetRenderMapper.kt | 541 +++++++ .../relations/RelationExtensions.kt | 93 ++ .../RelationFileValueAddViewModel.kt | 191 +++ .../relations/RelationFormatView.kt | 8 + .../relations/RelationViewDescriptor.kt | 8 + .../relations/ViewerRelationsViewModel.kt | 134 ++ .../DataViewObjectRelationProvider.kt | 28 + .../providers/DataViewObjectValueProvider.kt | 32 + .../DefaultObjectRelationProvider.kt | 21 + .../providers/DefaultObjectValueProvider.kt | 21 + .../providers/ObjectDetailProvider.kt | 8 + .../providers/ObjectRelationProvider.kt | 10 + .../relations/providers/ObjectTypeProvider.kt | 7 + .../providers/ObjectValueProvider.kt | 9 + .../anytype/presentation/sets/CellAction.kt | 20 + .../sets/CreateDataViewViewerViewModel.kt | 74 + .../sets/CreateObjectSetObjectTypeView.kt | 8 + .../sets/CreateObjectSetViewModel.kt | 127 ++ .../presentation/sets/CreateObjectTypeView.kt | 11 + .../sets/CreateObjectTypeViewModel.kt | 51 + .../sets/DataViewViewerActionViewModel.kt | 82 + .../sets/EditDataViewViewerViewModel.kt | 91 ++ .../sets/ManageViewerViewModel.kt | 108 ++ .../sets/ObjectRelationDateValueViewModel.kt | 146 ++ .../sets/ObjectRelationTextValueViewModel.kt | 67 + .../sets/ObjectRelationValueViewModel.kt | 589 +++++++ .../anytype/presentation/sets/ObjectSet.kt | 39 + .../presentation/sets/ObjectSetCommand.kt | 72 + .../presentation/sets/ObjectSetExtension.kt | 55 + .../presentation/sets/ObjectSetRecordCache.kt | 14 + .../sets/ObjectSetRecordViewModel.kt | 67 + .../presentation/sets/ObjectSetReducer.kt | 208 +++ .../presentation/sets/ObjectSetSession.kt | 5 + .../presentation/sets/ObjectSetUiEvent.kt | 3 + .../presentation/sets/ObjectSetViewModel.kt | 508 ++++++ .../sets/ObjectSetViewModelFactory.kt | 49 + .../presentation/sets/ObjectSetViewState.kt | 9 + .../sets/SearchRelationViewModel.kt | 51 + .../sets/SelectFilterRelationViewModel.kt | 30 + .../sets/SelectSortRelationViewModel.kt | 67 + .../presentation/sets/SetsExtension.kt | 314 ++++ .../sets/ViewerCustomizeViewModel.kt | 52 + .../presentation/sets/ViewerSortByCommand.kt | 21 + .../sets/ViewerSortByViewModel.kt | 171 ++ .../sets/ViewerSortByViewState.kt | 9 + .../sets/filter/CreateFilterFlowViewModel.kt | 42 + .../sets/filter/CreateFilterView.kt | 53 + .../presentation/sets/filter/FilterClick.kt | 8 + .../sets/filter/FilterConditionView.kt | 8 + .../sets/filter/FilterViewModel.kt | 608 +++++++ .../filter/PickFilterConditionViewModel.kt | 52 + .../sets/filter/ViewerFilterCommand.kt | 26 + .../sets/filter/ViewerFilterViewModel.kt | 250 +++ .../sets/filter/ViewerFilterViewState.kt | 11 + .../presentation/sets/model/CellView.kt | 106 ++ .../presentation/sets/model/ColumnView.kt | 34 + .../anytype/presentation/sets/model/Filter.kt | 126 ++ .../presentation/sets/model/FilterView.kt | 165 ++ .../sets/model/SimpleRelationView.kt | 17 + .../presentation/sets/model/Sorting.kt | 11 + .../presentation/sets/model/SortingView.kt | 30 + .../presentation/sets/model/TagView.kt | 25 + .../anytype/presentation/sets/model/Viewer.kt | 217 +++ .../presentation/sets/model/ViewerTabView.kt | 7 + .../sets/sort/ModifyViewerSortViewModel.kt | 96 ++ .../sets/sort/ViewerSortViewModel.kt | 145 ++ .../anytype/presentation/util/Bridge.kt | 14 - .../anytype/presentation/util/Dispatcher.kt | 18 + .../presentation/page/TurnIntoConstants.kt | 39 + .../presentation/page/picker/AddBlockView.kt | 67 + .../page/picker/DocumentAddBlockViewModel.kt | 48 + .../anytype/presentation/MockBlockFactory.kt | 2 +- .../MockTypicalDocumentFactory.kt | 6 +- .../presentation/TypicalTwoRecordObjectSet.kt | 123 ++ .../extension/DashboardViewExtensionKtTest.kt | 2 +- .../extension/FilterConditionExtensionTest.kt | 1440 +++++++++++++++++ .../extension/MapExtensionKtTest.kt | 39 + .../home/DashboardDragAndDropTest.kt | 8 +- .../presentation/home/DashboardTestSetup.kt | 6 +- .../home/HomeDashboardViewModelTest.kt | 9 +- .../mapper/MapperExtensionKtTest.kt | 2 +- .../presentation/page/BlockReadModeTest.kt | 6 +- .../page/ControlPanelStateReducerTest.kt | 2 +- .../page/DefaultBlockViewRendererTest.kt | 12 +- .../page/DocumentExternalEventReducerTest.kt | 8 +- .../presentation/page/PageViewModelTest.kt | 96 +- .../page/editor/EditorAddBlockTest.kt | 6 +- .../page/editor/EditorBackButtonTest.kt | 4 +- .../editor/EditorBackspaceNestedDeleteTest.kt | 6 +- .../page/editor/EditorCheckboxTest.kt | 6 +- .../editor/EditorEmptySpaceInteractionTest.kt | 4 +- .../page/editor/EditorErrorMessageTest.kt | 2 +- .../editor/EditorEventSubscriptionTest.kt | 2 +- .../page/editor/EditorFocusTest.kt | 2 +- .../page/editor/EditorGranularChangeTest.kt | 6 +- .../page/editor/EditorListBlockTest.kt | 6 +- .../page/editor/EditorMentionTest.kt | 4 +- .../page/editor/EditorMenuTest.kt | 2 +- .../page/editor/EditorMergeTest.kt | 4 +- .../page/editor/EditorMoveToTest.kt | 2 +- .../page/editor/EditorMultiSelectModeTest.kt | 6 +- .../editor/EditorPresentationTestSetup.kt | 36 +- .../EditorQuickStartingScrollAndMoveTest.kt | 4 +- .../page/editor/EditorRelationBlockTest.kt | 138 ++ .../page/editor/EditorScrollAndMoveTest.kt | 6 +- .../page/editor/EditorStartupTest.kt | 2 +- .../page/editor/EditorTextUpdateTest.kt | 14 +- .../page/editor/EditorTitleAddBlockTest.kt | 6 +- .../page/editor/EditorTitleTest.kt | 2 +- .../page/editor/EditorTurnIntoTest.kt | 5 +- .../page/editor/StyleConfigKtTest.kt | 2 +- .../presentation/sets/MockObjectSetFactory.kt | 142 ++ .../presentation/sets/ObjectSetReducerTest.kt | 364 +++++ .../presentation/sets/TagAndStatusTests.kt | 310 ++++ .../sets/ViewerSortByViewModelTest.kt | 461 ++++++ .../main/ObjectSetAddOrUpdateViewerTest.kt | 241 +++ .../sets/main/ObjectSetHeaderTest.kt | 160 ++ .../main/ObjectSetSettingActiveViewerTest.kt | 215 +++ .../main/ObjectSetUpdateViewerSortTest.kt | 113 ++ .../sets/main/ObjectSetViewModelTestSetup.kt | 148 ++ .../sets/main/ObjectSetViewerFilterTest.kt | 129 ++ .../anytype/presentation/util/Alias.kt | 2 +- protocol/src/main/proto/changes.proto | 48 +- protocol/src/main/proto/commands.proto | 1192 +++++++++++--- protocol/src/main/proto/events.proto | 222 ++- protocol/src/main/proto/localstore.proto | 41 +- protocol/src/main/proto/models.proto | 41 +- protocol/src/main/proto/relation.proto | 123 ++ sample/build.gradle | 10 + settings.gradle | 3 +- 911 files changed, 51701 insertions(+), 3827 deletions(-) create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/editor/CreateRelationBlockTesting.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/editor/RelationBlockUITesting.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/AddRelationStatusValueTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/AddRelationTagValueTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayObjectRelationTextValueTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationObjectValueTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationStatusValueTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationTagValueTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/EditRelationTagValueTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/ObjectRelationDateValueTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/ObjectRelationListTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestAddObjectSetObjectRelationValueFragment.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectRelationDateValueFragment.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectRelationListFragment.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectRelationTextValueFragment.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectSetObjectRelationValueFragment.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridColumnRenderingTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridFileCellRenderingTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridObjectCellRenderingTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridPrimitiveRelationTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridTagCellRenderingTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetHeaderTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetFragment.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/ModifyInputValueFilterTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/ModifyStatusFilterTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/ModifyTagFilterTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/TestModifyFilterFromInputFieldValueFragment.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/TestModifyFilterFromSelectedValueFragment.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/sort/TestViewerSortFragment.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/features/sets/sort/ViewerObjectSortTest.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/utils/RecyclerViewItemCountAssertion.kt create mode 100644 app/src/androidTest/java/com/anytypeio/anytype/utils/espresso/ViewMatchers.kt create mode 100644 app/src/debug/google-services.json create mode 100644 app/src/debug/res/values/technical_strings.xml create mode 100644 app/src/experimental/java/com/anytypeio/anytype/ui/desktop/HomeDashboardFragment.kt create mode 100644 app/src/experimental/java/com/anytypeio/anytype/ui/page/sheets/DocMenuBottomSheet.kt create mode 100644 app/src/experimental/res/layout/fragment_desktop.xml create mode 100644 app/src/experimental/res/layout/fragment_doc_menu_bottom_sheet.xml create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/AddObjectRelationObjectValuDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/AddObjectRelationValueDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/CreateDataViewRelationDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/CreateDataViewViewerDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/CreateObjectSetDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/CreateObjectTypeDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/DataViewViewerActionDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/DocumentAddNewBlockDi.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/EditDataViewViewer.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/EditGridCellDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/ManageViewerDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/ModifyViewerSort.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationValueDi.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetRecordDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/RelationFileValueAddDi.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/SelectSortRelationDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/ViewerCustomizeDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/ViewerFilter.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/ViewerRelationListDi.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/ViewerSort.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/ViewerSortByDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/sets/CreateFilter.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/sets/ModifyFilter.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/sets/PickConditionDi.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/sets/SelectFilterRelationDI.kt delete mode 100644 app/src/main/java/com/anytypeio/anytype/di/main/BridgeModule.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/database/modals/ObjectRelationValueFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/RelationDefaultActionToolbar.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/RelationPlaceholderActionToolbar.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/relations/AddObjectRelationObjectValueFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/relations/AddObjectRelationValueFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/relations/CreateDataViewRelationFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationDateValueFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationListFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationTextValueFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/relations/RelationFileValueActionsFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/relations/RelationFileValueAddFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/CreateObjectSetFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/CreateObjectTypeFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/SetObjectSetRecordNameFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/ViewerFilterFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/ViewerSortByFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/BaseDialogListFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/CreateDataViewViewerFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/DataViewViewerActionFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/DatePickerFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/EditDataViewViewerFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ManageViewerFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/PickFilterConditionFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/PickSortingKeyFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/PickSortingTypeFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerBottomSheetRootFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerCustomizeFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerRelationOptionFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerRelationsFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFlow.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFlowRootFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFromInputFieldValueFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFromSelectedValueFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/ModifyFilterFromInputFieldValueFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/ModifyFilterFromSelectedValueFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/SelectFilterRelationFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/UpdateConditionActionReceiver.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/search/SearchRelationFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/sort/ModifyViewerSortFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/sort/SelectSortRelationFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/sets/modals/sort/ViewerSortFragment.kt create mode 100644 app/src/main/res/anim/slide_bottom.xml create mode 100644 app/src/main/res/anim/slide_up.xml create mode 100644 app/src/main/res/drawable/divider_sort_or_filter_relation.xml create mode 100644 app/src/main/res/drawable/ic_arrow_expand_dv_viewer.xml create mode 100644 app/src/main/res/drawable/ic_create_viewer_gallery.xml create mode 100644 app/src/main/res/drawable/ic_create_viewer_grid.xml create mode 100644 app/src/main/res/drawable/ic_create_viewer_kanban.xml create mode 100644 app/src/main/res/drawable/ic_create_viewer_list.xml create mode 100644 app/src/main/res/drawable/ic_data_view_viewer_delete.xml create mode 100644 app/src/main/res/drawable/ic_data_view_viewer_duplicate.xml create mode 100644 app/src/main/res/drawable/ic_data_view_viewer_edit.xml create mode 100644 app/src/main/res/drawable/ic_dv_relation_search.xml create mode 100644 app/src/main/res/drawable/ic_viewer_chosen.xml create mode 100644 app/src/main/res/drawable/rect_create_viewer_icon_background.xml create mode 100644 app/src/main/res/drawable/rect_dv_search_relation.xml create mode 100644 app/src/main/res/layout/add_object_relation_value_fragment.xml create mode 100644 app/src/main/res/layout/fragment_create_data_view_relation.xml create mode 100644 app/src/main/res/layout/fragment_create_data_view_viewer.xml create mode 100644 app/src/main/res/layout/fragment_create_filter_flow.xml create mode 100644 app/src/main/res/layout/fragment_create_object_type.xml create mode 100644 app/src/main/res/layout/fragment_create_or_update_filter.xml create mode 100644 app/src/main/res/layout/fragment_create_or_update_filter_input_field_value.xml create mode 100644 app/src/main/res/layout/fragment_create_set.xml create mode 100644 app/src/main/res/layout/fragment_data_view_viewer_actions.xml create mode 100644 app/src/main/res/layout/fragment_date_picker.xml create mode 100644 app/src/main/res/layout/fragment_edit_data_view_viewer.xml create mode 100644 app/src/main/res/layout/fragment_filter.xml create mode 100644 app/src/main/res/layout/fragment_list.xml create mode 100644 app/src/main/res/layout/fragment_manage_viewer.xml create mode 100644 app/src/main/res/layout/fragment_modify_viewer_sort.xml create mode 100644 app/src/main/res/layout/fragment_object_relation_date_value.xml create mode 100644 app/src/main/res/layout/fragment_object_relation_list.xml create mode 100644 app/src/main/res/layout/fragment_object_relation_text_value.xml create mode 100644 app/src/main/res/layout/fragment_object_set.xml create mode 100644 app/src/main/res/layout/fragment_relation_file_value_action.xml create mode 100644 app/src/main/res/layout/fragment_relation_value_file_add.xml create mode 100644 app/src/main/res/layout/fragment_relation_value_object_add.xml create mode 100644 app/src/main/res/layout/fragment_select_sort_or_filter_relation.xml create mode 100644 app/src/main/res/layout/fragment_set_object_set_record_name.xml create mode 100644 app/src/main/res/layout/fragment_sorting.xml create mode 100644 app/src/main/res/layout/fragment_viewer_bottom_sheet_root.xml create mode 100644 app/src/main/res/layout/fragment_viewer_customize.xml create mode 100644 app/src/main/res/layout/fragment_viewer_relation_option.xml create mode 100644 app/src/main/res/layout/fragment_viewer_relations_list.xml create mode 100644 app/src/main/res/layout/fragment_viewer_sort.xml create mode 100644 app/src/main/res/layout/item_desktop_object_set.xml create mode 100644 app/src/main/res/layout/object_relation_value_fragment.xml delete mode 100644 app/src/main/res/values/ic_app_back.xml create mode 100644 app/src/main/res/xml/fragment_object_set_scene.xml rename app/{ => src/release}/google-services.json (100%) create mode 100644 app/src/release/res/values/technical_strings.xml rename app/src/{main => stable}/java/com/anytypeio/anytype/ui/desktop/HomeDashboardFragment.kt (95%) create mode 100644 app/src/stable/java/com/anytypeio/anytype/ui/page/sheets/DocMenuBottomSheet.kt rename app/src/{main => stable}/res/layout/fragment_desktop.xml (100%) create mode 100644 app/src/stable/res/layout/fragment_doc_menu_bottom_sheet.xml create mode 100644 core-models/.gitignore create mode 100644 core-models/build.gradle create mode 100644 core-models/proguard-rules.pro create mode 100644 core-models/src/main/AndroidManifest.xml create mode 100644 core-models/src/main/java/com/anytypeio/anytype/core_models/Alias.kt rename {domain/src/main/java/com/anytypeio/anytype/domain/block/model => core-models/src/main/java/com/anytypeio/anytype/core_models}/Block.kt (72%) rename {domain/src/main/java/com/anytypeio/anytype/domain/block/model => core-models/src/main/java/com/anytypeio/anytype/core_models}/BlockSplitMode.kt (65%) rename {domain/src/main/java/com/anytypeio/anytype/domain/block/model => core-models/src/main/java/com/anytypeio/anytype/core_models}/Command.kt (94%) rename {domain/src/main/java/com/anytypeio/anytype/domain/config => core-models/src/main/java/com/anytypeio/anytype/core_models}/Config.kt (66%) create mode 100644 core-models/src/main/java/com/anytypeio/anytype/core_models/Event.kt create mode 100644 core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectType.kt rename {domain/src/main/java/com/anytypeio/anytype/domain/page/navigation => core-models/src/main/java/com/anytypeio/anytype/core_models}/PageLink.kt (76%) rename {domain/src/main/java/com/anytypeio/anytype/domain/event/model => core-models/src/main/java/com/anytypeio/anytype/core_models}/Payload.kt (69%) rename {domain/src/main/java/com/anytypeio/anytype/domain/block/model => core-models/src/main/java/com/anytypeio/anytype/core_models}/Position.kt (55%) create mode 100644 core-models/src/main/java/com/anytypeio/anytype/core_models/Relation.kt create mode 100644 core-models/src/main/java/com/anytypeio/anytype/core_models/Response.kt rename {domain/src/main/java/com/anytypeio/anytype/domain => core-models/src/main/java/com/anytypeio/anytype/core_models}/ext/BlockExt.kt (96%) create mode 100644 core-models/src/main/java/com/anytypeio/anytype/core_models/ext/DetailsExt.kt rename {domain/src/main/java/com/anytypeio/anytype/domain => core-models/src/main/java/com/anytypeio/anytype/core_models}/ext/MarkupExt.kt (98%) create mode 100644 core-models/src/main/java/com/anytypeio/anytype/core_models/ext/RelationExt.kt rename {domain/src/main/java/com/anytypeio/anytype/domain => core-models/src/main/java/com/anytypeio/anytype/core_models}/misc/Overlap.kt (87%) create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/AbstractAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/AbstractViewHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/ViewerGridAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/ViewerGridCellsAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/ViewerGridHeaderAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/ViewerListAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/ViewerModifyOrderAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/ViewerRelationsAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/ViewerTabItemAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/ViewerTitleAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/ViewerTypeAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/diff/CellViewDiffUtil.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/diff/ColumnViewDiffUtil.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/diff/GridRowDiffUtil.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/holders/DVGridCellDateHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/holders/DVGridCellDescriptionHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/holders/DVGridCellEmailHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/holders/DVGridCellFileHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/holders/DVGridCellNumberHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/holders/DVGridCellObjectHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/holders/DVGridCellPhoneHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/holders/DVGridCellStatusHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/holders/DVGridCellTagHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/holders/DVGridCellUrlHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/modals/FilterAddViewHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/modals/FilterByAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/modals/FilterDateViewHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/modals/FilterNumberViewHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/modals/FilterObjectViewHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/modals/FilterStatusViewHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/modals/FilterTagViewHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/modals/FilterTextViewHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/modals/FilterViewHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/modals/SortByAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/dataview/modals/TagAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/ext/EditorHolderExtensions.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/relations/RelationViewHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/DocumentRelationAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/ObjectRelationTextValueAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/RelationFileValueAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/RelationObjectValueAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/create/RelationFormatAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/holders/ObjectRelationBaseHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/holders/ObjectRelationObjectHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/holders/ObjectRelationTextHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/holders/RelationFileHolder.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/CreateFilterAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/CreateObjectTypeAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/CreateSetHeaderAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/CreateSetObjectTypeAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/FilterRelationPickerAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/ManageViewerAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/ObjectRelationValueAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/PickColumnAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/PickFilterColumnAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/PickFilterConditionAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/PickFilterOperatorAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/PickSortingTypeAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/RelationPickerAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/SearchRelationAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/SortingRelationPickerAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/sets/ViewerSortAdapter.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/layout/DividerVerticalItemDecoration.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/tools/BottomSheetSharedTransition.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/DoсIconWidget.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/GridCellFileItem.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/GridCellObjectItem.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/IconWidget.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconTextWidget.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconWidget.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/RelationObjectItem.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/text/StaticTextView.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/text/StatusWidget.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/text/TagWidget.kt create mode 100644 core-ui/src/main/res/color/selector_viewer_tab_text_color.xml create mode 100644 core-ui/src/main/res/drawable-xxxhdpi/ic_dv_filter_search.xml create mode 100644 core-ui/src/main/res/drawable-xxxhdpi/rect_dv_filter_search.xml create mode 100644 core-ui/src/main/res/drawable/checkbox_selected_relation_not_selected.xml create mode 100644 core-ui/src/main/res/drawable/checkbox_selected_relation_selected.xml create mode 100644 core-ui/src/main/res/drawable/checkbox_selected_relation_selector.xml create mode 100644 core-ui/src/main/res/drawable/circle_cell_action.xml create mode 100644 core-ui/src/main/res/drawable/circle_grid_cell_profile.xml create mode 100644 core-ui/src/main/res/drawable/circle_object_icon_emoji_background.xml create mode 100644 core-ui/src/main/res/drawable/cursor.xml create mode 100644 core-ui/src/main/res/drawable/decoration_viewer_sort.xml create mode 100644 core-ui/src/main/res/drawable/divider_filter_edit.xml create mode 100644 core-ui/src/main/res/drawable/divider_filter_list.xml create mode 100644 core-ui/src/main/res/drawable/divider_relation_layer.xml create mode 100644 core-ui/src/main/res/drawable/divider_relations.xml create mode 100644 core-ui/src/main/res/drawable/dragger.xml create mode 100644 core-ui/src/main/res/drawable/ic_add_24.xml create mode 100644 core-ui/src/main/res/drawable/ic_add_block_or_turn_into_relation.xml create mode 100644 core-ui/src/main/res/drawable/ic_add_new_object_to_set.xml create mode 100644 core-ui/src/main/res/drawable/ic_add_new_relation.xml create mode 100644 core-ui/src/main/res/drawable/ic_arrow_back.xml create mode 100644 core-ui/src/main/res/drawable/ic_arrow_forward_24.xml create mode 100644 core-ui/src/main/res/drawable/ic_block_relation_placeholder.xml create mode 100644 core-ui/src/main/res/drawable/ic_cell_relation_call_with.xml create mode 100644 core-ui/src/main/res/drawable/ic_cell_relation_go_to_link.xml create mode 100644 core-ui/src/main/res/drawable/ic_cell_relation_go_to_mail_client.xml rename core-ui/src/main/res/drawable/{ic_multi_select_more.xml => ic_circle_more.xml} (100%) create mode 100644 core-ui/src/main/res/drawable/ic_delete_option.xml create mode 100644 core-ui/src/main/res/drawable/ic_dnd.xml create mode 100644 core-ui/src/main/res/drawable/ic_dragger_modify_relation_order.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_add_new_view.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_customize_panel_filter.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_customize_panel_group.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_customize_panel_relations.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_customize_panel_sort.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_customize_view.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_edit_view.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_gallery_view.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_kanban_view.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_list_view.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_manage_view_dnd_dragger.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_modal_plus.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_plus.xml create mode 100644 core-ui/src/main/res/drawable/ic_dv_table_view.xml create mode 100644 core-ui/src/main/res/drawable/ic_ellipse_small.xml create mode 100644 core-ui/src/main/res/drawable/ic_list_arrow.xml create mode 100644 core-ui/src/main/res/drawable/ic_manage_dv_viewer_gallery.xml create mode 100644 core-ui/src/main/res/drawable/ic_manage_dv_viewer_grid.xml create mode 100644 core-ui/src/main/res/drawable/ic_manage_dv_viewer_kanban.xml create mode 100644 core-ui/src/main/res/drawable/ic_manage_dv_viewer_list.xml create mode 100644 core-ui/src/main/res/drawable/ic_object_relation_format_date.xml create mode 100644 core-ui/src/main/res/drawable/ic_object_relation_format_emoji.xml create mode 100644 core-ui/src/main/res/drawable/ic_object_relation_format_number.xml create mode 100644 core-ui/src/main/res/drawable/ic_object_relation_format_object.xml create mode 100644 core-ui/src/main/res/drawable/ic_object_relation_format_text.xml create mode 100644 core-ui/src/main/res/drawable/ic_object_relation_format_title.xml create mode 100644 core-ui/src/main/res/drawable/ic_open_to_edit.xml create mode 100644 core-ui/src/main/res/drawable/ic_option_add.xml create mode 100644 core-ui/src/main/res/drawable/ic_option_checked.xml create mode 100644 core-ui/src/main/res/drawable/ic_plus_white.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_attachment_32.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_attachment_48.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_checkbox_32.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_checkbox_48.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_date_32.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_date_48.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_email_32.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_email_48.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_name_32.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_name_48.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_number_32.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_number_48.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_object_32.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_object_48.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_phone_number_32.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_phone_number_48.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_status_32.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_status_48.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_tag_32.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_tag_48.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_text_32.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_text_48.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_url_32.xml create mode 100644 core-ui/src/main/res/drawable/ic_relation_url_48.xml create mode 100644 core-ui/src/main/res/drawable/ic_remove.xml create mode 100644 core-ui/src/main/res/drawable/ic_remove_red.xml create mode 100644 core-ui/src/main/res/drawable/ic_remove_viewer_sort.xml create mode 100644 core-ui/src/main/res/drawable/ic_set_big.xml create mode 100644 core-ui/src/main/res/drawable/ic_tag_not_selected.xml create mode 100644 core-ui/src/main/res/drawable/ic_tag_selected.xml create mode 100644 core-ui/src/main/res/drawable/ic_tag_selected_selector.xml create mode 100644 core-ui/src/main/res/drawable/ic_viewer_chosen.xml create mode 100644 core-ui/src/main/res/drawable/ic_viewer_sort_arrow.xml create mode 100644 core-ui/src/main/res/drawable/item_viewer_item_selected_background.xml create mode 100644 core-ui/src/main/res/drawable/item_viewer_list_selected.xml create mode 100644 core-ui/src/main/res/drawable/rect_data_view_customize_view_button.xml create mode 100644 core-ui/src/main/res/drawable/rect_dv_cell_tag_item.xml create mode 100644 core-ui/src/main/res/drawable/rect_dv_manage_viewer_background_active.xml create mode 100644 core-ui/src/main/res/drawable/rect_dv_manage_viewer_background_selector.xml create mode 100644 core-ui/src/main/res/drawable/rect_dv_manage_viewers.xml create mode 100644 core-ui/src/main/res/drawable/rect_orange_action_button.xml create mode 100644 core-ui/src/main/res/drawable/rect_relation_tag.xml create mode 100644 core-ui/src/main/res/drawable/rectangle_avatar_initial_background.xml create mode 100644 core-ui/src/main/res/drawable/rectangle_item_relation_format_selected.xml create mode 100644 core-ui/src/main/res/drawable/rectangle_item_relation_format_selector.xml create mode 100644 core-ui/src/main/res/drawable/rectangle_modifier_viewer_relation_order_divider.xml create mode 100644 core-ui/src/main/res/drawable/rectangle_modify_viewer_relation_order_dnd.xml create mode 100644 core-ui/src/main/res/drawable/rectangle_object_icon_emoji_background.xml create mode 100644 core-ui/src/main/res/drawable/rectangle_relation_icon_container_background.xml create mode 100644 core-ui/src/main/res/drawable/rounded_rectangle_data_view_action.xml create mode 100644 core-ui/src/main/res/drawable/sort_divider.xml create mode 100644 core-ui/src/main/res/drawable/viewer_relation_switch_thumb.xml create mode 100644 core-ui/src/main/res/drawable/viewer_relation_switch_track.xml create mode 100644 core-ui/src/main/res/layout/item_add_block_or_turn_into_object_type.xml create mode 100644 core-ui/src/main/res/layout/item_block_relation_default.xml create mode 100644 core-ui/src/main/res/layout/item_block_relation_file.xml create mode 100644 core-ui/src/main/res/layout/item_block_relation_object.xml create mode 100644 core-ui/src/main/res/layout/item_block_relation_placeholder.xml create mode 100644 core-ui/src/main/res/layout/item_block_relation_status.xml create mode 100644 core-ui/src/main/res/layout/item_block_relation_tag.xml create mode 100644 core-ui/src/main/res/layout/item_create_data_view_relation_relation_format.xml create mode 100644 core-ui/src/main/res/layout/item_create_filter_date.xml create mode 100644 core-ui/src/main/res/layout/item_create_filter_object.xml create mode 100644 core-ui/src/main/res/layout/item_create_filter_status.xml create mode 100644 core-ui/src/main/res/layout/item_create_filter_tag.xml create mode 100644 core-ui/src/main/res/layout/item_create_set_header.xml create mode 100644 core-ui/src/main/res/layout/item_create_set_object_type.xml create mode 100644 core-ui/src/main/res/layout/item_document_relation_default.xml create mode 100644 core-ui/src/main/res/layout/item_document_relation_file.xml create mode 100644 core-ui/src/main/res/layout/item_document_relation_object.xml create mode 100644 core-ui/src/main/res/layout/item_document_relation_status.xml create mode 100644 core-ui/src/main/res/layout/item_document_relation_tag.xml create mode 100644 core-ui/src/main/res/layout/item_dv_manage_viewer.xml create mode 100644 core-ui/src/main/res/layout/item_dv_viewer_filter_add.xml create mode 100644 core-ui/src/main/res/layout/item_dv_viewer_filter_condition.xml create mode 100644 core-ui/src/main/res/layout/item_dv_viewer_filter_date.xml create mode 100644 core-ui/src/main/res/layout/item_dv_viewer_filter_number.xml create mode 100644 core-ui/src/main/res/layout/item_dv_viewer_filter_object.xml create mode 100644 core-ui/src/main/res/layout/item_dv_viewer_filter_status.xml create mode 100644 core-ui/src/main/res/layout/item_dv_viewer_filter_tag.xml create mode 100644 core-ui/src/main/res/layout/item_dv_viewer_filter_tag_value.xml create mode 100644 core-ui/src/main/res/layout/item_dv_viewer_filter_text.xml create mode 100644 core-ui/src/main/res/layout/item_dv_viewer_tab.xml create mode 100644 core-ui/src/main/res/layout/item_dv_viewer_tab_group.xml create mode 100644 core-ui/src/main/res/layout/item_dv_viewer_tab_plus.xml create mode 100644 core-ui/src/main/res/layout/item_edit_cell_file.xml create mode 100644 core-ui/src/main/res/layout/item_edit_cell_object.xml create mode 100644 core-ui/src/main/res/layout/item_edit_cell_option_create.xml create mode 100644 core-ui/src/main/res/layout/item_edit_cell_status.xml create mode 100644 core-ui/src/main/res/layout/item_edit_cell_tag.xml create mode 100644 core-ui/src/main/res/layout/item_edit_cell_tag_or_status_empty.xml create mode 100644 core-ui/src/main/res/layout/item_filter_condition.xml create mode 100644 core-ui/src/main/res/layout/item_filter_operator.xml create mode 100644 core-ui/src/main/res/layout/item_grid_column_header.xml create mode 100644 core-ui/src/main/res/layout/item_grid_column_header_plus.xml create mode 100644 core-ui/src/main/res/layout/item_list_base.xml create mode 100644 core-ui/src/main/res/layout/item_modify_viewer_relation_order.xml create mode 100644 core-ui/src/main/res/layout/item_object_relation_text.xml create mode 100644 core-ui/src/main/res/layout/item_object_type.xml create mode 100644 core-ui/src/main/res/layout/item_search_relation.xml create mode 100644 core-ui/src/main/res/layout/item_sorting_key.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_container.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_grid.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_grid_cell_description.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_grid_cell_file.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_grid_cell_object.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_grid_cell_tag.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_grid_column.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_grid_row.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_grid_row_title.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_relation_list.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_sort.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_sort_add.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_sort_apply.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_sort_set.xml create mode 100644 core-ui/src/main/res/layout/item_viewer_title.xml create mode 100644 core-ui/src/main/res/layout/layout_object_set_header.xml create mode 100644 core-ui/src/main/res/layout/modal_item_filter_tag_value.xml rename core-ui/src/main/res/layout/{modal_item_properties.xml => modal_item_relations.xml} (72%) create mode 100644 core-ui/src/main/res/layout/viewer_cell.xml create mode 100644 core-ui/src/main/res/layout/widget_data_view_customize_view.xml create mode 100644 core-ui/src/main/res/layout/widget_dv_grid_file.xml create mode 100644 core-ui/src/main/res/layout/widget_dv_grid_object.xml create mode 100644 core-ui/src/main/res/layout/widget_icon.xml create mode 100644 core-ui/src/main/res/layout/widget_object_icon.xml create mode 100644 core-ui/src/main/res/layout/widget_object_icon_text.xml create mode 100644 core-ui/src/main/res/layout/widget_relation_object.xml create mode 100644 core-ui/src/main/res/menu/menu_edit_grid_cell_email.xml create mode 100644 core-ui/src/main/res/menu/menu_edit_grid_cell_phone.xml create mode 100644 core-ui/src/main/res/menu/menu_edit_grid_cell_url.xml create mode 100644 core-ui/src/test/java/com/anytypeio/anytype/core_ui/features/dv/CellViewDiffUtilTest.kt create mode 100644 core-ui/src/test/java/com/anytypeio/anytype/core_ui/features/dv/ColumnViewDiffUtilTest.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/OnSwipeListener.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/const/DetailsKeys.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/di/scope/PerDialog.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/diff/DefaultDiffUtil.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/diff/DefaultObjectDiffIdentifier.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/DateUtils.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/text/ActionDoneListener.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/tools/Tools.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/BaseDialogFragment.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/DragAndDropViewHolder.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/ItemTouchHelperViewHolder.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/NonScrollLinearLayoutManager.kt create mode 100644 core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/OnStartDragListener.kt delete mode 100644 data/src/main/java/com/anytypeio/anytype/data/auth/model/BlockEntity.kt delete mode 100644 data/src/main/java/com/anytypeio/anytype/data/auth/model/CommandEntity.kt delete mode 100644 data/src/main/java/com/anytypeio/anytype/data/auth/model/ConfigEntity.kt delete mode 100644 data/src/main/java/com/anytypeio/anytype/data/auth/model/EventEntity.kt delete mode 100644 data/src/main/java/com/anytypeio/anytype/data/auth/model/Navigation.kt delete mode 100644 data/src/main/java/com/anytypeio/anytype/data/auth/model/PayloadEntity.kt delete mode 100644 data/src/main/java/com/anytypeio/anytype/data/auth/model/PositionEntity.kt delete mode 100644 data/src/main/java/com/anytypeio/anytype/data/auth/model/Response.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/block/interactor/sets/AddObjectType.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/block/interactor/sets/AddObjectTypeRelation.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/block/interactor/sets/CreateObjectSet.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/block/interactor/sets/CreateObjectType.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/block/interactor/sets/GetObjectTypes.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/block/interactor/sets/RemoveObjectType.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/block/interactor/sets/RemoveObjectTypeRelation.kt delete mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/common/Alias.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/AddDataViewRelation.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/AddDataViewRelationOption.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/AddDataViewViewer.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/AddDataViewViewerSort.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/AddStatusToDataViewRecord.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/AddTagToDataViewRecord.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/CreateDataViewRecord.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/DebugSync.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/DeleteDataViewViewer.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/DuplicateDataViewViewer.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/ModifyDataViewViewerRelationOrder.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/ObjectRelationList.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/RemoveTagFromDataViewRecord.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/RenameDataViewViewer.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/SearchObjects.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/SetActiveViewer.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/SetRelationKey.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/UpdateDataViewRecord.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/dataview/interactor/UpdateDataViewViewer.kt delete mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/event/model/Event.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/object/UpdateDetail.kt rename domain/src/main/java/com/anytypeio/anytype/domain/page/{ClosePage.kt => CloseBlock.kt} (85%) create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/page/CreateObject.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/relations/AddObjectRelationOption.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/sets/OpenObjectSet.kt delete mode 100644 middleware/src/main/java/com/anytypeio/anytype/middleware/converters/ToMiddleware.kt delete mode 100644 middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareMapper.kt create mode 100644 middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/Alias.kt create mode 100644 middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt create mode 100644 middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToMiddlewareModelMappers.kt rename presentation/src/{main => experimental}/java/com/anytypeio/anytype/presentation/page/TurnIntoConstants.kt (76%) create mode 100644 presentation/src/experimental/java/com/anytypeio/anytype/presentation/page/picker/AddBlockView.kt create mode 100644 presentation/src/experimental/java/com/anytypeio/anytype/presentation/page/picker/DocumentAddBlockViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/common/BaseListViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/common/BaseViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/extension/FilterConditionExtension.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/extension/FilterExtension.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/extension/MapExtension.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/page/editor/DetailModificator.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/AddObjectRelationObjectValueViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/AddObjectRelationValueViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/CreateDataViewRelationViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/CreateDataViewRelationViewModelFactory.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/DocumentRelationView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ObjectRelationListViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ObjectRelationListViewModelFactory.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ObjectSetConfig.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ObjectSetRenderMapper.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationExtensions.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationFileValueAddViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationFormatView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationViewDescriptor.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ViewerRelationsViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/DataViewObjectRelationProvider.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/DataViewObjectValueProvider.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/DefaultObjectRelationProvider.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/DefaultObjectValueProvider.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/ObjectDetailProvider.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/ObjectRelationProvider.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/ObjectTypeProvider.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/ObjectValueProvider.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/CellAction.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/CreateDataViewViewerViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/CreateObjectSetObjectTypeView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/CreateObjectSetViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/CreateObjectTypeView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/CreateObjectTypeViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/DataViewViewerActionViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/EditDataViewViewerViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ManageViewerViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectRelationDateValueViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectRelationTextValueViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectRelationValueViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSet.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetCommand.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetRecordCache.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetRecordViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetReducer.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetSession.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetUiEvent.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewModelFactory.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetViewState.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/SearchRelationViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/SelectFilterRelationViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/SelectSortRelationViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/SetsExtension.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ViewerCustomizeViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ViewerSortByCommand.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ViewerSortByViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ViewerSortByViewState.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/CreateFilterFlowViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/CreateFilterView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/FilterClick.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/FilterConditionView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/FilterViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/PickFilterConditionViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/ViewerFilterCommand.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/ViewerFilterViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/filter/ViewerFilterViewState.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/CellView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/ColumnView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/Filter.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/FilterView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/SimpleRelationView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/Sorting.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/SortingView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/TagView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/Viewer.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/ViewerTabView.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/sort/ModifyViewerSortViewModel.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/sets/sort/ViewerSortViewModel.kt delete mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/util/Bridge.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/util/Dispatcher.kt create mode 100644 presentation/src/stable/java/com/anytypeio/anytype/presentation/page/TurnIntoConstants.kt create mode 100644 presentation/src/stable/java/com/anytypeio/anytype/presentation/page/picker/AddBlockView.kt create mode 100644 presentation/src/stable/java/com/anytypeio/anytype/presentation/page/picker/DocumentAddBlockViewModel.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/TypicalTwoRecordObjectSet.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/extension/FilterConditionExtensionTest.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/extension/MapExtensionKtTest.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/page/editor/EditorRelationBlockTest.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/sets/MockObjectSetFactory.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/sets/ObjectSetReducerTest.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/sets/TagAndStatusTests.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/sets/ViewerSortByViewModelTest.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetAddOrUpdateViewerTest.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetHeaderTest.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetSettingActiveViewerTest.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetUpdateViewerSortTest.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewModelTestSetup.kt create mode 100644 presentation/src/test/java/com/anytypeio/anytype/presentation/sets/main/ObjectSetViewerFilterTest.kt create mode 100644 protocol/src/main/proto/relation.proto diff --git a/analytics/src/main/java/com/anytypeio/anytype/analytics/base/EventsDictionary.kt b/analytics/src/main/java/com/anytypeio/anytype/analytics/base/EventsDictionary.kt index 789444e061..47067b473a 100644 --- a/analytics/src/main/java/com/anytypeio/anytype/analytics/base/EventsDictionary.kt +++ b/analytics/src/main/java/com/anytypeio/anytype/analytics/base/EventsDictionary.kt @@ -63,6 +63,7 @@ object EventsDictionary { const val ACCOUNT_STOP = "AccountStop" const val PAGE_CREATE = "BlockCreatePage" + const val OBJECT_CREATE = "BlockCreateObject" const val PAGE_MENTION_CREATE = "PageCreate" const val BLOCK_CREATE = "BlockCreate" diff --git a/app/build.gradle b/app/build.gradle index be8e991228..377bca7dcc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,10 +49,40 @@ android { } debug { + applicationIdSuffix ".debug" debuggable true } } + flavorDimensions 'default' + productFlavors { + stable { + dimension 'default' + } + experimental{ + dimension 'default' + } + } + + sourceSets { + stable { + java { + srcDirs 'src/stable/java' + } + res { + srcDirs 'src/stable/res' + } + } + experimental { + java { + srcDirs 'src/experimental/java' + } + res { + srcDirs 'src/experimental/res' + } + } + } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -74,6 +104,7 @@ ext { dependencies { implementation project(':domain') + implementation project(':core-models') implementation project(':data') implementation project(':device') implementation project(':persistence') @@ -135,6 +166,7 @@ dependencies { implementation applicationDependencies.exoPlayer implementation analyticsDependencies.amplitude + implementation analyticsDependencies.okhttp implementation applicationDependencies.blurry implementation applicationDependencies.shimmerLayout @@ -158,7 +190,10 @@ dependencies { androidTestImplementation unitTestDependencies.kotlinTest androidTestImplementation acceptanceTesting.testRules androidTestImplementation acceptanceTesting.disableAnimation - androidTestImplementation unitTestDependencies.coroutineTesting + + androidTestImplementation(unitTestDependencies.coroutineTesting) { + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" + } debugImplementation acceptanceTesting.fragmentTesting diff --git a/app/gradle.properties b/app/gradle.properties index 9b142f23b2..457c76a46b 100644 --- a/app/gradle.properties +++ b/app/gradle.properties @@ -1,3 +1,3 @@ version.versionMajor=0 version.versionMinor=1 -version.versionPatch=7 +version.versionPatch=6 diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/auth/SetupSelectedAccountTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/auth/SetupSelectedAccountTest.kt index c50926d04b..7f3828076c 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/auth/SetupSelectedAccountTest.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/auth/SetupSelectedAccountTest.kt @@ -11,6 +11,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.anytypeio.anytype.R +import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.domain.auth.interactor.StartAccount import com.anytypeio.anytype.domain.auth.repo.AuthRepository import com.anytypeio.anytype.domain.device.PathProvider @@ -50,6 +51,9 @@ class SetupSelectedAccountTest { @Mock lateinit var authRepository: AuthRepository + @Mock + lateinit var analytics: Analytics + @Mock lateinit var pathProvider: PathProvider @@ -62,7 +66,8 @@ class SetupSelectedAccountTest { TestSetupSelectedAccountFragment.testViewModelFactory = SetupSelectedAccountViewModelFactory( startAccount = startAccount, - pathProvider = pathProvider + pathProvider = pathProvider, + analytics = analytics ) } diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ClipboardTesting.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ClipboardTesting.kt index 5de84b8767..64896fa5dc 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ClipboardTesting.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ClipboardTesting.kt @@ -14,16 +14,13 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.* import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget import com.anytypeio.anytype.data.auth.model.ClipEntity -import com.anytypeio.anytype.domain.block.model.Block -import com.anytypeio.anytype.domain.block.model.Command -import com.anytypeio.anytype.domain.clipboard.Paste -import com.anytypeio.anytype.domain.event.model.Event -import com.anytypeio.anytype.domain.event.model.Payload import com.anytypeio.anytype.features.editor.base.EditorTestSetup import com.anytypeio.anytype.features.editor.base.TestPageFragment import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.page.PageViewModel import com.anytypeio.anytype.ui.page.PageFragment import com.anytypeio.anytype.utils.CoroutinesTestRule import com.anytypeio.anytype.utils.TestUtils.withRecyclerView @@ -190,7 +187,7 @@ class ClipboardTesting : EditorTestSetup() { } repo.stub { - onBlocking { paste(any()) } doReturn Paste.Response( + onBlocking { paste(any()) } doReturn Response.Clipboard.Paste( cursor = 6, payload = Payload( context = root, @@ -202,6 +199,7 @@ class ClipboardTesting : EditorTestSetup() { } stubInterceptEvents() + stubInterceptThreadStatus( ) stubOpenDocument(document) stubUpdateText() @@ -209,7 +207,7 @@ class ClipboardTesting : EditorTestSetup() { // TESTING - val target = onView(withRecyclerView(R.id.recycler).atPositionOnView(1, view)) + val target = onView(withRecyclerView(R.id.recycler).atPositionOnView(0, view)) // Click to open action mode @@ -229,7 +227,7 @@ class ClipboardTesting : EditorTestSetup() { } scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(1) + val item = fragment.recycler.getChildAt(0) item.findViewById(view).apply { assertEquals(expected = result.length, actual = selectionStart) assertEquals(expected = result.length, actual = selectionEnd) @@ -255,6 +253,8 @@ class ClipboardTesting : EditorTestSetup() { } ) } + + advance(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION) } //endregion @@ -464,7 +464,7 @@ class ClipboardTesting : EditorTestSetup() { } repo.stub { - onBlocking { paste(any()) } doReturn Paste.Response( + onBlocking { paste(any()) } doReturn Response.Clipboard.Paste( cursor = -1, payload = Payload( context = root, @@ -476,6 +476,7 @@ class ClipboardTesting : EditorTestSetup() { } stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() @@ -483,7 +484,7 @@ class ClipboardTesting : EditorTestSetup() { // TESTING - val target = onView(withRecyclerView(R.id.recycler).atPositionOnView(1, targetBlockView)) + val target = onView(withRecyclerView(R.id.recycler).atPositionOnView(0, targetBlockView)) // Click to open action mode @@ -517,21 +518,21 @@ class ClipboardTesting : EditorTestSetup() { ) } - onView(withRecyclerView(R.id.recycler).atPositionOnView(1, targetBlockView)).apply { + onView(withRecyclerView(R.id.recycler).atPositionOnView(0, targetBlockView)).apply { check(matches(withText(text))) } - onView(withRecyclerView(R.id.recycler).atPositionOnView(2, firstPastedBlockView)).apply { + onView(withRecyclerView(R.id.recycler).atPositionOnView(1, firstPastedBlockView)).apply { check(matches(withText(pasted.first))) } - onView(withRecyclerView(R.id.recycler).atPositionOnView(3, secondPastedBlockView)).apply { + onView(withRecyclerView(R.id.recycler).atPositionOnView(2, secondPastedBlockView)).apply { check(matches(withText(pasted.second))) check(matches(hasFocus())) } scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(3) + val item = fragment.recycler.getChildAt(2) item.findViewById(secondPastedBlockView).apply { assertEquals(expected = pasted.second.length, actual = selectionStart) assertEquals(expected = pasted.second.length, actual = selectionEnd) diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/CreateBlockTesting.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/CreateBlockTesting.kt index 2268465c4e..0879fa8e51 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/CreateBlockTesting.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/CreateBlockTesting.kt @@ -11,13 +11,13 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Event +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_models.Position import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget import com.anytypeio.anytype.domain.base.Either import com.anytypeio.anytype.domain.block.interactor.CreateBlock -import com.anytypeio.anytype.domain.block.model.Block -import com.anytypeio.anytype.domain.block.model.Position -import com.anytypeio.anytype.domain.event.model.Event -import com.anytypeio.anytype.domain.event.model.Payload import com.anytypeio.anytype.features.editor.base.EditorTestSetup import com.anytypeio.anytype.features.editor.base.TestPageFragment import com.anytypeio.anytype.mocking.MockDataFactory @@ -171,6 +171,7 @@ class CreateBlockTesting : EditorTestSetup() { ) stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() stubCreateBlocks(params, new, events) @@ -180,7 +181,7 @@ class CreateBlockTesting : EditorTestSetup() { // TESTING val target = Espresso.onView( - TestUtils.withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + TestUtils.withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ) target.apply { @@ -196,13 +197,13 @@ class CreateBlockTesting : EditorTestSetup() { verifyBlocking(createBlock, times(1)) { invoke(params) } Espresso.onView( - TestUtils.withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + TestUtils.withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText(""))) } Espresso.onView( - TestUtils.withRecyclerView(R.id.recycler).atPositionOnView(2, R.id.textContent) + TestUtils.withRecyclerView(R.id.recycler).atPositionOnView(1, R.id.textContent) ).apply { check(ViewAssertions.matches(ViewMatchers.withText(""))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -211,7 +212,7 @@ class CreateBlockTesting : EditorTestSetup() { // Check cursor position at block B scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(2) + val item = fragment.recycler.getChildAt(1) item.findViewById(R.id.textContent).apply { assertEquals( expected = 0, diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/CreateRelationBlockTesting.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/CreateRelationBlockTesting.kt new file mode 100644 index 0000000000..6c64b6295d --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/CreateRelationBlockTesting.kt @@ -0,0 +1,349 @@ +package com.anytypeio.anytype.features.editor + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.action.ViewActions.click +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.Event +import com.anytypeio.anytype.core_models.Position +import com.anytypeio.anytype.core_models.ext.content +import com.anytypeio.anytype.domain.block.interactor.CreateBlock +import com.anytypeio.anytype.emojifier.data.DefaultDocumentEmojiIconProvider +import com.anytypeio.anytype.features.editor.base.EditorTestSetup +import com.anytypeio.anytype.features.editor.base.TestPageFragment +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.page.editor.model.UiBlock +import com.anytypeio.anytype.ui.page.PageFragment +import com.anytypeio.anytype.utils.* +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class CreateRelationBlockTesting : EditorTestSetup() { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + private val args = bundleOf(PageFragment.ID_KEY to root) + + private val defaultDetails = Block.Details( + mapOf( + root to Block.Fields( + mapOf( + "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random() + ) + ) + ) + ) + + private val title = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Text( + style = Block.Content.Text.Style.TITLE, + text = "CreateRelationBlockTesting", + marks = emptyList() + ), + children = emptyList(), + fields = Block.Fields.empty() + ) + + private val header = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Layout( + type = Block.Content.Layout.Type.HEADER + ), + fields = Block.Fields.empty(), + children = listOf(title.id) + ) + + @Before + override fun setup() { + super.setup() + } + + @Test + fun shouldAddNewRelationBlockPlaceholderWithOnCreateAfterOnAddRelationCommand() { + + val new = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.RelationBlock( + key = null + ), + children = emptyList(), + fields = Block.Fields.empty() + ) + + val paragraph = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.Text( + text = "Foo", + marks = emptyList(), + style = Block.Content.Text.Style.P + ) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, paragraph.id) + ) + + val document = listOf(page, header, title, paragraph) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument(document, defaultDetails) + stubCreateBlock( + params = CreateBlock.Params( + context = root, + target = paragraph.id, + position = Position.BOTTOM, + prototype = Block.Prototype.Relation("") + ), + events = listOf( + Event.Command.UpdateStructure( + context = root, + id = root, + children = page.children + listOf(new.id) + ), + Event.Command.AddBlock( + context = root, + blocks = listOf(new) + ) + ) + ) + + // TESTING + + val fragment = launchFragment(args) + + with(R.id.recycler.rVMatcher()) { + checkIsRecyclerSize(2) + onItemView(0, R.id.title).checkHasText(title.content().text) + onItemView( + 1, + R.id.textContent + ).checkHasText(paragraph.content().text) + } + + with(R.id.recycler.rVMatcher()) { + onItemView(1, R.id.textContent).perform(click()) + } + + Thread.sleep(200) + + fragment.onFragment { fr -> + fr.onAddBlockClicked(UiBlock.RELATION) + } + + Thread.sleep(200) + + with(R.id.recycler.rVMatcher()) { + checkIsRecyclerSize(3) + onItemView(0, R.id.title).checkHasText(title.content().text) + onItemView( + 1, + R.id.textContent + ).checkHasText(paragraph.content().text) + onItemView(2, R.id.tvPlaceholder).checkHasText(R.string.set_new_relation) + } + } + + @Test + fun shouldAddNewRelationBlockPlaceholderWithOnCreateAfterOnAddRelationCommandAfterTitle() { + + val new = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.RelationBlock( + key = null + ), + children = emptyList(), + fields = Block.Fields.empty() + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id) + ) + + val document = listOf(page, header, title) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument(document, defaultDetails) + stubCreateBlock( + params = CreateBlock.Params( + context = root, + target = title.id, + position = Position.BOTTOM, + prototype = Block.Prototype.Relation("") + ), + events = listOf( + Event.Command.UpdateStructure( + context = root, + id = root, + children = page.children + listOf(new.id) + ), + Event.Command.AddBlock( + context = root, + blocks = listOf(new) + ) + ) + ) + + // TESTING + + val fragment = launchFragment(args) + + with(R.id.recycler.rVMatcher()) { + checkIsRecyclerSize(1) + onItemView(0, R.id.title).checkHasText(title.content().text) + } + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.title).perform(click()) + } + + Thread.sleep(200) + + fragment.onFragment { fr -> + fr.onAddBlockClicked(UiBlock.RELATION) + } + + Thread.sleep(100) + + with(R.id.recycler.rVMatcher()) { + checkIsRecyclerSize(2) + onItemView(0, R.id.title).checkHasText(title.content().text) + onItemView(1, R.id.tvPlaceholder).checkHasText(R.string.set_new_relation) + } + } + + @Test + fun shouldAddNewRelationBlockPlaceholderWithOnReplaceAfterOnAddRelationCommand() { + + val new = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.RelationBlock( + key = null + ), + children = emptyList(), + fields = Block.Fields.empty() + ) + + val paragraph = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.Text( + text = "", + marks = emptyList(), + style = Block.Content.Text.Style.P + ) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, paragraph.id) + ) + + val document = listOf(page, header, title, paragraph) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument(document, defaultDetails) + stubReplaceBlock( + command = Command.Replace( + context = root, + target = paragraph.id, + prototype = Block.Prototype.Relation("") + ), + events = listOf( + Event.Command.UpdateStructure( + context = root, + id = root, + children = listOf(page.id, header.id, new.id) + ), + Event.Command.AddBlock( + context = root, + blocks = listOf(new) + ), + Event.Command.DeleteBlock( + context = root, + targets = listOf(paragraph.id) + ) + ) + ) + + // TESTING + + val fragment = launchFragment(args) + + with(R.id.recycler.rVMatcher()) { + checkIsRecyclerSize(2) + onItemView(0, R.id.title).checkHasText(title.content().text) + onItemView(1, R.id.textContent).checkHasText("") + } + + with(R.id.recycler.rVMatcher()) { + onItemView(1, R.id.textContent).perform(click()) + } + + Thread.sleep(200) + + fragment.onFragment { fr -> + fr.onAddBlockClicked(UiBlock.RELATION) + } + + Thread.sleep(100) + + with(R.id.recycler.rVMatcher()) { + checkIsRecyclerSize(2) + onItemView(0, R.id.title).checkHasText(title.content().text) + onItemView(1, R.id.tvPlaceholder).checkHasText(R.string.set_new_relation) + } + } + + // STUBBING & SETUP + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } + + /** + * Moves coroutines clock time. + */ + private fun advance(millis: Long) { + coroutineTestRule.advanceTime(millis) + } +} + diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/DeleteBlockTesting.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/DeleteBlockTesting.kt index 2844918212..a44c8979c0 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/DeleteBlockTesting.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/DeleteBlockTesting.kt @@ -12,12 +12,12 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Event +import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget import com.anytypeio.anytype.domain.base.Either import com.anytypeio.anytype.domain.block.interactor.UnlinkBlocks -import com.anytypeio.anytype.domain.block.model.Block -import com.anytypeio.anytype.domain.event.model.Event -import com.anytypeio.anytype.domain.event.model.Payload import com.anytypeio.anytype.features.editor.base.EditorTestSetup import com.anytypeio.anytype.features.editor.base.TestPageFragment import com.anytypeio.anytype.mocking.MockDataFactory @@ -322,6 +322,7 @@ class DeleteBlockTesting : EditorTestSetup() { stubInterceptEvents() stubOpenDocument(document) + stubInterceptThreadStatus() stubUpdateText() stubUnlinkBlocks(params, events) @@ -331,7 +332,7 @@ class DeleteBlockTesting : EditorTestSetup() { // TESTING val target = Espresso.onView( - withRecyclerView(R.id.recycler).atPositionOnView(2, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) ) target.apply { @@ -347,7 +348,7 @@ class DeleteBlockTesting : EditorTestSetup() { verifyBlocking(unlinkBlocks, times(1)) { invoke(params) } Espresso.onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, firstViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, firstViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Foo"))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -356,7 +357,7 @@ class DeleteBlockTesting : EditorTestSetup() { // Check cursor position scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(1) + val item = fragment.recycler.getChildAt(0) val view = item.findViewById(firstViewId) assertEquals( expected = 3, @@ -567,8 +568,6 @@ class DeleteBlockTesting : EditorTestSetup() { Thread.sleep(100) - - val target = Espresso.onView( withRecyclerView(R.id.recycler).atPositionOnView(1, firstViewId) ) diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/EditorIntegrationTesting.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/EditorIntegrationTesting.kt index 413c1412ba..f6db2c02ea 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/EditorIntegrationTesting.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/EditorIntegrationTesting.kt @@ -11,12 +11,15 @@ import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.anytypeio.anytype.R -import com.anytypeio.anytype.core_ui.features.page.BlockViewHolder +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.BlockSplitMode +import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.Event +import com.anytypeio.anytype.core_ui.features.editor.holders.text.Checkbox +import com.anytypeio.anytype.core_ui.features.editor.holders.text.Numbered +import com.anytypeio.anytype.core_ui.features.editor.holders.text.Toggle import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget import com.anytypeio.anytype.domain.base.Either -import com.anytypeio.anytype.domain.block.model.Block -import com.anytypeio.anytype.domain.block.model.Command -import com.anytypeio.anytype.domain.event.model.Event import com.anytypeio.anytype.features.editor.base.EditorTestSetup import com.anytypeio.anytype.features.editor.base.TestPageFragment import com.anytypeio.anytype.mocking.MockDataFactory @@ -122,7 +125,7 @@ class EditorIntegrationTesting : EditorTestSetup() { onView(withRecyclerView(R.id.recycler).atPositionOnView(6, R.id.bulletedListContent)) .check(matches(withText(BLOCK_BULLET.content.asText().text))) - R.id.recycler.scrollTo(7) + R.id.recycler.scrollTo(7) onView(withRecyclerView(R.id.recycler).atPositionOnView(7, R.id.numberedListContent)) .check(matches(withText(BLOCK_NUMBERED_1.content.asText().text))) @@ -130,12 +133,12 @@ class EditorIntegrationTesting : EditorTestSetup() { onView(withRecyclerView(R.id.recycler).atPositionOnView(7, R.id.number)) .check(matches(withText("1."))) - R.id.recycler.scrollTo(8) + R.id.recycler.scrollTo(8) onView(withRecyclerView(R.id.recycler).atPositionOnView(8, R.id.toggleContent)) .check(matches(withText(BLOCK_TOGGLE.content.asText().text))) - R.id.recycler.scrollTo(9) + R.id.recycler.scrollTo(9) onView(withRecyclerView(R.id.recycler).atPositionOnView(9, R.id.checkboxContent)) .check(matches(withText(BLOCK_CHECKBOX.content.asText().text))) @@ -300,8 +303,9 @@ class EditorIntegrationTesting : EditorTestSetup() { val command = Command.Split( context = root, target = paragraph.id, - index = 3, - style = Block.Content.Text.Style.P + style = Block.Content.Text.Style.P, + mode = BlockSplitMode.BOTTOM, + range = 3..3 ) stubSplitBlocks( diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ListBlockTesting.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ListBlockTesting.kt index 01a6c1649f..c09899e7a5 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ListBlockTesting.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ListBlockTesting.kt @@ -9,15 +9,15 @@ import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Event +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_models.Position +import com.anytypeio.anytype.core_models.ext.content import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget import com.anytypeio.anytype.domain.base.Either import com.anytypeio.anytype.domain.block.interactor.CreateBlock import com.anytypeio.anytype.domain.block.interactor.UpdateTextStyle -import com.anytypeio.anytype.domain.block.model.Block -import com.anytypeio.anytype.domain.block.model.Position -import com.anytypeio.anytype.domain.event.model.Event -import com.anytypeio.anytype.domain.event.model.Payload -import com.anytypeio.anytype.domain.ext.content import com.anytypeio.anytype.features.editor.base.EditorTestSetup import com.anytypeio.anytype.features.editor.base.TestPageFragment import com.anytypeio.anytype.mocking.MockDataFactory @@ -146,6 +146,7 @@ class ListBlockTesting : EditorTestSetup() { ) stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() stubCreateBlocks(params, new, events) @@ -155,7 +156,7 @@ class ListBlockTesting : EditorTestSetup() { // TESTING val target = Espresso.onView( - TestUtils.withRecyclerView(R.id.recycler).atPositionOnView(1, view) + TestUtils.withRecyclerView(R.id.recycler).atPositionOnView(0, view) ) target.apply { @@ -177,13 +178,13 @@ class ListBlockTesting : EditorTestSetup() { verifyBlocking(createBlock, times(1)) { invoke(params) } Espresso.onView( - TestUtils.withRecyclerView(R.id.recycler).atPositionOnView(1, view) + TestUtils.withRecyclerView(R.id.recycler).atPositionOnView(0, view) ).apply { check(ViewAssertions.matches(ViewMatchers.withText(a.content().text))) } Espresso.onView( - TestUtils.withRecyclerView(R.id.recycler).atPositionOnView(2, view) + TestUtils.withRecyclerView(R.id.recycler).atPositionOnView(1, view) ).apply { check(ViewAssertions.matches(ViewMatchers.withText(""))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -192,7 +193,7 @@ class ListBlockTesting : EditorTestSetup() { // Check cursor position at block B scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(2) + val item = fragment.recycler.getChildAt(1) item.findViewById(view).apply { assertEquals( expected = 0, diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/MergeBlockTesting.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/MergeBlockTesting.kt index 65a86a5a9a..828a0cb4a5 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/MergeBlockTesting.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/MergeBlockTesting.kt @@ -12,12 +12,12 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Event +import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget import com.anytypeio.anytype.domain.base.Either import com.anytypeio.anytype.domain.block.interactor.MergeBlocks -import com.anytypeio.anytype.domain.block.model.Block -import com.anytypeio.anytype.domain.event.model.Event -import com.anytypeio.anytype.domain.event.model.Payload import com.anytypeio.anytype.features.editor.base.EditorTestSetup import com.anytypeio.anytype.features.editor.base.TestPageFragment import com.anytypeio.anytype.mocking.MockDataFactory @@ -241,6 +241,7 @@ class MergeBlockTesting : EditorTestSetup() { ) stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() stubMergelocks( @@ -253,7 +254,7 @@ class MergeBlockTesting : EditorTestSetup() { // TESTING val target = Espresso.onView( - withRecyclerView(R.id.recycler).atPositionOnView(2, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) ) target.apply { @@ -263,7 +264,7 @@ class MergeBlockTesting : EditorTestSetup() { // Set cursor at the beginning of B scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(2) + val item = fragment.recycler.getChildAt(1) val view = item.findViewById(targetViewId) view.setSelection(0) } @@ -282,7 +283,7 @@ class MergeBlockTesting : EditorTestSetup() { verifyBlocking(mergeBlocks, times(1)) { invoke(params) } Espresso.onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("FooBar"))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -291,7 +292,7 @@ class MergeBlockTesting : EditorTestSetup() { // Check cursor position scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(1) + val item = fragment.recycler.getChildAt(0) val view = item.findViewById(targetViewId) assertEquals( expected = 3, diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/RelationBlockUITesting.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/RelationBlockUITesting.kt new file mode 100644 index 0000000000..57577dc080 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/RelationBlockUITesting.kt @@ -0,0 +1,1101 @@ +package com.anytypeio.anytype.features.editor + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.core_models.ext.content +import com.anytypeio.anytype.emojifier.data.DefaultDocumentEmojiIconProvider +import com.anytypeio.anytype.features.editor.base.EditorTestSetup +import com.anytypeio.anytype.features.editor.base.TestPageFragment +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.page.PageViewModel +import com.anytypeio.anytype.presentation.page.editor.ThemeColor +import com.anytypeio.anytype.ui.page.PageFragment +import com.anytypeio.anytype.utils.* +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class RelationBlockUITesting : EditorTestSetup() { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + private val args = bundleOf(PageFragment.ID_KEY to root) + + private val defaultDetails = Block.Details( + mapOf( + root to Block.Fields( + mapOf( + "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random() + ) + ) + ) + ) + + private val title = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Text( + style = Block.Content.Text.Style.TITLE, + text = "Relation Block UI Testing", + marks = emptyList() + ), + children = emptyList(), + fields = Block.Fields.empty() + ) + + private val header = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Layout( + type = Block.Content.Layout.Type.HEADER + ), + fields = Block.Fields.empty(), + children = listOf(title.id) + ) + + @Before + override fun setup() { + super.setup() + } + + @Test + fun shouldDisplayRelationBlockPlaceholderAtTheEnd() { + + val a = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.Text( + text = "Foo", + marks = emptyList(), + style = Block.Content.Text.Style.P + ) + ) + + // This should be rendered as placeholder because it's not related to any relation. + + val b = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = null) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, a.id, b.id) + ) + + val document = listOf(page, header, title, a, b) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument(document, defaultDetails) + + // TESTING + + launchFragment(args) + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.title).checkHasText(title.content().text) + onItemView(1, R.id.textContent).checkHasText(a.content().text) + onItemView(2, R.id.tvPlaceholder).checkHasText(R.string.set_new_relation) + checkIsRecyclerSize(3) + } + } + + @Test + fun shouldDisplayPrimitiveTextRelationBlocksAtTheEnd() { + + val relation1 = Relation( + key = MockDataFactory.randomString(), + name = "Company", + format = Relation.Format.SHORT_TEXT, + source = Relation.Source.values().random() + ) + + val relation2 = Relation( + key = MockDataFactory.randomString(), + name = "Year", + format = Relation.Format.NUMBER, + source = Relation.Source.values().random() + ) + + val relation3 = Relation( + key = MockDataFactory.randomString(), + name = "Phone", + format = Relation.Format.PHONE, + source = Relation.Source.values().random() + ) + + val relation4 = Relation( + key = MockDataFactory.randomString(), + name = "Website", + format = Relation.Format.URL, + source = Relation.Source.values().random() + ) + + val relation5 = Relation( + key = MockDataFactory.randomString(), + name = "Email", + format = Relation.Format.EMAIL, + source = Relation.Source.values().random() + ) + + val value1 = "Anytype" + val value2 = "2021" + val value3 = "+00000000000" + val value4 = "https://anytype.io/" + val value5 = "team@anytype.io" + + val customDetails = Block.Details( + mapOf( + root to Block.Fields( + mapOf( + "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + relation1.key to value1, + relation2.key to value2, + relation3.key to value3, + relation4.key to value4, + relation5.key to value5, + ) + ) + ) + ) + + val paragraph = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.Text( + text = "Foo", + marks = emptyList(), + style = Block.Content.Text.Style.P + ) + ) + + val block1 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation1.key) + ) + + val block2 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation2.key) + ) + + val block3 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation3.key) + ) + + val block4 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation4.key) + ) + + val block5 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation5.key) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, paragraph.id, block1.id, block2.id, block3.id, block4.id, block5.id) + ) + + val document = listOf(page, header, title, paragraph, block1, block2, block3, block4, block5) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument( + document = document, + details = customDetails, + relations = listOf(relation1, relation2, relation3, relation4, relation5) + ) + + // TESTING + + launchFragment(args) + + with(R.id.recycler.rVMatcher()) { + checkIsRecyclerSize(7) + onItemView(0, R.id.title).checkHasText(title.content().text) + onItemView(1, R.id.textContent).checkHasText(paragraph.content().text) + onItemView(2, R.id.tvRelationTitle).checkHasText(relation1.name) + onItemView(2, R.id.tvRelationValue).checkHasText(value1) + onItemView(3, R.id.tvRelationTitle).checkHasText(relation2.name) + onItemView(3, R.id.tvRelationValue).checkHasText(value2) + onItemView(4, R.id.tvRelationTitle).checkHasText(relation3.name) + onItemView(4, R.id.tvRelationValue).checkHasText(value3) + onItemView(5, R.id.tvRelationTitle).checkHasText(relation4.name) + onItemView(5, R.id.tvRelationValue).checkHasText(value4) + onItemView(6, R.id.tvRelationTitle).checkHasText(relation5.name) + onItemView(6, R.id.tvRelationValue).checkHasText(value5) + } + } + + @Test + fun shouldDisplayPrimitiveTextRelationBlocksWithBackgroundColorSetExceptForOneBlock() { + + val relation1 = Relation( + key = MockDataFactory.randomString(), + name = "Company", + format = Relation.Format.SHORT_TEXT, + source = Relation.Source.values().random() + ) + + val relation2 = Relation( + key = MockDataFactory.randomString(), + name = "Year", + format = Relation.Format.NUMBER, + source = Relation.Source.values().random() + ) + + val relation3 = Relation( + key = MockDataFactory.randomString(), + name = "Phone", + format = Relation.Format.PHONE, + source = Relation.Source.values().random() + ) + + val relation4 = Relation( + key = MockDataFactory.randomString(), + name = "Website", + format = Relation.Format.URL, + source = Relation.Source.values().random() + ) + + val relation5 = Relation( + key = MockDataFactory.randomString(), + name = "Email", + format = Relation.Format.EMAIL, + source = Relation.Source.values().random() + ) + + val value1 = "Anytype" + val value2 = "2021" + val value3 = "+00000000000" + val value4 = "https://anytype.io/" + val value5 = "team@anytype.io" + + val background1 = ThemeColor.PURPLE + val background2 = ThemeColor.RED + val background3 = ThemeColor.BLUE + val background4 = ThemeColor.ORANGE + + val customDetails = Block.Details( + mapOf( + root to Block.Fields( + mapOf( + "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + relation1.key to value1, + relation2.key to value2, + relation3.key to value3, + relation4.key to value4, + relation5.key to value5, + ) + ) + ) + ) + + val paragraph = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.Text( + text = "Foo", + marks = emptyList(), + style = Block.Content.Text.Style.P + ) + ) + + val block1 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock( + key = relation1.key, + background = background1.title + ) + ) + + val block2 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock( + key = relation2.key, + background = background2.title + ) + ) + + val block3 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock( + key = relation3.key, + background = background3.title + ) + ) + + val block4 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock( + key = relation4.key, + background = background4.title + ) + ) + + val block5 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation5.key) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, paragraph.id, block1.id, block2.id, block3.id, block4.id, block5.id) + ) + + val document = listOf(page, header, title, paragraph, block1, block2, block3, block4, block5) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument( + document = document, + details = customDetails, + relations = listOf(relation1, relation2, relation3, relation4, relation5) + ) + + // TESTING + + launchFragment(args) + + with(R.id.recycler.rVMatcher()) { + checkIsRecyclerSize(7) + onItem(2).checkHasBackgroundColor(background1.background) + onItem(3).checkHasBackgroundColor(background2.background) + onItem(4).checkHasBackgroundColor(background3.background) + onItem(5).checkHasBackgroundColor(background4.background) + onItem(6).checkHasNoBackground() + } + } + + @Test + fun shouldDisplayOneStatusRelationBlockAtTheEnd() { + + val option = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Done", + color = ThemeColor.PURPLE.title + ) + + val relation1 = Relation( + key = MockDataFactory.randomString(), + name = "Status", + format = Relation.Format.STATUS, + source = Relation.Source.values().random(), + selections = listOf(option) + ) + + val customDetails = Block.Details( + mapOf( + root to Block.Fields( + mapOf( + "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + relation1.key to option.id + ) + ) + ) + ) + + val a = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.Text( + text = "Foo", + marks = emptyList(), + style = Block.Content.Text.Style.P + ) + ) + + val b = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation1.key) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, a.id, b.id) + ) + + val document = listOf(page, header, title, a, b) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument( + document = document, + details = customDetails, + relations = listOf(relation1) + ) + + // TESTING + + launchFragment(args) + + with(R.id.recycler.rVMatcher()) { + checkIsRecyclerSize(3) + onItemView(0, R.id.title).checkHasText(title.content().text) + onItemView(1, R.id.textContent).checkHasText(a.content().text) + onItemView(2, R.id.tvRelationTitle).checkHasText(relation1.name) + onItemView(2, R.id.tvRelationValue).checkHasText(option.text) + } + } + + @Test + fun shouldDisplayOneFileRelationBlockContainingTwoFiles() { + + val file1 = MockDataFactory.randomUuid() + val file2 = MockDataFactory.randomUuid() + + val relation1 = Relation( + key = MockDataFactory.randomString(), + name = "Attachement", + format = Relation.Format.FILE, + source = Relation.Source.values().random(), + selections = emptyList() + ) + + val customDetails = Block.Details( + mapOf( + root to Block.Fields( + mapOf( + "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + relation1.key to listOf(file1, file2) + ) + ), + file1 to Block.Fields( + mapOf( + "name" to "Document", + "ext" to "pdf", + "mime" to "application/pdf" + ) + ), + file2 to Block.Fields( + mapOf( + "name" to "Image", + "ext" to "jpg", + "mime" to "image/jpeg" + ) + ) + ) + ) + + val a = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.Text( + text = "Foo", + marks = emptyList(), + style = Block.Content.Text.Style.P + ) + ) + + val b = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation1.key) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, a.id, b.id) + ) + + val document = listOf(page, header, title, a, b) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument( + document = document, + details = customDetails, + relations = listOf(relation1) + ) + + // TESTING + + launchFragment(args) + + with(R.id.recycler.rVMatcher()) { + checkIsRecyclerSize(3) + onItemView(0, R.id.title).checkHasText(title.content().text) + onItemView(1, R.id.textContent).checkHasText(a.content().text) + onItemView(2, R.id.tvRelationTitle).checkHasText(relation1.name) + onItemView(2, R.id.fileContainer).checkHasChildViewCount(2) + } + } + + @Test + fun shouldSelectRelationBlockPlaceholder() { + + val paragraph = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.Text( + text = "Foo", + marks = emptyList(), + style = Block.Content.Text.Style.P + ) + ) + + val block = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = null) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, paragraph.id, block.id) + ) + + val document = listOf(page, header, title, paragraph, block) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument(document, defaultDetails) + + // TESTING + + launchFragment(args) + + val rvMatcher = R.id.recycler.rVMatcher() + + with(rvMatcher) { + checkIsRecyclerSize(3) + onItemView(0, R.id.title).checkHasText(title.content().text) + onItemView(1, R.id.textContent).checkHasText(paragraph.content().text) + onItemView(1, R.id.textContent).checkIsNotSelected() + onItemView(2, R.id.tvPlaceholder).checkHasText(R.string.set_new_relation) + onItemView(2, R.id.tvPlaceholder).checkIsNotSelected() + } + + rvMatcher.onItemView(1, R.id.textContent).perform(click()) + + onView(withId(R.id.multiSelectModeButton)).perform(click()) + + advance(PageViewModel.DELAY_REFRESH_DOCUMENT_TO_ENTER_MULTI_SELECT_MODE) + + rvMatcher.onItemView(1, R.id.textContent).perform(click()) + rvMatcher.onItemView(2, R.id.placeholderContainer).perform(click()) + + advance(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION) + } + + @Test + fun shouldIndentRelationBlockPlaceholder() { + + val block = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = null) + ) + + val paragraph = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = listOf(block.id), + content = Block.Content.Text( + text = "Foo", + marks = emptyList(), + style = Block.Content.Text.Style.P + ) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, paragraph.id) + ) + + val document = listOf(page, header, title, paragraph, block) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument(document, defaultDetails) + + // TESTING + + launchFragment(args) + + val rvMatcher = R.id.recycler.rVMatcher() + + rvMatcher.onItemView(2, R.id.relationIcon).checkHasMarginStart(dimen = R.dimen.indent, coefficient = 1) + } + + @Test + fun shouldIndentPrimitiveTextRelationBlocksAtTheEnd() { + + val relation1 = Relation( + key = MockDataFactory.randomString(), + name = "Company", + format = Relation.Format.SHORT_TEXT, + source = Relation.Source.values().random() + ) + + val relation2 = Relation( + key = MockDataFactory.randomString(), + name = "Year", + format = Relation.Format.NUMBER, + source = Relation.Source.values().random() + ) + + val relation3 = Relation( + key = MockDataFactory.randomString(), + name = "Phone", + format = Relation.Format.PHONE, + source = Relation.Source.values().random() + ) + + val relation4 = Relation( + key = MockDataFactory.randomString(), + name = "Website", + format = Relation.Format.URL, + source = Relation.Source.values().random() + ) + + val relation5 = Relation( + key = MockDataFactory.randomString(), + name = "Email", + format = Relation.Format.EMAIL, + source = Relation.Source.values().random() + ) + + val value1 = "Anytype" + val value2 = "2021" + val value3 = "+00000000000" + val value4 = "https://anytype.io/" + val value5 = "team@anytype.io" + + val customDetails = Block.Details( + mapOf( + root to Block.Fields( + mapOf( + "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + relation1.key to value1, + relation2.key to value2, + relation3.key to value3, + relation4.key to value4, + relation5.key to value5, + ) + ) + ) + ) + + val block1 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation1.key) + ) + + val block2 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation2.key) + ) + + val block3 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation3.key) + ) + + val block4 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation4.key) + ) + + val block5 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation5.key) + ) + + val paragraph = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = listOf(block1.id, block2.id, block3.id, block4.id, block5.id), + content = Block.Content.Text( + text = "Foo", + marks = emptyList(), + style = Block.Content.Text.Style.P + ) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, paragraph.id) + ) + + val document = listOf(page, header, title, paragraph, block1, block2, block3, block4, block5) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument( + document = document, + details = customDetails, + relations = listOf(relation1, relation2, relation3, relation4, relation5) + ) + + // TESTING + + launchFragment(args) + + with(R.id.recycler.rVMatcher()) { + checkIsRecyclerSize(7) + onItemView(2, R.id.tvRelationTitle).checkHasPaddingLeft(R.dimen.indent, 1) + onItemView(3, R.id.tvRelationTitle).checkHasPaddingLeft(R.dimen.indent, 1) + onItemView(4, R.id.tvRelationTitle).checkHasPaddingLeft(R.dimen.indent, 1) + onItemView(5, R.id.tvRelationTitle).checkHasPaddingLeft(R.dimen.indent, 1) + onItemView(6, R.id.tvRelationTitle).checkHasPaddingLeft(R.dimen.indent, 1) + } + } + + @Test + fun shouldIndentStatusRelationBlockAtTheEnd() { + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + color = ThemeColor.BLUE.title, + text = "In testing" + ) + + val relation1 = Relation( + key = MockDataFactory.randomString(), + name = "Status", + format = Relation.Format.STATUS, + source = Relation.Source.values().random(), + selections = listOf(option1) + ) + + val customDetails = Block.Details( + mapOf( + root to Block.Fields( + mapOf( + "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + relation1.key to option1.id + ) + ) + ) + ) + + val block1 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation1.key) + ) + + val paragraph = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = listOf(block1.id), + content = Block.Content.Text( + text = "Foo", + marks = emptyList(), + style = Block.Content.Text.Style.P + ) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, paragraph.id) + ) + + val document = listOf(page, header, title, paragraph, block1) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument( + document = document, + details = customDetails, + relations = listOf(relation1) + ) + + // TESTING + + launchFragment(args) + + with(R.id.recycler.rVMatcher()) { + checkIsRecyclerSize(3) + onItemView(2, R.id.tvRelationTitle).checkHasPaddingLeft(R.dimen.indent, 1) + } + } + + @Test + fun shouldSelectAllBasicTextRelationBlocks() { + + val relation1 = Relation( + key = MockDataFactory.randomString(), + name = "Company", + format = Relation.Format.SHORT_TEXT, + source = Relation.Source.values().random() + ) + + val relation2 = Relation( + key = MockDataFactory.randomString(), + name = "Year", + format = Relation.Format.NUMBER, + source = Relation.Source.values().random() + ) + + val relation3 = Relation( + key = MockDataFactory.randomString(), + name = "Phone", + format = Relation.Format.PHONE, + source = Relation.Source.values().random() + ) + + val relation4 = Relation( + key = MockDataFactory.randomString(), + name = "Website", + format = Relation.Format.URL, + source = Relation.Source.values().random() + ) + + val relation5 = Relation( + key = MockDataFactory.randomString(), + name = "Email", + format = Relation.Format.EMAIL, + source = Relation.Source.values().random() + ) + + val value1 = "Anytype" + val value2 = "2021" + val value3 = "+00000000000" + val value4 = "https://anytype.io/" + val value5 = "team@anytype.io" + + val customDetails = Block.Details( + mapOf( + root to Block.Fields( + mapOf( + "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + relation1.key to value1, + relation2.key to value2, + relation3.key to value3, + relation4.key to value4, + relation5.key to value5, + ) + ) + ) + ) + + val paragraph = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.Text( + text = "Foo", + marks = emptyList(), + style = Block.Content.Text.Style.P + ) + ) + + val block1 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation1.key) + ) + + val block2 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation2.key) + ) + + val block3 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation3.key) + ) + + val block4 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation4.key) + ) + + val block5 = Block( + id = MockDataFactory.randomUuid(), + fields = Block.Fields.empty(), + children = emptyList(), + content = Block.Content.RelationBlock(key = relation5.key) + ) + + val page = Block( + id = root, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.PAGE + ), + children = listOf(header.id, paragraph.id, block1.id, block2.id, block3.id, block4.id, block5.id) + ) + + val document = listOf(page, header, title, paragraph, block1, block2, block3, block4, block5) + + stubInterceptEvents() + stubInterceptThreadStatus() + stubOpenDocument( + document = document, + details = customDetails, + relations = listOf(relation1, relation2, relation3, relation4, relation5) + ) + + // TESTING + + launchFragment(args) + + val rvMatcher = R.id.recycler.rVMatcher() + + with(rvMatcher) { + checkIsRecyclerSize(7) + onItemView(1, R.id.textContent).checkIsNotSelected() + onItemView(2, R.id.tvRelationTitle).checkIsNotSelected() + onItemView(2, R.id.tvRelationValue).checkIsNotSelected() + onItemView(3, R.id.tvRelationTitle).checkIsNotSelected() + onItemView(3, R.id.tvRelationValue).checkIsNotSelected() + onItemView(4, R.id.tvRelationTitle).checkIsNotSelected() + onItemView(4, R.id.tvRelationValue).checkIsNotSelected() + onItemView(5, R.id.tvRelationTitle).checkIsNotSelected() + onItemView(5, R.id.tvRelationValue).checkIsNotSelected() + onItemView(6, R.id.tvRelationTitle).checkIsNotSelected() + onItemView(6, R.id.tvRelationValue).checkIsNotSelected() + } + + rvMatcher.onItemView(1, R.id.textContent).perform(click()) + + onView(withId(R.id.multiSelectModeButton)).perform(click()) + + advance(PageViewModel.DELAY_REFRESH_DOCUMENT_TO_ENTER_MULTI_SELECT_MODE) + + rvMatcher.onItemView(1, R.id.textContent).perform(click()) + rvMatcher.onItemView(2, R.id.content).perform(click()) + rvMatcher.onItemView(3, R.id.content).perform(click()) + rvMatcher.onItemView(4, R.id.content).perform(click()) + rvMatcher.onItemView(5, R.id.content).perform(click()) + rvMatcher.onItemView(6, R.id.content).perform(click()) + + advance(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION) + + with(rvMatcher) { + checkIsRecyclerSize(7) + onItemView(1, R.id.textContent).checkIsSelected() + onItemView(2, R.id.content).checkIsSelected() + onItemView(3, R.id.content).checkIsSelected() + onItemView(4, R.id.content).checkIsSelected() + onItemView(5, R.id.content).checkIsSelected() + onItemView(6, R.id.content).checkIsSelected() + } + } + + // STUBBING & SETUP + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } + + /** + * Moves coroutines clock time. + */ + private fun advance(millis: Long) { + coroutineTestRule.advanceTime(millis) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ScrollAndMoveTesting.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ScrollAndMoveTesting.kt index 2063415067..c73be21b88 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ScrollAndMoveTesting.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ScrollAndMoveTesting.kt @@ -10,7 +10,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.anytypeio.anytype.R -import com.anytypeio.anytype.domain.block.model.Block +import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.features.editor.base.EditorTestSetup import com.anytypeio.anytype.features.editor.base.TestPageFragment import com.anytypeio.anytype.mocking.MockDataFactory diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/SplitBlockTesting.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/SplitBlockTesting.kt index 924ff196e0..ca5ba894fa 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/SplitBlockTesting.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/SplitBlockTesting.kt @@ -11,10 +11,11 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.BlockSplitMode +import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.Event import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget -import com.anytypeio.anytype.domain.block.model.Block -import com.anytypeio.anytype.domain.block.model.Command -import com.anytypeio.anytype.domain.event.model.Event import com.anytypeio.anytype.features.editor.base.EditorTestSetup import com.anytypeio.anytype.features.editor.base.TestPageFragment import com.anytypeio.anytype.mocking.MockDataFactory @@ -26,7 +27,6 @@ import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verifyBlocking -import com.nhaarman.mockitokotlin2.verifyZeroInteractions import kotlinx.android.synthetic.main.fragment_page.* import org.junit.Before import org.junit.Rule @@ -49,86 +49,6 @@ class SplitBlockTesting : EditorTestSetup() { super.setup() } - @Test - fun shouldNotSplitTitle() { - - // SETUP - - val args = bundleOf(PageFragment.ID_KEY to root) - - val title = "Indivisible title" - - val page = Block( - id = root, - fields = Block.Fields(emptyMap()), - content = Block.Content.Smart( - type = Block.Content.Smart.Type.PAGE - ), - children = emptyList() - ) - - val document = listOf(page) - - stubInterceptEvents() - stubOpenDocument( - document = document, - details = Block.Details( - mapOf( - root to Block.Fields( - mapOf("name" to title) - ) - ) - ) - ) - - val scenario = launchFragment(args) - - // TESTING - - val target = onView( - withRecyclerView(R.id.recycler).atPositionOnView(0, R.id.title) - ) - - // Set cursor programmatically - - scenario.onFragment { fragment -> - fragment.recycler.findViewById(R.id.title).setSelection(3) - } - - // Press ENTER - - target.perform(ViewActions.pressImeActionButton()) - - // Check results - - verifyZeroInteractions(updateText) - verifyZeroInteractions(repo) - - target.apply { - check(ViewAssertions.matches(ViewMatchers.withText(title))) - check(ViewAssertions.matches(ViewMatchers.hasFocus())) - } - - // Check cursor position - - scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(0) - val view = item.findViewById(R.id.title) - assertEquals( - expected = 3, - actual = view.selectionStart - ) - assertEquals( - expected = 3, - actual = view.selectionEnd - ) - } - - // Release pending coroutines - - advance(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION) - } - @Test fun shouldSplitParagraph() { @@ -191,13 +111,15 @@ class SplitBlockTesting : EditorTestSetup() { ) stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() val command = Command.Split( context = root, target = block.id, - index = 3, + mode = BlockSplitMode.BOTTOM, + range = 3..3, style = style ) @@ -214,7 +136,7 @@ class SplitBlockTesting : EditorTestSetup() { // TESTING val target = onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ) target.apply { @@ -244,13 +166,13 @@ class SplitBlockTesting : EditorTestSetup() { verifyBlocking(repo, times(1)) { split(command) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Foo"))) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(2, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Bar"))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -259,7 +181,7 @@ class SplitBlockTesting : EditorTestSetup() { // Check cursor position scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(2) + val item = fragment.recycler.getChildAt(1) val view = item.findViewById(targetViewId) assertEquals( expected = 0, @@ -338,13 +260,15 @@ class SplitBlockTesting : EditorTestSetup() { ) stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() val command = Command.Split( context = root, target = block.id, - index = 3, + range = 3..3, + mode = BlockSplitMode.BOTTOM, style = style ) @@ -361,7 +285,7 @@ class SplitBlockTesting : EditorTestSetup() { // TESTING val target = onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ) target.apply { @@ -391,13 +315,13 @@ class SplitBlockTesting : EditorTestSetup() { verifyBlocking(repo, times(1)) { split(command) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Foo"))) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(2, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Bar"))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -406,7 +330,7 @@ class SplitBlockTesting : EditorTestSetup() { // Check cursor position scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(2) + val item = fragment.recycler.getChildAt(1) val view = item.findViewById(targetViewId) assertEquals( expected = 0, @@ -485,13 +409,15 @@ class SplitBlockTesting : EditorTestSetup() { ) stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() val command = Command.Split( context = root, target = block.id, - index = 3, + range = 3..3, + mode = BlockSplitMode.BOTTOM, style = style ) @@ -508,7 +434,7 @@ class SplitBlockTesting : EditorTestSetup() { // TESTING val target = onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ) target.apply { @@ -538,13 +464,13 @@ class SplitBlockTesting : EditorTestSetup() { verifyBlocking(repo, times(1)) { split(command) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Foo"))) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(2, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Bar"))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -553,7 +479,7 @@ class SplitBlockTesting : EditorTestSetup() { // Check cursor position scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(2) + val item = fragment.recycler.getChildAt(1) val view = item.findViewById(targetViewId) assertEquals( expected = 0, @@ -632,13 +558,15 @@ class SplitBlockTesting : EditorTestSetup() { ) stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() val command = Command.Split( context = root, target = block.id, - index = 3, + range = 3..3, + mode = BlockSplitMode.BOTTOM, style = style ) @@ -655,7 +583,7 @@ class SplitBlockTesting : EditorTestSetup() { // TESTING val target = onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ) target.apply { @@ -683,13 +611,13 @@ class SplitBlockTesting : EditorTestSetup() { verifyBlocking(repo, times(1)) { split(command) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Foo"))) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(2, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Bar"))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -698,7 +626,7 @@ class SplitBlockTesting : EditorTestSetup() { // Check cursor position scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(2) + val item = fragment.recycler.getChildAt(1) val view = item.findViewById(targetViewId) assertEquals( expected = 0, @@ -777,13 +705,15 @@ class SplitBlockTesting : EditorTestSetup() { ) stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() val command = Command.Split( context = root, target = block.id, - index = 3, + range = 3..3, + mode = BlockSplitMode.BOTTOM, style = style ) @@ -800,7 +730,7 @@ class SplitBlockTesting : EditorTestSetup() { // TESTING val target = onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ) target.apply { @@ -828,13 +758,13 @@ class SplitBlockTesting : EditorTestSetup() { verifyBlocking(repo, times(1)) { split(command) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Foo"))) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(2, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Bar"))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -843,7 +773,7 @@ class SplitBlockTesting : EditorTestSetup() { // Check cursor position scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(2) + val item = fragment.recycler.getChildAt(1) val view = item.findViewById(targetViewId) assertEquals( expected = 0, @@ -922,13 +852,15 @@ class SplitBlockTesting : EditorTestSetup() { ) stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() val command = Command.Split( context = root, target = block.id, - index = 3, + range = 3..3, + mode = BlockSplitMode.BOTTOM, style = style ) @@ -973,13 +905,13 @@ class SplitBlockTesting : EditorTestSetup() { verifyBlocking(repo, times(1)) { split(command) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Foo"))) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(2, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Bar"))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -988,7 +920,7 @@ class SplitBlockTesting : EditorTestSetup() { // Check cursor position scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(2) + val item = fragment.recycler.getChildAt(1) val view = item.findViewById(targetViewId) assertEquals( expected = 0, @@ -1067,13 +999,15 @@ class SplitBlockTesting : EditorTestSetup() { ) stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() val command = Command.Split( context = root, target = block.id, - index = 3, + range = 3..3, + mode = BlockSplitMode.BOTTOM, style = style ) @@ -1090,7 +1024,7 @@ class SplitBlockTesting : EditorTestSetup() { // TESTING val target = onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ) target.apply { @@ -1116,13 +1050,13 @@ class SplitBlockTesting : EditorTestSetup() { verifyBlocking(repo, times(1)) { split(command) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Foo"))) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(2, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Bar"))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -1131,7 +1065,7 @@ class SplitBlockTesting : EditorTestSetup() { // Check cursor position scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(2) + val item = fragment.recycler.getChildAt(1) val view = item.findViewById(targetViewId) assertEquals( expected = 0, @@ -1210,13 +1144,15 @@ class SplitBlockTesting : EditorTestSetup() { ) stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() val command = Command.Split( context = root, target = block.id, - index = 3, + range = 3..3, + mode = BlockSplitMode.BOTTOM, style = style ) @@ -1233,7 +1169,7 @@ class SplitBlockTesting : EditorTestSetup() { // TESTING val target = onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ) target.apply { @@ -1259,13 +1195,13 @@ class SplitBlockTesting : EditorTestSetup() { verifyBlocking(repo, times(1)) { split(command) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Foo"))) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(2, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Bar"))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -1274,7 +1210,7 @@ class SplitBlockTesting : EditorTestSetup() { // Check cursor position scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(2) + val item = fragment.recycler.getChildAt(1) val view = item.findViewById(targetViewId) assertEquals( expected = 0, @@ -1353,13 +1289,15 @@ class SplitBlockTesting : EditorTestSetup() { ) stubInterceptEvents() + stubInterceptThreadStatus() stubOpenDocument(document) stubUpdateText() val command = Command.Split( context = root, target = block.id, - index = 3, + range = 3..3, + mode = BlockSplitMode.BOTTOM, style = style ) @@ -1376,7 +1314,7 @@ class SplitBlockTesting : EditorTestSetup() { // TESTING val target = onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ) target.apply { @@ -1402,13 +1340,13 @@ class SplitBlockTesting : EditorTestSetup() { verifyBlocking(repo, times(1)) { split(command) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(0, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Foo"))) } onView( - withRecyclerView(R.id.recycler).atPositionOnView(2, targetViewId) + withRecyclerView(R.id.recycler).atPositionOnView(1, targetViewId) ).apply { check(ViewAssertions.matches(ViewMatchers.withText("Bar"))) check(ViewAssertions.matches(ViewMatchers.hasFocus())) @@ -1417,7 +1355,7 @@ class SplitBlockTesting : EditorTestSetup() { // Check cursor position scenario.onFragment { fragment -> - val item = fragment.recycler.getChildAt(2) + val item = fragment.recycler.getChildAt(1) val view = item.findViewById(targetViewId) assertEquals( expected = 0, diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/base/EditorTestSetup.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/base/EditorTestSetup.kt index de4b3db881..4b2e9776c5 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/base/EditorTestSetup.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/base/EditorTestSetup.kt @@ -1,36 +1,44 @@ package com.anytypeio.anytype.features.editor.base +import com.anytypeio.anytype.analytics.base.Analytics +import com.anytypeio.anytype.core_models.* import com.anytypeio.anytype.core_utils.tools.Counter +import com.anytypeio.anytype.domain.`object`.UpdateDetail import com.anytypeio.anytype.domain.base.Either +import com.anytypeio.anytype.domain.base.Result +import com.anytypeio.anytype.domain.block.UpdateDivider import com.anytypeio.anytype.domain.block.interactor.* -import com.anytypeio.anytype.domain.block.model.Block -import com.anytypeio.anytype.domain.block.model.Command import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.clipboard.Clipboard import com.anytypeio.anytype.domain.clipboard.Copy import com.anytypeio.anytype.domain.clipboard.Paste -import com.anytypeio.anytype.domain.common.Id -import com.anytypeio.anytype.domain.config.Config +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.cover.RemoveDocCover +import com.anytypeio.anytype.domain.cover.SetDocCoverImage +import com.anytypeio.anytype.domain.dataview.interactor.SetRelationKey import com.anytypeio.anytype.domain.download.DownloadFile import com.anytypeio.anytype.domain.event.interactor.InterceptEvents -import com.anytypeio.anytype.domain.event.model.Event -import com.anytypeio.anytype.domain.event.model.Payload import com.anytypeio.anytype.domain.icon.DocumentEmojiIconProvider import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.page.* import com.anytypeio.anytype.domain.page.bookmark.SetupBookmark import com.anytypeio.anytype.domain.page.navigation.GetListPages +import com.anytypeio.anytype.domain.status.InterceptThreadStatus +import com.anytypeio.anytype.domain.status.ThreadStatusChannel import com.anytypeio.anytype.mocking.MockDataFactory import com.anytypeio.anytype.presentation.page.DocumentExternalEventReducer import com.anytypeio.anytype.presentation.page.Editor import com.anytypeio.anytype.presentation.page.PageViewModelFactory +import com.anytypeio.anytype.presentation.page.cover.CoverImageHashProvider import com.anytypeio.anytype.presentation.page.editor.Interactor +import com.anytypeio.anytype.presentation.page.editor.InternalDetailModificationManager import com.anytypeio.anytype.presentation.page.editor.Orchestrator import com.anytypeio.anytype.presentation.page.editor.Proxy import com.anytypeio.anytype.presentation.page.editor.pattern.DefaultPatternMatcher import com.anytypeio.anytype.presentation.page.render.DefaultBlockViewRenderer import com.anytypeio.anytype.presentation.page.selection.SelectionStateHolder import com.anytypeio.anytype.presentation.page.toggle.ToggleStateHolder +import com.anytypeio.anytype.presentation.util.Dispatcher import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock @@ -42,6 +50,7 @@ import org.mockito.MockitoAnnotations open class EditorTestSetup { + lateinit var createObject: CreateObject lateinit var archiveDocument: ArchiveDocument lateinit var createDocument: CreateDocument lateinit var downloadFile: DownloadFile @@ -58,11 +67,14 @@ open class EditorTestSetup { lateinit var createPage: CreatePage lateinit var updateBackgroundColor: UpdateBackgroundColor lateinit var move: Move + lateinit var setRelationKey: SetRelationKey + lateinit var updateDetail: UpdateDetail + @Mock lateinit var openPage: OpenPage @Mock - lateinit var closePage: ClosePage + lateinit var closePage: CloseBlock @Mock lateinit var updateText: UpdateText @Mock @@ -77,25 +89,56 @@ open class EditorTestSetup { lateinit var getListPages: GetListPages @Mock lateinit var duplicateBlock: DuplicateBlock + @Mock lateinit var updateTextStyle: UpdateTextStyle + @Mock lateinit var updateTextColor: UpdateTextColor + @Mock lateinit var updateLinkMarks: UpdateLinkMarks + @Mock lateinit var removeLinkMark: RemoveLinkMark + @Mock lateinit var mergeBlocks: MergeBlocks + lateinit var createNewDocument: CreateNewDocument + lateinit var interceptThreadStatus: InterceptThreadStatus + + lateinit var setDocCoverImage: SetDocCoverImage + lateinit var removeDocCover: RemoveDocCover + + lateinit var updateFields: UpdateFields + lateinit var turnIntoDocument: TurnIntoDocument + lateinit var turnIntoStyle: TurnIntoStyle + + @Mock + lateinit var updateDivider: UpdateDivider + @Mock lateinit var uriMatcher: Clipboard.UriMatcher + @Mock lateinit var repo: BlockRepository + @Mock + lateinit var coverImageHashProvider: CoverImageHashProvider + @Mock lateinit var clipboard: Clipboard + @Mock + lateinit var gateway: Gateway + + @Mock + lateinit var analytics: Analytics + + @Mock + lateinit var threadStatusChannel: ThreadStatusChannel + @Mock lateinit var documentEmojiIconProvider: DocumentEmojiIconProvider @@ -107,9 +150,11 @@ open class EditorTestSetup { profile = MockDataFactory.randomUuid() ) - private val urlBuilder = UrlBuilder( - config = config - ) + private val urlBuilder by lazy { + UrlBuilder( + gateway = gateway + ) + } private val intents = Proxy.Intents() @@ -133,6 +178,12 @@ open class EditorTestSetup { updateAlignment = UpdateAlignment(repo) updateTitle = UpdateTitle(repo) uploadBlock = UploadBlock(repo) + createObject = CreateObject(repo, documentEmojiIconProvider) + setRelationKey = SetRelationKey(repo) + turnIntoDocument = TurnIntoDocument(repo) + updateFields = UpdateFields(repo) + createNewDocument = CreateNewDocument(repo, documentEmojiIconProvider) + interceptThreadStatus = InterceptThreadStatus(channel = threadStatusChannel) downloadFile = DownloadFile( downloader = mock(), context = Dispatchers.Main @@ -151,6 +202,11 @@ open class EditorTestSetup { updateBackgroundColor = UpdateBackgroundColor(repo) + setDocCoverImage = SetDocCoverImage(repo) + removeDocCover = RemoveDocCover(repo) + turnIntoStyle = TurnIntoStyle(repo) + updateDetail = UpdateDetail(repo) + TestPageFragment.testViewModelFactory = PageViewModelFactory( openPage = openPage, closePage = closePage, @@ -158,6 +214,7 @@ open class EditorTestSetup { updateLinkMarks = updateLinkMarks, removeLinkMark = removeLinkMark, createPage = createPage, + createObject = createObject, documentEventReducer = DocumentExternalEventReducer(), archiveDocument = archiveDocument, createDocument = createDocument, @@ -165,10 +222,11 @@ open class EditorTestSetup { renderer = DefaultBlockViewRenderer( urlBuilder = urlBuilder, counter = Counter.Default(), - toggleStateHolder = ToggleStateHolder.Default() + toggleStateHolder = ToggleStateHolder.Default(), + coverImageHashProvider = coverImageHashProvider ), getListPages = getListPages, - interactor = Orchestrator( + orchestrator = Orchestrator( createBlock = createBlock, splitBlock = splitBlock, unlinkBlocks = unlinkBlocks, @@ -188,6 +246,7 @@ open class EditorTestSetup { updateTextColor = updateTextColor, replaceBlock = replaceBlock, setupBookmark = setupBookmark, + setRelationKey = setRelationKey, memory = Editor.Memory( selections = SelectionStateHolder.Default() ), @@ -199,8 +258,21 @@ open class EditorTestSetup { matcher = DefaultPatternMatcher() ), uploadBlock = uploadBlock, - move = move - ) + move = move, + analytics = analytics, + updateDivider = updateDivider, + updateFields = updateFields, + turnIntoDocument = turnIntoDocument, + turnIntoStyle = turnIntoStyle + ), + createNewDocument = createNewDocument, + interceptThreadStatus = interceptThreadStatus, + analytics = analytics, + dispatcher = Dispatcher.Default(), + setDocCoverImage = setDocCoverImage, + removeDocCover = removeDocCover, + detailModificationManager = InternalDetailModificationManager(stores.details), + updateDetail = updateDetail ) } @@ -210,24 +282,36 @@ open class EditorTestSetup { fun stubInterceptEvents() { interceptEvents.stub { - onBlocking { build() } doReturn emptyFlow() + onBlocking { build(any()) } doReturn emptyFlow() + } + } + + fun stubInterceptThreadStatus( + params: InterceptThreadStatus.Params = InterceptThreadStatus.Params(ctx = root) + ) { + interceptThreadStatus.stub { + onBlocking { build(params) } doReturn emptyFlow() } } fun stubOpenDocument( document: List, - details: Block.Details = Block.Details() + details: Block.Details = Block.Details(), + relations: List = emptyList() ) { openPage.stub { onBlocking { invoke(any()) } doReturn Either.Right( - Payload( - context = root, - events = listOf( - Event.Command.ShowBlock( - context = root, - root = root, - details = details, - blocks = document + Result.Success( + Payload( + context = root, + events = listOf( + Event.Command.ShowBlock( + context = root, + root = root, + details = details, + blocks = document, + relations = relations + ) ) ) ) @@ -235,6 +319,31 @@ open class EditorTestSetup { } } + fun stubCreateBlock( + params: CreateBlock.Params, + events: List + ) { + createBlock.stub { + onBlocking { invoke(params) } doReturn Either.Right( + Pair( + MockDataFactory.randomUuid(), + Payload(context = root, events = events) + ) + ) + } + } + + fun stubReplaceBlock( + command: Command.Replace, + events: List + ) { + repo.stub { + onBlocking { + replace(command = command) + } doReturn Pair(command.context, Payload(command.context, events)) + } + } + fun stubSplitBlocks( command: Command.Split, new: Id, diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/emoji/DocumentEmojiPickerFragmentTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/emoji/DocumentEmojiPickerFragmentTest.kt index 158d1ead4f..391f95139d 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/emoji/DocumentEmojiPickerFragmentTest.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/emoji/DocumentEmojiPickerFragmentTest.kt @@ -19,7 +19,9 @@ import com.anytypeio.anytype.emojifier.data.EmojiProvider import com.anytypeio.anytype.emojifier.suggest.EmojiSuggester import com.anytypeio.anytype.emojifier.suggest.model.EmojiModel import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.page.editor.DetailModificationManager import com.anytypeio.anytype.presentation.page.picker.DocumentEmojiIconPickerViewModelFactory +import com.anytypeio.anytype.presentation.util.Dispatcher import com.anytypeio.anytype.utils.TestUtils.withRecyclerView import com.nhaarman.mockitokotlin2.* import kotlinx.android.synthetic.main.fragment_page_icon_picker.* @@ -35,6 +37,9 @@ import kotlin.test.assertEquals @LargeTest class DocumentEmojiPickerFragmentTest { + @Mock + lateinit var detailModificationManager: DetailModificationManager + @Mock lateinit var suggester: EmojiSuggester @@ -54,7 +59,9 @@ class DocumentEmojiPickerFragmentTest { DocumentEmojiIconPickerViewModelFactory( emojiProvider = provider, emojiSuggester = suggester, - setEmojiIcon = setEmojiIcon + setEmojiIcon = setEmojiIcon, + dispatcher = Dispatcher.Default(), + details = detailModificationManager ) } diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/AddRelationStatusValueTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/AddRelationStatusValueTest.kt new file mode 100644 index 0000000000..3eeea235c5 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/AddRelationStatusValueTest.kt @@ -0,0 +1,606 @@ +package com.anytypeio.anytype.features.relations + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.* +import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewRelationOption +import com.anytypeio.anytype.domain.dataview.interactor.AddStatusToDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.AddTagToDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.RemoveTagFromDataViewRecord +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.relations.AddObjectRelationOption +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.page.editor.ThemeColor +import com.anytypeio.anytype.presentation.relations.AddObjectSetObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectValueProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectTypeProvider +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.relations.AddObjectRelationValueFragment +import com.anytypeio.anytype.utils.* +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import com.nhaarman.mockitokotlin2.* +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +@LargeTest +class AddRelationStatusValueTest { + + @Mock + lateinit var repo: BlockRepository + + @Mock + lateinit var gateway: Gateway + + @Mock + lateinit var dispatcher: Dispatcher + + private lateinit var addRelationOption: AddDataViewRelationOption + private lateinit var addObjectRelationOption: AddObjectRelationOption + private lateinit var removeTagFromDataViewRecord: RemoveTagFromDataViewRecord + private lateinit var addTagToDataViewRecord: AddTagToDataViewRecord + private lateinit var addStatusToDataViewRecord: AddStatusToDataViewRecord + private lateinit var updateDetail: UpdateDetail + private lateinit var urlBuilder: UrlBuilder + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + private val ctx = MockDataFactory.randomUuid() + private val state = MutableStateFlow(ObjectSet.init()) + private val session = ObjectSetSession() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + addRelationOption = AddDataViewRelationOption(repo) + addTagToDataViewRecord = AddTagToDataViewRecord(repo) + addObjectRelationOption = AddObjectRelationOption(repo) + addStatusToDataViewRecord = AddStatusToDataViewRecord(repo) + removeTagFromDataViewRecord = RemoveTagFromDataViewRecord(repo) + updateDetail = UpdateDetail(repo) + urlBuilder = UrlBuilder(gateway) + TestAddObjectSetObjectRelationValueFragment.testVmFactory = AddObjectSetObjectRelationValueViewModel.Factory( + relations = DataViewObjectRelationProvider(state), + values = DataViewObjectValueProvider(state, session), + details = object : ObjectDetailProvider { + override fun provide(): Map = state.value.details + }, + types = object : ObjectTypeProvider { + override fun provide(): List = state.value.objectTypes + }, + addDataViewRelationOption = addRelationOption, + addTagToDataViewRecord = addTagToDataViewRecord, + addStatusToDataViewRecord = addStatusToDataViewRecord, + urlBuilder = urlBuilder, + dispatcher = dispatcher, + ) + } + + @Test + fun shouldStartCreatingDataViewOptionWhenTypeAndButtonClicked() { + + // SETUP + + val relation = Relation( + key = MockDataFactory.randomUuid(), + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = MockDataFactory.randomString(), + source = Relation.Source.values().random(), + format = Relation.Format.STATUS, + selections = emptyList() + ) + + val obj = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to obj, + relation.key to emptyList() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val dv = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + state.value = ObjectSet( + blocks = listOf(dv), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + repo.stub { + onBlocking { + addDataViewRelationOption( + ctx = any(), + dataview = any(), + relation = any(), + color = any(), + name = any(), + record = any() + ) + } doReturn Pair( + Payload( + context = ctx, + events = emptyList() + ), + MockDataFactory.randomUuid() + ) + } + + // TESTING + + launchFragment( + bundleOf( + AddObjectRelationValueFragment.CTX_KEY to ctx, + AddObjectRelationValueFragment.RELATION_KEY to relation.key, + AddObjectRelationValueFragment.DATAVIEW_KEY to dv.id, + AddObjectRelationValueFragment.VIEWER_KEY to viewer.id, + AddObjectRelationValueFragment.TARGET_KEY to obj + ) + ) + + // Creating name for a new option + + R.id.filterInput.type("In progress") + + val btn = R.id.recycler.rVMatcher().onItemView(0, R.id.tvCreateOptionValue) + + btn.checkHasText("Create option \"In progress\"") + + // Pressing button, in order to trigger request. + + btn.performClick() + + // Verifying that the request is made. + + verifyBlocking(repo, times(1)) { + addDataViewRelationOption( + ctx = any(), + dataview = any(), + relation = any(), + color = any(), + name = any(), + record = any() + ) + } + } + + @Test + fun addButtonShouldNotBeVisible() { + + // SETUP + + val relation = Relation( + key = MockDataFactory.randomUuid(), + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = MockDataFactory.randomString(), + source = Relation.Source.values().random(), + format = Relation.Format.STATUS, + selections = emptyList() + ) + + val obj = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to obj, + relation.key to emptyList() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val dv = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + state.value = ObjectSet( + blocks = listOf(dv), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + repo.stub { + onBlocking { + addDataViewRelationOption( + ctx = any(), + dataview = any(), + relation = any(), + color = any(), + name = any(), + record = any() + ) + } doReturn Pair( + Payload( + context = ctx, + events = emptyList() + ), + MockDataFactory.randomUuid() + ) + } + + // TESTING + + launchFragment( + bundleOf( + AddObjectRelationValueFragment.CTX_KEY to ctx, + AddObjectRelationValueFragment.RELATION_KEY to relation.key, + AddObjectRelationValueFragment.DATAVIEW_KEY to dv.id, + AddObjectRelationValueFragment.VIEWER_KEY to viewer.id, + AddObjectRelationValueFragment.TARGET_KEY to obj + ) + ) + + R.id.btnAdd.matchView().checkIsNotDisplayed() + } + + @Test + fun shouldRenderOnlyStatusesWhichRelationValueDoesNotContain() { + + // SETUP + + val option1Color = ThemeColor.values().random() + val option2Color = ThemeColor.values().random() + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "In progress", + color = option1Color.title + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Done", + color = option2Color.title + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Development", + color = "" + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to listOf(option2.id, option3.id) + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = "Roles", + source = Relation.Source.values().random(), + format = Relation.Format.STATUS, + selections = listOf(option1, option2, option3) + ) + + val dv = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + state.value = ObjectSet( + blocks = listOf(dv), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + AddObjectRelationValueFragment.CTX_KEY to ctx, + AddObjectRelationValueFragment.RELATION_KEY to relation.key, + AddObjectRelationValueFragment.DATAVIEW_KEY to dv.id, + AddObjectRelationValueFragment.VIEWER_KEY to viewer.id, + AddObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + R.id.recycler.rVMatcher().apply { + onItemView(0, R.id.tvStatusName).checkHasText(option1.text) + onItemView(0, R.id.tvStatusName).checkHasTextColor(option1Color.text) + checkIsRecyclerSize(1) + } + } + + @Test + fun statusesShouldBeInTheListWhenTypingToCreateNewOption() { + + // SETUP + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "In Testing", + color = ThemeColor.values().random().title + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Done", + color = ThemeColor.values().random().title + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Development", + color = ThemeColor.values().random().title + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to emptyList() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = "Roles", + source = Relation.Source.values().random(), + format = Relation.Format.STATUS, + selections = listOf(option1, option2, option3) + ) + + val dv = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + state.value = ObjectSet( + blocks = listOf(dv), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + AddObjectRelationValueFragment.CTX_KEY to ctx, + AddObjectRelationValueFragment.RELATION_KEY to relation.key, + AddObjectRelationValueFragment.DATAVIEW_KEY to dv.id, + AddObjectRelationValueFragment.VIEWER_KEY to viewer.id, + AddObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + // Typing name for a new option + + R.id.filterInput.type("Backlog") + + // Checking that not only create-option view button, but also tags are visible + + R.id.recycler.rVMatcher().apply { + onItemView(0, R.id.tvCreateOptionValue).checkHasText("Create option \"Backlog\"") + onItemView(1, R.id.tvStatusName).checkHasText(option1.text) + onItemView(2, R.id.tvStatusName).checkHasText(option2.text) + onItemView(3, R.id.tvStatusName).checkHasText(option3.text) + checkIsRecyclerSize(4) + } + } + + @Test + fun shouldRequestAddingStatusToDataViewRecordWhenStatusClicked() { + + // SETUP + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "In progress", + color = ThemeColor.values().random().title + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Testing", + color = ThemeColor.values().random().title + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Development", + color = ThemeColor.values().random().title + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to emptyList() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = "Roles", + source = Relation.Source.values().random(), + format = Relation.Format.STATUS, + selections = listOf(option1, option2, option3) + ) + + val dv = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + state.value = ObjectSet( + blocks = listOf(dv), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + AddObjectRelationValueFragment.CTX_KEY to ctx, + AddObjectRelationValueFragment.RELATION_KEY to relation.key, + AddObjectRelationValueFragment.DATAVIEW_KEY to dv.id, + AddObjectRelationValueFragment.VIEWER_KEY to viewer.id, + AddObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + + // Selecting the first two tags + + R.id.recycler.rVMatcher().onItemView(1, R.id.tvStatusName).performClick() + + // Veryfying UI + + verifyBlocking(repo, times(1)) { + updateDataViewRecord( + context = ctx, + target = dv.id, + record = target, + values = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to listOf(option2.id) + ) + ) + } + } + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/AddRelationTagValueTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/AddRelationTagValueTest.kt new file mode 100644 index 0000000000..1aee8d158e --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/AddRelationTagValueTest.kt @@ -0,0 +1,532 @@ +package com.anytypeio.anytype.features.relations + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.* +import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewRelationOption +import com.anytypeio.anytype.domain.dataview.interactor.AddStatusToDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.AddTagToDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.RemoveTagFromDataViewRecord +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.relations.AddObjectRelationOption +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.page.editor.ThemeColor +import com.anytypeio.anytype.presentation.relations.AddObjectSetObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectValueProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectTypeProvider +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.relations.AddObjectRelationValueFragment +import com.anytypeio.anytype.utils.* +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import com.nhaarman.mockitokotlin2.* +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +@LargeTest +class AddRelationTagValueTest { + + @Mock + lateinit var repo: BlockRepository + + @Mock + lateinit var gateway: Gateway + + @Mock + lateinit var dispatcher: Dispatcher + + private lateinit var addRelationOption: AddDataViewRelationOption + private lateinit var addObjectRelationOption: AddObjectRelationOption + private lateinit var removeTagFromDataViewRecord: RemoveTagFromDataViewRecord + private lateinit var addTagToDataViewRecord: AddTagToDataViewRecord + private lateinit var addStatusToDataViewRecord: AddStatusToDataViewRecord + private lateinit var updateDetail: UpdateDetail + private lateinit var urlBuilder: UrlBuilder + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + private val ctx = MockDataFactory.randomUuid() + private val state = MutableStateFlow(ObjectSet.init()) + private val session = ObjectSetSession() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + addRelationOption = AddDataViewRelationOption(repo) + addTagToDataViewRecord = AddTagToDataViewRecord(repo) + addObjectRelationOption = AddObjectRelationOption(repo) + addStatusToDataViewRecord = AddStatusToDataViewRecord(repo) + removeTagFromDataViewRecord = RemoveTagFromDataViewRecord(repo) + updateDetail = UpdateDetail(repo) + urlBuilder = UrlBuilder(gateway) + TestAddObjectSetObjectRelationValueFragment.testVmFactory = AddObjectSetObjectRelationValueViewModel.Factory( + relations = DataViewObjectRelationProvider(state), + values = DataViewObjectValueProvider(state, session), + details = object : ObjectDetailProvider { + override fun provide(): Map = state.value.details + }, + types = object : ObjectTypeProvider { + override fun provide(): List = state.value.objectTypes + }, + addDataViewRelationOption = addRelationOption, + addTagToDataViewRecord = addTagToDataViewRecord, + addStatusToDataViewRecord = addStatusToDataViewRecord, + urlBuilder = urlBuilder, + dispatcher = dispatcher + ) + } + + @Test + fun shouldStartCreatingDataViewOptionWhenTypeAndButtonClicked() { + + // SETUP + + val relation = Relation( + key = MockDataFactory.randomUuid(), + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = MockDataFactory.randomString(), + source = Relation.Source.values().random(), + format = Relation.Format.TAG, + selections = emptyList() + ) + + val obj = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to obj, + relation.key to emptyList() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val dv = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + state.value = ObjectSet( + blocks = listOf(dv), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + repo.stub { + onBlocking { + addDataViewRelationOption( + ctx = any(), + dataview = any(), + relation = any(), + color = any(), + name = any(), + record = any() + ) + } doReturn Pair( + Payload( + context = ctx, + events = emptyList() + ), + MockDataFactory.randomUuid() + ) + } + + // TESTING + + launchFragment( + bundleOf( + AddObjectRelationValueFragment.CTX_KEY to ctx, + AddObjectRelationValueFragment.RELATION_KEY to relation.key, + AddObjectRelationValueFragment.DATAVIEW_KEY to dv.id, + AddObjectRelationValueFragment.VIEWER_KEY to viewer.id, + AddObjectRelationValueFragment.TARGET_KEY to obj + ) + ) + + // Creating name for a new option + + R.id.filterInput.type("Writer") + + val btn = R.id.recycler.rVMatcher().onItemView(0, R.id.tvCreateOptionValue) + + btn.checkHasText("Create option \"Writer\"") + + // Pressing button, in order to trigger request. + + btn.performClick() + + // Verifying that the request is made. + + verifyBlocking(repo, times(1)) { + addDataViewRelationOption( + ctx = any(), + dataview = any(), + relation = any(), + color = any(), + name = any(), + record = any() + ) + } + } + + @Test + fun shouldRenderOnlyTagsWhichRelationValueDoesNotContain() { + + // SETUP + + val option1Color = ThemeColor.values().random() + val option2Color = ThemeColor.values().random() + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Architect", + color = option1Color.title + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Manager", + color = option2Color.title + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Developer", + color = "" + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to listOf(option2.id, option3.id) + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = "Roles", + source = Relation.Source.values().random(), + format = Relation.Format.TAG, + selections = listOf(option1, option2, option3) + ) + + val dv = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + state.value = ObjectSet( + blocks = listOf(dv), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + AddObjectRelationValueFragment.CTX_KEY to ctx, + AddObjectRelationValueFragment.RELATION_KEY to relation.key, + AddObjectRelationValueFragment.DATAVIEW_KEY to dv.id, + AddObjectRelationValueFragment.VIEWER_KEY to viewer.id, + AddObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + R.id.recycler.rVMatcher().apply { + onItemView(0, R.id.tvTagName).checkHasText(option1.text) + onItemView(0, R.id.tvTagName).checkHasTextColor(option1Color.text) + checkIsRecyclerSize(1) + } + } + + @Test + fun tagsShouldBeInTheListWhenTypingToCreateNewOption() { + + // SETUP + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Architect", + color = ThemeColor.values().random().title + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Manager", + color = ThemeColor.values().random().title + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Developer", + color = ThemeColor.values().random().title + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to emptyList() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = "Roles", + source = Relation.Source.values().random(), + format = Relation.Format.TAG, + selections = listOf(option1, option2, option3) + ) + + val dv = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + state.value = ObjectSet( + blocks = listOf(dv), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + AddObjectRelationValueFragment.CTX_KEY to ctx, + AddObjectRelationValueFragment.RELATION_KEY to relation.key, + AddObjectRelationValueFragment.DATAVIEW_KEY to dv.id, + AddObjectRelationValueFragment.VIEWER_KEY to viewer.id, + AddObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + // Typing name for a new option + + R.id.filterInput.type("Writer") + + // Checking that not only create-option view button, but also tags are visible + + R.id.recycler.rVMatcher().apply { + onItemView(0, R.id.tvCreateOptionValue).checkHasText("Create option \"Writer\"") + onItemView(1, R.id.tvTagName).checkHasText(option1.text) + onItemView(2, R.id.tvTagName).checkHasText(option2.text) + onItemView(3, R.id.tvTagName).checkHasText(option3.text) + checkIsRecyclerSize(4) + } + } + + @Test + fun shouldSelectFirstTwoTagsUpdateCounterAndRequestAddingThisTagToDataViewRecord() { + + // SETUP + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Architect", + color = ThemeColor.values().random().title + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Manager", + color = ThemeColor.values().random().title + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Developer", + color = ThemeColor.values().random().title + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to emptyList() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = "Roles", + source = Relation.Source.values().random(), + format = Relation.Format.TAG, + selections = listOf(option1, option2, option3) + ) + + val dv = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + state.value = ObjectSet( + blocks = listOf(dv), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + AddObjectRelationValueFragment.CTX_KEY to ctx, + AddObjectRelationValueFragment.RELATION_KEY to relation.key, + AddObjectRelationValueFragment.DATAVIEW_KEY to dv.id, + AddObjectRelationValueFragment.VIEWER_KEY to viewer.id, + AddObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + + // Selecting the first two tags + + R.id.recycler.rVMatcher().apply { + onItemView(0, R.id.tvTagName).performClick() + onItemView(1, R.id.tvTagName).performClick() + } + + // Clicking twice on the last tag, in order to check select / unselect logic. + + R.id.recycler.rVMatcher().apply { + onItemView(2, R.id.tvTagName).performClick() + onItemView(2, R.id.tvTagName).performClick() + } + + // Veryfying UI + + R.id.tvSelectionCounter.matchView().checkHasText("2") + + R.id.btnAdd.performClick() + + verifyBlocking(repo, times(1)) { + updateDataViewRecord( + context = ctx, + target = dv.id, + record = target, + values = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to listOf(option1.id, option2.id) + ) + ) + } + } + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayObjectRelationTextValueTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayObjectRelationTextValueTest.kt new file mode 100644 index 0000000000..ff37b935a2 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayObjectRelationTextValueTest.kt @@ -0,0 +1,447 @@ +package com.anytypeio.anytype.features.relations + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectValueProvider +import com.anytypeio.anytype.presentation.sets.ObjectRelationTextValueViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.ui.relations.ObjectRelationTextValueFragment +import com.anytypeio.anytype.utils.CoroutinesTestRule +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import kotlinx.coroutines.flow.MutableStateFlow +import org.hamcrest.CoreMatchers.not +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +@LargeTest +class DisplayObjectRelationTextValueTest { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + val root = MockDataFactory.randomUuid() + + private val state = MutableStateFlow(ObjectSet.init()) + private val session = ObjectSetSession() + + @Before + fun before() { + MockitoAnnotations.initMocks(this) + TestObjectRelationTextValueFragment.testVmFactory = ObjectRelationTextValueViewModel.Factory( + relations = DataViewObjectRelationProvider(state), + values = DataViewObjectValueProvider(state, session) + ) + } + + @Test + fun shouldSetDescriptionTextAndNotDisplayActionButton() { + + // SETUP + + val relationText = "Architect" + val valueText = "Filippo Brunelleschi" + + val target = MockDataFactory.randomUuid() + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + name = relationText, + format = Relation.Format.SHORT_TEXT, + source = Relation.Source.values().random() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation.key to valueText + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + launchFragment( + bundleOf( + ObjectRelationTextValueFragment.CONTEXT_ID to root, + ObjectRelationTextValueFragment.RELATION_ID to relation.key, + ObjectRelationTextValueFragment.OBJECT_ID to target, + ObjectRelationTextValueFragment.FLOW_KEY to ObjectRelationTextValueFragment.FLOW_DATAVIEW + ) + ) + + // TESTING + + onView(withId(R.id.textInputField)).apply { + check(matches(withText(valueText))) + } + + onView(withId(R.id.tvRelationHeader)).apply { + check(matches(withText(relationText))) + } + + onView(withId(R.id.btnAction)).apply { + check(matches(not(isDisplayed()))) + } + } + + @Test + fun shouldSetNumberTextAndNotDisplayActionButton() { + + // SETUP + + val relationText = "Year" + val valueText = "1446" + + val target = MockDataFactory.randomUuid() + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + name = relationText, + format = Relation.Format.NUMBER, + source = Relation.Source.values().random() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation.key to valueText + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationTextValueFragment.CONTEXT_ID to root, + ObjectRelationTextValueFragment.RELATION_ID to relation.key, + ObjectRelationTextValueFragment.OBJECT_ID to target, + ObjectRelationTextValueFragment.FLOW_KEY to ObjectRelationTextValueFragment.FLOW_DATAVIEW + ) + ) + + // TESTING + + onView(withId(R.id.textInputField)).apply { + check(matches(withText(valueText))) + } + + onView(withId(R.id.tvRelationHeader)).apply { + check(matches(withText(relationText))) + } + + onView(withId(R.id.btnAction)).apply { + check(matches(not(isDisplayed()))) + } + } + + @Test + fun shouldSetPhoneTextAndDisplayActionButton() { + + // SETUP + + val relationText = "Phone number" + val valueText = "+ 124242423423" + + val target = MockDataFactory.randomUuid() + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + name = relationText, + format = Relation.Format.PHONE, + source = Relation.Source.values().random() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation.key to valueText + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationTextValueFragment.CONTEXT_ID to root, + ObjectRelationTextValueFragment.RELATION_ID to relation.key, + ObjectRelationTextValueFragment.OBJECT_ID to target, + ObjectRelationTextValueFragment.FLOW_KEY to ObjectRelationTextValueFragment.FLOW_DATAVIEW + ) + ) + + // TESTING + + onView(withId(R.id.textInputField)).apply { + check(matches(withText(valueText))) + } + + onView(withId(R.id.tvRelationHeader)).apply { + check(matches(withText(relationText))) + } + + onView(withId(R.id.btnAction)).apply { + check(matches(isDisplayed())) + } + } + + @Test + fun shouldSetEmailTextAndDisplayActionButton() { + + // SETUP + + val relationText = "Email" + val valueText = "foo.bar@foobar.com" + + val target = MockDataFactory.randomUuid() + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + name = relationText, + format = Relation.Format.EMAIL, + source = Relation.Source.values().random() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation.key to valueText + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + launchFragment( + bundleOf( + ObjectRelationTextValueFragment.CONTEXT_ID to root, + ObjectRelationTextValueFragment.RELATION_ID to relation.key, + ObjectRelationTextValueFragment.OBJECT_ID to target, + ObjectRelationTextValueFragment.FLOW_KEY to ObjectRelationTextValueFragment.FLOW_DATAVIEW + ) + ) + + // TESTING + + onView(withId(R.id.textInputField)).apply { + check(matches(withText(valueText))) + } + + onView(withId(R.id.tvRelationHeader)).apply { + check(matches(withText(relationText))) + } + + onView(withId(R.id.btnAction)).apply { + check(matches(isDisplayed())) + } + } + + @Test + fun shouldSetUrlTextAndDisplayActionButton() { + + // SETUP + + val relationText = "Url" + val valueText = "https://anytype.io" + + val target = MockDataFactory.randomUuid() + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + name = relationText, + format = Relation.Format.URL, + source = Relation.Source.values().random() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation.key to valueText + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + launchFragment( + bundleOf( + ObjectRelationTextValueFragment.CONTEXT_ID to root, + ObjectRelationTextValueFragment.RELATION_ID to relation.key, + ObjectRelationTextValueFragment.OBJECT_ID to target, + ObjectRelationTextValueFragment.FLOW_KEY to ObjectRelationTextValueFragment.FLOW_DATAVIEW + ) + ) + + // TESTING + + onView(withId(R.id.textInputField)).apply { + check(matches(withText(valueText))) + } + + onView(withId(R.id.tvRelationHeader)).apply { + check(matches(withText(relationText))) + } + + onView(withId(R.id.btnAction)).apply { + check(matches(isDisplayed())) + } + } + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationObjectValueTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationObjectValueTest.kt new file mode 100644 index 0000000000..09eee8cb8b --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationObjectValueTest.kt @@ -0,0 +1,640 @@ +package com.anytypeio.anytype.features.relations + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.* +import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewRelationOption +import com.anytypeio.anytype.domain.dataview.interactor.AddTagToDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.RemoveTagFromDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewRecord +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectValueProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectTypeProvider +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.database.modals.ObjectRelationValueFragment +import com.anytypeio.anytype.utils.* +import com.anytypeio.anytype.utils.TestUtils.withRecyclerView +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +@LargeTest +class DisplayRelationObjectValueTest { + + @Mock + lateinit var repo: BlockRepository + + @Mock + lateinit var dispatcher: Dispatcher + + @Mock + lateinit var gateway: Gateway + + private lateinit var addRelationOption: AddDataViewRelationOption + private lateinit var removeTagFromDataViewRecord: RemoveTagFromDataViewRecord + private lateinit var addTagToDataViewRecord: AddTagToDataViewRecord + private lateinit var updateDataViewRecord: UpdateDataViewRecord + private lateinit var updateDetail: UpdateDetail + private lateinit var urlBuilder: UrlBuilder + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + private val root = MockDataFactory.randomUuid() + private val state = MutableStateFlow(ObjectSet.init()) + private val session = ObjectSetSession() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + addRelationOption = AddDataViewRelationOption(repo) + addTagToDataViewRecord = AddTagToDataViewRecord(repo) + removeTagFromDataViewRecord = RemoveTagFromDataViewRecord(repo) + updateDataViewRecord = UpdateDataViewRecord(repo) + updateDetail = UpdateDetail(repo) + urlBuilder = UrlBuilder(gateway) + TestObjectSetObjectRelationValueFragment.testVmFactory = ObjectSetObjectRelationValueViewModel.Factory( + relations = DataViewObjectRelationProvider(state), + values = DataViewObjectValueProvider(state, session), + details = object: ObjectDetailProvider { + override fun provide(): Map = state.value.details + }, + types = object: ObjectTypeProvider { + override fun provide(): List = state.value.objectTypes + }, + removeTagFromRecord = removeTagFromDataViewRecord, + urlBuilder = urlBuilder, + dispatcher = dispatcher, + updateDataViewRecord = updateDataViewRecord + ) + } + + @Test + fun shouldDisplayEditButtonAndPlusButton() { + // SETUP + + val relation = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation to emptyList() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relation, + isMulti = true, + name = MockDataFactory.randomString(), + format = Relation.Format.OBJECT, + source = Relation.Source.values().random() + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relation, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + // Checking that the buttons are invisible + + onView(withId(R.id.btnEditOrDone)).apply { + check(matches(isDisplayed())) + } + + onView(withId(R.id.btnAddValue)).apply { + check(matches(isDisplayed())) + } + } + + @Test + fun shouldSetRelationName() { + + // SETUP + + val name = "Object" + + val relation = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation to emptyList() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relation, + isMulti = true, + name = name, + format = Relation.Format.OBJECT, + source = Relation.Source.values().random() + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relation, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + // Checking that the name is set + + onView(withId(R.id.tvTagOrStatusRelationHeader)).apply { + check(matches(withText(name))) + } + } + + @Test + fun shouldRenderEmptyState() { + + // SETUP + + val name = "Object" + + val relationId = MockDataFactory.randomUuid() + val targetId = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to targetId, + relationId to emptyList() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relationId, + name = name, + format = Relation.Format.OBJECT, + source = Relation.Source.values().random() + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relationId, + ObjectRelationValueFragment.TARGET_KEY to targetId + ) + ) + + val rvMatcher = withRecyclerView(R.id.recycler) + + onView(rvMatcher.atPositionOnView(0, R.id.tvEmptyMessage)).apply { + check(matches(isDisplayed())) + } + } + + @Test + fun shouldRenderTwoObjectsWithNames() { + + // SETUP + + val relationName = "Cast" + val object1Name = "Charlie Chaplin" + val object1Id = MockDataFactory.randomUuid() + val object2Name = "Jean-Pierre Léaud" + val object2Id = MockDataFactory.randomUuid() + + val objectType1 = ObjectType( + url = MockDataFactory.randomUuid(), + name = "Director", + relations = emptyList(), + emoji = "", + layout = ObjectType.Layout.values().random(), + description = "", + isHidden = false + ) + + val objectType2 = ObjectType( + url = MockDataFactory.randomUuid(), + name = "Actor", + relations = emptyList(), + emoji = "", + layout = ObjectType.Layout.values().random(), + description = "", + isHidden = false + ) + + val relationId = MockDataFactory.randomUuid() + val recordId = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to recordId, + relationId to listOf(object1Id, object2Id) + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relationId, + name = relationName, + format = Relation.Format.OBJECT, + source = Relation.Source.values().random() + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ), + details = mapOf( + object1Id to Block.Fields( + mapOf( + Block.Fields.NAME_KEY to object1Name, + Block.Fields.TYPE_KEY to objectType1.url, + "iconEmoji" to "👤" + ) + ), + object2Id to Block.Fields( + mapOf( + Block.Fields.NAME_KEY to object2Name, + Block.Fields.TYPE_KEY to objectType2.url, + "iconEmoji" to "👤" + ) + ) + ), + objectTypes = listOf(objectType1, objectType2) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relationId, + ObjectRelationValueFragment.TARGET_KEY to recordId + ) + ) + + val rvMatcher = withRecyclerView(R.id.recycler) + + onView(rvMatcher.atPositionOnView(0, R.id.tvTitle)).apply { + check(matches(withText(object1Name))) + } + + onView(rvMatcher.atPositionOnView(0, R.id.tvSubtitle)).apply { + check(matches(withText(objectType1.name))) + } + + onView(rvMatcher.atPositionOnView(1, R.id.tvTitle)).apply { + check(matches(withText(object2Name))) + } + + onView(rvMatcher.atPositionOnView(1, R.id.tvSubtitle)).apply { + check(matches(withText(objectType2.name))) + } + } + + @Test + fun shouldRenderTwoObjectsWithoutObjectTypes() { + + // SETUP + + val relationName = "Cast" + val object1Name = "Charlie Chaplin" + val object1Id = MockDataFactory.randomUuid() + val object2Name = "Jean-Pierre Léaud" + val object2Id = MockDataFactory.randomUuid() + + val relationId = MockDataFactory.randomUuid() + val recordId = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to recordId, + relationId to listOf(object1Id, object2Id) + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relationId, + name = relationName, + format = Relation.Format.OBJECT, + source = Relation.Source.values().random() + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ), + details = mapOf( + object1Id to Block.Fields( + mapOf( + Block.Fields.NAME_KEY to object1Name, + "iconEmoji" to "👤" + ) + ), + object2Id to Block.Fields( + mapOf( + Block.Fields.NAME_KEY to object2Name, + "iconEmoji" to "👤" + ) + ) + ), + objectTypes = listOf() + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relationId, + ObjectRelationValueFragment.TARGET_KEY to recordId + ) + ) + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.tvTitle).checkHasText(object1Name) + onItemView(0, R.id.tvSubtitle).checkHasText(R.string.unknown_object_type) + onItemView(1, R.id.tvTitle).checkHasText(object2Name) + onItemView(1, R.id.tvSubtitle).checkHasText(R.string.unknown_object_type) + checkIsRecyclerSize(2) + } + } + + @Test + fun shouldRenderProfileObjectWithNameAndInitial() { + + // SETUP + + val relationName = "Writers" + val object1Name = "Virginia Woolf" + val object1Id = MockDataFactory.randomUuid() + val object2Name = "Réné-Auguste Chateaubriand" + val object2Id = MockDataFactory.randomUuid() + + val objectType1 = ObjectType( + url = MockDataFactory.randomUuid(), + name = "Writer", + relations = emptyList(), + emoji = "", + layout = ObjectType.Layout.PROFILE, + description = "", + isHidden = false + ) + + val objectType2 = ObjectType( + url = MockDataFactory.randomUuid(), + name = "Writer", + relations = emptyList(), + emoji = "", + layout = ObjectType.Layout.PROFILE, + description = "", + isHidden = false + ) + + val relationId = MockDataFactory.randomUuid() + val recordId = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to recordId, + relationId to listOf(object1Id, object2Id) + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relationId, + name = relationName, + format = Relation.Format.OBJECT, + source = Relation.Source.values().random() + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ), + details = mapOf( + object1Id to Block.Fields( + mapOf( + Block.Fields.NAME_KEY to object1Name, + Block.Fields.TYPE_KEY to objectType1.url + ) + ), + object2Id to Block.Fields( + mapOf( + Block.Fields.NAME_KEY to object2Name, + Block.Fields.TYPE_KEY to objectType2.url + ) + ) + ), + objectTypes = listOf(objectType1, objectType2) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relationId, + ObjectRelationValueFragment.TARGET_KEY to recordId + ) + ) + + val rvMatcher = withRecyclerView(R.id.recycler) + + onView(rvMatcher.atPositionOnView(0, R.id.initial)).apply { + check(matches(withText(object1Name.first().toString()))) + } + + onView(rvMatcher.atPositionOnView(1, R.id.initial)).apply { + check(matches(withText(object2Name.first().toString()))) + } + } + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationStatusValueTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationStatusValueTest.kt new file mode 100644 index 0000000000..a294f10195 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationStatusValueTest.kt @@ -0,0 +1,508 @@ +package com.anytypeio.anytype.features.relations + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.* +import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewRelationOption +import com.anytypeio.anytype.domain.dataview.interactor.AddTagToDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.RemoveTagFromDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewRecord +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.page.editor.ThemeColor +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectValueProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectTypeProvider +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.database.modals.ObjectRelationValueFragment +import com.anytypeio.anytype.utils.CoroutinesTestRule +import com.anytypeio.anytype.utils.TestUtils.withRecyclerView +import com.anytypeio.anytype.utils.WithTextColor +import com.anytypeio.anytype.utils.WithTextColorRes +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +@LargeTest +class DisplayRelationStatusValueTest { + + @Mock + lateinit var repo: BlockRepository + + @Mock + lateinit var dispatcher: Dispatcher + + @Mock + lateinit var gateway: Gateway + + private lateinit var addRelationOption: AddDataViewRelationOption + private lateinit var removeTagFromDataViewRecord: RemoveTagFromDataViewRecord + private lateinit var addTagToDataViewRecord: AddTagToDataViewRecord + private lateinit var updateDataViewRecord: UpdateDataViewRecord + private lateinit var updateDetail: UpdateDetail + private lateinit var urlBuilder: UrlBuilder + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + private val root = MockDataFactory.randomUuid() + private val state = MutableStateFlow(ObjectSet.init()) + private val session = ObjectSetSession() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + addRelationOption = AddDataViewRelationOption(repo) + addTagToDataViewRecord = AddTagToDataViewRecord(repo) + updateDataViewRecord = UpdateDataViewRecord(repo) + removeTagFromDataViewRecord = RemoveTagFromDataViewRecord(repo) + updateDetail = UpdateDetail(repo) + urlBuilder = UrlBuilder(gateway) + TestObjectSetObjectRelationValueFragment.testVmFactory = ObjectSetObjectRelationValueViewModel.Factory( + relations = DataViewObjectRelationProvider(state), + values = DataViewObjectValueProvider(state, session), + details = object: ObjectDetailProvider { + override fun provide(): Map = state.value.details + }, + types = object: ObjectTypeProvider { + override fun provide(): List = state.value.objectTypes + }, + removeTagFromRecord = removeTagFromDataViewRecord, + urlBuilder = urlBuilder, + dispatcher = dispatcher, + updateDataViewRecord = updateDataViewRecord + ) + } + + @Test + fun shouldDisplayEditButtonAndPlusButton() { + // SETUP + + val relation = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relation, + isMulti = true, + name = MockDataFactory.randomString(), + format = Relation.Format.STATUS, + source = Relation.Source.values().random() + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relation, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + // Checking that the buttons are invisible + + onView(withId(R.id.btnEditOrDone)).apply { + check(matches(isDisplayed())) + } + + onView(withId(R.id.btnAddValue)).apply { + check(matches(isDisplayed())) + } + } + + @Test + fun shouldSetRelationName() { + + // SETUP + + val name = "Status" + + val relation = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relation, + isMulti = true, + name = name, + format = Relation.Format.STATUS, + source = Relation.Source.values().random() + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relation, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + // Checking that the name is set + + onView(withId(R.id.tvTagOrStatusRelationHeader)).apply { + check(matches(withText(name))) + } + } + + @Test + fun shouldRenderStatusFromDV() { + + // SETUP + + val option1Color = ThemeColor.values().random() + val option2Color = ThemeColor.values().random() + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "In progress", + color = option1Color.title + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Done", + color = option2Color.title + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Todo", + color = "" + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to listOf(option2.id) + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = MockDataFactory.randomString(), + source = Relation.Source.values().random(), + format = Relation.Format.STATUS, + selections = listOf(option1, option2, option3) + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relationKey, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + val rvMatcher = withRecyclerView(R.id.recycler) + + onView(rvMatcher.atPositionOnView(0, R.id.tvStatusName)).apply { + check(matches(withText(option2.text))) + check(matches(WithTextColor(option2Color.text))) + } + } + + @Test + fun shouldRenderStatusWithDefaultColor() { + + // SETUP + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "In progress", + color = "" + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to listOf(option1.id) + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = MockDataFactory.randomString(), + source = Relation.Source.values().random(), + format = Relation.Format.STATUS, + selections = listOf(option1) + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relationKey, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + val rvMatcher = withRecyclerView(R.id.recycler) + + onView(rvMatcher.atPositionOnView(0, R.id.tvStatusName)).apply { + check(matches(WithTextColorRes(R.color.default_filter_tag_text_color))) + } + } + + @Test + fun shouldRenderEmptyState() { + + // SETUP + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "In progress", + color = MockDataFactory.randomString() + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Done", + color = MockDataFactory.randomString() + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Todo", + color = MockDataFactory.randomString() + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = MockDataFactory.randomString(), + source = Relation.Source.values().random(), + format = Relation.Format.STATUS, + selections = listOf(option1, option2, option3) + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relationKey, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + val rvMatcher = withRecyclerView(R.id.recycler) + + onView(rvMatcher.atPositionOnView(0, R.id.tvEmptyMessage)).apply { + check(matches(isDisplayed())) + } + } + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationTagValueTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationTagValueTest.kt new file mode 100644 index 0000000000..ce8cf9bd0c --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationTagValueTest.kt @@ -0,0 +1,518 @@ +package com.anytypeio.anytype.features.relations + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.* +import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewRelationOption +import com.anytypeio.anytype.domain.dataview.interactor.AddTagToDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.RemoveTagFromDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewRecord +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.page.editor.ThemeColor +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectValueProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectTypeProvider +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.database.modals.ObjectRelationValueFragment +import com.anytypeio.anytype.utils.CoroutinesTestRule +import com.anytypeio.anytype.utils.TestUtils.withRecyclerView +import com.anytypeio.anytype.utils.WithTextColor +import com.anytypeio.anytype.utils.WithTextColorRes +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import kotlinx.coroutines.flow.MutableStateFlow +import org.hamcrest.core.IsNot.not +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +@LargeTest +class DisplayRelationTagValueTest { + + @Mock + lateinit var repo: BlockRepository + + @Mock + lateinit var gateway: Gateway + + @Mock + lateinit var dispatcher: Dispatcher + + private lateinit var addRelationOption: AddDataViewRelationOption + private lateinit var removeTagFromDataViewRecord: RemoveTagFromDataViewRecord + private lateinit var addTagToDataViewRecord: AddTagToDataViewRecord + private lateinit var updateDataViewRecord: UpdateDataViewRecord + private lateinit var updateDetail: UpdateDetail + private lateinit var urlBuilder: UrlBuilder + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + private val root = MockDataFactory.randomUuid() + private val state = MutableStateFlow(ObjectSet.init()) + private val session = ObjectSetSession() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + addRelationOption = AddDataViewRelationOption(repo) + addTagToDataViewRecord = AddTagToDataViewRecord(repo) + removeTagFromDataViewRecord = RemoveTagFromDataViewRecord(repo) + updateDataViewRecord = UpdateDataViewRecord(repo) + updateDetail = UpdateDetail(repo) + urlBuilder = UrlBuilder(gateway) + TestObjectSetObjectRelationValueFragment.testVmFactory = ObjectSetObjectRelationValueViewModel.Factory( + relations = DataViewObjectRelationProvider(state), + values = DataViewObjectValueProvider(state, session), + details = object: ObjectDetailProvider { + override fun provide(): Map = state.value.details + }, + types = object: ObjectTypeProvider { + override fun provide(): List = state.value.objectTypes + }, + removeTagFromRecord = removeTagFromDataViewRecord, + urlBuilder = urlBuilder, + dispatcher = dispatcher, + updateDataViewRecord = updateDataViewRecord + ) + } + + @Test + fun shouldDisplayEditButtonAndPlusButton() { + // SETUP + + val relation = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relation, + isMulti = true, + name = MockDataFactory.randomString(), + format = Relation.Format.TAG, + source = Relation.Source.values().random() + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relation, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + // Checking that the buttons are invisible + + onView(withId(R.id.btnEditOrDone)).apply { + check(matches(isDisplayed())) + } + + onView(withId(R.id.btnAddValue)).apply { + check(matches(isDisplayed())) + } + } + + @Test + fun shouldSetRelationName() { + + // SETUP + + val name = "Tag" + + val relation = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relation, + isMulti = true, + name = name, + format = Relation.Format.TAG, + source = Relation.Source.values().random() + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relation, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + // Checking that the name is set + + onView(withId(R.id.tvTagOrStatusRelationHeader)).apply { + check(matches(withText(name))) + } + } + + @Test + fun shouldRenderTwoLastTagsFromDvWithFilterContainerInvisible() { + + // SETUP + + val option1Color = ThemeColor.values().random() + val option2Color = ThemeColor.values().random() + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Architect", + color = option1Color.title + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Manager", + color = option2Color.title + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Developer", + color = "" + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to listOf(option2.id, option3.id) + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = MockDataFactory.randomString(), + source = Relation.Source.values().random(), + format = Relation.Format.TAG, + selections = listOf(option1, option2, option3) + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relationKey, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + val rvMatcher = withRecyclerView(R.id.recycler) + + onView(withId(R.id.filterInputContainer)).apply { + check(matches(not(isDisplayed()))) + } + + onView(rvMatcher.atPositionOnView(0, R.id.tvTagName)).apply { + check(matches(withText(option2.text))) + check(matches(WithTextColor(option2Color.text))) + } + + onView(rvMatcher.atPositionOnView(1, R.id.tvTagName)).apply { + check(matches(withText(option3.text))) + check(matches(WithTextColorRes(R.color.default_filter_tag_text_color))) + } + } + + @Test + fun shouldRenderOnlyOneTagWithDefaultColor() { + + // SETUP + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Architect", + color = "" + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to listOf(option1.id) + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = MockDataFactory.randomString(), + source = Relation.Source.values().random(), + format = Relation.Format.TAG, + selections = listOf(option1) + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relationKey, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + val rvMatcher = withRecyclerView(R.id.recycler) + + onView(rvMatcher.atPositionOnView(0, R.id.tvTagName)).apply { + check(matches(WithTextColorRes(R.color.default_filter_tag_text_color))) + } + } + + @Test + fun shouldRenderEmptyState() { + + // SETUP + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "TAG1", + color = MockDataFactory.randomString() + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "TAG 2", + color = MockDataFactory.randomString() + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "TAG 3", + color = MockDataFactory.randomString() + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = MockDataFactory.randomString(), + source = Relation.Source.values().random(), + format = Relation.Format.TAG, + selections = listOf(option1, option2, option3) + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to root, + ObjectRelationValueFragment.RELATION_KEY to relationKey, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + val rvMatcher = withRecyclerView(R.id.recycler) + + onView(rvMatcher.atPositionOnView(0, R.id.tvEmptyMessage)).apply { + check(matches(isDisplayed())) + } + } + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/EditRelationTagValueTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/EditRelationTagValueTest.kt new file mode 100644 index 0000000000..0070893075 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/EditRelationTagValueTest.kt @@ -0,0 +1,328 @@ +package com.anytypeio.anytype.features.relations + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.* +import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewRelationOption +import com.anytypeio.anytype.domain.dataview.interactor.AddTagToDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.RemoveTagFromDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewRecord +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.page.editor.ThemeColor +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectValueProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectTypeProvider +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.database.modals.ObjectRelationValueFragment +import com.anytypeio.anytype.utils.* +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verifyBlocking +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +@LargeTest +class EditRelationTagValueTest { + + @Mock + lateinit var repo: BlockRepository + + @Mock + lateinit var gateway: Gateway + + @Mock + lateinit var dispatcher: Dispatcher + + private lateinit var addRelationOption: AddDataViewRelationOption + private lateinit var removeTagFromDataViewRecord: RemoveTagFromDataViewRecord + private lateinit var updateDataViewRecord: UpdateDataViewRecord + private lateinit var addTagToDataViewRecord: AddTagToDataViewRecord + private lateinit var updateDetail: UpdateDetail + private lateinit var urlBuilder: UrlBuilder + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + private val ctx = MockDataFactory.randomUuid() + private val state = MutableStateFlow(ObjectSet.init()) + private val session = ObjectSetSession() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + addRelationOption = AddDataViewRelationOption(repo) + addTagToDataViewRecord = AddTagToDataViewRecord(repo) + removeTagFromDataViewRecord = RemoveTagFromDataViewRecord(repo) + updateDataViewRecord = UpdateDataViewRecord(repo) + updateDetail = UpdateDetail(repo) + urlBuilder = UrlBuilder(gateway) + TestObjectSetObjectRelationValueFragment.testVmFactory = ObjectSetObjectRelationValueViewModel.Factory( + relations = DataViewObjectRelationProvider(state), + values = DataViewObjectValueProvider(state, session), + details = object : ObjectDetailProvider { + override fun provide(): Map = state.value.details + }, + types = object : ObjectTypeProvider { + override fun provide(): List = state.value.objectTypes + }, + removeTagFromRecord = removeTagFromDataViewRecord, + urlBuilder = urlBuilder, + dispatcher = dispatcher, + updateDataViewRecord = updateDataViewRecord + ) + } + + @Test + fun shouldRenderTwoTagsInReadThenInEditMode() { + + // SETUP + + val option1Color = ThemeColor.values().random() + val option2Color = ThemeColor.values().random() + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Architect", + color = option1Color.title + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Manager", + color = option2Color.title + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Developer", + color = "" + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to listOf(option2.id, option3.id) + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = "Roles", + source = Relation.Source.values().random(), + format = Relation.Format.TAG, + selections = listOf(option1, option2, option3) + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to ctx, + ObjectRelationValueFragment.RELATION_KEY to relationKey, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + val editOrDoneBtn = R.id.btnEditOrDone.matchView() + + editOrDoneBtn.checkHasText(R.string.edit) + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.tvTagName).checkHasText(option2.text) + onItemView(0, R.id.tvTagName).checkHasTextColor(option2Color.text) + onItemView(0, R.id.btnRemoveTag).checkIsNotDisplayed() + onItemView(0, R.id.btnDragAndDropTag).checkIsNotDisplayed() + onItemView(1, R.id.tvTagName).checkHasText(option3.text) + onItemView(1, R.id.btnRemoveTag).checkIsNotDisplayed() + onItemView(1, R.id.btnDragAndDropTag).checkIsNotDisplayed() + checkIsRecyclerSize(2) + } + + editOrDoneBtn.performClick() + + editOrDoneBtn.checkHasText(R.string.done) + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.tvTagName).checkHasText(option2.text) + onItemView(0, R.id.tvTagName).checkHasTextColor(option2Color.text) + onItemView(0, R.id.btnRemoveTag).checkIsDisplayed() + onItemView(0, R.id.btnDragAndDropTag).checkIsDisplayed() + onItemView(1, R.id.tvTagName).checkHasText(option3.text) + onItemView(1, R.id.btnRemoveTag).checkIsDisplayed() + onItemView(1, R.id.btnDragAndDropTag).checkIsDisplayed() + checkIsRecyclerSize(2) + } + } + + @Test + fun shouldRequestRemovingLastTagWhenRemoveButtonPressed() { + + // SETUP + + val option1Color = ThemeColor.values().random() + val option2Color = ThemeColor.values().random() + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Architect", + color = option1Color.title + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Manager", + color = option2Color.title + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Developer", + color = "" + ) + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to listOf(option2.id, option3.id) + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val dv = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf( + Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = "Roles", + source = Relation.Source.values().random(), + format = Relation.Format.TAG, + selections = listOf(option1, option2, option3) + ) + ), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + state.value = ObjectSet( + blocks = listOf(dv), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // TESTING + + launchFragment( + bundleOf( + ObjectRelationValueFragment.CTX_KEY to ctx, + ObjectRelationValueFragment.DATAVIEW_KEY to dv.id, + ObjectRelationValueFragment.VIEWER_KEY to viewer.id, + ObjectRelationValueFragment.RELATION_KEY to relationKey, + ObjectRelationValueFragment.TARGET_KEY to target + ) + ) + + R.id.btnEditOrDone.performClick() + + val rvMatcher = R.id.recycler.rVMatcher() + + rvMatcher.onItemView(1, R.id.btnRemoveTag).performClick() + + verifyBlocking(repo, times(1)) { + updateDataViewRecord( + context = ctx, + target = dv.id, + record = target, + values = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to listOf(option2.id) + ) + ) + } + } + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/ObjectRelationDateValueTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/ObjectRelationDateValueTest.kt new file mode 100644 index 0000000000..460743d0d7 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/ObjectRelationDateValueTest.kt @@ -0,0 +1,590 @@ +package com.anytypeio.anytype.features.relations + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.core_utils.ext.timeInSecondsFormat +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectValueProvider +import com.anytypeio.anytype.presentation.sets.ObjectRelationDateValueViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.TIME_FORMAT_DEFAULT +import com.anytypeio.anytype.ui.relations.ObjectRelationDateValueFragment +import com.anytypeio.anytype.utils.CoroutinesTestRule +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import kotlinx.coroutines.flow.MutableStateFlow +import org.hamcrest.CoreMatchers.not +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations +import java.util.* + +@RunWith(AndroidJUnit4::class) +@LargeTest +class ObjectRelationDateValueTest { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + val root = MockDataFactory.randomUuid() + + private val state = MutableStateFlow(ObjectSet.init()) + private val session = ObjectSetSession() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + TestObjectRelationDateValueFragment.testVmFactory = + ObjectRelationDateValueViewModel.Factory( + relations = DataViewObjectRelationProvider(state), + values = DataViewObjectValueProvider(state, session) + ) + } + + @Test + fun shouldSetNullDateValue() { + + // SETUP + + val relationText = "Birth date" + val valueDate: Long? = null + + val target = MockDataFactory.randomUuid() + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + name = relationText, + format = Relation.Format.DATE, + source = Relation.Source.values().random() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation.key to valueDate + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + launchFragment( + bundleOf( + ObjectRelationDateValueFragment.CONTEXT_ID to root, + ObjectRelationDateValueFragment.RELATION_ID to relation.key, + ObjectRelationDateValueFragment.OBJECT_ID to target, + ObjectRelationDateValueFragment.FLOW_KEY to ObjectRelationDateValueFragment.FLOW_DATAVIEW + ) + ) + + onView(withId(R.id.ivTodayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivYesterdayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivTomorrowCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivExactDayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.tvDate)).check(matches(withText(""))) + } + + @Test + fun shouldSetTodayDateValue() { + + // SETUP + + val relationText = "Birth date" + val valueDate: Long = System.currentTimeMillis() / 1000 + + val target = MockDataFactory.randomUuid() + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + name = relationText, + format = Relation.Format.DATE, + source = Relation.Source.values().random() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation.key to valueDate + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + launchFragment( + bundleOf( + ObjectRelationDateValueFragment.CONTEXT_ID to root, + ObjectRelationDateValueFragment.RELATION_ID to relation.key, + ObjectRelationDateValueFragment.OBJECT_ID to target, + ObjectRelationDateValueFragment.FLOW_KEY to ObjectRelationDateValueFragment.FLOW_DATAVIEW + ) + ) + + onView(withId(R.id.ivTodayCheck)).check(matches(isDisplayed())) + onView(withId(R.id.ivYesterdayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivTomorrowCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivExactDayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.tvDate)).check(matches(withText(""))) + } + + @Test + fun shouldSetTomorrowDateValue() { + + // SETUP + + val relationText = "Birth date" + val calendar = Calendar.getInstance().apply { add(Calendar.DATE, 1) } + val valueDate: Long = calendar.timeInMillis / 1000 + + val target = MockDataFactory.randomUuid() + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + name = relationText, + format = Relation.Format.DATE, + source = Relation.Source.values().random() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation.key to valueDate + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + launchFragment( + bundleOf( + ObjectRelationDateValueFragment.CONTEXT_ID to root, + ObjectRelationDateValueFragment.RELATION_ID to relation.key, + ObjectRelationDateValueFragment.OBJECT_ID to target, + ObjectRelationDateValueFragment.FLOW_KEY to ObjectRelationDateValueFragment.FLOW_DATAVIEW + ) + ) + + onView(withId(R.id.ivTomorrowCheck)).check(matches((isDisplayed()))) + onView(withId(R.id.ivTodayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivYesterdayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivExactDayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.tvDate)).check(matches(withText(""))) + } + + @Test + fun shouldSetYesterdayDateValue() { + + // SETUP + + val relationText = "Birth date" + val calendar = Calendar.getInstance().apply { add(Calendar.DATE, -1) } + val valueDate: Long = calendar.timeInMillis / 1000 + + val target = MockDataFactory.randomUuid() + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + name = relationText, + format = Relation.Format.DATE, + source = Relation.Source.values().random() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation.key to valueDate + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + launchFragment( + bundleOf( + ObjectRelationDateValueFragment.CONTEXT_ID to root, + ObjectRelationDateValueFragment.RELATION_ID to relation.key, + ObjectRelationDateValueFragment.OBJECT_ID to target, + ObjectRelationDateValueFragment.FLOW_KEY to ObjectRelationDateValueFragment.FLOW_DATAVIEW + ) + ) + + onView(withId(R.id.ivYesterdayCheck)).check(matches((isDisplayed()))) + onView(withId(R.id.ivTodayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivTomorrowCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivExactDayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.tvDate)).check(matches(withText(""))) + } + + @Test + fun shouldSetExactDayDateValue() { + + // SETUP + + val relationText = "Birth date" + val calendar = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, 45) } + val valueDate: Long = calendar.timeInMillis / 1000 + + val target = MockDataFactory.randomUuid() + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + name = relationText, + format = Relation.Format.DATE, + source = Relation.Source.values().random() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation.key to valueDate + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + launchFragment( + bundleOf( + ObjectRelationDateValueFragment.CONTEXT_ID to root, + ObjectRelationDateValueFragment.RELATION_ID to relation.key, + ObjectRelationDateValueFragment.OBJECT_ID to target, + ObjectRelationDateValueFragment.FLOW_KEY to ObjectRelationDateValueFragment.FLOW_DATAVIEW + ) + ) + + val exactDateFormat = valueDate.timeInSecondsFormat(TIME_FORMAT_DEFAULT) + onView(withId(R.id.ivYesterdayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivTodayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivTomorrowCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivExactDayCheck)).check(matches((isDisplayed()))) + onView(withId(R.id.tvDate)).check(matches(withText(exactDateFormat))) + } + + @Test + fun shouldSetExactDayDateValueAndThenUpdateWithDatePicker() { + + // SETUP + + val relationText = "Birth date" + val calendar = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, 45) } + val valueDate: Long = calendar.timeInMillis / 1000 + + val target = MockDataFactory.randomUuid() + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + name = relationText, + format = Relation.Format.DATE, + source = Relation.Source.values().random() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation.key to valueDate + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + val fragment = launchFragment( + bundleOf( + ObjectRelationDateValueFragment.CONTEXT_ID to root, + ObjectRelationDateValueFragment.RELATION_ID to relation.key, + ObjectRelationDateValueFragment.OBJECT_ID to target, + ObjectRelationDateValueFragment.FLOW_KEY to ObjectRelationDateValueFragment.FLOW_DATAVIEW + ) + ) + + val exactDateFormat = valueDate.timeInSecondsFormat(TIME_FORMAT_DEFAULT) + onView(withId(R.id.ivYesterdayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivTodayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivTomorrowCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivExactDayCheck)).check(matches((isDisplayed()))) + onView(withId(R.id.tvDate)).check(matches(withText(exactDateFormat))) + + val dateUpdate = Calendar.getInstance().apply { add(Calendar.DATE, 17) } + val valueUpdate: Long = dateUpdate.timeInMillis / 1000 + fragment.onFragment { + it.onPickDate(valueUpdate) + } + + val updatedDateFormat = valueUpdate.timeInSecondsFormat(TIME_FORMAT_DEFAULT) + onView(withId(R.id.ivTomorrowCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivTodayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivYesterdayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivExactDayCheck)).check(matches((isDisplayed()))) + onView(withId(R.id.tvDate)).check(matches(withText(updatedDateFormat))) + } + + @Test + fun shouldSetExactDayDateValueAndThenUpdateToTodayTomorrowAndYesterday() { + + // SETUP + + val relationText = "Birth date" + val calendar = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, 45) } + val valueDate: Long = calendar.timeInMillis / 1000 + + val target = MockDataFactory.randomUuid() + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + name = relationText, + format = Relation.Format.DATE, + source = Relation.Source.values().random() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relation.key to valueDate + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + val fragment = launchFragment( + bundleOf( + ObjectRelationDateValueFragment.CONTEXT_ID to root, + ObjectRelationDateValueFragment.RELATION_ID to relation.key, + ObjectRelationDateValueFragment.OBJECT_ID to target, + ObjectRelationDateValueFragment.FLOW_KEY to ObjectRelationDateValueFragment.FLOW_DATAVIEW + ) + ) + + val exactDateFormat = valueDate.timeInSecondsFormat(TIME_FORMAT_DEFAULT) + onView(withId(R.id.ivYesterdayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivTodayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivTomorrowCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivExactDayCheck)).check(matches((isDisplayed()))) + onView(withId(R.id.tvDate)).check(matches(withText(exactDateFormat))) + + fragment.onFragment { it.vm.onTodayClicked() } + + onView(withId(R.id.ivTomorrowCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivTodayCheck)).check(matches((isDisplayed()))) + onView(withId(R.id.ivYesterdayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivExactDayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.tvDate)).check(matches(withText(""))) + + fragment.onFragment { it.vm.onTomorrowClicked() } + + onView(withId(R.id.ivTomorrowCheck)).check(matches((isDisplayed()))) + onView(withId(R.id.ivTodayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivYesterdayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivExactDayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.tvDate)).check(matches(withText(""))) + + fragment.onFragment { it.vm.onYesterdayClicked() } + + onView(withId(R.id.ivTomorrowCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivTodayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.ivYesterdayCheck)).check(matches((isDisplayed()))) + onView(withId(R.id.ivExactDayCheck)).check(matches(not(isDisplayed()))) + onView(withId(R.id.tvDate)).check(matches(withText(""))) + } + + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/ObjectRelationListTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/ObjectRelationListTest.kt new file mode 100644 index 0000000000..2c556cc132 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/ObjectRelationListTest.kt @@ -0,0 +1,631 @@ +package com.anytypeio.anytype.features.relations + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.core_utils.ext.toTimeSeconds +import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.dataview.interactor.ObjectRelationList +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.page.Editor +import com.anytypeio.anytype.presentation.page.editor.DetailModificationManager +import com.anytypeio.anytype.presentation.page.editor.ThemeColor +import com.anytypeio.anytype.presentation.relations.ObjectRelationListViewModelFactory +import com.anytypeio.anytype.presentation.sets.MONTH_DAY_AND_YEAR +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.relations.ObjectRelationListFragment +import com.anytypeio.anytype.utils.* +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import java.text.SimpleDateFormat +import java.util.* + +@RunWith(AndroidJUnit4::class) +@LargeTest +class ObjectRelationListTest { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + @Mock + lateinit var gateway: Gateway + + @Mock + lateinit var repo: BlockRepository + + @Mock + lateinit var dispatcher: Dispatcher + + @Mock + lateinit var detailModificationManager: DetailModificationManager + + private lateinit var objectRelationList: ObjectRelationList + private lateinit var updateDetail: UpdateDetail + + private val ctx = MockDataFactory.randomUuid() + private val storage = Editor.Storage() + + lateinit var urlBuilder: UrlBuilder + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + urlBuilder = UrlBuilder(gateway) + objectRelationList = ObjectRelationList(repo) + updateDetail = UpdateDetail(repo) + TestObjectRelationListFragment.testVmFactory = ObjectRelationListViewModelFactory( + stores = storage, + urlBuilder = urlBuilder, + objectRelationList = objectRelationList, + dispatcher = dispatcher, + detailModificationManager = detailModificationManager, + updateDetail = updateDetail + ) + } + + @Test(expected = RuntimeException::class) + fun shouldThrowAnExceptionIfArgsNotProvided() { + launchFragment(bundleOf()) + } + + @Test + fun shouldDisplayOneRelationWithoutValue() { + + // SETUP + + val name = "Description" + + val relation = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.LONG_TEXT, + source = Relation.Source.values().random(), + name = name + ) + + runBlocking { + storage.relations.update( + listOf(relation) + ) + } + + launchFragment(bundleOf( + ObjectRelationListFragment.ARG_CTX to ctx, + ObjectRelationListFragment.ARG_MODE to ObjectRelationListFragment.MODE_LIST + )) + + // TESTING + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.tvRelationTitle).checkHasText(name) + onItemView(0, R.id.tvRelationValue).checkHasText("") + checkIsRecyclerSize(1) + } + } + + @Test + fun shouldDisplayOnlyFirstRelationBecauseSecondIsHidden() { + + // SETUP + + val name1 = "Description" + val name2 = "Identifier" + + val relation1 = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.LONG_TEXT, + source = Relation.Source.values().random(), + isHidden = true, + name = name1 + ) + + val relation2 = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.LONG_TEXT, + source = Relation.Source.values().random(), + name = name2 + ) + + runBlocking { + storage.relations.update( + listOf(relation1, relation2) + ) + } + + launchFragment(bundleOf( + ObjectRelationListFragment.ARG_CTX to ctx, + ObjectRelationListFragment.ARG_MODE to ObjectRelationListFragment.MODE_LIST + )) + + // TESTING + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.tvRelationTitle).checkHasText(name2) + onItemView(0, R.id.tvRelationValue).checkHasText("") + checkIsRecyclerSize(1) + } + } + + @Test + fun shouldDisplayTwoRelationsWithoutValue() { + + // SETUP + + val name1 = "Description" + val name2 = "Comment" + + val relation1 = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.LONG_TEXT, + source = Relation.Source.values().random(), + name = name1 + ) + + val relation2 = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.LONG_TEXT, + source = Relation.Source.values().random(), + name = name2 + ) + + runBlocking { + storage.relations.update( + listOf(relation1, relation2) + ) + } + + launchFragment(bundleOf( + ObjectRelationListFragment.ARG_CTX to ctx, + ObjectRelationListFragment.ARG_MODE to ObjectRelationListFragment.MODE_LIST + )) + + // TESTING + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.tvRelationTitle).checkHasText(name1) + onItemView(0, R.id.tvRelationValue).checkHasText("") + onItemView(1, R.id.tvRelationTitle).checkHasText(name2) + onItemView(1, R.id.tvRelationValue).checkHasText("") + checkIsRecyclerSize(2) + } + } + + @Test + fun shouldDisplayTwoRelationsWithValues() { + + // SETUP + + val name1 = "Description" + val name2 = "Comment" + val value1 = "A mountain is an elevated portion of the Earth's crust, generally with steep sides that show significant exposed bedrock." + val value2 = "We've never seen that mountain before." + + val relation1 = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.LONG_TEXT, + source = Relation.Source.values().random(), + name = name1 + ) + + val relation2 = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.LONG_TEXT, + source = Relation.Source.values().random(), + name = name2 + ) + + val relations = listOf(relation1, relation2) + val details = Block.Details( + mapOf( + ctx to Block.Fields( + mapOf( + relation1.key to value1, + relation2.key to value2, + ) + ) + ) + ) + + runBlocking { + storage.relations.update(relations) + storage.details.update(details) + } + + launchFragment( + bundleOf( + ObjectRelationListFragment.ARG_CTX to ctx, + ObjectRelationListFragment.ARG_MODE to ObjectRelationListFragment.MODE_LIST + ) + ) + + // TESTING + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.tvRelationTitle).checkHasText(name1) + onItemView(0, R.id.tvRelationValue).checkHasText(value1) + onItemView(1, R.id.tvRelationTitle).checkHasText(name2) + onItemView(1, R.id.tvRelationValue).checkHasText(value2) + checkIsRecyclerSize(2) + } + } + + @Test + fun shouldDisplayTwoObjectRelationsWithNameAndAvatarInitials() { + + // SETUP + + val name1 = "Assignee" + val target1: Id = MockDataFactory.randomUuid() + val username1 = "Konstantin" + + val name2 = "Created by" + val target2: Id = MockDataFactory.randomUuid() + val username2 = "Roman" + + val relation1 = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.OBJECT, + source = Relation.Source.values().random(), + name = name1 + ) + + val relation2 = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.OBJECT, + source = Relation.Source.values().random(), + name = name2 + ) + + val relations = listOf(relation1, relation2) + + val details = Block.Details( + mapOf( + ctx to Block.Fields( + mapOf( + relation1.key to target1, + relation2.key to target2, + ) + ), + target1 to Block.Fields( + mapOf( + Block.Fields.NAME_KEY to username1 + ) + ), + target2 to Block.Fields( + mapOf( + Block.Fields.NAME_KEY to username2 + ) + ) + ) + ) + + runBlocking { + storage.relations.update(relations) + storage.details.update(details) + } + + launchFragment(bundleOf( + ObjectRelationListFragment.ARG_CTX to ctx, + ObjectRelationListFragment.ARG_MODE to ObjectRelationListFragment.MODE_LIST + )) + + // TESTING + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.tvRelationTitle).checkHasText(name1) + onItemView(0, R.id.obj0).check(matches(hasDescendant(withText(username1)))) + onItemView(1, R.id.tvRelationTitle).checkHasText(name2) + onItemView(1, R.id.obj0).check(matches(hasDescendant(withText(username2)))) + checkIsRecyclerSize(2) + } + } + + @Test + fun shouldDisplayTwoDateRelations() { + + // SETUP + + val format = SimpleDateFormat(MONTH_DAY_AND_YEAR, Locale.US) + + val name1 = "Date of birth" + val date1 = System.currentTimeMillis() + val date1Screen = format.format(Date(date1)) + + val name2 = "Last modified at" + val date2 = System.currentTimeMillis() + val date2Screen = format.format(Date(date2)) + + val relation1 = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.DATE, + source = Relation.Source.values().random(), + name = name1, + ) + + val relation2 = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.DATE, + source = Relation.Source.values().random(), + name = name2 + ) + + val relations = listOf(relation1, relation2) + + val details = Block.Details( + mapOf( + ctx to Block.Fields( + mapOf( + relation1.key to date1.toTimeSeconds(), + relation2.key to date2.toTimeSeconds(), + ) + ) + ) + ) + + runBlocking { + storage.relations.update(relations) + storage.details.update(details) + } + + launchFragment(bundleOf( + ObjectRelationListFragment.ARG_CTX to ctx, + ObjectRelationListFragment.ARG_MODE to ObjectRelationListFragment.MODE_LIST + )) + + // TESTING + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.tvRelationTitle).checkHasText(name1) + onItemView(0, R.id.tvRelationValue).checkHasText(date1Screen) + onItemView(1, R.id.tvRelationTitle).checkHasText(name2) + onItemView(1, R.id.tvRelationValue).checkHasText(date2Screen) + checkIsRecyclerSize(2) + } + } + + @Test + fun shouldDisplayTwoStatusRelations() { + + // SETUP + + val color1 = ThemeColor.RED + val color2 = ThemeColor.TEAL + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "In progress", + color = color1.title + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Done", + color = color2.title + ) + + val name1 = "Status 1" + val name2 = "Status 2" + + val relation1 = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.STATUS, + source = Relation.Source.values().random(), + name = name1, + selections = listOf(option1) + ) + + val relation2 = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.STATUS, + source = Relation.Source.values().random(), + name = name2, + selections = listOf(option2) + ) + + val relations = listOf(relation1, relation2) + + val details = Block.Details( + mapOf( + ctx to Block.Fields( + mapOf( + relation1.key to option1.id, + relation2.key to option2.id, + ) + ) + ) + ) + + runBlocking { + storage.relations.update(relations) + storage.details.update(details) + } + + launchFragment(bundleOf( + ObjectRelationListFragment.ARG_CTX to ctx, + ObjectRelationListFragment.ARG_MODE to ObjectRelationListFragment.MODE_LIST + )) + + // TESTING + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.tvRelationTitle).checkHasText(name1) + onItemView(0, R.id.tvRelationValue).checkHasText(option1.text) + onItemView(0, R.id.tvRelationValue).checkHasTextColor(color1.text) + onItemView(1, R.id.tvRelationTitle).checkHasText(name2) + onItemView(1, R.id.tvRelationValue).checkHasText(option2.text) + onItemView(1, R.id.tvRelationValue).checkHasTextColor(color2.text) + checkIsRecyclerSize(2) + } + } + + @Test + fun shouldDisplayFourTagRelations() { + + // SETUP + + val color1 = ThemeColor.RED + val color2 = ThemeColor.TEAL + val color3 = ThemeColor.ICE + val color4 = ThemeColor.PURPLE + + val name = "Role" + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Essayist", + color = color1.title + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Journalist", + color = color2.title + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Politik", + color = color3.title + ) + + val option4 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Critic", + color = color4.title + ) + + val relation = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.TAG, + source = Relation.Source.values().random(), + name = name, + selections = listOf(option1, option2, option3, option4) + ) + + val relations = listOf(relation) + + val details = Block.Details( + mapOf( + ctx to Block.Fields( + mapOf( + relation.key to listOf(option1.id, option2.id, option3.id, option4.id) + ) + ) + ) + ) + + runBlocking { + storage.relations.update(relations) + storage.details.update(details) + } + + launchFragment(bundleOf( + ObjectRelationListFragment.ARG_CTX to ctx, + ObjectRelationListFragment.ARG_MODE to ObjectRelationListFragment.MODE_LIST + )) + + // TESTING + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.tvRelationTitle).checkHasText(name) + onItemView(0, R.id.tag0).check(matches((withText(option1.text)))) + onItemView(0, R.id.tag1).check(matches((withText(option2.text)))) + onItemView(0, R.id.tag2).check(matches((withText(option3.text)))) + onItemView(0, R.id.tag3).check(matches((withText(option4.text)))) + checkIsRecyclerSize(1) + } + } + + @Test + fun shouldDisplayTwoFileRelations() { + + // SETUP + + val name = "Attachement" + + val relation = Relation( + key = MockDataFactory.randomUuid(), + format = Relation.Format.FILE, + source = Relation.Source.values().random(), + name = name, + selections = emptyList() + ) + + val relations = listOf(relation) + + val file1 = MockDataFactory.randomUuid() + val file2 = MockDataFactory.randomUuid() + + val details = Block.Details( + mapOf( + ctx to Block.Fields( + mapOf( + relation.key to listOf(file1, file2) + ) + ), + file1 to Block.Fields( + mapOf( + "name" to "Document", + "ext" to "pdf", + "mime" to "application/pdf" + ) + ), + file2 to Block.Fields( + mapOf( + "name" to "Image", + "ext" to "jpg", + "mime" to "image/jpeg" + ) + ) + ) + ) + + runBlocking { + storage.relations.update(relations) + storage.details.update(details) + } + + launchFragment(bundleOf( + ObjectRelationListFragment.ARG_CTX to ctx, + ObjectRelationListFragment.ARG_MODE to ObjectRelationListFragment.MODE_LIST + )) + + // TESTING + + with(R.id.recycler.rVMatcher()) { + onItemView(0, R.id.tvRelationTitle).checkHasText(name) + onItemView(0, R.id.file0).check(matches(hasDescendant(withText("Document")))) + onItemView(0, R.id.file1).check(matches(hasDescendant(withText("Image")))) + checkIsRecyclerSize(1) + } + } + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestAddObjectSetObjectRelationValueFragment.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestAddObjectSetObjectRelationValueFragment.kt new file mode 100644 index 0000000000..42d60daf8d --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestAddObjectSetObjectRelationValueFragment.kt @@ -0,0 +1,18 @@ +package com.anytypeio.anytype.features.relations + +import com.anytypeio.anytype.presentation.relations.AddObjectSetObjectRelationValueViewModel +import com.anytypeio.anytype.ui.relations.AddObjectSetObjectRelationValueFragment + +class TestAddObjectSetObjectRelationValueFragment : AddObjectSetObjectRelationValueFragment() { + init { + factory = testVmFactory + } + + override fun injectDependencies() {} + override fun releaseDependencies() {} + override fun proceedWithExiting() {} + + companion object { + lateinit var testVmFactory: AddObjectSetObjectRelationValueViewModel.Factory + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectRelationDateValueFragment.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectRelationDateValueFragment.kt new file mode 100644 index 0000000000..648e3468d9 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectRelationDateValueFragment.kt @@ -0,0 +1,18 @@ +package com.anytypeio.anytype.features.relations + +import com.anytypeio.anytype.presentation.sets.ObjectRelationDateValueViewModel +import com.anytypeio.anytype.ui.relations.ObjectRelationDateValueFragment + +class TestObjectRelationDateValueFragment: ObjectRelationDateValueFragment() { + + init { + factory = testVmFactory + } + + override fun injectDependencies() {} + override fun releaseDependencies() {} + + companion object { + lateinit var testVmFactory: ObjectRelationDateValueViewModel.Factory + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectRelationListFragment.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectRelationListFragment.kt new file mode 100644 index 0000000000..c381546449 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectRelationListFragment.kt @@ -0,0 +1,16 @@ +package com.anytypeio.anytype.features.relations + +import com.anytypeio.anytype.presentation.relations.ObjectRelationListViewModelFactory +import com.anytypeio.anytype.ui.relations.ObjectRelationListFragment + +class TestObjectRelationListFragment : ObjectRelationListFragment() { + override fun injectDependencies() { + factory = testVmFactory + } + + override fun releaseDependencies() {} + + companion object { + lateinit var testVmFactory: ObjectRelationListViewModelFactory + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectRelationTextValueFragment.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectRelationTextValueFragment.kt new file mode 100644 index 0000000000..6a1a4a9257 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectRelationTextValueFragment.kt @@ -0,0 +1,18 @@ +package com.anytypeio.anytype.features.relations + +import com.anytypeio.anytype.presentation.sets.ObjectRelationTextValueViewModel +import com.anytypeio.anytype.ui.relations.ObjectRelationTextValueFragment + +class TestObjectRelationTextValueFragment : ObjectRelationTextValueFragment() { + + init { + factory = testVmFactory + } + + override fun injectDependencies() {} + override fun releaseDependencies() {} + + companion object { + lateinit var testVmFactory: ObjectRelationTextValueViewModel.Factory + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectSetObjectRelationValueFragment.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectSetObjectRelationValueFragment.kt new file mode 100644 index 0000000000..e045a20059 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/TestObjectSetObjectRelationValueFragment.kt @@ -0,0 +1,17 @@ +package com.anytypeio.anytype.features.relations + +import com.anytypeio.anytype.presentation.sets.ObjectSetObjectRelationValueViewModel +import com.anytypeio.anytype.ui.database.modals.ObjectSetObjectRelationValueFragment + +class TestObjectSetObjectRelationValueFragment : ObjectSetObjectRelationValueFragment() { + init { + factory = testVmFactory + } + + override fun injectDependencies() {} + override fun releaseDependencies() {} + + companion object { + lateinit var testVmFactory: ObjectSetObjectRelationValueViewModel.Factory + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridColumnRenderingTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridColumnRenderingTest.kt new file mode 100644 index 0000000000..6b1abb172c --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridColumnRenderingTest.kt @@ -0,0 +1,172 @@ +package com.anytypeio.anytype.features.sets.dv + +import androidx.core.os.bundleOf +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.DVViewerRelation +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.ui.sets.ObjectSetFragment +import com.anytypeio.anytype.utils.checkHasText +import com.anytypeio.anytype.utils.checkIsRecyclerSize +import com.anytypeio.anytype.utils.onItemView +import com.anytypeio.anytype.utils.rVMatcher +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class ObjectSetGridColumnRenderingTest : TestObjectSetSetup() { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + override val title: Block = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Text( + style = Block.Content.Text.Style.TITLE, + text = "Data View UI Testing", + marks = emptyList() + ), + children = emptyList(), + fields = Block.Fields.empty() + ) + + @Before + override fun setup() { + super.setup() + } + + @Test + fun shouldRenderAllColumnHeaderNamesBasedOnViewerRelations() { + + val type = ObjectType( + url = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + emoji = MockDataFactory.randomString(), + layout = ObjectType.Layout.PAGE, + relations = emptyList(), + description = "", + isHidden = false + ) + + val relation1 = Relation( + key = MockDataFactory.randomString(), + name = "Description", + format = Relation.Format.SHORT_TEXT, + source = Relation.Source.values().random() + ) + + val relation2 = Relation( + key = MockDataFactory.randomString(), + name = "Year", + format = Relation.Format.NUMBER, + source = Relation.Source.values().random() + ) + + val relation3 = Relation( + key = MockDataFactory.randomString(), + name = "Phone", + format = Relation.Format.PHONE, + source = Relation.Source.values().random() + ) + + val relation4 = Relation( + key = MockDataFactory.randomString(), + name = "Website", + format = Relation.Format.URL, + source = Relation.Source.values().random() + ) + + val relation5 = Relation( + key = MockDataFactory.randomString(), + name = "Email", + format = Relation.Format.EMAIL, + source = Relation.Source.values().random() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = "Default Grid View", + filters = emptyList(), + sorts = emptyList(), + viewerRelations = listOf( + DVViewerRelation( + key = relation1.key, + isVisible = true + ), + DVViewerRelation( + key = relation2.key, + isVisible = true + ), + DVViewerRelation( + key = relation3.key, + isVisible = true + ), + DVViewerRelation( + key = relation4.key, + isVisible = true + ), + DVViewerRelation( + key = relation5.key, + isVisible = true + ) + ), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val dataview = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation1, relation2, relation3, relation4, relation5), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + val root = Block( + id = ctx, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.SET + ), + children = listOf(header.id, dataview.id) + ) + + val set = listOf(root, header, title, dataview) + + stubInterceptEvents() + stubOpenObjectSetWithRecord( + set = set, + relations = listOf(relation1, relation2, relation3, relation4, relation5), + details = defaultDetails, + viewer = viewer.id, + dataview = dataview.id, + records = emptyList(), + total = 1, + objectTypes = listOf(type) + ) + + // TESTING + + launchFragment(bundleOf(ObjectSetFragment.CONTEXT_ID_KEY to ctx)) + + with(R.id.rvHeader.rVMatcher()) { + // There should be 5 column headers + 1 plus button + checkIsRecyclerSize(6) + onItemView(0, R.id.cellText).checkHasText(relation1.name) + onItemView(1, R.id.cellText).checkHasText(relation2.name) + onItemView(2, R.id.cellText).checkHasText(relation3.name) + onItemView(3, R.id.cellText).checkHasText(relation4.name) + onItemView(4, R.id.cellText).checkHasText(relation5.name) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridFileCellRenderingTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridFileCellRenderingTest.kt new file mode 100644 index 0000000000..2a994ebdec --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridFileCellRenderingTest.kt @@ -0,0 +1,191 @@ +package com.anytypeio.anytype.features.sets.dv + +import androidx.core.os.bundleOf +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.DVViewerRelation +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.emojifier.data.DefaultDocumentEmojiIconProvider +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.ui.sets.ObjectSetFragment +import com.anytypeio.anytype.utils.checkHasText +import com.anytypeio.anytype.utils.checkIsRecyclerSize +import com.anytypeio.anytype.utils.onItemView +import com.anytypeio.anytype.utils.rVMatcher +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class ObjectSetGridFileCellRenderingTest : TestObjectSetSetup() { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + override val title: Block = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Text( + style = Block.Content.Text.Style.TITLE, + text = "Data View UI Testing", + marks = emptyList() + ), + children = emptyList(), + fields = Block.Fields.empty() + ) + + @Before + override fun setup() { + super.setup() + } + + @Test + fun shouldRenderTwoFilesInTwoRecords() { + + // SETUP + + val relationName = "Files" + val file1Name = "CharlieChaplin" + val file1Id = MockDataFactory.randomUuid() + val file1Ext = "txt" + val file2Name = "Jean-PierreLéaud" + val file2Id = MockDataFactory.randomUuid() + val file2Ext = "jpeg" + + val objectType = ObjectType( + url = MockDataFactory.randomUuid(), + name = "Movie", + relations = emptyList(), + emoji = MockDataFactory.randomString(), + layout = ObjectType.Layout.PROFILE, + description = "", + isHidden = false + ) + + val relationId = MockDataFactory.randomUuid() + val record1Id = MockDataFactory.randomUuid() + val record2Id = MockDataFactory.randomUuid() + + val record1: Map = mapOf( + ObjectSetConfig.ID_KEY to record1Id, + ObjectSetConfig.NAME_KEY to "The Great Dictator", + ObjectSetConfig.EMOJI_KEY to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + ObjectSetConfig.TYPE_KEY to objectType.url, + relationId to file1Id + ) + + val record2: Map = mapOf( + ObjectSetConfig.ID_KEY to record2Id, + ObjectSetConfig.NAME_KEY to "Les Quatre Cents Coups", + ObjectSetConfig.EMOJI_KEY to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + ObjectSetConfig.TYPE_KEY to objectType.url, + relationId to file2Id + ) + + val relation = Relation( + key = relationId, + name = relationName, + format = Relation.Format.FILE, + source = Relation.Source.values().random() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = listOf( + DVViewerRelation( + key = relation.key, + isVisible = true + ) + ), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val dataview = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + val details = Block.Details( + details = defaultDetails.details + mapOf( + file1Id to Block.Fields( + mapOf( + ObjectSetConfig.NAME_KEY to file1Name, + ObjectSetConfig.TYPE_KEY to objectType.url, + "fileExt" to file1Ext + ) + ), + file2Id to Block.Fields( + mapOf( + ObjectSetConfig.NAME_KEY to file2Name, + ObjectSetConfig.TYPE_KEY to objectType.url, + "fileExt" to file2Ext + ) + ) + ) + ) + + val root = Block( + id = ctx, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.SET + ), + children = listOf(header.id, dataview.id) + ) + + val set = listOf(root, header, title, dataview) + + stubInterceptEvents() + stubOpenObjectSetWithRecord( + set = set, + relations = listOf(relation), + details = details, + viewer = viewer.id, + dataview = dataview.id, + records = listOf(record1, record2), + total = 1, + objectTypes = listOf(objectType) + ) + + // TESTING + + launchFragment(bundleOf(ObjectSetFragment.CONTEXT_ID_KEY to ctx)) + + with(R.id.rvRows.rVMatcher()) { + checkIsRecyclerSize(2) + onItemView(0, R.id.tvTitle).checkHasText("The Great Dictator") + onItemView(0, R.id.file0).check( + ViewAssertions.matches( + ViewMatchers.hasDescendant( + ViewMatchers.withText(file1Name) + ) + ) + ) + onItemView(1, R.id.tvTitle).checkHasText("Les Quatre Cents Coups") + onItemView(1, R.id.file0).check( + ViewAssertions.matches( + ViewMatchers.hasDescendant( + ViewMatchers.withText(file2Name) + ) + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridObjectCellRenderingTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridObjectCellRenderingTest.kt new file mode 100644 index 0000000000..778c616d78 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridObjectCellRenderingTest.kt @@ -0,0 +1,296 @@ +package com.anytypeio.anytype.features.sets.dv + +import androidx.core.os.bundleOf +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.DVViewerRelation +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.emojifier.data.DefaultDocumentEmojiIconProvider +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.ui.sets.ObjectSetFragment +import com.anytypeio.anytype.utils.checkHasText +import com.anytypeio.anytype.utils.checkIsRecyclerSize +import com.anytypeio.anytype.utils.onItemView +import com.anytypeio.anytype.utils.rVMatcher +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class ObjectSetGridObjectCellRenderingTest : TestObjectSetSetup() { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + override val title: Block = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Text( + style = Block.Content.Text.Style.TITLE, + text = "Data View UI Testing", + marks = emptyList() + ), + children = emptyList(), + fields = Block.Fields.empty() + ) + + @Before + override fun setup() { + super.setup() + } + + @Test + fun shouldRenderOneHumanObjectFromEachOfTwoRecords() { + + // SETUP + + val relationName = "Starring" + val object1Name = "Charlie Chaplin" + val object1Id = MockDataFactory.randomUuid() + val object2Name = "Jean-Pierre Léaud" + val object2Id = MockDataFactory.randomUuid() + + val objectType = ObjectType( + url = MockDataFactory.randomUuid(), + name = "Movie", + relations = emptyList(), + emoji = MockDataFactory.randomString(), + layout = ObjectType.Layout.PROFILE, + description = "", + isHidden = false + ) + + val relationId = MockDataFactory.randomUuid() + val record1Id = MockDataFactory.randomUuid() + val record2Id = MockDataFactory.randomUuid() + + val record1: Map = mapOf( + ObjectSetConfig.ID_KEY to record1Id, + ObjectSetConfig.NAME_KEY to "The Great Dictator", + ObjectSetConfig.EMOJI_KEY to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + ObjectSetConfig.TYPE_KEY to objectType.url, + relationId to object1Id + ) + + val record2: Map = mapOf( + ObjectSetConfig.ID_KEY to record2Id, + ObjectSetConfig.NAME_KEY to "Les Quatre Cents Coups", + ObjectSetConfig.EMOJI_KEY to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + ObjectSetConfig.TYPE_KEY to objectType.url, + relationId to object2Id + ) + + val relation = Relation( + key = relationId, + name = relationName, + format = Relation.Format.OBJECT, + source = Relation.Source.values().random() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = listOf( + DVViewerRelation( + key = relation.key, + isVisible = true + ) + ), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val dataview = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + val details = Block.Details( + details = defaultDetails.details + mapOf( + object1Id to Block.Fields( + mapOf( + ObjectSetConfig.NAME_KEY to object1Name, + ObjectSetConfig.TYPE_KEY to objectType.url, + "iconEmoji" to "👤" + ) + ), + object2Id to Block.Fields( + mapOf( + ObjectSetConfig.NAME_KEY to object2Name, + ObjectSetConfig.TYPE_KEY to objectType.url, + "iconEmoji" to "👤" + ) + ) + ) + ) + + val root = Block( + id = ctx, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.SET + ), + children = listOf(header.id, dataview.id) + ) + + val set = listOf(root, header, title, dataview) + + stubInterceptEvents() + stubOpenObjectSetWithRecord( + set = set, + relations = listOf(relation), + details = details, + viewer = viewer.id, + dataview = dataview.id, + records = listOf(record1, record2), + total = 1, + objectTypes = listOf(objectType) + ) + + // TESTING + + launchFragment(bundleOf(ObjectSetFragment.CONTEXT_ID_KEY to ctx)) + + with(R.id.rvRows.rVMatcher()) { + checkIsRecyclerSize(2) + onItemView(0, R.id.tvTitle).checkHasText("The Great Dictator") + onItemView(0, R.id.object0).check(matches(hasDescendant(withText(object1Name)))) + onItemView(1, R.id.tvTitle).checkHasText("Les Quatre Cents Coups") + onItemView(1, R.id.object0).check(matches(hasDescendant(withText(object2Name)))) + } + } + + @Test + fun shouldRenderTwoHumanObjectsFromOneRecord() { + + // SETUP + + val relationName = "Starring" + val object1Name = "Maurice Ronet" + val object1Id = MockDataFactory.randomUuid() + val object2Name = "Jeanne Moreau" + val object2Id = MockDataFactory.randomUuid() + + val objectType = ObjectType( + url = MockDataFactory.randomUuid(), + name = "Movie", + relations = emptyList(), + emoji = MockDataFactory.randomString(), + layout = ObjectType.Layout.PROFILE, + description = "", + isHidden = false + ) + + val relationId = MockDataFactory.randomUuid() + val recordId = MockDataFactory.randomUuid() + + val record1: Map = mapOf( + ObjectSetConfig.ID_KEY to recordId, + ObjectSetConfig.NAME_KEY to "Le Feu Follet", + ObjectSetConfig.EMOJI_KEY to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + ObjectSetConfig.TYPE_KEY to objectType.url, + relationId to listOf(object1Id, object2Id) + ) + + val relation = Relation( + key = relationId, + name = relationName, + format = Relation.Format.OBJECT, + source = Relation.Source.values().random() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = listOf( + DVViewerRelation( + key = relation.key, + isVisible = true + ) + ), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val dataview = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + val details = Block.Details( + details = defaultDetails.details + mapOf( + object1Id to Block.Fields( + mapOf( + ObjectSetConfig.NAME_KEY to object1Name, + ObjectSetConfig.TYPE_KEY to objectType.url, + "iconEmoji" to "👤" + ) + ), + object2Id to Block.Fields( + mapOf( + ObjectSetConfig.NAME_KEY to object2Name, + ObjectSetConfig.TYPE_KEY to objectType.url, + "iconEmoji" to "👤" + ) + ) + ) + ) + + val root = Block( + id = ctx, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.SET + ), + children = listOf(header.id, dataview.id) + ) + + val set = listOf(root, header, title, dataview) + + stubInterceptEvents() + stubOpenObjectSetWithRecord( + set = set, + relations = listOf(relation), + details = details, + viewer = viewer.id, + dataview = dataview.id, + records = listOf(record1), + total = 1, + objectTypes = listOf(objectType) + ) + + // TESTING + + launchFragment(bundleOf(ObjectSetFragment.CONTEXT_ID_KEY to ctx)) + + with(R.id.rvRows.rVMatcher()) { + checkIsRecyclerSize(1) + onItemView(0, R.id.tvTitle).checkHasText("Le Feu Follet") + onItemView(0, R.id.object0).check(matches(hasDescendant(withText(object1Name)))) + onItemView(0, R.id.object1).check(matches(hasDescendant(withText(object2Name)))) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridPrimitiveRelationTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridPrimitiveRelationTest.kt new file mode 100644 index 0000000000..f7efe9e565 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridPrimitiveRelationTest.kt @@ -0,0 +1,261 @@ +package com.anytypeio.anytype.features.sets.dv + +import androidx.core.os.bundleOf +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.DVViewerRelation +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.emojifier.data.DefaultDocumentEmojiIconProvider +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.ui.sets.ObjectSetFragment +import com.anytypeio.anytype.utils.* +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class ObjectSetGridPrimitiveRelationTest : TestObjectSetSetup() { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + override val title: Block = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Text( + style = Block.Content.Text.Style.TITLE, + text = "Data View UI Testing", + marks = emptyList() + ), + children = emptyList(), + fields = Block.Fields.empty() + ) + + @Before + override fun setup() { + super.setup() + } + + @Test + fun shouldRenderAllObjectPrimitiveRelationsValuesFromTwoRecords() { + + val type = ObjectType( + url = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + emoji = MockDataFactory.randomString(), + layout = ObjectType.Layout.PAGE, + relations = emptyList(), + description = "", + isHidden = false + ) + + val relation1 = Relation( + key = MockDataFactory.randomString(), + name = "Description", + format = Relation.Format.SHORT_TEXT, + source = Relation.Source.values().random() + ) + + val relation2 = Relation( + key = MockDataFactory.randomString(), + name = "Year", + format = Relation.Format.NUMBER, + source = Relation.Source.values().random() + ) + + val relation3 = Relation( + key = MockDataFactory.randomString(), + name = "Phone", + format = Relation.Format.PHONE, + source = Relation.Source.values().random() + ) + + val relation4 = Relation( + key = MockDataFactory.randomString(), + name = "Website", + format = Relation.Format.URL, + source = Relation.Source.values().random() + ) + + val relation5 = Relation( + key = MockDataFactory.randomString(), + name = "Email", + format = Relation.Format.EMAIL, + source = Relation.Source.values().random() + ) + + val object1value1 = "Operating environment for the new Internet" + val object1value2 = "2021" + val object1value3 = "+00000000000" + val object1value4 = "https://anytype.io/" + val object1value5 = "team@anytype.io" + + val object2value1 = "A peer-to-peer hypermedia protocol designed to make the web faster, safer, and more open." + val object2value2 = "2021" + val object2value3 = "+00000000000" + val object2value4 = "https://ipfs.io/" + val object2value5 = "team@ipfs.io" + + val record1 = mapOf( + ObjectSetConfig.ID_KEY to MockDataFactory.randomUuid(), + ObjectSetConfig.TYPE_KEY to type.url, + ObjectSetConfig.NAME_KEY to "Anytype", + "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + relation1.key to object1value1, + relation2.key to object1value2, + relation3.key to object1value3, + relation4.key to object1value4, + relation5.key to object1value5, + ) + + val record2 = mapOf( + ObjectSetConfig.ID_KEY to MockDataFactory.randomUuid(), + ObjectSetConfig.TYPE_KEY to type.url, + ObjectSetConfig.NAME_KEY to "IPFS", + "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + relation1.key to object2value1, + relation2.key to object2value2, + relation3.key to object2value3, + relation4.key to object2value4, + relation5.key to object2value5, + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = "Default Grid View", + filters = emptyList(), + sorts = emptyList(), + viewerRelations = listOf( + DVViewerRelation( + key = relation1.key, + isVisible = true + ), + DVViewerRelation( + key = relation2.key, + isVisible = true + ), + DVViewerRelation( + key = relation3.key, + isVisible = true + ), + DVViewerRelation( + key = relation4.key, + isVisible = true + ), + DVViewerRelation( + key = relation5.key, + isVisible = true + ) + ), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val dataview = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation1, relation2, relation3, relation4, relation5), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + val root = Block( + id = ctx, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.SET + ), + children = listOf(header.id, dataview.id) + ) + + val set = listOf(root, header, title, dataview) + + stubInterceptEvents() + stubOpenObjectSetWithRecord( + set = set, + relations = listOf(relation1, relation2, relation3, relation4, relation5), + details = defaultDetails, + viewer = viewer.id, + dataview = dataview.id, + records = listOf(record1, record2), + total = 1, + objectTypes = listOf(type) + ) + + // TESTING + + launchFragment(bundleOf(ObjectSetFragment.CONTEXT_ID_KEY to ctx)) + + with(R.id.rvRows.rVMatcher()) { + checkIsRecyclerSize(2) + + onItemView(0, R.id.rowCellRecycler) + .checkHasChildViewCount(5) + .checkHasChildViewWithText( + pos = 0, + text = object1value1, + target = R.id.tvText + ) + .checkHasChildViewWithText( + pos = 1, + text = object1value2, + target = R.id.tvText + ) + .checkHasChildViewWithText( + pos = 2, + text = object1value3, + target = R.id.tvText + ) + .checkHasChildViewWithText( + pos = 3, + text = object1value4, + target = R.id.tvText + ) + .checkHasChildViewWithText( + pos = 4, + text = object1value5, + target = R.id.tvText + ) + + onItemView(0, R.id.tvTitle).checkHasText("Anytype") + + onItemView(1, R.id.rowCellRecycler) + .checkHasChildViewCount(5) + .checkHasChildViewWithText( + pos = 0, + text = object2value1, + target = R.id.tvText + ) + .checkHasChildViewWithText( + pos = 1, + text = object2value2, + target = R.id.tvText + ) + .checkHasChildViewWithText( + pos = 2, + text = object2value3, + target = R.id.tvText + ) + .checkHasChildViewWithText( + pos = 3, + text = object2value4, + target = R.id.tvText + ) + .checkHasChildViewWithText( + pos = 4, + text = object2value5, + target = R.id.tvText + ) + + onItemView(1, R.id.tvTitle).checkHasText("IPFS") + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridTagCellRenderingTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridTagCellRenderingTest.kt new file mode 100644 index 0000000000..e1518977f4 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetGridTagCellRenderingTest.kt @@ -0,0 +1,202 @@ +package com.anytypeio.anytype.features.sets.dv + +import androidx.core.os.bundleOf +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.DVViewerRelation +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.emojifier.data.DefaultDocumentEmojiIconProvider +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.ui.sets.ObjectSetFragment +import com.anytypeio.anytype.utils.checkHasText +import com.anytypeio.anytype.utils.checkIsRecyclerSize +import com.anytypeio.anytype.utils.onItemView +import com.anytypeio.anytype.utils.rVMatcher +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import org.hamcrest.CoreMatchers.not +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class ObjectSetGridTagCellRenderingTest : TestObjectSetSetup() { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + override val title: Block = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Text( + style = Block.Content.Text.Style.TITLE, + text = "Data View UI Testing", + marks = emptyList() + ), + children = emptyList(), + fields = Block.Fields.empty() + ) + + @Before + override fun setup() { + super.setup() + } + + @Test + fun shouldRenderOneSameTagAndTwoDifferentTagsOfTwoRecords() { + + // SETUP + + val relationName = "FilmTags" + val tag1Name = "Silent film" + val tag1Id = MockDataFactory.randomUuid() + val tag2Name = "Sound film" + val tag2Id = MockDataFactory.randomUuid() + val tag3Name = "Director" + val tag3Id = MockDataFactory.randomUuid() + + val objectType = ObjectType( + url = MockDataFactory.randomUuid(), + name = "Film", + relations = emptyList(), + emoji = MockDataFactory.randomString(), + layout = ObjectType.Layout.PAGE, + description = "", + isHidden = false + ) + + val relationId = MockDataFactory.randomUuid() + val record1Id = MockDataFactory.randomUuid() + val record2Id = MockDataFactory.randomUuid() + + val record1: Map = mapOf( + ObjectSetConfig.ID_KEY to record1Id, + ObjectSetConfig.NAME_KEY to "The Face on the Bar Room Floor", + ObjectSetConfig.EMOJI_KEY to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + ObjectSetConfig.TYPE_KEY to objectType.url, + relationId to listOf(tag1Id, tag3Id) + ) + + val record2: Map = mapOf( + ObjectSetConfig.ID_KEY to record2Id, + ObjectSetConfig.NAME_KEY to "The Great Dictator", + ObjectSetConfig.EMOJI_KEY to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), + ObjectSetConfig.TYPE_KEY to objectType.url, + relationId to listOf(tag2Id, tag3Id) + ) + + val relation = Relation( + key = relationId, + name = relationName, + format = Relation.Format.TAG, + selections = listOf( + Relation.Option(id = tag1Id, text = tag1Name, color = "blue"), + Relation.Option(id = tag2Id, text = tag2Name, color = "red"), + Relation.Option(id = tag3Id, text = tag3Name, color = "black"), + Relation.Option( + id = MockDataFactory.randomUuid(), + text = MockDataFactory.randomString(), + color = "black" + ) + ), + source = Relation.Source.values().random() + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = emptyList(), + viewerRelations = listOf( + DVViewerRelation( + key = relation.key, + isVisible = true + ) + ), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val dataview = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + val details = Block.Details() + + val root = Block( + id = ctx, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.SET + ), + children = listOf(header.id, dataview.id) + ) + + val set = listOf(root, header, title, dataview) + + stubInterceptEvents() + stubOpenObjectSetWithRecord( + set = set, + relations = listOf(relation), + details = details, + viewer = viewer.id, + dataview = dataview.id, + records = listOf(record1, record2), + total = 1, + objectTypes = listOf(objectType) + ) + + // TESTING + + launchFragment(bundleOf(ObjectSetFragment.CONTEXT_ID_KEY to ctx)) + + with(R.id.rvRows.rVMatcher()) { + checkIsRecyclerSize(2) + onItemView(0, R.id.tvTitle).checkHasText("The Face on the Bar Room Floor") + onItemView(0, R.id.tag0).check( + ViewAssertions.matches( + ViewMatchers.withText("Silent film") + ) + ) + onItemView(0, R.id.tag1).check( + ViewAssertions.matches( + ViewMatchers.withText("Director") + ) + ) + onItemView(0, R.id.tag2).check( + ViewAssertions.matches( + not(ViewMatchers.isDisplayed()) + ) + ) + onItemView(1, R.id.tvTitle).checkHasText("The Great Dictator") + onItemView(1, R.id.tag0).check( + ViewAssertions.matches( + ViewMatchers.withText("Sound film") + ) + ) + onItemView(1, R.id.tag1).check( + ViewAssertions.matches( + ViewMatchers.withText("Director") + ) + ) + onItemView(1, R.id.tag2).check( + ViewAssertions.matches( + not(ViewMatchers.isDisplayed()) + ) + ) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetHeaderTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetHeaderTest.kt new file mode 100644 index 0000000000..14e451b66f --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/ObjectSetHeaderTest.kt @@ -0,0 +1,93 @@ +package com.anytypeio.anytype.features.sets.dv + +import androidx.core.os.bundleOf +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.ext.content +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.ui.sets.ObjectSetFragment +import com.anytypeio.anytype.utils.checkHasText +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class ObjectSetHeaderTest : TestObjectSetSetup() { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + override val title: Block = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Text( + style = Block.Content.Text.Style.TITLE, + text = "Data View UI Testing", + marks = emptyList() + ), + children = emptyList(), + fields = Block.Fields.empty() + ) + + @Before + override fun setup() { + super.setup() + } + + @Test + fun shouldRenderObjectSetTitleWithViewerTitle() { + + // SETUP + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = "Default Grid View", + filters = emptyList(), + sorts = emptyList(), + viewerRelations = emptyList(), + type = Block.Content.DataView.Viewer.Type.GRID + ) + + val dataview = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = emptyList(), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + val root = Block( + id = ctx, + fields = Block.Fields(emptyMap()), + content = Block.Content.Smart( + type = Block.Content.Smart.Type.SET + ), + children = listOf(header.id, dataview.id) + ) + + val set = listOf(root, header, title, dataview) + + stubInterceptEvents() + stubOpenObjectSet( + set = set, + relations = emptyList(), + details = defaultDetails + ) + + // TESTING + + launchFragment(bundleOf(ObjectSetFragment.CONTEXT_ID_KEY to ctx)) + + onView(withId(R.id.title)).checkHasText(title.content().text) + onView(withId(R.id.tvCurrentViewerName)).checkHasText(viewer.name) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetFragment.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetFragment.kt new file mode 100644 index 0000000000..5837c800dc --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetFragment.kt @@ -0,0 +1,17 @@ +package com.anytypeio.anytype.features.sets.dv + +import com.anytypeio.anytype.presentation.sets.ObjectSetViewModelFactory +import com.anytypeio.anytype.ui.sets.ObjectSetFragment + +class TestObjectSetFragment : ObjectSetFragment() { + init { + factory = testVmFactory + } + + override fun injectDependencies() {} + override fun releaseDependencies() {} + + companion object { + lateinit var testVmFactory: ObjectSetViewModelFactory + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt new file mode 100644 index 0000000000..bf7645f943 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt @@ -0,0 +1,176 @@ +package com.anytypeio.anytype.features.sets.dv + +import android.os.Bundle +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.* +import com.anytypeio.anytype.domain.block.interactor.UpdateText +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.dataview.interactor.* +import com.anytypeio.anytype.domain.event.interactor.InterceptEvents +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.page.CloseBlock +import com.anytypeio.anytype.domain.sets.OpenObjectSet +import com.anytypeio.anytype.emojifier.data.DefaultDocumentEmojiIconProvider +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.sets.ObjectSetRecordCache +import com.anytypeio.anytype.presentation.sets.ObjectSetReducer +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.ObjectSetViewModelFactory +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.stub +import kotlinx.coroutines.flow.emptyFlow +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +abstract class TestObjectSetSetup { + + private lateinit var openObjectSet: OpenObjectSet + private lateinit var addDataViewRelation: AddDataViewRelation + private lateinit var updateDataViewViewer: UpdateDataViewViewer + private lateinit var updateDataViewRecord: UpdateDataViewRecord + private lateinit var updateText: UpdateText + private lateinit var createDataViewRecord: CreateDataViewRecord + private lateinit var closeBlock: CloseBlock + private lateinit var setActiveViewer: SetActiveViewer + + lateinit var urlBuilder: UrlBuilder + + @Mock + lateinit var repo: BlockRepository + @Mock + lateinit var gateway: Gateway + @Mock + lateinit var interceptEvents: InterceptEvents + + private val session = ObjectSetSession() + private val reducer = ObjectSetReducer() + private val dispatcher: Dispatcher = Dispatcher.Default() + private val objectSetRecordCache = ObjectSetRecordCache() + + val ctx : Id = MockDataFactory.randomUuid() + + abstract val title : Block + + val header get() = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Layout( + type = Block.Content.Layout.Type.HEADER + ), + fields = Block.Fields.empty(), + children = listOf(title.id) + ) + + val defaultDetails = Block.Details( + mapOf( + ctx to Block.Fields( + mapOf( + "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random() + ) + ) + ) + ) + + open fun setup() { + MockitoAnnotations.initMocks(this) + + addDataViewRelation = AddDataViewRelation(repo) + updateText = UpdateText(repo) + openObjectSet = OpenObjectSet(repo) + createDataViewRecord = CreateDataViewRecord(repo) + updateDataViewRecord = UpdateDataViewRecord(repo) + updateDataViewViewer = UpdateDataViewViewer(repo) + setActiveViewer = SetActiveViewer(repo) + closeBlock = CloseBlock(repo) + urlBuilder = UrlBuilder(gateway) + + TestObjectSetFragment.testVmFactory = ObjectSetViewModelFactory( + openObjectSet = openObjectSet, + closeBlock = closeBlock, + addDataViewRelation = addDataViewRelation, + interceptEvents = interceptEvents, + updateDataViewViewer = updateDataViewViewer, + setActiveViewer = setActiveViewer, + createDataViewRecord = createDataViewRecord, + updateDataViewRecord = updateDataViewRecord, + updateText = updateText, + urlBuilder = urlBuilder, + session = session, + dispatcher = dispatcher, + reducer = reducer, + objectSetRecordCache = objectSetRecordCache + ) + } + + fun stubInterceptEvents() { + interceptEvents.stub { + onBlocking { build(any()) } doReturn emptyFlow() + } + } + + fun stubOpenObjectSet( + set: List, + details: Block.Details = Block.Details(), + relations: List = emptyList() + ) { + repo.stub { + onBlocking { openObjectSet(ctx) } doReturn Payload( + context = ctx, + events = listOf( + Event.Command.ShowBlock( + context = ctx, + root = ctx, + details = details, + blocks = set, + relations = relations + ) + ) + ) + } + } + + fun stubOpenObjectSetWithRecord( + set: List, + details: Block.Details = Block.Details(), + relations: List = emptyList(), + dataview: Id, + viewer: Id, + total: Int, + records: List, + objectTypes: List + ) { + repo.stub { + onBlocking { openObjectSet(ctx) } doReturn Payload( + context = ctx, + events = listOf( + Event.Command.ShowBlock( + context = ctx, + root = ctx, + details = details, + blocks = set, + relations = relations, + objectTypes = objectTypes + ), + Event.Command.DataView.SetRecords( + context = ctx, + id = dataview, + view = viewer, + total = total, + records = records + ) + ) + ) + } + } + + fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/ModifyInputValueFilterTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/ModifyInputValueFilterTest.kt new file mode 100644 index 0000000000..6a58f1f704 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/ModifyInputValueFilterTest.kt @@ -0,0 +1,316 @@ +package com.anytypeio.anytype.features.sets.filter + +import android.os.Bundle +import android.text.InputType +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.* +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.dataview.interactor.SearchObjects +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.filter.FilterViewModel +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.filter.ModifyFilterFromInputFieldValueFragment +import com.anytypeio.anytype.utils.CoroutinesTestRule +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verifyBlocking +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +class ModifyInputValueFilterTest { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + @Mock + lateinit var repo: BlockRepository + @Mock + lateinit var gateway: Gateway + + lateinit var updateDataViewViewer: UpdateDataViewViewer + lateinit var searchObjects: SearchObjects + lateinit var urlBuilder: UrlBuilder + + private val root = MockDataFactory.randomUuid() + private val session = ObjectSetSession() + private val state = MutableStateFlow(ObjectSet.init()) + private val dispatcher = Dispatcher.Default() + +// @Before +// fun setup() { +// MockitoAnnotations.initMocks(this) +// updateDataViewViewer = UpdateDataViewViewer(repo) +// searchObjects = SearchObjects(repo) +// urlBuilder = UrlBuilder(gateway) +// TestModifyFilterFromInputFieldValueFragment.testVmFactory = FilterViewModel.Factory( +// objectSetState = state, +// session = session, +// updateDataViewViewer = updateDataViewViewer, +// dispatcher = dispatcher, +// searchObjects = searchObjects, +// urlBuilder = urlBuilder +// ) +// } + + //todo Тесты выключены пока DataView in read mode only 15/03/21 +// @Test +// fun shouldTypeTextThenClickActionButtonToApplyChanges() { +// +// val relationKey = MockDataFactory.randomUuid() +// +// val target = MockDataFactory.randomUuid() +// +// val record: Map = mapOf( +// ObjectSetConfig.ID_KEY to target, +// relationKey to emptyList() +// ) +// +// // Defining viewer containing one filter +// +// val initialFilterText = "Foo" +// val textToType = "Bar" +// +// val filter = DVFilter( +// relationKey = relationKey, +// value = initialFilterText, +// condition = DVFilterCondition.EQUAL +// ) +// +// val viewer = Block.Content.DataView.Viewer( +// id = MockDataFactory.randomUuid(), +// name = MockDataFactory.randomString(), +// filters = listOf(filter), +// sorts = emptyList(), +// viewerRelations = listOf( +// Block.Content.DataView.Viewer.ViewerRelation( +// key = relationKey, +// isVisible = true +// ) +// ), +// type = Block.Content.DataView.Viewer.Type.values().random() +// ) +// +// val relation = Relation( +// key = relationKey, +// defaultValue = null, +// isHidden = false, +// isReadOnly = false, +// isMulti = true, +// name = MockDataFactory.randomString(), +// source = Relation.Source.values().random(), +// format = Relation.Format.LONG_TEXT, +// selections = emptyList() +// ) +// +// val dv = Block( +// id = MockDataFactory.randomUuid(), +// children = emptyList(), +// fields = Block.Fields.empty(), +// content = Block.Content.DataView( +// relations = listOf(relation), +// viewers = listOf(viewer), +// source = MockDataFactory.randomUuid() +// ) +// ) +// +// state.value = ObjectSet( +// blocks = listOf(dv), +// viewerDb = mapOf( +// viewer.id to ObjectSet.ViewerData( +// records = listOf(record), +// total = 1 +// ) +// ) +// ) +// +// // Launching fragment +// +// launchFragment( +// bundleOf( +// ModifyFilterFromInputFieldValueFragment.CTX_KEY to root, +// ModifyFilterFromInputFieldValueFragment.IDX_KEY to 0, +// ModifyFilterFromInputFieldValueFragment.RELATION_KEY to relationKey, +// ) +// ) +// +// // Veryfying that the initial filter text is visibile to our user +// +// val inputFieldInteraction = onView(withId(R.id.enterTextValueInputField)) +// +// inputFieldInteraction.check(matches(withText(initialFilterText))) +// +// // Checking input type +// +// inputFieldInteraction.check(matches(withInputType(InputType.TYPE_CLASS_TEXT))) +// +// // Typing additional text before pressing action button +// +// inputFieldInteraction.perform( +// typeText(textToType) +// ) +// +// // Clicking to apply button, in order to save filter changes +// +// onView(withId(R.id.btnBottomAction)).apply { +// perform(click()) +// } +// +// // Veryfying that the appropriate request was made +// +// verifyBlocking(repo, times(1)) { +// updateDataViewViewer( +// context = root, +// target = dv.id, +// viewer = viewer.copy( +// filters = listOf(filter.copy(value = initialFilterText + textToType)) +// ) +// ) +// } +// } +// +// @Test +// fun shouldTypeNumberFilterTextThenClickActionButtonToApplyChanges() { +// +// val relationKey = MockDataFactory.randomUuid() +// +// val target = MockDataFactory.randomUuid() +// +// val record: Map = mapOf( +// ObjectSetConfig.ID_KEY to target, +// relationKey to emptyList() +// ) +// +// // Defining viewer containing one filter +// +// val initialFilterText = "1" +// val textToType = "2" +// +// val filter = DVFilter( +// relationKey = relationKey, +// value = initialFilterText, +// condition = DVFilterCondition.EQUAL +// ) +// +// val viewer = Block.Content.DataView.Viewer( +// id = MockDataFactory.randomUuid(), +// name = MockDataFactory.randomString(), +// filters = listOf(filter), +// sorts = emptyList(), +// viewerRelations = listOf( +// Block.Content.DataView.Viewer.ViewerRelation( +// key = relationKey, +// isVisible = true +// ) +// ), +// type = Block.Content.DataView.Viewer.Type.values().random() +// ) +// +// val relation = Relation( +// key = relationKey, +// defaultValue = null, +// isHidden = false, +// isReadOnly = false, +// isMulti = true, +// name = MockDataFactory.randomString(), +// source = Relation.Source.values().random(), +// format = Relation.Format.NUMBER, +// selections = emptyList() +// ) +// +// val dv = Block( +// id = MockDataFactory.randomUuid(), +// children = emptyList(), +// fields = Block.Fields.empty(), +// content = Block.Content.DataView( +// relations = listOf(relation), +// viewers = listOf(viewer), +// source = MockDataFactory.randomUuid() +// ) +// ) +// +// state.value = ObjectSet( +// blocks = listOf(dv), +// viewerDb = mapOf( +// viewer.id to ObjectSet.ViewerData( +// records = listOf(record), +// total = 1 +// ) +// ) +// ) +// +// // Launching fragment +// +// launchFragment( +// bundleOf( +// ModifyFilterFromInputFieldValueFragment.CTX_KEY to root, +// ModifyFilterFromInputFieldValueFragment.IDX_KEY to 0, +// ModifyFilterFromInputFieldValueFragment.RELATION_KEY to relationKey, +// ) +// ) +// +// // Veryfying that the initial filter text is visibile to our user +// +// val inputFieldInteraction = onView(withId(R.id.enterTextValueInputField)) +// +// inputFieldInteraction.check(matches(withText(initialFilterText))) +// +// // Checking input type +// +// inputFieldInteraction.check(matches(withInputType(InputType.TYPE_CLASS_NUMBER))) +// +// // Typing additional text before pressing action button +// +// inputFieldInteraction.perform( +// typeText(textToType) +// ) +// +// // Clicking to apply button, in order to save filter changes +// +// onView(withId(R.id.btnBottomAction)).apply { +// perform(click()) +// } +// +// // Veryfying that the appropriate request was made +// +// verifyBlocking(repo, times(1)) { +// updateDataViewViewer( +// context = root, +// target = dv.id, +// viewer = viewer.copy( +// filters = listOf(filter.copy(value = initialFilterText + textToType)) +// ) +// ) +// } +// } + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/ModifyStatusFilterTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/ModifyStatusFilterTest.kt new file mode 100644 index 0000000000..d57f7ca782 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/ModifyStatusFilterTest.kt @@ -0,0 +1,266 @@ +package com.anytypeio.anytype.features.sets.filter + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.* +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.dataview.interactor.SearchObjects +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.filter.FilterViewModel +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.filter.ModifyFilterFromSelectedValueFragment +import com.anytypeio.anytype.utils.CoroutinesTestRule +import com.anytypeio.anytype.utils.TestUtils +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verifyBlocking +import kotlinx.coroutines.flow.MutableStateFlow +import org.hamcrest.CoreMatchers.not +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +@LargeTest +class ModifyStatusFilterTest { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + @Mock + lateinit var repo: BlockRepository + @Mock + lateinit var gateway: Gateway + + lateinit var updateDataViewViewer: UpdateDataViewViewer + lateinit var searchObjects: SearchObjects + lateinit var urlBuilder: UrlBuilder + + private val root = MockDataFactory.randomUuid() + private val session = ObjectSetSession() + private val state = MutableStateFlow(ObjectSet.init()) + private val dispatcher = Dispatcher.Default() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + updateDataViewViewer = UpdateDataViewViewer(repo) + searchObjects = SearchObjects(repo) + urlBuilder = UrlBuilder(gateway) + TestModifyFilterFromSelectedValueFragment.testVmFactory = FilterViewModel.Factory( + objectSetState = state, + session = session, + updateDataViewViewer = updateDataViewViewer, + dispatcher = dispatcher, + searchObjects = searchObjects, + urlBuilder = urlBuilder + ) + } + + @Test + fun shouldSelectSecondStatusAndApplyChangesOnClick() { + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + // Defining three different statuses: + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "In progress", + color = MockDataFactory.randomString() + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "In Testing", + color = MockDataFactory.randomString() + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Done", + color = MockDataFactory.randomString() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to emptyList() + ) + + // Defining viewer containing one filter + + val filter = DVFilter( + relationKey = relationKey, + value = listOf(option1.id), + condition = DVFilterCondition.EQUAL + ) + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = listOf(filter), + sorts = emptyList(), + viewerRelations = listOf( + Block.Content.DataView.Viewer.ViewerRelation( + key = relationKey, + isVisible = true + ) + ), + type = Block.Content.DataView.Viewer.Type.values().random() + ) + + val relation = Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = MockDataFactory.randomString(), + source = Relation.Source.values().random(), + format = Relation.Format.STATUS, + selections = listOf(option1, option2, option3) + ) + + val dv = Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + + state.value = ObjectSet( + blocks = listOf(dv), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // Launching fragment + + launchFragment( + bundleOf( + ModifyFilterFromSelectedValueFragment.CTX_KEY to root, + ModifyFilterFromSelectedValueFragment.IDX_KEY to 0, + ModifyFilterFromSelectedValueFragment.RELATION_KEY to relationKey, + ) + ) + + // TESTING + + val rvMatcher = TestUtils.withRecyclerView(R.id.rvViewerFilterRecycler) + + // Checking names + + onView(rvMatcher.atPositionOnView(0, R.id.tvStatusName)).apply { + check(matches(withText(option1.text))) + } + + onView(rvMatcher.atPositionOnView(1, R.id.tvStatusName)).apply { + check(matches(withText(option2.text))) + } + + onView(rvMatcher.atPositionOnView(2, R.id.tvStatusName)).apply { + check(matches(withText(option3.text))) + } + + // Veryfing that only the first status is selected + + onView(rvMatcher.atPositionOnView(0, R.id.ivSelectStatusIcon)).apply { + check(matches(isSelected())) + } + + onView(rvMatcher.atPositionOnView(1, R.id.ivSelectStatusIcon)).apply { + check(matches(not(isSelected()))) + } + + onView(rvMatcher.atPositionOnView(2, R.id.ivSelectStatusIcon)).apply { + check(matches(not(isSelected()))) + } + + // Verifying that the selection counter is equal to 1 + + onView(withId(R.id.tvOptionCount)).apply { + check(matches(withText("1"))) + } + + // Performing click, in order to sellect second status + + onView(rvMatcher.atPositionOnView(1, R.id.ivSelectStatusIcon)).apply { + perform(click()) + } + + // Veryfing that only the second status is selected + + onView(rvMatcher.atPositionOnView(0, R.id.ivSelectStatusIcon)).apply { + check(matches(not(isSelected()))) + } + + onView(rvMatcher.atPositionOnView(1, R.id.ivSelectStatusIcon)).apply { + check(matches((isSelected()))) + } + + onView(rvMatcher.atPositionOnView(2, R.id.ivSelectStatusIcon)).apply { + check(matches(not(isSelected()))) + } + + // Verifying that the selection counter is still equal to 1 + + onView(withId(R.id.tvOptionCount)).apply { + check(matches(withText("1"))) + } + + // Performing a click to apply filter changes. + + onView(withId(R.id.btnBottomAction)).apply { + perform(click()) + } + + // Verifying that viewer's filters are updated. + + verifyBlocking(repo, times(1)) { + updateDataViewViewer( + context = root, + target = dv.id, + viewer = viewer.copy( + filters = listOf(filter.copy(value = listOf(option2.id))) + ) + ) + } + } + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/ModifyTagFilterTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/ModifyTagFilterTest.kt new file mode 100644 index 0000000000..e7a61546ee --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/ModifyTagFilterTest.kt @@ -0,0 +1,246 @@ +package com.anytypeio.anytype.features.sets.filter + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.* +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.dataview.interactor.SearchObjects +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.filter.FilterViewModel +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.filter.ModifyFilterFromSelectedValueFragment +import com.anytypeio.anytype.utils.CoroutinesTestRule +import com.anytypeio.anytype.utils.TestUtils +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import kotlinx.coroutines.flow.MutableStateFlow +import org.hamcrest.CoreMatchers.not +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +@LargeTest +class ModifyTagFilterTest { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + @Mock + lateinit var repo: BlockRepository + @Mock + lateinit var gateway: Gateway + + lateinit var updateDataViewViewer: UpdateDataViewViewer + lateinit var searchObjects: SearchObjects + lateinit var urlBuilder: UrlBuilder + + private val root = MockDataFactory.randomUuid() + private val session = ObjectSetSession() + private val state = MutableStateFlow(ObjectSet.init()) + private val dispatcher = Dispatcher.Default() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + updateDataViewViewer = UpdateDataViewViewer(repo) + searchObjects = SearchObjects(repo) + urlBuilder = UrlBuilder(gateway) + TestModifyFilterFromSelectedValueFragment.testVmFactory = FilterViewModel.Factory( + objectSetState = state, + session = session, + updateDataViewViewer = updateDataViewViewer, + dispatcher = dispatcher, + searchObjects = searchObjects, + urlBuilder = urlBuilder + ) + } + + @Test + fun tagSelectionTest1() { + + val relationKey = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + // Defining three different tags: + + val option1 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Architect", + color = MockDataFactory.randomString() + ) + + val option2 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Manager", + color = MockDataFactory.randomString() + ) + + val option3 = Relation.Option( + id = MockDataFactory.randomUuid(), + text = "Developer", + color = MockDataFactory.randomString() + ) + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationKey to emptyList() + ) + + // Defining viewer containing one filter + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = listOf( + DVFilter( + relationKey = relationKey, + value = listOf(option1.id), + condition = DVFilterCondition.ALL_IN + ) + ), + sorts = emptyList(), + viewerRelations = listOf( + Block.Content.DataView.Viewer.ViewerRelation( + key = relationKey, + isVisible = true + ) + ), + type = Block.Content.DataView.Viewer.Type.values().random() + ) + + val relation = Relation( + key = relationKey, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = MockDataFactory.randomString(), + source = Relation.Source.values().random(), + format = Relation.Format.TAG, + selections = listOf(option1, option2, option3) + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // Launching fragment + + launchFragment( + bundleOf( + ModifyFilterFromSelectedValueFragment.CTX_KEY to root, + ModifyFilterFromSelectedValueFragment.IDX_KEY to 0, + ModifyFilterFromSelectedValueFragment.RELATION_KEY to relationKey, + ) + ) + + // TESTING + + val rvMatcher = TestUtils.withRecyclerView(R.id.rvViewerFilterRecycler) + + // Checking names + + onView(rvMatcher.atPositionOnView(0, R.id.tvTagName)).apply { + check(matches(withText(option1.text))) + } + + onView(rvMatcher.atPositionOnView(1, R.id.tvTagName)).apply { + check(matches(withText(option2.text))) + } + + onView(rvMatcher.atPositionOnView(2, R.id.tvTagName)).apply { + check(matches(withText(option3.text))) + } + + // Veryfing that only the first tag is selected + + onView(rvMatcher.atPositionOnView(0, R.id.ivSelectTagIcon)).apply { + check(matches(isSelected())) + } + + onView(rvMatcher.atPositionOnView(1, R.id.ivSelectTagIcon)).apply { + check(matches(not(isSelected()))) + } + + onView(rvMatcher.atPositionOnView(2, R.id.ivSelectTagIcon)).apply { + check(matches(not(isSelected()))) + } + + // Verifying that the selection counter is equal to 1 + + onView(withId(R.id.tvOptionCount)).apply { + check(matches(withText("1"))) + } + + // Performing click, in order to sellect second status + + onView(rvMatcher.atPositionOnView(1, R.id.ivSelectTagIcon)).apply { + perform(click()) + } + + // Veryfing that only the first tag and the second tag are selected + + onView(rvMatcher.atPositionOnView(0, R.id.ivSelectTagIcon)).apply { + check(matches(isSelected())) + } + + onView(rvMatcher.atPositionOnView(1, R.id.ivSelectTagIcon)).apply { + check(matches((isSelected()))) + } + + onView(rvMatcher.atPositionOnView(2, R.id.ivSelectTagIcon)).apply { + check(matches(not(isSelected()))) + } + + // Verifying that the selection counter is now equal to 2 + + onView(withId(R.id.tvOptionCount)).apply { + check(matches(withText("2"))) + } + } + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/TestModifyFilterFromInputFieldValueFragment.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/TestModifyFilterFromInputFieldValueFragment.kt new file mode 100644 index 0000000000..95651f2587 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/TestModifyFilterFromInputFieldValueFragment.kt @@ -0,0 +1,18 @@ +package com.anytypeio.anytype.features.sets.filter + +import com.anytypeio.anytype.presentation.sets.filter.FilterViewModel +import com.anytypeio.anytype.ui.sets.modals.filter.ModifyFilterFromInputFieldValueFragment + +class TestModifyFilterFromInputFieldValueFragment : ModifyFilterFromInputFieldValueFragment() { + + init { + factory = testVmFactory + } + + override fun injectDependencies() {} + override fun releaseDependencies() {} + + companion object { + lateinit var testVmFactory: FilterViewModel.Factory + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/TestModifyFilterFromSelectedValueFragment.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/TestModifyFilterFromSelectedValueFragment.kt new file mode 100644 index 0000000000..8fd9fea5fb --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/filter/TestModifyFilterFromSelectedValueFragment.kt @@ -0,0 +1,18 @@ +package com.anytypeio.anytype.features.sets.filter + +import com.anytypeio.anytype.presentation.sets.filter.FilterViewModel +import com.anytypeio.anytype.ui.sets.modals.filter.ModifyFilterFromSelectedValueFragment + +class TestModifyFilterFromSelectedValueFragment : ModifyFilterFromSelectedValueFragment() { + + init { + factory = testVmFactory + } + + override fun injectDependencies() {} + override fun releaseDependencies() {} + + companion object { + lateinit var testVmFactory: FilterViewModel.Factory + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/sort/TestViewerSortFragment.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/sort/TestViewerSortFragment.kt new file mode 100644 index 0000000000..656a969aee --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/sort/TestViewerSortFragment.kt @@ -0,0 +1,18 @@ +package com.anytypeio.anytype.features.sets.sort + +import com.anytypeio.anytype.presentation.sets.sort.ViewerSortViewModel +import com.anytypeio.anytype.ui.sets.modals.sort.ViewerSortFragment + +class TestViewerSortFragment : ViewerSortFragment() { + + init { + factory = testVmFactory + } + + override fun injectDependencies() {} + override fun releaseDependencies() {} + + companion object { + lateinit var testVmFactory: ViewerSortViewModel.Factory + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/sort/ViewerObjectSortTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/sort/ViewerObjectSortTest.kt new file mode 100644 index 0000000000..0d66230979 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/sort/ViewerObjectSortTest.kt @@ -0,0 +1,165 @@ +package com.anytypeio.anytype.features.sets.sort + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.DVSort +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer +import com.anytypeio.anytype.mocking.MockDataFactory +import com.anytypeio.anytype.presentation.relations.ObjectSetConfig +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.sort.ViewerSortViewModel +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.sort.ViewerSortFragment +import com.anytypeio.anytype.utils.CoroutinesTestRule +import com.anytypeio.anytype.utils.TestUtils.withRecyclerView +import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +@LargeTest +class ViewerObjectSortTest { + + @get:Rule + val animationsRule = DisableAnimationsRule() + + @get:Rule + val coroutineTestRule = CoroutinesTestRule() + + @Mock + lateinit var repo: BlockRepository + + private lateinit var updateDataViewViewer: UpdateDataViewViewer + + private val root = MockDataFactory.randomUuid() + private val session = ObjectSetSession() + private val state = MutableStateFlow(ObjectSet.init()) + private val dispatcher = Dispatcher.Default() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + updateDataViewViewer = UpdateDataViewViewer(repo) + TestViewerSortFragment.testVmFactory = ViewerSortViewModel.Factory( + state = state, + session = session, + updateDataViewViewer = updateDataViewViewer, + dispatcher = dispatcher + ) + } + + @Test + fun shouldDisplayObjectRelationSortWithRelationNameAndSortingType() { + + // SETUP + + val name = "Some object" + + val relationId = MockDataFactory.randomUuid() + val target = MockDataFactory.randomUuid() + + val record: Map = mapOf( + ObjectSetConfig.ID_KEY to target, + relationId to emptyList() + ) + + // Defining viewer containing one filter + + val viewer = Block.Content.DataView.Viewer( + id = MockDataFactory.randomUuid(), + name = MockDataFactory.randomString(), + filters = emptyList(), + sorts = listOf( + DVSort( + relationKey = relationId, + type = Block.Content.DataView.Sort.Type.DESC + ) + ), + viewerRelations = listOf( + Block.Content.DataView.Viewer.ViewerRelation( + key = relationId, + isVisible = true + ) + ), + type = Block.Content.DataView.Viewer.Type.values().random() + ) + + val relation = Relation( + key = relationId, + defaultValue = null, + isHidden = false, + isReadOnly = false, + isMulti = true, + name = name, + source = Relation.Source.values().random(), + format = Relation.Format.OBJECT, + selections = emptyList() + ) + + state.value = ObjectSet( + blocks = listOf( + Block( + id = MockDataFactory.randomUuid(), + children = emptyList(), + fields = Block.Fields.empty(), + content = Block.Content.DataView( + relations = listOf(relation), + viewers = listOf(viewer), + source = MockDataFactory.randomUuid() + ) + ) + ), + viewerDb = mapOf( + viewer.id to ObjectSet.ViewerData( + records = listOf(record), + total = 1 + ) + ) + ) + + // Launching fragment + + launchFragment(bundleOf(ViewerSortFragment.CTX_KEY to root)) + + // TESTING + + val rvMatcher = withRecyclerView(R.id.viewerSortRecycler) + + // Checking that the relation name is set + + onView(rvMatcher.atPositionOnView(0, R.id.tvTitle)).apply { + check(matches(withText(name))) + } + + // Checking that the sorting type is set + + onView(rvMatcher.atPositionOnView(0, R.id.tvSubtitle)).apply { + check(matches(withText(R.string.sort_from_z_to_a))) + } + } + + private fun launchFragment(args: Bundle): FragmentScenario { + return launchFragmentInContainer( + fragmentArgs = args, + themeResId = R.style.AppTheme + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/mocking/MockUiTests.kt b/app/src/androidTest/java/com/anytypeio/anytype/mocking/MockUiTests.kt index 6f46127fbe..b7cd010f7b 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/mocking/MockUiTests.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/mocking/MockUiTests.kt @@ -1,6 +1,6 @@ package com.anytypeio.anytype.mocking -import com.anytypeio.anytype.domain.block.model.Block +import com.anytypeio.anytype.core_models.Block object MockUiTests { diff --git a/app/src/androidTest/java/com/anytypeio/anytype/utils/EspressoExt.kt b/app/src/androidTest/java/com/anytypeio/anytype/utils/EspressoExt.kt index a92d6e2192..bc064e5025 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/utils/EspressoExt.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/utils/EspressoExt.kt @@ -1,11 +1,21 @@ package com.anytypeio.anytype.utils +import android.support.annotation.DimenRes +import android.support.annotation.IntegerRes +import android.support.annotation.StringRes import androidx.test.espresso.Espresso.onView import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import com.anytypeio.anytype.core_ui.features.page.BlockViewHolder import com.anytypeio.anytype.utils.TestUtils.withRecyclerView +import org.hamcrest.Matchers.not fun Int.scrollTo(position: Int) { onView(withId(this)).perform(RecyclerViewActions.scrollToPosition(position)) @@ -13,4 +23,81 @@ fun Int.scrollTo(position: Int) { fun Int.findItemAt(position: Int, layoutId: Int): ViewInteraction { return onView(withRecyclerView(this).atPositionOnView(position, layoutId)) +} + +fun ViewInteraction.performClick(): ViewInteraction = perform(ViewActions.click()) +fun Int.matchView(): ViewInteraction = onView(withId(this)) +fun Int.performClick(): ViewInteraction = matchView().performClick() +fun Int.type(text: String) = matchView().perform(click(), typeText(text)) + +fun ViewInteraction.checkHasText(text: String) { + check(matches(ViewMatchers.withText(text))) +} + +fun ViewInteraction.checkHasMarginStart(@DimenRes dimen: Int, coefficient: Int) { + check(matches(WithMarginStart(dimen, coefficient))) +} + +fun ViewInteraction.checkHasPaddingLeft(@DimenRes dimen: Int, coefficient: Int) { + check(matches(WithPaddingLeft(dimen, coefficient))) +} + +fun ViewInteraction.checkIsSelected() { + check(matches(ViewMatchers.isSelected())) +} + +fun ViewInteraction.checkIsDisplayed() { + check(matches(isDisplayed())) +} + +fun ViewInteraction.checkIsNotDisplayed() { + check(matches(not(isDisplayed()))) +} + +fun ViewInteraction.checkIsNotSelected() { + check(matches(not(ViewMatchers.isSelected()))) +} + +fun ViewInteraction.checkHasText(@StringRes resId: Int) { + check(matches(ViewMatchers.withText(resId))) +} + +fun ViewInteraction.checkHasTextColor(color: Int) { + check(matches(WithTextColor(color))) +} + +fun ViewInteraction.checkHasBackgroundColor(color: Int) { + check(matches(WithBackgroundColor(color))) +} + +fun ViewInteraction.checkHasNoBackground() { + check(matches(WithoutBackgroundColor())) +} + +fun ViewInteraction.checkHasChildViewCount(count: Int) : ViewInteraction { + return check(matches(WithChildViewCount(count))) +} + +fun Int.rVMatcher(): RecyclerViewMatcher = RecyclerViewMatcher(this) + +fun Int.checkRecyclerItemCount(expected: Int) = matchView().check(RecyclerViewItemCountAssertion(expected)) + +fun RecyclerViewMatcher.onItemView(pos: Int, @IntegerRes target: Int): ViewInteraction { + return onView(atPositionOnView(pos, target)) +} + +fun ViewInteraction.checkHasChildViewWithText( + pos: Int, + text: String, + @IntegerRes target: Int +) : ViewInteraction { + return check(matches(HasChildViewWithText(pos, text, target))) +} + +fun RecyclerViewMatcher.onItem(pos: Int): ViewInteraction { + return onView(atPosition(pos)) +} + +fun RecyclerViewMatcher.checkIsRecyclerSize(expected: Int) { + recyclerViewId.matchView().check(RecyclerViewItemCountAssertion(expected)) } \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/utils/RecyclerViewItemCountAssertion.kt b/app/src/androidTest/java/com/anytypeio/anytype/utils/RecyclerViewItemCountAssertion.kt new file mode 100644 index 0000000000..386841fc66 --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/utils/RecyclerViewItemCountAssertion.kt @@ -0,0 +1,19 @@ +package com.anytypeio.anytype.utils + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.NoMatchingViewException +import androidx.test.espresso.ViewAssertion +import androidx.test.espresso.matcher.ViewMatchers.assertThat +import org.hamcrest.CoreMatchers.`is` + + +class RecyclerViewItemCountAssertion(val expected: Int) : ViewAssertion { + override fun check(view: View?, noViewFoundException: NoMatchingViewException?) { + if (noViewFoundException != null) throw noViewFoundException + val recycler = view as RecyclerView + val adapter = recycler.adapter + checkNotNull(adapter) { "Adapter wasn't set" } + assertThat(adapter.itemCount, `is`(expected)) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/anytypeio/anytype/utils/RecyclerViewMatcher.java b/app/src/androidTest/java/com/anytypeio/anytype/utils/RecyclerViewMatcher.java index 29296ea4d2..5c07584032 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/utils/RecyclerViewMatcher.java +++ b/app/src/androidTest/java/com/anytypeio/anytype/utils/RecyclerViewMatcher.java @@ -11,7 +11,7 @@ import org.hamcrest.TypeSafeMatcher; public class RecyclerViewMatcher { - private final int recyclerViewId; + final int recyclerViewId; public RecyclerViewMatcher(int recyclerViewId) { this.recyclerViewId = recyclerViewId; @@ -28,18 +28,21 @@ public class RecyclerViewMatcher { View childView; public void describeTo(Description description) { - String idDescription = Integer.toString(recyclerViewId); + String idRecyclerDescription = Integer.toString(recyclerViewId); + String idTargetViewDescription = Integer.toString(targetViewId); if (this.resources != null) { try { - idDescription = this.resources.getResourceName(recyclerViewId); - } catch (Resources.NotFoundException var4) { - idDescription = String.format("%s (resource name not found)", - Integer.valueOf - (recyclerViewId)); + idRecyclerDescription = this.resources.getResourceName(recyclerViewId); + } catch (Resources.NotFoundException e) { + idRecyclerDescription = String.format("%s (resource name not found)", recyclerViewId); + } + try { + idTargetViewDescription = this.resources.getResourceName(targetViewId); + } catch (Resources.NotFoundException e) { + idTargetViewDescription = String.format("%s (resource name not found)", targetViewId); } } - - description.appendText("with id: " + idDescription); + description.appendText("\nwith id: [" + idTargetViewDescription + "] inside: [" + idRecyclerDescription + "]"); } public boolean matchesSafely(View view) { @@ -50,7 +53,14 @@ public class RecyclerViewMatcher { RecyclerView recyclerView = view.getRootView().findViewById(recyclerViewId); if (recyclerView != null && recyclerView.getId() == recyclerViewId) { - childView = recyclerView.findViewHolderForAdapterPosition(position).itemView; + RecyclerView.ViewHolder holder = recyclerView.findViewHolderForAdapterPosition(position); + if (holder == null) { + throw new IllegalStateException( + "No view holder found at position: " + position + + ". Actual child count: " + recyclerView.getChildCount() + ); + } + childView = holder.itemView; } else { return false; } diff --git a/app/src/androidTest/java/com/anytypeio/anytype/utils/TestUtils.java b/app/src/androidTest/java/com/anytypeio/anytype/utils/TestUtils.java index 6b4c02daa1..3778a9be1f 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/utils/TestUtils.java +++ b/app/src/androidTest/java/com/anytypeio/anytype/utils/TestUtils.java @@ -102,7 +102,6 @@ public class TestUtils { public static RecyclerViewMatcher withRecyclerView(final int recyclerViewId) { - return new RecyclerViewMatcher(recyclerViewId); } } diff --git a/app/src/androidTest/java/com/anytypeio/anytype/utils/espresso/ViewMatchers.kt b/app/src/androidTest/java/com/anytypeio/anytype/utils/espresso/ViewMatchers.kt new file mode 100644 index 0000000000..ca9cf0996c --- /dev/null +++ b/app/src/androidTest/java/com/anytypeio/anytype/utils/espresso/ViewMatchers.kt @@ -0,0 +1,116 @@ +package com.anytypeio.anytype.utils + +import android.graphics.drawable.ColorDrawable +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.marginStart +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.matcher.BoundedMatcher +import com.anytypeio.anytype.core_utils.ext.dimen +import org.hamcrest.Description +import org.hamcrest.TypeSafeMatcher + + +class WithTextColor(private val expectedColor: Int) : BoundedMatcher(TextView::class.java) { + override fun matchesSafely(item: TextView) = item.currentTextColor == expectedColor + override fun describeTo(description: Description) { + description.appendText("with text color:") + description.appendValue(expectedColor) + } +} + +class WithBackgroundColor(private val expected: Int) : BoundedMatcher(View::class.java) { + override fun describeTo(description: Description) { + description.appendText("with background color:") + description.appendValue(expected) + } + + override fun matchesSafely(item: View): Boolean { + val actual = (item.background as ColorDrawable).color + return actual == expected + } +} + +class WithoutBackgroundColor : BoundedMatcher(View::class.java) { + override fun describeTo(description: Description) { + description.appendText("with background color:") + } + + override fun matchesSafely(item: View): Boolean { + return item.background == null + } +} + +class WithChildViewCount(private val expectedCount: Int) : BoundedMatcher(ViewGroup::class.java) { + override fun matchesSafely(item: ViewGroup): Boolean = item.childCount == expectedCount + override fun describeTo(description: Description) { + description.appendText("ViewGroup with child-count = $expectedCount"); + } +} + +class HasChildViewWithText(private val pos: Int, val text: String, val target: Int) : BoundedMatcher(RecyclerView::class.java) { + + private var actual: String? = null + + override fun matchesSafely(item: RecyclerView): Boolean { + val holder = item.findViewHolderForLayoutPosition(pos) + checkNotNull(holder) { throw IllegalStateException("No holder at position: $pos") } + val target = holder.itemView.findViewById(target) + actual = target.text.toString() + return actual == text + } + override fun describeTo(description: Description) { + if (actual != null) { + description.appendText("Should have text [$text] at position: $pos but was: $actual"); + } + } +} + +class WithMarginStart(private val dimen: Int, private val coefficient: Int) : TypeSafeMatcher() { + + var actual: Int = 0 + var expected: Int = 0 + + override fun describeTo(description: Description) { + description.appendText("with actual margin start:") + description.appendValue(actual) + description.appendText("with expected margin start:") + description.appendValue(expected) + } + override fun matchesSafely(item: View): Boolean { + actual = item.marginStart + expected = (item.context.dimen(dimen) * coefficient).toInt() + return actual == expected + } +} + +class WithPaddingLeft(private val dimen: Int, private val coefficient: Int) : TypeSafeMatcher() { + + var actual: Int = 0 + var expected: Int = 0 + + override fun describeTo(description: Description) { + description.appendText("with actual padding start:") + description.appendValue(actual) + description.appendText("with expected padding start:") + description.appendValue(expected) + } + override fun matchesSafely(item: View): Boolean { + actual = item.paddingLeft + expected = (item.context.dimen(dimen) * coefficient).toInt() + return actual == expected + } +} + +class WithTextColorRes(private val expectedColorRes: Int) : BoundedMatcher(TextView::class.java) { + override fun matchesSafely(item: TextView): Boolean { + val color = ContextCompat.getColor(item.context, expectedColorRes) + return item.currentTextColor == color + } + override fun describeTo(description: Description) { + description.appendText("with text color:") + description.appendValue(expectedColorRes) + } +} diff --git a/app/src/debug/google-services.json b/app/src/debug/google-services.json new file mode 100644 index 0000000000..63acac6021 --- /dev/null +++ b/app/src/debug/google-services.json @@ -0,0 +1,39 @@ +{ + "project_info": { + "project_number": "687711214896", + "project_id": "anytype-debug", + "storage_bucket": "anytype-debug.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:687711214896:android:20001e745ee8af044f7cf5", + "android_client_info": { + "package_name": "com.anytypeio.anytype.debug" + } + }, + "oauth_client": [ + { + "client_id": "687711214896-otmhog2ql4ngk1gcatbc3b1afkudra8s.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyC83X8q2Ya3-jTMp7b7i85WoY-TeBnX1qQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "687711214896-otmhog2ql4ngk1gcatbc3b1afkudra8s.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/debug/res/values/technical_strings.xml b/app/src/debug/res/values/technical_strings.xml new file mode 100644 index 0000000000..d85663e67e --- /dev/null +++ b/app/src/debug/res/values/technical_strings.xml @@ -0,0 +1,5 @@ + + + DEBUG-Anytype + #000 + \ No newline at end of file diff --git a/app/src/experimental/java/com/anytypeio/anytype/ui/desktop/HomeDashboardFragment.kt b/app/src/experimental/java/com/anytypeio/anytype/ui/desktop/HomeDashboardFragment.kt new file mode 100644 index 0000000000..c6ac4c1043 --- /dev/null +++ b/app/src/experimental/java/com/anytypeio/anytype/ui/desktop/HomeDashboardFragment.kt @@ -0,0 +1,191 @@ +package com.anytypeio.anytype.ui.desktop + +import android.os.Bundle +import android.view.View +import android.view.View.OVER_SCROLL_NEVER +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_utils.ext.dimen +import com.anytypeio.anytype.core_utils.ext.toast +import com.anytypeio.anytype.core_utils.ext.visible +import com.anytypeio.anytype.core_utils.ui.EqualSpacingItemDecoration +import com.anytypeio.anytype.core_utils.ui.EqualSpacingItemDecoration.Companion.GRID +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.presentation.desktop.DashboardView +import com.anytypeio.anytype.presentation.desktop.HomeDashboardStateMachine.State +import com.anytypeio.anytype.presentation.desktop.HomeDashboardViewModel +import com.anytypeio.anytype.presentation.desktop.HomeDashboardViewModelFactory +import com.anytypeio.anytype.presentation.extension.filterByNotArchivedPages +import com.anytypeio.anytype.ui.base.ViewStateFragment +import com.anytypeio.anytype.ui.page.PageFragment +import kotlinx.android.synthetic.experimental.fragment_desktop.* +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import javax.inject.Inject + +class HomeDashboardFragment : ViewStateFragment(R.layout.fragment_desktop) { + + private val vm by viewModels { factory } + + private val dndBehavior by lazy { + DashboardDragAndDropBehavior( + onItemMoved = { from, to -> + dashboardAdapter + .onItemMove(from, to) + .also { + vm.onItemMoved( + views = dashboardAdapter.provideAdapterData(), + from = from, + to = to + ) + } + }, + onItemDropped = { index -> + try { + vm.onItemDropped(dashboardAdapter.provideAdapterData()[index]) + } catch (e: Exception) { + Timber.e(e, "Error while dropping item at index: $index") + } + } + ) + } + + @Inject + lateinit var factory: HomeDashboardViewModelFactory + + @Inject + lateinit var builder: UrlBuilder + + private val dashboardAdapter by lazy { + DashboardAdapter( + data = mutableListOf(), + onDocumentClicked = { target, isLoading -> vm.onDocumentClicked(target, isLoading) }, + onArchiveClicked = { vm.onArchivedClicked(it) }, + onObjectSetClicked = { vm.onObjectSetClicked(it) } + ) + } + + private val profileAdapter by lazy { + DashboardProfileAdapter( + data = mutableListOf(), + onProfileClicked = { vm.onProfileClicked() } + ) + } + + private val concatAdapter by lazy { + ConcatAdapter(ProfileContainerAdapter(profileAdapter), dashboardAdapter) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setup() + + with(vm) { + state.observe(viewLifecycleOwner, this@HomeDashboardFragment) + navigation.observe(viewLifecycleOwner, navObserver) + } + + parseIntent() + } + + private fun parseIntent() { + val deepLinkPage = arguments?.getString(PageFragment.ID_KEY, null) + if (deepLinkPage != null) { + arguments?.remove(PageFragment.ID_KEY) + + vm.onNavigationDeepLink(deepLinkPage) + } else { + vm.onViewCreated() + } + } + + override fun render(state: State) { + when { + state.error != null -> { + requireActivity().toast("Error: ${state.error}") + } + state.isInitialzed -> { + bottomToolbar.visible() + state.blocks.let { views -> + val profile = views.filterIsInstance() + val links = views.filterByNotArchivedPages() + if (profile.isNotEmpty()) { + profileAdapter.update(profile) + } + dashboardAdapter.update(links) + } + } + } + } + + private fun setup() { + + val spacing = requireContext().dimen(R.dimen.default_dashboard_item_spacing).toInt() + val decoration = EqualSpacingItemDecoration( + topSpacing = spacing, + leftSpacing = spacing, + rightSpacing = spacing, + bottomSpacing = 0, + displayMode = GRID + ) + + desktopRecycler.apply { + overScrollMode = OVER_SCROLL_NEVER + layoutManager = GridLayoutManager(context, 2).apply { + spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return if (position == 0) { + 2 + } else { + 1 + } + } + } + } + + adapter = concatAdapter + ItemTouchHelper(dndBehavior).attachToRecyclerView(this) + addItemDecoration(decoration) + setHasFixedSize(true) + } + + bottomToolbar + .navigationClicks() + .onEach { vm.onPageNavigationClicked() } + .launchIn(lifecycleScope) + + bottomToolbar + .addPageClick() + .onEach { vm.onAddNewDocumentClicked() } + .launchIn(lifecycleScope) + + bottomToolbar + .searchClicks() + .onEach { vm.onPageSearchClicked() } + .launchIn(lifecycleScope) + + createSetButton + .clicks() + .onEach { vm.onCreateNewObjectSetClicked() } + .launchIn(lifecycleScope) + } + + override fun injectDependencies() { + componentManager().desktopComponent.get().inject(this) + } + + override fun releaseDependencies() { + componentManager().desktopComponent.release() + } + + companion object { + const val COLUMN_COUNT = 2 + } +} diff --git a/app/src/experimental/java/com/anytypeio/anytype/ui/page/sheets/DocMenuBottomSheet.kt b/app/src/experimental/java/com/anytypeio/anytype/ui/page/sheets/DocMenuBottomSheet.kt new file mode 100644 index 0000000000..447bce833e --- /dev/null +++ b/app/src/experimental/java/com/anytypeio/anytype/ui/page/sheets/DocMenuBottomSheet.kt @@ -0,0 +1,159 @@ +package com.anytypeio.anytype.ui.page.sheets + +import android.graphics.Color +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Url +import com.anytypeio.anytype.core_ui.extensions.* +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.domain.status.SyncStatus +import kotlinx.android.synthetic.experimental.fragment_doc_menu_bottom_sheet.* +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class DocMenuBottomSheet : BaseBottomSheetFragment() { + + private val title get() = arg(TITLE_KEY) + private val status get() = SyncStatus.valueOf(arg(STATUS_KEY)) + private val image get() = arg(IMAGE_KEY) + private val emoji get() = arg(EMOJI_KEY) + private val isProfile get() = arg(IS_PROFILE_KEY) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_doc_menu_bottom_sheet, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + bindTitle() + bindSyncStatus(status) + closeButton.clicks().onEach { dismiss() }.launchIn(lifecycleScope) + + searchOnPageContainer + .clicks() + .onEach { + withParent { onSearchOnPageClicked() }.also { dismiss() } + } + .launchIn(lifecycleScope) + + archiveContainer + .clicks() + .onEach { + withParent { onArchiveClicked() }.also { dismiss() } + } + .launchIn(lifecycleScope) + + addCoverContainer + .clicks() + .onEach { + withParent { onAddCoverClicked() }.also { dismiss() } + } + .launchIn(lifecycleScope) + + relationContainer + .clicks() + .onEach { + withParent { onDocRelationsClicked() }.also { dismiss() } + } + .launchIn(lifecycleScope) + + if (image != null && !isProfile) icon.setImageOrNull(image) + if (emoji != null && !isProfile) icon.setEmojiOrNull(emoji) + + if (isProfile) { + avatar.visible() + image?.let { avatar.icon(it) } ?: avatar.bind( + name = title.orEmpty(), + color = title.orEmpty().firstDigitByHash().let { + requireContext().avatarColor(it) + } + ) + archiveContainer.gone() + addCoverContainer.setBackgroundResource(R.drawable.rectangle_doc_menu_bottom) + searchOnPageContainer.setBackgroundResource(R.drawable.rectangle_doc_menu_top) + } + } + + private fun bindTitle() { + tvTitle.text = title ?: getString(R.string.untitled) + } + + private fun bindSyncStatus(status: SyncStatus) { + when (status) { + SyncStatus.UNKNOWN -> { + badge.tint( + color = requireContext().color(R.color.sync_status_red) + ) + tvSubtitle.setText(R.string.sync_status_unknown) + } + SyncStatus.FAILED -> { + badge.tint( + color = requireContext().color(R.color.sync_status_red) + ) + tvSubtitle.setText(R.string.sync_status_failed) + } + SyncStatus.OFFLINE -> { + badge.tint( + color = requireContext().color(R.color.sync_status_red) + ) + tvSubtitle.setText(R.string.sync_status_offline) + } + SyncStatus.SYNCING -> { + badge.tint( + color = requireContext().color(R.color.sync_status_orange) + ) + tvSubtitle.setText(R.string.sync_status_syncing) + } + SyncStatus.SYNCED -> { + badge.tint( + color = requireContext().color(R.color.sync_status_green) + ) + tvSubtitle.setText(R.string.sync_status_synced) + } + else -> badge.tint(Color.WHITE) + } + } + + override fun injectDependencies() {} + override fun releaseDependencies() {} + + companion object { + fun new( + title: String?, + status: SyncStatus, + image: Url?, + emoji: String?, + isProfile: Boolean = false + ) = DocMenuBottomSheet().apply { + arguments = bundleOf( + TITLE_KEY to title, + STATUS_KEY to status.name, + IMAGE_KEY to image, + EMOJI_KEY to emoji, + IS_PROFILE_KEY to isProfile + ) + } + + private const val TITLE_KEY = "arg.doc-menu-bottom-sheet.title" + private const val IMAGE_KEY = "arg.doc-menu-bottom-sheet.image" + private const val EMOJI_KEY = "arg.doc-menu-bottom-sheet.emoji" + private const val STATUS_KEY = "arg.doc-menu-bottom-sheet.status" + private const val IS_PROFILE_KEY = "arg.doc-menu-bottom-sheet.is-profile" + } + + interface DocumentMenuActionReceiver { + fun onArchiveClicked() + fun onSearchOnPageClicked() + fun onDocRelationsClicked() + fun onAddCoverClicked() + } +} \ No newline at end of file diff --git a/app/src/experimental/res/layout/fragment_desktop.xml b/app/src/experimental/res/layout/fragment_desktop.xml new file mode 100644 index 0000000000..1aef8eaeac --- /dev/null +++ b/app/src/experimental/res/layout/fragment_desktop.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/experimental/res/layout/fragment_doc_menu_bottom_sheet.xml b/app/src/experimental/res/layout/fragment_doc_menu_bottom_sheet.xml new file mode 100644 index 0000000000..c3940afed9 --- /dev/null +++ b/app/src/experimental/res/layout/fragment_doc_menu_bottom_sheet.xml @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 21bd873cf5..af540640d2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,9 +38,6 @@ - \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/app/AndroidApplication.kt b/app/src/main/java/com/anytypeio/anytype/app/AndroidApplication.kt index bae13841f6..d67c478f52 100644 --- a/app/src/main/java/com/anytypeio/anytype/app/AndroidApplication.kt +++ b/app/src/main/java/com/anytypeio/anytype/app/AndroidApplication.kt @@ -14,7 +14,6 @@ import com.anytypeio.anytype.di.main.ContextModule import com.anytypeio.anytype.di.main.DaggerMainComponent import com.anytypeio.anytype.di.main.MainComponent import com.facebook.stetho.Stetho -import com.google.firebase.crashlytics.FirebaseCrashlytics import timber.log.Timber import javax.inject.Inject @@ -38,7 +37,6 @@ class AndroidApplication : Application() { super.onCreate() main.inject(this) setupAnalytics() - setupCrashlytics() setupEmojiCompat() setupTimber() setupStetho() @@ -74,11 +72,4 @@ class AndroidApplication : Application() { Amplitude.getInstance().initialize(this, getString(R.string.amplitude_api_key)) } } - - private fun setupCrashlytics() { - if (BuildConfig.DEBUG) - FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false) - else - FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true) - } } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/device/DefaultGradientCollectionProvider.kt b/app/src/main/java/com/anytypeio/anytype/device/DefaultGradientCollectionProvider.kt index 4b081afcc1..d6b272c230 100644 --- a/app/src/main/java/com/anytypeio/anytype/device/DefaultGradientCollectionProvider.kt +++ b/app/src/main/java/com/anytypeio/anytype/device/DefaultGradientCollectionProvider.kt @@ -1,6 +1,6 @@ package com.anytypeio.anytype.device -import com.anytypeio.anytype.domain.common.Id +import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.domain.cover.GradientCollectionProvider import com.anytypeio.anytype.presentation.page.cover.CoverGradient diff --git a/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt b/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt index 84e76a8666..26b876ea32 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt @@ -1,8 +1,12 @@ package com.anytypeio.anytype.di.common +import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.di.feature.* +import com.anytypeio.anytype.di.feature.sets.CreateFilterModule +import com.anytypeio.anytype.di.feature.sets.ModifyFilterModule +import com.anytypeio.anytype.di.feature.sets.PickConditionModule +import com.anytypeio.anytype.di.feature.sets.SelectFilterRelationModule import com.anytypeio.anytype.di.main.MainComponent -import com.anytypeio.anytype.domain.common.Id class ComponentManager(private val main: MainComponent) { @@ -134,15 +138,17 @@ class ComponentManager(private val main: MainComponent) { .build() } - val documentIconActionMenuComponent = Component { - main + val documentIconActionMenuComponent = DependentComponentMap { ctx -> + pageComponent + .get(ctx) .documentActionMenuComponentBuilder() .documentIconActionMenuModule(DocumentIconActionMenuModule()) .build() } - val documentEmojiIconPickerComponent = Component { - main + val documentEmojiIconPickerComponent = DependentComponentMap { ctx -> + pageComponent + .get(ctx) .documentEmojiIconPickerComponentBuilder() .documentIconActionMenuModule(DocumentEmojiIconPickerModule()) .build() @@ -180,6 +186,278 @@ class ComponentManager(private val main: MainComponent) { .build() } + val createSetComponent = Component { + main.createSetComponentBuilder() + .module(CreateSetModule) + .build() + } + + val createObjectTypeComponent = Component { + main.createObjectTypeComponentBuilder() + .module(CreateObjectTypeModule) + .build() + } + + val objectSetComponent = ComponentMap { + main.objectSetComponentBuilder() + .module(ObjectSetModule) + .build() + } + + val documentRelationComponent = DependentComponentMap { id -> + pageComponent + .get(id) + .documentRelationSubComponent() + .module(DocumentRelationModule) + .build() + } + + val viewerSortByComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .viewerSortBySubComponent() + .module(ViewerSortByModule) + .build() + } + + val createDataViewRelationComponent = Component { + main.createDataViewRelationBuilder() + .module(CreateDataViewRelationModule) + .build() + } + + val editGridCellComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .editCellsComponent() + .module(EditGridCellModule) + .build() + } + + val editRelationCellComponent = DependentComponentMap { ctx -> + pageComponent + .get(ctx) + .editRelationCellComponent() + .module(EditGridCellModule) + .build() + } + + val objectSetObjectRelationDataValueComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .editCellDateComponent() + .module(EditGridCellDateModule) + .build() + } + + val objectObjectRelationDateValueComponet = DependentComponentMap { ctx -> + pageComponent + .get(ctx) + .editRelationDateComponent() + .module(EditGridCellDateModule) + .build() + } + + val documentAddNewBlockComponent = DependentComponentMap { ctx -> + pageComponent + .get(ctx) + .documentAddNewBlockComponentBuilder() + .documentAddNewBlockModule(DocumentAddNewBlockModule) + .build() + } + + val viewerFilterComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .viewerFilterBySubComponent() + .module(ViewerFilterModule) + .build() + } + + val viewerCustomizeComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .viewerCustomizeSubComponent() + .module(ViewerCustomizeModule) + .build() + } + + val objectSetRecordComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .objectSetRecordComponent() + .module(ObjectSetRecordModule) + .build() + } + + val createDataViewViewerComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .createDataViewViewerSubComponent() + .module(CreateDataViewViewerModule) + .build() + } + + val editDataViewViewerComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .editDataViewViewerComponent() + .module(EditDataViewViewerModule) + .build() + } + + val objectSetObjectRelationValueComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .objectRelationValueComponent() + .module(ObjectRelationValueModule) + .build() + } + + val addObjectSetObjectRelationValueComponent = DependentComponentMap { ctx -> + objectSetObjectRelationValueComponent + .get(ctx) + .addObjectRelationValueComponent() + .module(AddObjectRelationValueModule) + .build() + } + + val objectObjectRelationValueComponent = DependentComponentMap { ctx -> + pageComponent + .get(ctx) + .editDocRelationComponent() + .module(ObjectRelationValueModule) + .build() + } + + val addObjectObjectRelationValueComponent = DependentComponentMap { ctx -> + objectObjectRelationValueComponent + .get(ctx) + .addObjectRelationValueComponent() + .module(AddObjectRelationValueModule) + .build() + } + + val addObjectSetObjectRelationObjectValueComponent = DependentComponentMap { ctx -> + objectSetObjectRelationValueComponent + .get(ctx) + .addObjectRelationObjectValueComponent() + .module(AddObjectRelationObjectValueModule) + .build() + } + + val addObjectRelationObjectValueComponent = DependentComponentMap { ctx -> + objectObjectRelationValueComponent + .get(ctx) + .addObjectRelationObjectValueComponent() + .module(AddObjectRelationObjectValueModule) + .build() + } + + val relationFileValueComponent = DependentComponentMap { ctx -> + objectObjectRelationValueComponent + .get(ctx) + .addRelationFileValueAddComponent() + .module(RelationFileValueAddModule) + .build() + } + + val relationFileValueDVComponent = DependentComponentMap { ctx -> + objectSetObjectRelationValueComponent + .get(ctx) + .addRelationFileValueAddComponent() + .module(RelationFileValueAddModule) + .build() + } + + val manageViewerComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .manageViewerComponent() + .module(ManageViewerModule) + .build() + } + + val viewerRelationsComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .viewerRelationsComponent() + .module(ViewerRelationsModule) + .build() + } + + val dataviewViewerActionComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .dataviewViewerActionComponent() + .module(DataViewViewerActionModule) + .build() + } + + val selectSortRelationComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .selectSortRelationComponent() + .module(SelectSortRelationModule) + .build() + } + + val selectFilterRelationComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .selectFilterRelationComponent() + .module(SelectFilterRelationModule) + .build() + } + + val createFilterComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .createFilterComponent() + .module(CreateFilterModule) + .build() + } + + val pickFilterConditionComponentCreate = DependentComponentMap { ctx -> + createFilterComponent + .get(ctx) + .createPickConditionComponent() + .module(PickConditionModule) + .build() + } + + val pickFilterConditionComponentModify = DependentComponentMap { ctx -> + modifyFilterComponent + .get(ctx) + .createPickConditionComponent() + .module(PickConditionModule) + .build() + } + + val modifyFilterComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .modifyFilterComponent() + .module(ModifyFilterModule) + .build() + } + + val viewerSortComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .viewerSortComponent() + .module(ViewerSortModule) + .build() + } + + val modifyViewerSortComponent = DependentComponentMap { ctx -> + objectSetComponent + .get(ctx) + .modifyViewerSortComponent() + .module(ModifyViewerSortModule) + .build() + } + val docCoverGalleryComponent = DependentComponentMap { ctx -> pageComponent .get(ctx) diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/AddObjectRelationObjectValuDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/AddObjectRelationObjectValuDI.kt new file mode 100644 index 0000000000..19db2d6e63 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/AddObjectRelationObjectValuDI.kt @@ -0,0 +1,50 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.core_utils.di.scope.PerDialog +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.SearchObjects +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.presentation.relations.AddObjectRelationObjectValueViewModel +import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider +import com.anytypeio.anytype.ui.relations.AddObjectRelationObjectValueFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [AddObjectRelationObjectValueModule::class]) +@PerDialog +interface AddObjectRelationObjectValueSubComponent { + + @Subcomponent.Builder + interface Builder { + fun module(module: AddObjectRelationObjectValueModule): Builder + fun build(): AddObjectRelationObjectValueSubComponent + } + + fun inject(fragment: AddObjectRelationObjectValueFragment) +} + +@Module +object AddObjectRelationObjectValueModule { + + @JvmStatic + @Provides + @PerDialog + fun provideViewModelFactory( + relations: ObjectRelationProvider, + values: ObjectValueProvider, + searchObjects: SearchObjects, + urlBuilder: UrlBuilder + ): AddObjectRelationObjectValueViewModel.Factory = + AddObjectRelationObjectValueViewModel.Factory( + relations, values, searchObjects, urlBuilder + ) + + @JvmStatic + @Provides + @PerDialog + fun provideSearchObjectsUseCase( + repo: BlockRepository + ): SearchObjects = SearchObjects(repo = repo) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/AddObjectRelationValueDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/AddObjectRelationValueDI.kt new file mode 100644 index 0000000000..aecede1a5d --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/AddObjectRelationValueDI.kt @@ -0,0 +1,102 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerDialog +import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewRelationOption +import com.anytypeio.anytype.domain.dataview.interactor.AddStatusToDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.AddTagToDataViewRecord +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.relations.AddObjectRelationOption +import com.anytypeio.anytype.presentation.relations.AddObjectObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.relations.AddObjectSetObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectTypeProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.relations.AddObjectObjectRelationValueFragment +import com.anytypeio.anytype.ui.relations.AddObjectSetObjectRelationValueFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [AddObjectRelationValueModule::class]) +@PerDialog +interface AddObjectRelationValueSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: AddObjectRelationValueModule): Builder + fun build(): AddObjectRelationValueSubComponent + } + + fun inject(fragment: AddObjectSetObjectRelationValueFragment) + fun inject(fragment: AddObjectObjectRelationValueFragment) +} + +@Module +object AddObjectRelationValueModule { + + @JvmStatic + @Provides + @PerDialog + fun provideViewModelFactoryForSets( + relations: ObjectRelationProvider, + values: ObjectValueProvider, + details: ObjectDetailProvider, + types: ObjectTypeProvider, + dispatcher: Dispatcher, + addDataViewRelationOption: AddDataViewRelationOption, + addTagToDataViewRecord: AddTagToDataViewRecord, + addStatusToDataViewRecord: AddStatusToDataViewRecord, + urlBuilder: UrlBuilder + ): AddObjectSetObjectRelationValueViewModel.Factory = AddObjectSetObjectRelationValueViewModel.Factory( + relations = relations, + values = values, + details = details, + types = types, + urlBuilder = urlBuilder, + dispatcher = dispatcher, + addDataViewRelationOption = addDataViewRelationOption, + addTagToDataViewRecord = addTagToDataViewRecord, + addStatusToDataViewRecord = addStatusToDataViewRecord + ) + + @JvmStatic + @Provides + @PerDialog + fun provideViewModelFactoryForObjects( + relations: ObjectRelationProvider, + values: ObjectValueProvider, + details: ObjectDetailProvider, + types: ObjectTypeProvider, + dispatcher: Dispatcher, + addObjectRelationOption: AddObjectRelationOption, + updateDetail: UpdateDetail, + urlBuilder: UrlBuilder + ): AddObjectObjectRelationValueViewModel.Factory = AddObjectObjectRelationValueViewModel.Factory( + relations = relations, + values = values, + details = details, + types = types, + urlBuilder = urlBuilder, + dispatcher = dispatcher, + addObjectRelationOption = addObjectRelationOption, + updateDetail = updateDetail + ) + + @JvmStatic + @Provides + @PerDialog + fun provideAddObjectRelationOptionUseCase( + repo: BlockRepository + ): AddObjectRelationOption = AddObjectRelationOption(repo = repo) + + @JvmStatic + @Provides + @PerDialog + fun provideAddStatusToDataViewRecordUseCase( + repo: BlockRepository + ): AddStatusToDataViewRecord = AddStatusToDataViewRecord(repo) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ArchiveDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ArchiveDI.kt index 6f8b5e9ef4..cee06aa0db 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/ArchiveDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ArchiveDI.kt @@ -4,7 +4,7 @@ import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_utils.di.scope.PerScreen import com.anytypeio.anytype.domain.event.interactor.InterceptEvents import com.anytypeio.anytype.domain.page.ArchiveDocument -import com.anytypeio.anytype.domain.page.ClosePage +import com.anytypeio.anytype.domain.page.CloseBlock import com.anytypeio.anytype.domain.page.OpenPage import com.anytypeio.anytype.presentation.page.DocumentExternalEventReducer import com.anytypeio.anytype.presentation.page.archive.ArchiveViewModelFactory @@ -42,7 +42,7 @@ object ArchiveModule { @Provides fun provideArchiveViewModelFactory( openPage: OpenPage, - closePage: ClosePage, + closePage: CloseBlock, archiveDocument: ArchiveDocument, interceptEvents: InterceptEvents, renderer: DefaultBlockViewRenderer, diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/CreateDataViewRelationDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/CreateDataViewRelationDI.kt new file mode 100644 index 0000000000..da6b72e8fd --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/CreateDataViewRelationDI.kt @@ -0,0 +1,31 @@ +package com.anytypeio.anytype.di.feature; + +import com.anytypeio.anytype.core_utils.di.scope.PerScreen +import com.anytypeio.anytype.presentation.relations.CreateDataViewRelationViewModelFactory +import com.anytypeio.anytype.ui.relations.CreateDataViewRelationFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [CreateDataViewRelationModule::class]) +@PerScreen +interface CreateDataViewRelationSubComponent { + + @Subcomponent.Builder + interface Builder { + fun module(module: CreateDataViewRelationModule): Builder + fun build(): CreateDataViewRelationSubComponent + } + + fun inject(fragment: CreateDataViewRelationFragment) + +} + +@Module +object CreateDataViewRelationModule { + + @JvmStatic + @Provides + @PerScreen + fun provideCreateDVRelationViewModelFactory() = CreateDataViewRelationViewModelFactory() +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/CreateDataViewViewerDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/CreateDataViewViewerDI.kt new file mode 100644 index 0000000000..c446c447c3 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/CreateDataViewViewerDI.kt @@ -0,0 +1,46 @@ +package com.anytypeio.anytype.di.feature; + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewViewer +import com.anytypeio.anytype.presentation.sets.CreateDataViewViewerViewModel +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.CreateDataViewViewerFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [CreateDataViewViewerModule::class]) +@PerModal +interface CreateDataViewViewerSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: CreateDataViewViewerModule): Builder + fun build(): CreateDataViewViewerSubComponent + } + + fun inject(fragment: CreateDataViewViewerFragment) +} + +@Module +object CreateDataViewViewerModule { + + @JvmStatic + @Provides + @PerModal + fun provideCreateDataViewViewerViewModelFactory( + dispatcher: Dispatcher, + addDataViewViewer: AddDataViewViewer + ): CreateDataViewViewerViewModel.Factory = CreateDataViewViewerViewModel.Factory( + dispatcher = dispatcher, + addDataViewViewer = addDataViewViewer + ) + + @JvmStatic + @Provides + @PerModal + fun provideAddDataViewViewerUseCase( + repo: BlockRepository + ): AddDataViewViewer = AddDataViewViewer(repo = repo) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/CreateObjectSetDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/CreateObjectSetDI.kt new file mode 100644 index 0000000000..ed3305ff17 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/CreateObjectSetDI.kt @@ -0,0 +1,70 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.core_utils.di.scope.PerScreen +import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet +import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectType +import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.icon.DocumentEmojiIconProvider +import com.anytypeio.anytype.presentation.sets.CreateObjectSetViewModel +import com.anytypeio.anytype.ui.sets.CreateObjectSetFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [CreateSetModule::class]) +@PerScreen +interface CreateSetSubComponent { + + @Subcomponent.Builder + interface Builder { + fun module(module: CreateSetModule): Builder + fun build(): CreateSetSubComponent + } + + fun inject(fragment: CreateObjectSetFragment) +} + +@Module +object CreateSetModule { + + @JvmStatic + @Provides + @PerScreen + fun provideCreateSetViewModelFactory( + getObjectTypes: GetObjectTypes, + createObjectSet: CreateObjectSet, + createObjectType: CreateObjectType + ): CreateObjectSetViewModel.Factory { + return CreateObjectSetViewModel.Factory( + getObjectTypes = getObjectTypes, + createObjectSet = createObjectSet, + createObjectType = createObjectType + ) + } + + @JvmStatic + @Provides + @PerScreen + fun provideCreateObjectTypeUseCase( + repo: BlockRepository, + documentEmojiProvider: DocumentEmojiIconProvider + ): CreateObjectType = CreateObjectType( + repo = repo, + documentEmojiProvider = documentEmojiProvider + ) + + @JvmStatic + @Provides + @PerScreen + fun provideGetObjectTypesUseCase( + repo: BlockRepository + ): GetObjectTypes = GetObjectTypes(repo = repo) + + @JvmStatic + @Provides + @PerScreen + fun provideCreateObjectSetUseCase( + repo: BlockRepository + ): CreateObjectSet = CreateObjectSet(repo = repo) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/CreateObjectTypeDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/CreateObjectTypeDI.kt new file mode 100644 index 0000000000..d442ef8e6b --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/CreateObjectTypeDI.kt @@ -0,0 +1,33 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.presentation.sets.CreateObjectTypeViewModel +import com.anytypeio.anytype.core_utils.di.scope.PerScreen +import com.anytypeio.anytype.ui.sets.CreateObjectTypeFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [CreateObjectTypeModule::class]) +@PerScreen +interface CreateObjectTypeSubComponent { + + @Subcomponent.Builder + interface Builder { + fun module(module: CreateObjectTypeModule): Builder + fun build(): CreateObjectTypeSubComponent + } + + fun inject(fragment: CreateObjectTypeFragment) +} + +@Module +object CreateObjectTypeModule { + + @JvmStatic + @Provides + @PerScreen + fun provideCreateObjectTypeViewModelFactory( + ): CreateObjectTypeViewModel.Factory { + return CreateObjectTypeViewModel.Factory() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/DataViewViewerActionDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/DataViewViewerActionDI.kt new file mode 100644 index 0000000000..993e71253f --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/DataViewViewerActionDI.kt @@ -0,0 +1,68 @@ +package com.anytypeio.anytype.di.feature; + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.DeleteDataViewViewer +import com.anytypeio.anytype.domain.dataview.interactor.DuplicateDataViewViewer +import com.anytypeio.anytype.domain.dataview.interactor.RenameDataViewViewer +import com.anytypeio.anytype.presentation.sets.DataViewViewerActionViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.DataViewViewerActionFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow + +@Subcomponent(modules = [DataViewViewerActionModule::class]) +@PerModal +interface DataViewViewerActionSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: DataViewViewerActionModule): Builder + fun build(): DataViewViewerActionSubComponent + } + + fun inject(fragment: DataViewViewerActionFragment) +} + +@Module +object DataViewViewerActionModule { + + @JvmStatic + @Provides + @PerModal + fun provideEditDataViewViewerViewModelFactory( + duplicateDataViewViewer: DuplicateDataViewViewer, + deleteDataViewViewer: DeleteDataViewViewer, + dispatcher: Dispatcher, + objectSetState: StateFlow + ): DataViewViewerActionViewModel.Factory = DataViewViewerActionViewModel.Factory( + duplicateDataViewViewer = duplicateDataViewViewer, + deleteDataViewViewer = deleteDataViewViewer, + dispatcher = dispatcher, + objectSetState = objectSetState + ) + + @JvmStatic + @Provides + @PerModal + fun provideRenameDataViewViewerUseCase( + repo: BlockRepository + ): RenameDataViewViewer = RenameDataViewViewer(repo = repo) + + @JvmStatic + @Provides + @PerModal + fun provideDuplicateDataViewViewerUseCase( + repo: BlockRepository + ): DuplicateDataViewViewer = DuplicateDataViewViewer(repo = repo) + + @JvmStatic + @Provides + @PerModal + fun provideDeleteDataViewViewerUseCase( + repo: BlockRepository + ): DeleteDataViewViewer = DeleteDataViewViewer(repo = repo) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/DebugSettingsDi.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/DebugSettingsDi.kt index 876227f61e..e3727f3fd9 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/DebugSettingsDi.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/DebugSettingsDi.kt @@ -1,9 +1,11 @@ package com.anytypeio.anytype.di.feature import com.anytypeio.anytype.core_utils.di.scope.PerScreen +import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.config.GetDebugSettings import com.anytypeio.anytype.domain.config.InfrastructureRepository import com.anytypeio.anytype.domain.config.UseCustomContextMenu +import com.anytypeio.anytype.domain.dataview.interactor.DebugSync import com.anytypeio.anytype.ui.settings.DebugSettingsFragment import dagger.Module import dagger.Provides @@ -37,4 +39,8 @@ class DebugSettingsModule { ): GetDebugSettings = GetDebugSettings( repo = repo ) + + @Provides + @PerScreen + fun provideDebugSync(repo: BlockRepository) : DebugSync = DebugSync(repo = repo) } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/DocumentAddNewBlockDi.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/DocumentAddNewBlockDi.kt new file mode 100644 index 0000000000..4b649056ed --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/DocumentAddNewBlockDi.kt @@ -0,0 +1,40 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.presentation.page.picker.DocumentAddBlockViewModelFactory +import com.anytypeio.anytype.ui.page.modals.AddBlockFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [DocumentAddNewBlockModule::class]) +@PerModal +interface DocumentAddNewBlockSubComponent{ + + @Subcomponent.Builder + interface Builder { + fun documentAddNewBlockModule(module: DocumentAddNewBlockModule): Builder + fun build(): DocumentAddNewBlockSubComponent + } + + fun inject(fragment: AddBlockFragment) +} + +@Module +object DocumentAddNewBlockModule { + + @JvmStatic + @Provides + @PerModal + fun provideGetObjectTypesUseCase( + repo: BlockRepository + ): GetObjectTypes = GetObjectTypes(repo = repo) + + @JvmStatic + @Provides + @PerModal + fun provideFactory(getObjectTypes: GetObjectTypes): DocumentAddBlockViewModelFactory = + DocumentAddBlockViewModelFactory(getObjectTypes) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/DocumentEmojiIconPickerDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/DocumentEmojiIconPickerDI.kt index cc678c49e8..2387a9de93 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/DocumentEmojiIconPickerDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/DocumentEmojiIconPickerDI.kt @@ -1,20 +1,21 @@ package com.anytypeio.anytype.di.feature -import com.anytypeio.anytype.core_utils.di.scope.PerScreen +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal import com.anytypeio.anytype.domain.block.repo.BlockRepository -import com.anytypeio.anytype.domain.event.model.Payload import com.anytypeio.anytype.domain.icon.SetDocumentEmojiIcon import com.anytypeio.anytype.emojifier.data.Emoji import com.anytypeio.anytype.emojifier.suggest.EmojiSuggester +import com.anytypeio.anytype.presentation.page.editor.DetailModificationManager import com.anytypeio.anytype.presentation.page.picker.DocumentEmojiIconPickerViewModelFactory -import com.anytypeio.anytype.presentation.util.Bridge +import com.anytypeio.anytype.presentation.util.Dispatcher import com.anytypeio.anytype.ui.page.modals.DocumentEmojiIconPickerFragment import dagger.Module import dagger.Provides import dagger.Subcomponent @Subcomponent(modules = [DocumentEmojiIconPickerModule::class]) -@PerScreen +@PerModal interface DocumentEmojiIconPickerSubComponent { @Subcomponent.Builder @@ -30,20 +31,22 @@ interface DocumentEmojiIconPickerSubComponent { class DocumentEmojiIconPickerModule { @Provides - @PerScreen + @PerModal fun provideDocumentEmojiIconPickerViewModel( setEmojiIcon: SetDocumentEmojiIcon, emojiSuggester: EmojiSuggester, - bridge: Bridge + dispatcher: Dispatcher, + details: DetailModificationManager ): DocumentEmojiIconPickerViewModelFactory = DocumentEmojiIconPickerViewModelFactory( setEmojiIcon = setEmojiIcon, emojiSuggester = emojiSuggester, emojiProvider = Emoji, - bridge = bridge + dispatcher = dispatcher, + details = details ) @Provides - @PerScreen + @PerModal fun provideSetDocumentEmojiIconUseCase( repo: BlockRepository ): SetDocumentEmojiIcon = SetDocumentEmojiIcon( diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/DocumentIconActionMenuDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/DocumentIconActionMenuDI.kt index 5224aee006..3ee70fa289 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/DocumentIconActionMenuDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/DocumentIconActionMenuDI.kt @@ -1,12 +1,13 @@ package com.anytypeio.anytype.di.feature -import com.anytypeio.anytype.core_utils.di.scope.PerScreen +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal import com.anytypeio.anytype.domain.block.repo.BlockRepository -import com.anytypeio.anytype.domain.event.model.Payload import com.anytypeio.anytype.domain.icon.SetDocumentEmojiIcon import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon +import com.anytypeio.anytype.presentation.page.editor.DetailModificationManager import com.anytypeio.anytype.presentation.page.picker.DocumentIconActionMenuViewModelFactory -import com.anytypeio.anytype.presentation.util.Bridge +import com.anytypeio.anytype.presentation.util.Dispatcher import com.anytypeio.anytype.ui.page.modals.actions.DocumentIconActionMenuFragment import com.anytypeio.anytype.ui.page.modals.actions.ProfileIconActionMenuFragment import dagger.Module @@ -14,7 +15,7 @@ import dagger.Provides import dagger.Subcomponent @Subcomponent(modules = [DocumentIconActionMenuModule::class]) -@PerScreen +@PerModal interface DocumentActionMenuSubComponent { @Subcomponent.Builder @@ -31,19 +32,21 @@ interface DocumentActionMenuSubComponent { class DocumentIconActionMenuModule { @Provides - @PerScreen + @PerModal fun provideDocumentIconActionMenuViewModelFactory( setEmojiIcon: SetDocumentEmojiIcon, setImageIcon: SetDocumentImageIcon, - bridge: Bridge + dispatcher: Dispatcher, + details: DetailModificationManager ): DocumentIconActionMenuViewModelFactory = DocumentIconActionMenuViewModelFactory( setEmojiIcon = setEmojiIcon, setImageIcon = setImageIcon, - bridge = bridge + dispatcher = dispatcher, + details = details ) @Provides - @PerScreen + @PerModal fun provideSetDocumentEmojiIconUseCase( repo: BlockRepository ): SetDocumentEmojiIcon = SetDocumentEmojiIcon( @@ -51,7 +54,7 @@ class DocumentIconActionMenuModule { ) @Provides - @PerScreen + @PerModal fun provideSetDocumentImageIconUseCase( repo: BlockRepository ): SetDocumentImageIcon = SetDocumentImageIcon( diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/EditDataViewViewer.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/EditDataViewViewer.kt new file mode 100644 index 0000000000..bee6e6533e --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/EditDataViewViewer.kt @@ -0,0 +1,50 @@ +package com.anytypeio.anytype.di.feature; + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.RenameDataViewViewer +import com.anytypeio.anytype.presentation.sets.EditDataViewViewerViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.EditDataViewViewerFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow + +@Subcomponent(modules = [EditDataViewViewerModule::class]) +@PerModal +interface EditDataViewViewerSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: EditDataViewViewerModule): Builder + fun build(): EditDataViewViewerSubComponent + } + + fun inject(fragment: EditDataViewViewerFragment) +} + +@Module +object EditDataViewViewerModule { + + @JvmStatic + @Provides + @PerModal + fun provideEditDataViewViewerViewModelFactory( + renameDataViewViewer: RenameDataViewViewer, + dispatcher: Dispatcher, + objectSetState: StateFlow + ): EditDataViewViewerViewModel.Factory = EditDataViewViewerViewModel.Factory( + renameDataViewViewer = renameDataViewViewer, + dispatcher = dispatcher, + objectSetState = objectSetState + ) + + @JvmStatic + @Provides + @PerModal + fun provideRenameDataViewViewerUseCase( + repo: BlockRepository + ): RenameDataViewViewer = RenameDataViewViewer(repo = repo) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/EditGridCellDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/EditGridCellDI.kt new file mode 100644 index 0000000000..0fb4226e9a --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/EditGridCellDI.kt @@ -0,0 +1,61 @@ +package com.anytypeio.anytype.di.feature; + +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider +import com.anytypeio.anytype.presentation.sets.ObjectRelationDateValueViewModel +import com.anytypeio.anytype.presentation.sets.ObjectRelationTextValueViewModel +import com.anytypeio.anytype.ui.relations.ObjectRelationDateValueFragment +import com.anytypeio.anytype.ui.relations.ObjectRelationTextValueFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [EditGridCellModule::class]) +@PerModal +interface EditGridCellSubComponent { + + @Subcomponent.Builder + interface Builder { + fun module(module: EditGridCellModule): Builder + fun build(): EditGridCellSubComponent + } + + fun inject(fragment: ObjectRelationTextValueFragment) +} + +@Module +object EditGridCellModule { + @JvmStatic + @Provides + @PerModal + fun provideEditGridCellViewModelFactory( + relations: ObjectRelationProvider, + values: ObjectValueProvider + ) = ObjectRelationTextValueViewModel.Factory(relations, values) +} + +@Subcomponent(modules = [EditGridCellDateModule::class]) +@PerModal +interface EditGridCellDateSubComponent { + + @Subcomponent.Builder + interface Builder { + fun module(module: EditGridCellDateModule): Builder + fun build(): EditGridCellDateSubComponent + } + + fun inject(fragment: ObjectRelationDateValueFragment) +} + +@Module +object EditGridCellDateModule { + + @JvmStatic + @Provides + @PerModal + fun provideEditGridCellViewModelFactory( + relations: ObjectRelationProvider, + values: ObjectValueProvider + ) = ObjectRelationDateValueViewModel.Factory(relations, values) +} diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ManageViewerDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ManageViewerDI.kt new file mode 100644 index 0000000000..3ed8d3a978 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ManageViewerDI.kt @@ -0,0 +1,44 @@ +package com.anytypeio.anytype.di.feature; + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.dataview.interactor.SetActiveViewer +import com.anytypeio.anytype.presentation.sets.ManageViewerViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.ManageViewerFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow + +@Subcomponent(modules = [ManageViewerModule::class]) +@PerModal +interface ManageViewerSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: ManageViewerModule): Builder + fun build(): ManageViewerSubComponent + } + + fun inject(fragment: ManageViewerFragment) +} + +@Module +object ManageViewerModule { + @JvmStatic + @Provides + @PerModal + fun provideManageViewerViewModelFactory( + state: StateFlow, + session: ObjectSetSession, + dispatcher: Dispatcher, + setActiveViewer: SetActiveViewer + ): ManageViewerViewModel.Factory = ManageViewerViewModel.Factory( + state = state, + session = session, + dispatcher = dispatcher, + setActiveViewer = setActiveViewer + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ModifyViewerSort.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ModifyViewerSort.kt new file mode 100644 index 0000000000..8d868a95cb --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ModifyViewerSort.kt @@ -0,0 +1,44 @@ +package com.anytypeio.anytype.di.feature; + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.sort.ModifyViewerSortViewModel +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.sort.ModifyViewerSortFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow + +@Subcomponent(modules = [ModifyViewerSortModule::class]) +@PerModal +interface ModifyViewerSortSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: ModifyViewerSortModule): Builder + fun build(): ModifyViewerSortSubComponent + } + + fun inject(fragment: ModifyViewerSortFragment) +} + +@Module +object ModifyViewerSortModule { + @JvmStatic + @Provides + @PerModal + fun provideViewModelFactory( + state: StateFlow, + session: ObjectSetSession, + dispatcher: Dispatcher, + updateDataViewViewer: UpdateDataViewViewer + ): ModifyViewerSortViewModel.Factory = ModifyViewerSortViewModel.Factory( + state = state, + session = session, + dispatcher = dispatcher, + updateDataViewViewer = updateDataViewViewer + ) +} diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationDI.kt new file mode 100644 index 0000000000..b2578f9b5b --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationDI.kt @@ -0,0 +1,60 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.ObjectRelationList +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.presentation.page.Editor +import com.anytypeio.anytype.presentation.page.editor.DetailModificationManager +import com.anytypeio.anytype.presentation.relations.ObjectRelationListViewModelFactory +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.relations.ObjectRelationListFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [DocumentRelationModule::class]) +@PerModal +interface DocumentRelationSubComponent { + + @Subcomponent.Builder + interface Builder { + fun module(module: DocumentRelationModule): Builder + fun build(): DocumentRelationSubComponent + } + + fun inject(fragment: ObjectRelationListFragment) +} + +@Module +object DocumentRelationModule { + @JvmStatic + @Provides + @PerModal + fun provideObjectRelationViewModelFactory( + stores: Editor.Storage, + urlBuilder: UrlBuilder, + objectRelationList: ObjectRelationList, + dispatcher: Dispatcher, + updateDetail: UpdateDetail, + detailModificationManager: DetailModificationManager + ): ObjectRelationListViewModelFactory { + return ObjectRelationListViewModelFactory( + stores = stores, + urlBuilder = urlBuilder, + objectRelationList = objectRelationList, + dispatcher = dispatcher, + updateDetail = updateDetail, + detailModificationManager = detailModificationManager + ) + } + + @JvmStatic + @Provides + @PerModal + fun provideObjectRelationListUseCase( + repository: BlockRepository + ) : ObjectRelationList = ObjectRelationList(repository) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationValueDi.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationValueDi.kt new file mode 100644 index 0000000000..6530232a1c --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationValueDi.kt @@ -0,0 +1,130 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewRelationOption +import com.anytypeio.anytype.domain.dataview.interactor.AddTagToDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.RemoveTagFromDataViewRecord +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewRecord +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.presentation.relations.providers.ObjectDetailProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectTypeProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider +import com.anytypeio.anytype.presentation.sets.ObjectObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSetObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.database.modals.ObjectObjectRelationValueFragment +import com.anytypeio.anytype.ui.database.modals.ObjectSetObjectRelationValueFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [ObjectRelationValueModule::class, ObjectSetObjectRelationValueModule::class]) +@PerModal +interface ObjectSetObjectRelationValueSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: ObjectRelationValueModule): Builder + fun build(): ObjectSetObjectRelationValueSubComponent + } + + fun inject(fragment: ObjectSetObjectRelationValueFragment) + + fun addObjectRelationValueComponent(): AddObjectRelationValueSubComponent.Builder + fun addObjectRelationObjectValueComponent(): AddObjectRelationObjectValueSubComponent.Builder + fun addRelationFileValueAddComponent() : RelationFileValueAddSubComponent.Builder +} + +@Subcomponent(modules = [ObjectRelationValueModule::class, ObjectObjectRelationValueModule::class]) +@PerModal +interface ObjectObjectRelationValueSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: ObjectRelationValueModule): Builder + fun build(): ObjectObjectRelationValueSubComponent + } + + fun inject(fragment: ObjectObjectRelationValueFragment) + + fun addObjectRelationValueComponent(): AddObjectRelationValueSubComponent.Builder + fun addObjectRelationObjectValueComponent(): AddObjectRelationObjectValueSubComponent.Builder + fun addRelationFileValueAddComponent() : RelationFileValueAddSubComponent.Builder +} + +@Module +object ObjectRelationValueModule { + + @JvmStatic + @Provides + @PerModal + fun provideAddRelationOptionUseCase( + repo: BlockRepository + ): AddDataViewRelationOption = AddDataViewRelationOption(repo = repo) + + @JvmStatic + @Provides + @PerModal + fun provideAddTagToDataViewRecordUseCase( + repo: BlockRepository + ): AddTagToDataViewRecord = AddTagToDataViewRecord(repo = repo) + + @JvmStatic + @Provides + @PerModal + fun provideRemoveTagFromDataViewRecordUseCase( + repo: BlockRepository + ): RemoveTagFromDataViewRecord = RemoveTagFromDataViewRecord(repo = repo) +} + +@Module +object ObjectSetObjectRelationValueModule { + @JvmStatic + @Provides + @PerModal + fun provideViewModelFactoryForDataView( + relations: ObjectRelationProvider, + values: ObjectValueProvider, + details: ObjectDetailProvider, + types: ObjectTypeProvider, + removeTagFromDataViewRecord: RemoveTagFromDataViewRecord, + urlBuilder: UrlBuilder, + dispatcher: Dispatcher, + updateDataViewRecord: UpdateDataViewRecord + ): ObjectSetObjectRelationValueViewModel.Factory = ObjectSetObjectRelationValueViewModel.Factory( + relations = relations, + values = values, + details = details, + types = types, + removeTagFromRecord = removeTagFromDataViewRecord, + urlBuilder = urlBuilder, + dispatcher = dispatcher, + updateDataViewRecord = updateDataViewRecord + ) +} + +@Module +object ObjectObjectRelationValueModule { + @JvmStatic + @Provides + @PerModal + fun provideViewModelFactoryForObject( + relations: ObjectRelationProvider, + values: ObjectValueProvider, + details: ObjectDetailProvider, + types: ObjectTypeProvider, + urlBuilder: UrlBuilder, + dispatcher: Dispatcher, + updateDetail: UpdateDetail, + ): ObjectObjectRelationValueViewModel.Factory = ObjectObjectRelationValueViewModel.Factory( + relations = relations, + values = values, + details = details, + types = types, + urlBuilder = urlBuilder, + dispatcher = dispatcher, + updateDetail = updateDetail + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetDI.kt new file mode 100644 index 0000000000..ab85c1bf22 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetDI.kt @@ -0,0 +1,230 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerScreen +import com.anytypeio.anytype.di.feature.sets.CreateFilterSubComponent +import com.anytypeio.anytype.di.feature.sets.ModifyFilterSubComponent +import com.anytypeio.anytype.di.feature.sets.SelectFilterRelationSubComponent +import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.block.interactor.UpdateText +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.* +import com.anytypeio.anytype.domain.event.interactor.EventChannel +import com.anytypeio.anytype.domain.event.interactor.InterceptEvents +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.page.CloseBlock +import com.anytypeio.anytype.domain.sets.OpenObjectSet +import com.anytypeio.anytype.presentation.relations.providers.* +import com.anytypeio.anytype.presentation.sets.* +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.ObjectSetFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.StateFlow + +@Subcomponent(modules = [ObjectSetModule::class]) +@PerScreen +interface ObjectSetSubComponent { + + @Subcomponent.Builder + interface Builder { + fun module(module: ObjectSetModule): Builder + fun build(): ObjectSetSubComponent + } + + fun inject(fragment: ObjectSetFragment) + + fun objectSetRecordComponent(): ObjectSetRecordSubComponent.Builder + fun viewerCustomizeSubComponent(): ViewerCustomizeSubComponent.Builder + fun viewerSortBySubComponent(): ViewerSortBySubComponent.Builder + fun viewerFilterBySubComponent(): ViewerFilterSubComponent.Builder + fun createDataViewViewerSubComponent(): CreateDataViewViewerSubComponent.Builder + fun editDataViewViewerComponent(): EditDataViewViewerSubComponent.Builder + fun objectRelationValueComponent(): ObjectSetObjectRelationValueSubComponent.Builder + fun manageViewerComponent(): ManageViewerSubComponent.Builder + fun viewerRelationsComponent(): ViewerRelationsSubComponent.Builder + fun dataviewViewerActionComponent(): DataViewViewerActionSubComponent.Builder + fun selectSortRelationComponent(): SelectSortRelationSubComponent.Builder + fun selectFilterRelationComponent(): SelectFilterRelationSubComponent.Builder + fun createFilterComponent(): CreateFilterSubComponent.Builder + fun modifyFilterComponent(): ModifyFilterSubComponent.Builder + fun viewerSortComponent(): ViewerSortSubComponent.Builder + fun modifyViewerSortComponent(): ModifyViewerSortSubComponent.Builder + fun editCellsComponent(): EditGridCellSubComponent.Builder + fun editCellDateComponent(): EditGridCellDateSubComponent.Builder +} + +@Module +object ObjectSetModule { + + @JvmStatic + @Provides + @PerScreen + fun provideObjectSetViewModelFactory( + openObjectSet: OpenObjectSet, + closeBlock: CloseBlock, + setActiveViewer: SetActiveViewer, + addDataViewRelation: AddDataViewRelation, + updateDataViewViewer: UpdateDataViewViewer, + updateDataViewRecord: UpdateDataViewRecord, + updateText: UpdateText, + interceptEvents: InterceptEvents, + createDataViewRecord: CreateDataViewRecord, + reducer: ObjectSetReducer, + dispatcher: Dispatcher, + objectSetRecordCache: ObjectSetRecordCache, + urlBuilder: UrlBuilder, + session: ObjectSetSession + ): ObjectSetViewModelFactory = ObjectSetViewModelFactory( + openObjectSet = openObjectSet, + closeBlock = closeBlock, + setActiveViewer = setActiveViewer, + addDataViewRelation = addDataViewRelation, + updateDataViewViewer = updateDataViewViewer, + updateDataViewRecord = updateDataViewRecord, + createDataViewRecord = createDataViewRecord, + updateText = updateText, + interceptEvents = interceptEvents, + reducer = reducer, + dispatcher = dispatcher, + objectSetRecordCache = objectSetRecordCache, + urlBuilder = urlBuilder, + session = session + ) + + @JvmStatic + @Provides + @PerScreen + fun provideOpenObjectSetUseCase(repo: BlockRepository): OpenObjectSet = OpenObjectSet(repo) + + @JvmStatic + @Provides + @PerScreen + fun provideSetActiveViewerUseCase( + repo: BlockRepository + ): SetActiveViewer = SetActiveViewer(repo) + + @JvmStatic + @Provides + @PerScreen + fun provideAddDataViewRelationUseCase( + repo: BlockRepository + ): AddDataViewRelation = AddDataViewRelation(repo = repo) + + @JvmStatic + @Provides + @PerScreen + fun provideUpdateDataViewViewerUseCase( + repo: BlockRepository + ): UpdateDataViewViewer = UpdateDataViewViewer(repo = repo) + + @JvmStatic + @Provides + @PerScreen + fun provideCreateDataViewRecordUseCase( + repo: BlockRepository + ): CreateDataViewRecord = CreateDataViewRecord(repo = repo) + + @JvmStatic + @Provides + @PerScreen + fun provideUpdateDataViewRecordUseCase( + repo: BlockRepository + ): UpdateDataViewRecord = UpdateDataViewRecord(repo = repo) + + @JvmStatic + @Provides + @PerScreen + fun provideUpdateTextUseCase( + repo: BlockRepository + ): UpdateText = UpdateText(repo = repo) + + @JvmStatic + @Provides + @PerScreen + fun provideInterceptEventsUseCase( + channel: EventChannel + ): InterceptEvents = InterceptEvents( + channel = channel, + context = Dispatchers.IO + ) + + @JvmStatic + @Provides + @PerScreen + fun provideCloseBlockUseCase( + repo: BlockRepository + ): CloseBlock = CloseBlock(repo) + + @JvmStatic + @Provides + @PerScreen + fun provideObjectSetReducer(): ObjectSetReducer = ObjectSetReducer() + + @JvmStatic + @Provides + @PerScreen + fun provideState( + reducer: ObjectSetReducer + ): StateFlow = reducer.state + + @JvmStatic + @Provides + @PerScreen + fun provideObjectSetSession(): ObjectSetSession = ObjectSetSession() + + @JvmStatic + @Provides + @PerScreen + fun provideDispatcher(): Dispatcher = Dispatcher.Default() + + @JvmStatic + @Provides + @PerScreen + fun provideObjectSetRecordCache(): ObjectSetRecordCache = ObjectSetRecordCache() + + @JvmStatic + @Provides + @PerScreen + fun provideDataViewObjectRelationProvider( + state: StateFlow + ) : ObjectRelationProvider = DataViewObjectRelationProvider(state) + + @JvmStatic + @Provides + @PerScreen + fun provideDataViewObjectValueProvider( + state: StateFlow, + session: ObjectSetSession + ) : ObjectValueProvider = DataViewObjectValueProvider(state, session) + + @JvmStatic + @Provides + @PerScreen + fun provideObjectTypeProvider( + state: StateFlow, + ) : ObjectTypeProvider = object : ObjectTypeProvider { + override fun provide(): List = state.value.objectTypes + } + + @JvmStatic + @Provides + @PerScreen + fun provideObjectDetailProvider( + state: StateFlow, + ) : ObjectDetailProvider = object : ObjectDetailProvider { + override fun provide(): Map = state.value.details + } + + @JvmStatic + @Provides + @PerScreen + fun provideUpdateDetailUseCase( + repository: BlockRepository + ) : UpdateDetail = UpdateDetail(repository) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetRecordDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetRecordDI.kt new file mode 100644 index 0000000000..265901a39d --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectSetRecordDI.kt @@ -0,0 +1,45 @@ +package com.anytypeio.anytype.di.feature; + +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewRecord +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetRecordCache +import com.anytypeio.anytype.presentation.sets.ObjectSetRecordViewModel +import com.anytypeio.anytype.ui.sets.SetObjectSetRecordNameFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Scope + +@Subcomponent(modules = [ObjectSetRecordModule::class]) +@ObjectSetRecordScope +interface ObjectSetRecordSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: ObjectSetRecordModule): Builder + fun build(): ObjectSetRecordSubComponent + } + + fun inject(fragment: SetObjectSetRecordNameFragment) +} + +@Module +object ObjectSetRecordModule { + + @JvmStatic + @Provides + @ObjectSetRecordScope + fun provideObjectSetRecordViewModelFactory( + updateDataViewRecord: UpdateDataViewRecord, + objectSetState: StateFlow, + objectSetRecordCache: ObjectSetRecordCache + ): ObjectSetRecordViewModel.Factory = ObjectSetRecordViewModel.Factory( + objectSetState = objectSetState, + objectSetRecordCache = objectSetRecordCache, + updateDataViewRecord = updateDataViewRecord + ) +} + +@Scope +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class ObjectSetRecordScope \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/PageDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/PageDI.kt index 8d95618bd6..c27ca868e7 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/PageDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/PageDI.kt @@ -1,8 +1,13 @@ package com.anytypeio.anytype.di.feature import com.anytypeio.anytype.analytics.base.Analytics +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.core_utils.di.scope.PerScreen import com.anytypeio.anytype.core_utils.tools.Counter +import com.anytypeio.anytype.domain.`object`.UpdateDetail import com.anytypeio.anytype.domain.block.UpdateDivider import com.anytypeio.anytype.domain.block.interactor.* import com.anytypeio.anytype.domain.block.repo.BlockRepository @@ -11,11 +16,11 @@ import com.anytypeio.anytype.domain.clipboard.Copy import com.anytypeio.anytype.domain.clipboard.Paste import com.anytypeio.anytype.domain.cover.RemoveDocCover import com.anytypeio.anytype.domain.cover.SetDocCoverImage +import com.anytypeio.anytype.domain.dataview.interactor.SetRelationKey import com.anytypeio.anytype.domain.download.DownloadFile import com.anytypeio.anytype.domain.download.Downloader import com.anytypeio.anytype.domain.event.interactor.EventChannel import com.anytypeio.anytype.domain.event.interactor.InterceptEvents -import com.anytypeio.anytype.domain.event.model.Payload import com.anytypeio.anytype.domain.icon.DocumentEmojiIconProvider import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.page.* @@ -27,13 +32,16 @@ import com.anytypeio.anytype.presentation.page.DocumentExternalEventReducer import com.anytypeio.anytype.presentation.page.Editor import com.anytypeio.anytype.presentation.page.PageViewModelFactory import com.anytypeio.anytype.presentation.page.cover.CoverImageHashProvider +import com.anytypeio.anytype.presentation.page.editor.DetailModificationManager import com.anytypeio.anytype.presentation.page.editor.Interactor +import com.anytypeio.anytype.presentation.page.editor.InternalDetailModificationManager import com.anytypeio.anytype.presentation.page.editor.Orchestrator import com.anytypeio.anytype.presentation.page.editor.pattern.DefaultPatternMatcher import com.anytypeio.anytype.presentation.page.render.DefaultBlockViewRenderer import com.anytypeio.anytype.presentation.page.selection.SelectionStateHolder import com.anytypeio.anytype.presentation.page.toggle.ToggleStateHolder -import com.anytypeio.anytype.presentation.util.Bridge +import com.anytypeio.anytype.presentation.relations.providers.* +import com.anytypeio.anytype.presentation.util.Dispatcher import com.anytypeio.anytype.providers.DefaultCoverImageHashProvider import com.anytypeio.anytype.ui.page.PageFragment import dagger.Module @@ -54,16 +62,26 @@ interface PageSubComponent { fun inject(fragment: PageFragment) + fun documentEmojiIconPickerComponentBuilder(): DocumentEmojiIconPickerSubComponent.Builder + fun documentActionMenuComponentBuilder(): DocumentActionMenuSubComponent.Builder + + fun documentRelationSubComponent(): DocumentRelationSubComponent.Builder + fun editRelationCellComponent(): EditGridCellSubComponent.Builder + fun editDocRelationComponent() : ObjectObjectRelationValueSubComponent.Builder + fun editRelationDateComponent(): EditGridCellDateSubComponent.Builder + fun docCoverGalleryComponentBuilder(): SelectDocCoverSubComponent.Builder fun uploadDocCoverImageComponentBuilder(): UploadDocCoverImageSubComponent.Builder + + fun documentAddNewBlockComponentBuilder(): DocumentAddNewBlockSubComponent.Builder } /** - * Sesssion-related dependencies, session being defined as active work with a document visible to our user. + * Session-related dependencies, session being defined as active work with a document visible to our user. * Hence, these dependencies are stateful and therefore should not be shared between different sessions of the same document. * Consider the following navigation scenario: Document A > Document B > Document A'. - * In this case, statetul dependencies should not be shared between A and A'. + * In this case, stateful dependencies should not be shared between A and A'. */ @Module object EditorSessionModule { @@ -82,19 +100,21 @@ object EditorSessionModule { @JvmStatic @Provides + @PerScreen fun provideStorage(): Editor.Storage = Editor.Storage() @JvmStatic @Provides fun providePageViewModelFactory( openPage: OpenPage, - closePage: ClosePage, + closePage: CloseBlock, interceptEvents: InterceptEvents, interceptThreadStatus: InterceptThreadStatus, updateLinkMarks: UpdateLinkMarks, removeLinkMark: RemoveLinkMark, createPage: CreatePage, createDocument: CreateDocument, + createObject: CreateObject, createNewDocument: CreateNewDocument, documentExternalEventReducer: DocumentExternalEventReducer, setDocCoverImage: SetDocCoverImage, @@ -105,12 +125,15 @@ object EditorSessionModule { orchestrator: Orchestrator, getListPages: GetListPages, analytics: Analytics, - bridge: Bridge + dispatcher: Dispatcher, + detailModificationManager: DetailModificationManager, + updateDetail: UpdateDetail ): PageViewModelFactory = PageViewModelFactory( openPage = openPage, closePage = closePage, createPage = createPage, createDocument = createDocument, + createObject = createObject, createNewDocument = createNewDocument, interceptEvents = interceptEvents, interceptThreadStatus = interceptThreadStatus, @@ -125,7 +148,9 @@ object EditorSessionModule { orchestrator = orchestrator, getListPages = getListPages, analytics = analytics, - bridge = bridge + dispatcher = dispatcher, + detailModificationManager = detailModificationManager, + updateDetail = updateDetail ) @JvmStatic @@ -198,6 +223,7 @@ object EditorSessionModule { paste: Paste, undo: Undo, redo: Redo, + setRelationKey: SetRelationKey, analytics: Analytics ): Orchestrator = Orchestrator( stores = storage, @@ -231,6 +257,7 @@ object EditorSessionModule { move = move, paste = paste, copy = copy, + setRelationKey = setRelationKey, analytics = analytics, updateFields = updateFields, turnIntoStyle = turnInto @@ -265,7 +292,7 @@ object EditorUseCaseModule { @PerScreen fun provideClosePageUseCase( repo: BlockRepository - ): ClosePage = ClosePage( + ): CloseBlock = CloseBlock( repo = repo ) @@ -361,6 +388,15 @@ object EditorUseCaseModule { repo = repo ) + @JvmStatic + @Provides + @PerScreen + fun provideSetRelationKeyUseCase( + repo: BlockRepository + ): SetRelationKey = SetRelationKey( + repo = repo + ) + @JvmStatic @Provides @PerScreen @@ -457,6 +493,17 @@ object EditorUseCaseModule { documentEmojiProvider = documentEmojiIconProvider ) + @JvmStatic + @Provides + @PerScreen + fun provideCreateObjectUseCase( + repo: BlockRepository, + documentEmojiIconProvider: DocumentEmojiIconProvider + ): CreateObject = CreateObject( + repo = repo, + documentEmojiProvider = documentEmojiIconProvider + ) + @JvmStatic @Provides @PerScreen @@ -562,6 +609,52 @@ object EditorUseCaseModule { repo: BlockRepository ): UpdateFields = UpdateFields(repo) + @JvmStatic + @Provides + @PerScreen + fun provideDefaultObjectRelationProvider( + storage: Editor.Storage + ) : ObjectRelationProvider = DefaultObjectRelationProvider(storage.relations) + + @JvmStatic + @Provides + @PerScreen + fun provideDefaultObjectValueProvider( + storage: Editor.Storage + ) : ObjectValueProvider = DefaultObjectValueProvider(storage.details) + + @JvmStatic + @Provides + @PerScreen + fun provideObjectTypeProvider( + storage: Editor.Storage + ) : ObjectTypeProvider = object : ObjectTypeProvider { + override fun provide(): List = storage.objectTypes.current() + } + + @JvmStatic + @Provides + @PerScreen + fun provideObjectDetailProvider( + storage: Editor.Storage + ) : ObjectDetailProvider = object : ObjectDetailProvider { + override fun provide(): Map = storage.details.current().details + } + + @JvmStatic + @Provides + @PerScreen + fun providePayloadDispatcher() : Dispatcher = Dispatcher.Default() + + @JvmStatic + @Provides + @PerScreen + fun provideDetailManager( + storage: Editor.Storage + ) : DetailModificationManager = InternalDetailModificationManager( + store = storage.details + ) + @JvmStatic @Provides @PerScreen @@ -580,4 +673,11 @@ object EditorUseCaseModule { @Provides @PerScreen fun provideTurnIntoUseCase(repo: BlockRepository): TurnIntoStyle = TurnIntoStyle(repo) + + @JvmStatic + @Provides + @PerScreen + fun provideUpdateDetailUseCase( + repository: BlockRepository + ) : UpdateDetail = UpdateDetail(repository) } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/RelationFileValueAddDi.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/RelationFileValueAddDi.kt new file mode 100644 index 0000000000..606dbfe6a5 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/RelationFileValueAddDi.kt @@ -0,0 +1,50 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.core_utils.di.scope.PerDialog +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.SearchObjects +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.presentation.relations.RelationFileValueAddViewModel +import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider +import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider +import com.anytypeio.anytype.ui.relations.RelationFileValueAddFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [RelationFileValueAddModule::class]) +@PerDialog +interface RelationFileValueAddSubComponent { + + @Subcomponent.Builder + interface Builder { + fun module(module: RelationFileValueAddModule): Builder + fun build(): RelationFileValueAddSubComponent + } + + fun inject(fragment: RelationFileValueAddFragment) +} + +@Module +object RelationFileValueAddModule { + + @JvmStatic + @Provides + @PerDialog + fun provideViewModelFactory( + relations: ObjectRelationProvider, + values: ObjectValueProvider, + searchObjects: SearchObjects, + urlBuilder: UrlBuilder + ): RelationFileValueAddViewModel.Factory = + RelationFileValueAddViewModel.Factory( + relations, values, searchObjects, urlBuilder + ) + + @JvmStatic + @Provides + @PerDialog + fun provideSearchObjectsUseCase( + repo: BlockRepository + ): SearchObjects = SearchObjects(repo = repo) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/SelectDocCoverDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/SelectDocCoverDI.kt index 5b2ed680f0..a28f47728b 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/SelectDocCoverDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/SelectDocCoverDI.kt @@ -1,15 +1,16 @@ package com.anytypeio.anytype.di.feature; import android.content.Context +import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.core_utils.di.scope.PerModal import com.anytypeio.anytype.device.DefaultGradientCollectionProvider import com.anytypeio.anytype.device.DeviceCoverCollectionProvider import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.cover.* -import com.anytypeio.anytype.domain.event.model.Payload import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.presentation.page.cover.SelectDocCoverViewModel -import com.anytypeio.anytype.presentation.util.Bridge +import com.anytypeio.anytype.presentation.page.editor.DetailModificationManager +import com.anytypeio.anytype.presentation.util.Dispatcher import com.anytypeio.anytype.ui.page.cover.DocCoverGalleryFragment import com.google.gson.Gson import dagger.Module @@ -37,17 +38,19 @@ object SelectDocCoverModule { fun provideSelectDocCoverViewModelFactory( setDocCoverColor: SetDocCoverColor, setDocCoverGradient: SetDocCoverGradient, - payloadDispatcher: Bridge, + payloadDispatcher: Dispatcher, getCoverCollection: GetCoverImageCollection, getCoverGradientCollection: GetCoverGradientCollection, - urlBuilder: UrlBuilder + urlBuilder: UrlBuilder, + detailModificationManager: DetailModificationManager ): SelectDocCoverViewModel.Factory = SelectDocCoverViewModel.Factory( setDocCoverColor = setDocCoverColor, setDocCoverGradient = setDocCoverGradient, - payloadDispatcher = payloadDispatcher, + dispatcher = payloadDispatcher, getCoverCollection = getCoverCollection, getCoverGradientCollection = getCoverGradientCollection, - urlBuilder = urlBuilder + urlBuilder = urlBuilder, + details = detailModificationManager ) @JvmStatic diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/SelectSortRelationDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/SelectSortRelationDI.kt new file mode 100644 index 0000000000..3c9b97fea6 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/SelectSortRelationDI.kt @@ -0,0 +1,52 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.AddDataViewViewerSort +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.SelectSortRelationViewModel +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.sort.SelectSortRelationFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow + +@Subcomponent(modules = [SelectSortRelationModule::class]) +@PerModal +interface SelectSortRelationSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: SelectSortRelationModule): Builder + fun build(): SelectSortRelationSubComponent + } + + fun inject(fragment: SelectSortRelationFragment) +} + +@Module +object SelectSortRelationModule { + @JvmStatic + @Provides + @PerModal + fun provideSelectSortRelationViewModelFactory( + state: StateFlow, + session: ObjectSetSession, + dispatcher: Dispatcher, + addDataViewViewerSort: AddDataViewViewerSort + ): SelectSortRelationViewModel.Factory = SelectSortRelationViewModel.Factory( + state = state, + session = session, + dispatcher = dispatcher, + addDataViewViewerSort = addDataViewViewerSort + ) + + @JvmStatic + @Provides + @PerModal + fun provideAddDataViewViewerSort( + repo: BlockRepository + ): AddDataViewViewerSort = AddDataViewViewerSort(repo = repo) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/UploadDocCoverImageDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/UploadDocCoverImageDI.kt index afa5e2e286..b97ae36f77 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/UploadDocCoverImageDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/UploadDocCoverImageDI.kt @@ -1,11 +1,11 @@ package com.anytypeio.anytype.di.feature +import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.core_utils.di.scope.PerModal import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.cover.SetDocCoverImage -import com.anytypeio.anytype.domain.event.model.Payload import com.anytypeio.anytype.presentation.page.cover.UploadDocCoverImageViewModel -import com.anytypeio.anytype.presentation.util.Bridge +import com.anytypeio.anytype.presentation.util.Dispatcher import com.anytypeio.anytype.ui.page.cover.UploadCoverImageFragment import dagger.Module import dagger.Provides @@ -31,7 +31,7 @@ object UploadDocCoverImageModule { @PerModal fun provideViewModelFactory( setDocCoverImage: SetDocCoverImage, - payloadDispatcher: Bridge, + payloadDispatcher: Dispatcher, ): UploadDocCoverImageViewModel.Factory = UploadDocCoverImageViewModel.Factory( setDocCoverImage = setDocCoverImage, payloadDispatcher = payloadDispatcher diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerCustomizeDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerCustomizeDI.kt new file mode 100644 index 0000000000..2fa56d620f --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerCustomizeDI.kt @@ -0,0 +1,41 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ViewerCustomizeViewModel +import com.anytypeio.anytype.ui.sets.modals.ViewerCustomizeFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Scope + +@Subcomponent(modules = [ViewerCustomizeModule::class]) +@ViewerCustomizeScope +interface ViewerCustomizeSubComponent { + + @Subcomponent.Builder + interface Builder { + fun module(module: ViewerCustomizeModule): Builder + fun build(): ViewerCustomizeSubComponent + } + + fun inject(fragment: ViewerCustomizeFragment) + +} + +@Module +object ViewerCustomizeModule { + + @JvmStatic + @Provides + @ViewerCustomizeScope + fun provideViewerCustomizeViewModelFactory( + state: StateFlow + ): ViewerCustomizeViewModel.Factory = ViewerCustomizeViewModel.Factory( + state = state + ) +} + +@Scope +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class ViewerCustomizeScope \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerFilter.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerFilter.kt new file mode 100644 index 0000000000..381b628c9f --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerFilter.kt @@ -0,0 +1,52 @@ +package com.anytypeio.anytype.di.feature; + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.filter.ViewerFilterViewModel +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.ViewerFilterFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Scope + +@Subcomponent(modules = [ViewerFilterModule::class]) +@ViewerFilterByScope +interface ViewerFilterSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: ViewerFilterModule): Builder + fun build(): ViewerFilterSubComponent + } + + fun inject(fragment: ViewerFilterFragment) +} + +@Module +object ViewerFilterModule { + + @JvmStatic + @Provides + @ViewerFilterByScope + fun provideViewerFilterViewModelFactory( + state: StateFlow, + session: ObjectSetSession, + dispatcher: Dispatcher, + updateDataViewViewer: UpdateDataViewViewer, + urlBuilder: UrlBuilder + ): ViewerFilterViewModel.Factory = ViewerFilterViewModel.Factory( + state = state, + session = session, + dispatcher = dispatcher, + updateDataViewViewer = updateDataViewViewer, + urlBuilder = urlBuilder + ) +} + +@Scope +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class ViewerFilterByScope \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerRelationListDi.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerRelationListDi.kt new file mode 100644 index 0000000000..012ccc069b --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerRelationListDi.kt @@ -0,0 +1,56 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.ModifyDataViewViewerRelationOrder +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer +import com.anytypeio.anytype.presentation.relations.ViewerRelationsViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.ViewerRelationsFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow + +@Subcomponent(modules = [ViewerRelationsModule::class]) +@PerModal +interface ViewerRelationsSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: ViewerRelationsModule): Builder + fun build(): ViewerRelationsSubComponent + } + + fun inject(fragment: ViewerRelationsFragment) +} + +@Module +object ViewerRelationsModule { + + @JvmStatic + @Provides + @PerModal + fun provideViewerRelationsListViewModelFactory( + state: StateFlow, + session: ObjectSetSession, + dispatcher: Dispatcher, + modifyViewerRelationOrder: ModifyDataViewViewerRelationOrder, + updateDataViewViewer: UpdateDataViewViewer + ): ViewerRelationsViewModel.Factory = ViewerRelationsViewModel.Factory( + state = state, + session = session, + dispatcher = dispatcher, + modifyViewerRelationOrder = modifyViewerRelationOrder, + updateDataViewViewer = updateDataViewViewer + ) + + @JvmStatic + @Provides + @PerModal + fun provideModifyViewerRelationOrderUseCase( + repo: BlockRepository + ): ModifyDataViewViewerRelationOrder = ModifyDataViewViewerRelationOrder(repo = repo) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerSort.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerSort.kt new file mode 100644 index 0000000000..0078b6103b --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerSort.kt @@ -0,0 +1,44 @@ +package com.anytypeio.anytype.di.feature; + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.sort.ViewerSortViewModel +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.sort.ViewerSortFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow + +@Subcomponent(modules = [ViewerSortModule::class]) +@PerModal +interface ViewerSortSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: ViewerSortModule): Builder + fun build(): ViewerSortSubComponent + } + + fun inject(fragment: ViewerSortFragment) +} + +@Module +object ViewerSortModule { + @JvmStatic + @Provides + @PerModal + fun provideViewModelFactory( + state: StateFlow, + session: ObjectSetSession, + updateDataViewViewer: UpdateDataViewViewer, + dispatcher: Dispatcher + ): ViewerSortViewModel.Factory = ViewerSortViewModel.Factory( + state = state, + session = session, + updateDataViewViewer = updateDataViewViewer, + dispatcher = dispatcher + ) +} diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerSortByDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerSortByDI.kt new file mode 100644 index 0000000000..03eca00c36 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ViewerSortByDI.kt @@ -0,0 +1,38 @@ +package com.anytypeio.anytype.di.feature + +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ViewerSortByViewModel +import com.anytypeio.anytype.ui.sets.ViewerSortByFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Scope + +@Subcomponent(modules = [ViewerSortByModule::class]) +@ViewerSortByScope +interface ViewerSortBySubComponent { + + @Subcomponent.Builder + interface Builder { + fun module(module: ViewerSortByModule): Builder + fun build(): ViewerSortBySubComponent + } + + fun inject(fragment: ViewerSortByFragment) +} + +@Module +object ViewerSortByModule { + + @JvmStatic + @Provides + @ViewerSortByScope + fun provideViewerSortByViewModelFactory( + state: StateFlow + ): ViewerSortByViewModel.Factory = ViewerSortByViewModel.Factory(state) +} + +@Scope +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class ViewerSortByScope \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/sets/CreateFilter.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/sets/CreateFilter.kt new file mode 100644 index 0000000000..c59618b07e --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/sets/CreateFilter.kt @@ -0,0 +1,62 @@ +package com.anytypeio.anytype.di.feature.sets; + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.SearchObjects +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.filter.FilterViewModel +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.filter.CreateFilterFromInputFieldValueFragment +import com.anytypeio.anytype.ui.sets.modals.filter.CreateFilterFromSelectedValueFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow + +@Subcomponent(modules = [CreateFilterModule::class]) +@PerModal +interface CreateFilterSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: CreateFilterModule): Builder + fun build(): CreateFilterSubComponent + } + + fun inject(fragment: CreateFilterFromSelectedValueFragment) + fun inject(fragment: CreateFilterFromInputFieldValueFragment) + fun createPickConditionComponent(): PickFilterConditionSubComponent.Builder +} + +@Module +object CreateFilterModule { + + @JvmStatic + @Provides + @PerModal + fun provideViewModelFactory( + state: StateFlow, + session: ObjectSetSession, + dispatcher: Dispatcher, + updateDataViewViewer: UpdateDataViewViewer, + searchObjects: SearchObjects, + urlBuilder: UrlBuilder + ): FilterViewModel.Factory = FilterViewModel.Factory( + objectSetState = state, + session = session, + dispatcher = dispatcher, + updateDataViewViewer = updateDataViewViewer, + searchObjects = searchObjects, + urlBuilder = urlBuilder + ) + + @JvmStatic + @Provides + @PerModal + fun provideSearchObjectsUseCase( + repo: BlockRepository + ): SearchObjects = SearchObjects(repo = repo) +} diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/sets/ModifyFilter.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/sets/ModifyFilter.kt new file mode 100644 index 0000000000..5a8bba2e87 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/sets/ModifyFilter.kt @@ -0,0 +1,62 @@ +package com.anytypeio.anytype.di.feature.sets; + +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.dataview.interactor.SearchObjects +import com.anytypeio.anytype.domain.dataview.interactor.UpdateDataViewViewer +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.filter.FilterViewModel +import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.sets.modals.filter.ModifyFilterFromInputFieldValueFragment +import com.anytypeio.anytype.ui.sets.modals.filter.ModifyFilterFromSelectedValueFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow + +@Subcomponent(modules = [ModifyFilterModule::class]) +@PerModal +interface ModifyFilterSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: ModifyFilterModule): Builder + fun build(): ModifyFilterSubComponent + } + + fun inject(fragment: ModifyFilterFromInputFieldValueFragment) + fun inject(fragment: ModifyFilterFromSelectedValueFragment) + fun createPickConditionComponent(): PickFilterConditionSubComponent.Builder +} + +@Module +object ModifyFilterModule { + + @JvmStatic + @Provides + @PerModal + fun provideViewModelFactory( + state: StateFlow, + session: ObjectSetSession, + dispatcher: Dispatcher, + updateDataViewViewer: UpdateDataViewViewer, + searchObjects: SearchObjects, + urlBuilder: UrlBuilder + ): FilterViewModel.Factory = FilterViewModel.Factory( + objectSetState = state, + session = session, + dispatcher = dispatcher, + updateDataViewViewer = updateDataViewViewer, + searchObjects = searchObjects, + urlBuilder = urlBuilder + ) + + @JvmStatic + @Provides + @PerModal + fun provideSearchObjectsUseCase( + repo: BlockRepository + ): SearchObjects = SearchObjects(repo = repo) +} diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/sets/PickConditionDi.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/sets/PickConditionDi.kt new file mode 100644 index 0000000000..92547b8a01 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/sets/PickConditionDi.kt @@ -0,0 +1,33 @@ +package com.anytypeio.anytype.di.feature.sets + +import com.anytypeio.anytype.core_utils.di.scope.PerDialog +import com.anytypeio.anytype.presentation.sets.filter.PickFilterConditionViewModel +import com.anytypeio.anytype.ui.sets.modals.PickFilterConditionFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + +@Subcomponent(modules = [PickConditionModule::class]) +@PerDialog +interface PickFilterConditionSubComponent { + + @Subcomponent.Builder + interface Builder { + + fun module(module: PickConditionModule): Builder + fun build(): PickFilterConditionSubComponent + } + + fun inject(fragment: PickFilterConditionFragment) + +} + +@Module +object PickConditionModule { + + @JvmStatic + @Provides + @PerDialog + fun provideFactory(): PickFilterConditionViewModel.Factory = + PickFilterConditionViewModel.Factory() +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/sets/SelectFilterRelationDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/sets/SelectFilterRelationDI.kt new file mode 100644 index 0000000000..ec5bfb3c06 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/sets/SelectFilterRelationDI.kt @@ -0,0 +1,37 @@ +package com.anytypeio.anytype.di.feature.sets + +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.presentation.sets.ObjectSet +import com.anytypeio.anytype.presentation.sets.ObjectSetSession +import com.anytypeio.anytype.presentation.sets.SelectFilterRelationViewModel +import com.anytypeio.anytype.ui.sets.modals.filter.SelectFilterRelationFragment +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import kotlinx.coroutines.flow.StateFlow + +@Subcomponent(modules = [SelectFilterRelationModule::class]) +@PerModal +interface SelectFilterRelationSubComponent { + @Subcomponent.Builder + interface Builder { + fun module(module: SelectFilterRelationModule): Builder + fun build(): SelectFilterRelationSubComponent + } + + fun inject(fragment: SelectFilterRelationFragment) +} + +@Module +object SelectFilterRelationModule { + @JvmStatic + @Provides + @PerModal + fun provideSelectSortRelationViewModelFactory( + state: StateFlow, + session: ObjectSetSession + ): SelectFilterRelationViewModel.Factory = SelectFilterRelationViewModel.Factory( + state = state, + session = session + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/BridgeModule.kt b/app/src/main/java/com/anytypeio/anytype/di/main/BridgeModule.kt deleted file mode 100644 index a695fa4c9b..0000000000 --- a/app/src/main/java/com/anytypeio/anytype/di/main/BridgeModule.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.anytypeio.anytype.di.main - -import com.anytypeio.anytype.domain.event.model.Payload -import com.anytypeio.anytype.presentation.util.Bridge -import dagger.Module -import dagger.Provides -import javax.inject.Singleton - - -@Module -object BridgeModule { - - @JvmStatic - @Provides - @Singleton - fun providePayloadPridge(): Bridge = Bridge() -} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/ConfigModule.kt b/app/src/main/java/com/anytypeio/anytype/di/main/ConfigModule.kt index 72b7faf021..77638629ef 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/ConfigModule.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/ConfigModule.kt @@ -3,7 +3,7 @@ package com.anytypeio.anytype.di.main import com.anytypeio.anytype.data.auth.repo.config.Configuration import com.anytypeio.anytype.data.auth.repo.config.Configurator import com.anytypeio.anytype.data.auth.repo.config.GatewayProvider -import com.anytypeio.anytype.domain.config.Config +import com.anytypeio.anytype.core_models.Config import com.anytypeio.anytype.domain.config.Gateway import com.anytypeio.anytype.middleware.config.DefaultConfigurator import dagger.Module diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/DataModule.kt b/app/src/main/java/com/anytypeio/anytype/di/main/DataModule.kt index d2c35ca81b..16c7acd5ea 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/DataModule.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/DataModule.kt @@ -21,7 +21,6 @@ import com.anytypeio.anytype.middleware.auth.AuthMiddleware import com.anytypeio.anytype.middleware.block.BlockMiddleware import com.anytypeio.anytype.middleware.interactor.Middleware import com.anytypeio.anytype.middleware.interactor.MiddlewareFactory -import com.anytypeio.anytype.middleware.interactor.MiddlewareMapper import com.anytypeio.anytype.middleware.service.MiddlewareService import com.anytypeio.anytype.middleware.service.MiddlewareServiceImplementation import com.anytypeio.anytype.persistence.db.AnytypeDatabase @@ -213,20 +212,14 @@ object DataModule { @Singleton fun provideMiddleware( service: MiddlewareService, - factory: MiddlewareFactory, - mapper: MiddlewareMapper - ): Middleware = Middleware(service, factory, mapper) + factory: MiddlewareFactory + ): Middleware = Middleware(service, factory) @JvmStatic @Provides @Singleton fun provideMiddlewareFactory(): MiddlewareFactory = MiddlewareFactory() - @JvmStatic - @Provides - @Singleton - fun provideMiddlewareMapper(): MiddlewareMapper = MiddlewareMapper() - @JvmStatic @Provides @Singleton diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt b/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt index b709bffe94..8fe1f967ec 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt @@ -16,8 +16,7 @@ import javax.inject.Singleton UtilModule::class, EmojiModule::class, ClipboardModule::class, - AnalyticsModule::class, - BridgeModule::class + AnalyticsModule::class ] ) interface MainComponent { @@ -39,8 +38,6 @@ interface MainComponent { fun pageComponentBuilder(): PageSubComponent.Builder fun archiveComponentBuilder(): ArchiveSubComponent.Builder fun linkAddComponentBuilder(): LinkSubComponent.Builder - fun documentActionMenuComponentBuilder(): DocumentActionMenuSubComponent.Builder - fun documentEmojiIconPickerComponentBuilder(): DocumentEmojiIconPickerSubComponent.Builder fun createBookmarkBuilder(): CreateBookmarkSubComponent.Builder fun debugSettingsBuilder(): DebugSettingsSubComponent.Builder fun navigationComponentBuilder(): PageNavigationSubComponent.Builder @@ -48,4 +45,8 @@ interface MainComponent { fun moveToBuilder(): MoveToSubComponent.Builder fun pageSearchComponentBuilder(): PageSearchSubComponent.Builder fun mainEntryComponentBuilder(): MainEntrySubComponent.Builder + fun createSetComponentBuilder(): CreateSetSubComponent.Builder + fun createObjectTypeComponentBuilder(): CreateObjectTypeSubComponent.Builder + fun objectSetComponentBuilder(): ObjectSetSubComponent.Builder + fun createDataViewRelationBuilder(): CreateDataViewRelationSubComponent.Builder } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ext/MarkupExt.kt b/app/src/main/java/com/anytypeio/anytype/ext/MarkupExt.kt index 6d6d32b050..45881853b9 100644 --- a/app/src/main/java/com/anytypeio/anytype/ext/MarkupExt.kt +++ b/app/src/main/java/com/anytypeio/anytype/ext/MarkupExt.kt @@ -4,9 +4,9 @@ import android.text.Editable import android.text.Spanned import com.anytypeio.anytype.core_ui.common.Span import com.anytypeio.anytype.core_ui.widgets.text.MentionSpan -import com.anytypeio.anytype.domain.block.model.Block.Content.Text.Mark -import com.anytypeio.anytype.domain.ext.overlap -import com.anytypeio.anytype.domain.misc.Overlap +import com.anytypeio.anytype.core_models.Block.Content.Text.Mark +import com.anytypeio.anytype.core_models.ext.overlap +import com.anytypeio.anytype.core_models.misc.Overlap import com.anytypeio.anytype.presentation.page.editor.ThemeColor fun Editable.extractMarks(): List = getSpans(0, length, Span::class.java).mapNotNull { span -> diff --git a/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt b/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt index 68c9b30a63..2f7360be48 100644 --- a/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt +++ b/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt @@ -5,8 +5,8 @@ import androidx.core.os.bundleOf import androidx.navigation.NavController import androidx.navigation.navOptions import com.anytypeio.anytype.R -import com.anytypeio.anytype.domain.block.model.Position -import com.anytypeio.anytype.domain.common.Id +import com.anytypeio.anytype.core_models.Position +import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.presentation.navigation.AppNavigation import com.anytypeio.anytype.presentation.settings.EditorSettings import com.anytypeio.anytype.ui.archive.ArchiveFragment @@ -16,6 +16,8 @@ import com.anytypeio.anytype.ui.linking.LinkToObjectFragment import com.anytypeio.anytype.ui.moving.MoveToFragment import com.anytypeio.anytype.ui.navigation.PageNavigationFragment import com.anytypeio.anytype.ui.page.PageFragment +import com.anytypeio.anytype.ui.sets.CreateObjectSetFragment +import com.anytypeio.anytype.ui.sets.ObjectSetFragment class Navigator : AppNavigation { @@ -200,6 +202,20 @@ class Navigator : AppNavigation { ) } + override fun openObjectSet(target: String) { + navController?.navigate( + R.id.objectSetScreen, + bundleOf(ObjectSetFragment.CONTEXT_ID_KEY to target) + ) + } + + override fun openCreateSetScreen(ctx: Id) { + navController?.navigate( + R.id.from_desktop_to_create_sets, + bundleOf(CreateObjectSetFragment.CONTEXT_ID_KEY to ctx) + ) + } + fun bind(navController: NavController) { this.navController = navController } diff --git a/app/src/main/java/com/anytypeio/anytype/providers/DefaultCoverImageHashProvider.kt b/app/src/main/java/com/anytypeio/anytype/providers/DefaultCoverImageHashProvider.kt index c70e09a37e..1545ef8757 100644 --- a/app/src/main/java/com/anytypeio/anytype/providers/DefaultCoverImageHashProvider.kt +++ b/app/src/main/java/com/anytypeio/anytype/providers/DefaultCoverImageHashProvider.kt @@ -1,7 +1,7 @@ package com.anytypeio.anytype.providers -import com.anytypeio.anytype.domain.common.Hash -import com.anytypeio.anytype.domain.common.Id +import com.anytypeio.anytype.core_models.Hash +import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.presentation.page.cover.CoverImageHashProvider class DefaultCoverImageHashProvider : CoverImageHashProvider { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/base/NavigationFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/base/NavigationFragment.kt index a55a1a4d8c..5a0830f5ee 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/base/NavigationFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/base/NavigationFragment.kt @@ -6,6 +6,7 @@ import com.anytypeio.anytype.core_utils.common.EventWrapper import com.anytypeio.anytype.core_utils.ui.BaseFragment import com.anytypeio.anytype.presentation.navigation.AppNavigation import com.anytypeio.anytype.presentation.navigation.AppNavigation.Command +import timber.log.Timber abstract class NavigationFragment( @LayoutRes private val layout: Int @@ -15,12 +16,11 @@ abstract class NavigationFragment( event.getContentIfNotHandled()?.let { navigate(it) } } - private fun navigate(command: Command) { + fun navigate(command: Command) { val navigation = (requireActivity() as AppNavigation.Provider).nav() when (command) { - is Command.StartSplashFromDesktop -> navigation.startSplashFromDesktop() is Command.StartDesktopFromLogin -> navigation.startDesktopFromLogin() is Command.StartDesktopFromSplash -> navigation.startDesktopFromSplash() @@ -39,6 +39,7 @@ abstract class NavigationFragment( is Command.OpenProfile -> navigation.openProfile() is Command.OpenPage -> navigation.openDocument(command.id, command.editorSettings) is Command.OpenArchive -> navigation.openArchive(command.target) + is Command.OpenObjectSet -> navigation.openObjectSet(command.target) is Command.LaunchDocument -> navigation.launchDocument(command.id) is Command.OpenDatabaseViewAddView -> navigation.openDatabaseViewAddView() is Command.OpenEditDatabase -> navigation.openEditDatabase() @@ -65,6 +66,8 @@ abstract class NavigationFragment( ) is Command.ExitToDesktopAndOpenPage -> navigation.exitToDesktopAndOpenPage(command.pageId) is Command.OpenPageSearch -> navigation.openPageSearch() + is Command.OpenCreateSetScreen -> navigation.openCreateSetScreen(command.ctx) + else -> Timber.d("Nav command ignored: $command") } } } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/database/modals/ObjectRelationValueFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/database/modals/ObjectRelationValueFragment.kt new file mode 100644 index 0000000000..f297726926 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/database/modals/ObjectRelationValueFragment.kt @@ -0,0 +1,438 @@ +package com.anytypeio.anytype.ui.database.modals + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.features.sets.ObjectRelationValueAdapter +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_ui.reactive.textChanges +import com.anytypeio.anytype.core_ui.tools.DefaultDragAndDropBehavior +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.core_utils.ui.DragAndDropViewHolder +import com.anytypeio.anytype.core_utils.ui.OnStartDragListener +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.ObjectObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.sets.ObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSetObjectRelationValueViewModel +import com.anytypeio.anytype.ui.relations.* +import kotlinx.android.synthetic.main.object_relation_value_fragment.* +import javax.inject.Inject + +abstract class ObjectRelationValueFragment : BaseBottomSheetFragment(), + OnStartDragListener, + AddObjectRelationObjectValueFragment.AddObjectRelationObjectValueReceiver, + RelationFileValueActionsFragment.RelationFileValueActionReceiver, + RelationFileValueAddFragment.AddRelationFileValueReceiver +{ + + protected val ctx get() = argString(CTX_KEY) + protected val relation get() = argString(RELATION_KEY) + protected val target get() = argString(TARGET_KEY) + protected val dataview get() = argString(DATAVIEW_KEY) + protected val viewer get() = argString(VIEWER_KEY) + + abstract val vm: ObjectRelationValueViewModel + + private val dndItemTouchHelper: ItemTouchHelper by lazy { ItemTouchHelper(dndBehavior) } + + private val dndBehavior by lazy { + object : DefaultDragAndDropBehavior( + onItemMoved = { from, to -> editCellTagAdapter.onItemMove(from, to) }, + onItemDropped = { onItemDropped() } + ) { + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + if (viewHolder is DragAndDropViewHolder) return super.getMovementFlags(recyclerView, viewHolder) + return makeMovementFlags(0, 0) + } + } + } + + protected val editCellTagAdapter by lazy { + ObjectRelationValueAdapter( + onCreateOptionClicked = {}, + onTagClicked = {}, + onStatusClicked = {}, + onRemoveStatusClicked = {}, + onRemoveTagClicked = { tag -> onRemoveTagClicked(tag) }, + onObjectClicked = {}, + onRemoveObjectClicked = { obj -> onRemoveObjectClicked(obj) }, + onFileClicked = {}, + onRemoveFileClicked = { file -> onRemoveFileClicked(file) } + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.object_relation_value_fragment, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + recycler.apply { + layoutManager = LinearLayoutManager(context) + adapter = editCellTagAdapter + addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply { + setDrawable(drawable(R.drawable.divider_relations)) + } + ) + } + with(lifecycleScope) { + subscribe(filterInput.textChanges()) { vm.onFilterInputChanged(it.toString()) } + subscribe(btnEditOrDone.clicks()) { vm.onEditOrDoneClicked() } + subscribe(btnAddValue.clicks()) { vm.onAddValueClicked() } + } + } + + override fun onStart() { + jobs += lifecycleScope.subscribe(vm.commands) { observeCommands(it) } + jobs += lifecycleScope.subscribe(vm.isDimissed) { observeDismiss(it) } + jobs += lifecycleScope.subscribe(vm.isEditing) { observeEditing(it) } + jobs += lifecycleScope.subscribe(vm.views) { editCellTagAdapter.update(it) } + jobs += lifecycleScope.subscribe(vm.name) { tvTagOrStatusRelationHeader.text = it } + jobs += lifecycleScope.subscribe(vm.isFilterVisible) { + if (it) filterInputContainer.visible() else filterInputContainer.gone() + } + super.onStart() + vm.onStart(relationId = relation, objectId = target) + } + + override fun onStop() { + super.onStop() + vm.onStop() + } + + override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) { + dndItemTouchHelper.startDrag(viewHolder) + } + + private fun observeEditing(isEditing: Boolean) { + if (isEditing) { + btnEditOrDone.setText(R.string.done) + dndItemTouchHelper.attachToRecyclerView(recycler) + } else { + btnEditOrDone.setText(R.string.edit) + dndItemTouchHelper.attachToRecyclerView(null) + } + } + + private fun observeDismiss(isDismissed: Boolean) { + if (isDismissed) { + filterInput.apply { + clearFocus() + hideKeyboard() + } + dismiss() + } + } + + abstract fun observeCommands(command: ObjectRelationValueViewModel.ObjectRelationValueCommand) + abstract fun onItemDropped() + abstract fun onRemoveTagClicked(tag: ObjectRelationValueViewModel.ObjectRelationValueView.Tag) + abstract fun onRemoveObjectClicked(objectId: Id) + abstract fun onRemoveFileClicked(fileId: Id) + + companion object { + const val CTX_KEY = "arg.edit-cell-tag.ctx" + const val RELATION_KEY = "arg.edit-cell-tag.relation" + const val TARGET_KEY = "arg.edit-cell-tag.target" + const val DATAVIEW_KEY = "arg.edit-cell-tag.dataview" + const val VIEWER_KEY = "arg.edit-cell-tag.viewer" + } +} + +open class ObjectSetObjectRelationValueFragment : ObjectRelationValueFragment() { + + @Inject + lateinit var factory: ObjectSetObjectRelationValueViewModel.Factory + override val vm: ObjectSetObjectRelationValueViewModel by viewModels { factory } + + override fun onRemoveTagClicked(tag: ObjectRelationValueViewModel.ObjectRelationValueView.Tag) { + vm.onRemoveTagFromDataViewRecordClicked( + ctx = ctx, + dataview = dataview, + target = target, + relation = relation, + tag = tag.id, + viewer = viewer + ) + } + + override fun onRemoveObjectClicked(objectId: Id) { + vm.onRemoveObjectFromDataViewRecordClicked( + ctx = ctx, + dataview = dataview, + target = target, + relation = relation, + objectId = objectId + ) + } + + override fun onRemoveFileClicked(fileId: Id) { + vm.onRemoveFileFromDataViewRecordClicked( + ctx = ctx, + dataview = dataview, + target = target, + relation = relation, + fileId = fileId + ) + } + + override fun onItemDropped() { + vm.onDataViewValueOrderChanged( + ctx = ctx, + obj = target, + dv = dataview, + relation = relation, + order = editCellTagAdapter.order() + ) + } + + override fun onRelationObjectValueChanged( + ctx: Id, + objectId: Id, + relationId: Id, + ids: List + ) { + vm.onAddObjectsOrFilesValueToRecord( + ctx = ctx, + dataview = dataview, + record = target, + relation = relation, + ids = ids + ) + } + + override fun onRelationFileValueChanged(ctx: Id, objectId: Id, relationId: Id, ids: List) { + vm.onAddObjectsOrFilesValueToRecord( + ctx = ctx, + dataview = dataview, + record = target, + relation = relation, + ids = ids + ) + } + + override fun observeCommands(command: ObjectRelationValueViewModel.ObjectRelationValueCommand) { + when (command) { + ObjectRelationValueViewModel.ObjectRelationValueCommand.ShowAddObjectScreen -> { + val fragmentFlow = AddObjectRelationObjectValueFragment.FLOW_DATAVIEW + val fr = AddObjectRelationObjectValueFragment.new( + ctx = ctx, + relationId = relation, + objectId = target, + flow = fragmentFlow + ) + fr.show(childFragmentManager, null) + } + ObjectRelationValueViewModel.ObjectRelationValueCommand.ShowAddStatusOrTagScreen -> { + val fr = AddObjectSetObjectRelationValueFragment.new( + ctx = ctx, + target = target, + relation = relation, + dataview = dataview, + viewer = viewer + ) + fr.show(childFragmentManager, null) + } + ObjectRelationValueViewModel.ObjectRelationValueCommand.ShowAddFileScreen -> { + val fr = RelationFileValueAddFragment.new( + ctx = ctx, + relationId = relation, + objectId = target, + flow = RelationFileValueAddFragment.FLOW_DATAVIEW + ) + fr.show(childFragmentManager, null) + } + ObjectRelationValueViewModel.ObjectRelationValueCommand.ShowFileValueActionScreen -> { + RelationFileValueActionsFragment().show(childFragmentManager, null) + } + } + } + + override fun onFileValueActionAdd() { + vm.onFileValueActionAddClicked() + } + + override fun onFileValueActionUploadFromGallery() { + toast("Not implemented") + vm.onFileValueActionUploadFromGalleryClicked() + } + + override fun onFileValueActionUploadFromStorage() { + toast("Not implemented") + vm.onFileValueActionUploadFromStorageClicked() + } + + override fun injectDependencies() { + componentManager().objectSetObjectRelationValueComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().objectSetObjectRelationValueComponent.release(ctx) + } + + companion object { + fun new( + ctx: Id, + target: Id, + relation: Id, + dataview: Id, + viewer: Id + ) = ObjectSetObjectRelationValueFragment().apply { + arguments = bundleOf( + CTX_KEY to ctx, + TARGET_KEY to target, + RELATION_KEY to relation, + DATAVIEW_KEY to dataview, + VIEWER_KEY to viewer + ) + } + } +} + +class ObjectObjectRelationValueFragment : ObjectRelationValueFragment() { + + @Inject + lateinit var factory: ObjectObjectRelationValueViewModel.Factory + override val vm: ObjectObjectRelationValueViewModel by viewModels { factory } + + override fun onRemoveTagClicked(tag: ObjectRelationValueViewModel.ObjectRelationValueView.Tag) { + vm.onRemoveTagFromObjectClicked( + ctx = ctx, + target = target, + relation = relation, + tag = tag.id + ) + } + + override fun onRemoveObjectClicked(objectId: Id) { + vm.onRemoveObjectFromObjectClicked( + ctx = ctx, + target = target, + relation = relation, + objectId = objectId + ) + } + + override fun onRemoveFileClicked(fileId: Id) { + vm.onRemoveFileFromObjectClicked( + ctx = ctx, + target = target, + relation = relation, + fileId = fileId + ) + } + + override fun onItemDropped() { + vm.onObjectValueOrderChanged( + ctx = ctx, + relation = relation, + order = editCellTagAdapter.order() + ) + } + + override fun onRelationObjectValueChanged( + ctx: Id, + objectId: Id, + relationId: Id, + ids: List + ) { + vm.onAddObjectsOrFilesValueToObject( + ctx = ctx, + target = target, + relation = relation, + ids = ids + ) + } + + override fun onRelationFileValueChanged(ctx: Id, objectId: Id, relationId: Id, ids: List) { + vm.onAddObjectsOrFilesValueToObject( + ctx = ctx, + target = target, + relation = relation, + ids = ids + ) + } + + override fun observeCommands(command: ObjectRelationValueViewModel.ObjectRelationValueCommand) { + when (command) { + ObjectRelationValueViewModel.ObjectRelationValueCommand.ShowAddObjectScreen -> { + val fr = AddObjectRelationObjectValueFragment.new( + ctx = ctx, + relationId = relation, + objectId = target + ) + fr.show(childFragmentManager, null) + } + ObjectRelationValueViewModel.ObjectRelationValueCommand.ShowAddStatusOrTagScreen -> { + val fr = AddObjectObjectRelationValueFragment.new( + ctx = ctx, + objectId = target, + relationId = relation + ) + fr.show(childFragmentManager, null) + } + ObjectRelationValueViewModel.ObjectRelationValueCommand.ShowAddFileScreen -> { + val fr = RelationFileValueAddFragment.new( + ctx = ctx, + relationId = relation, + objectId = target, + flow = RelationFileValueAddFragment.FLOW_DEFAULT + ) + fr.show(childFragmentManager, null) + } + ObjectRelationValueViewModel.ObjectRelationValueCommand.ShowFileValueActionScreen -> { + RelationFileValueActionsFragment().show(childFragmentManager, null) + } + } + } + + override fun onFileValueActionAdd() { + vm.onFileValueActionAddClicked() + } + + override fun onFileValueActionUploadFromGallery() { + toast("Not implemented") + vm.onFileValueActionUploadFromGalleryClicked() + } + + override fun onFileValueActionUploadFromStorage() { + toast("Not implemented") + vm.onFileValueActionUploadFromStorageClicked() + } + + override fun injectDependencies() { + componentManager().objectObjectRelationValueComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().objectObjectRelationValueComponent.release(ctx) + } + + companion object { + fun new( + ctx: Id, + target: Id, + relation: Id + ) = ObjectObjectRelationValueFragment().apply { + arguments = bundleOf( + CTX_KEY to ctx, + TARGET_KEY to target, + RELATION_KEY to relation + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/desktop/DashboardAdapter.kt b/app/src/main/java/com/anytypeio/anytype/ui/desktop/DashboardAdapter.kt index d34037f1ee..36a2e5ca2d 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/desktop/DashboardAdapter.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/desktop/DashboardAdapter.kt @@ -11,7 +11,7 @@ import com.anytypeio.anytype.core_utils.ext.invisible import com.anytypeio.anytype.core_utils.ext.shift import com.anytypeio.anytype.core_utils.ext.typeOf import com.anytypeio.anytype.core_utils.ext.visible -import com.anytypeio.anytype.domain.common.Id +import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.emojifier.Emojifier import com.anytypeio.anytype.presentation.desktop.DashboardView import com.bumptech.glide.Glide @@ -23,12 +23,14 @@ import timber.log.Timber class DashboardAdapter( private var data: List, private val onDocumentClicked: (Id, Boolean) -> Unit, - private val onArchiveClicked: (Id) -> Unit + private val onArchiveClicked: (Id) -> Unit, + private val onObjectSetClicked: (Id) -> Unit ) : RecyclerView.Adapter(), SupportDragAndDropBehavior { companion object { const val VIEW_TYPE_DOCUMENT = 0 const val VIEW_TYPE_ARCHIVE = 1 + const val VIEW_TYPE_SET = 2 const val UNEXPECTED_TYPE_ERROR_MESSAGE = "Unexpected type" const val EMPTY_EMOJI = "" } @@ -46,6 +48,11 @@ class DashboardAdapter( ViewHolder.ArchiveHolder(it) } } + VIEW_TYPE_SET -> { + inflater.inflate(R.layout.item_desktop_object_set, parent, false).let { + ViewHolder.ObjectSetHolder(it) + } + } else -> throw IllegalStateException("Unexpected view type: $viewType") } } @@ -54,6 +61,7 @@ class DashboardAdapter( return when (data[position]) { is DashboardView.Document -> VIEW_TYPE_DOCUMENT is DashboardView.Archive -> VIEW_TYPE_ARCHIVE + is DashboardView.ObjectSet -> VIEW_TYPE_SET else -> throw IllegalStateException(UNEXPECTED_TYPE_ERROR_MESSAGE) } } @@ -75,6 +83,14 @@ class DashboardAdapter( bindLoading(item.isLoading) } } + is ViewHolder.ObjectSetHolder -> { + with(holder) { + val item = data[position] as DashboardView.ObjectSet + bindClick(item.target, onObjectSetClicked) + bindTitle(item.title) + bindEmoji(item.emoji) + } + } is ViewHolder.ArchiveHolder -> { with(holder) { val item = data[position] as DashboardView.Archive @@ -222,6 +238,40 @@ class DashboardAdapter( } } } + + class ObjectSetHolder(itemView: View) : ViewHolder(itemView) { + + private val tvTitle = itemView.title + private val ivEmoji = itemView.emojiIcon + + fun bindClick( + target: Id, + onClick: (Id) -> Unit + ) { + itemView.setOnClickListener { onClick(target) } + } + + fun bindTitle(title: String?) { + if (title.isNullOrEmpty()) + tvTitle.setText(R.string.untitled) + else + tvTitle.text = title + } + + fun bindEmoji(emoji: String?) { + try { + emoji?.let { unicode -> + Glide + .with(ivEmoji) + .load(Emojifier.uri(unicode)) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .into(ivEmoji) + } + } catch (e: Throwable) { + Timber.e(e, "Could not set emoji icon") + } + } + } } fun update(views: List) { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/linking/LinkToObjectFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/linking/LinkToObjectFragment.kt index f43d75e393..45a2d8ffb2 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/linking/LinkToObjectFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/linking/LinkToObjectFragment.kt @@ -14,7 +14,7 @@ import com.anytypeio.anytype.core_ui.layout.State import com.anytypeio.anytype.core_utils.ext.* import com.anytypeio.anytype.core_utils.ui.ViewState import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.domain.block.model.Position +import com.anytypeio.anytype.core_models.Position import com.anytypeio.anytype.emojifier.Emojifier import com.anytypeio.anytype.presentation.linking.LinkToObjectViewModel import com.anytypeio.anytype.presentation.linking.LinkToObjectViewModelFactory diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/PageFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/PageFragment.kt index 13c1def04e..6763530478 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/page/PageFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/PageFragment.kt @@ -39,6 +39,12 @@ import androidx.transition.TransitionManager import androidx.transition.TransitionSet import com.anytypeio.anytype.BuildConfig import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Block.Content.Text +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.ext.getFirstLinkMarkupParam +import com.anytypeio.anytype.core_models.ext.getSubstring import com.anytypeio.anytype.core_ui.extensions.color import com.anytypeio.anytype.core_ui.extensions.isKeyboardVisible import com.anytypeio.anytype.core_ui.extensions.tint @@ -57,11 +63,6 @@ import com.anytypeio.anytype.core_utils.common.EventWrapper import com.anytypeio.anytype.core_utils.ext.* import com.anytypeio.anytype.core_utils.ext.PopupExtensions.calculateRectInWindow import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.domain.block.model.Block -import com.anytypeio.anytype.domain.block.model.Block.Content.Text -import com.anytypeio.anytype.domain.common.Id -import com.anytypeio.anytype.domain.ext.getFirstLinkMarkupParam -import com.anytypeio.anytype.domain.ext.getSubstring import com.anytypeio.anytype.domain.status.SyncStatus import com.anytypeio.anytype.emojifier.Emojifier import com.anytypeio.anytype.ext.extractMarks @@ -78,6 +79,7 @@ import com.anytypeio.anytype.presentation.page.editor.sam.ScrollAndMoveTarget import com.anytypeio.anytype.presentation.page.editor.sam.ScrollAndMoveTargetDescriptor import com.anytypeio.anytype.ui.alert.AlertUpdateAppFragment import com.anytypeio.anytype.ui.base.NavigationFragment +import com.anytypeio.anytype.ui.database.modals.ObjectObjectRelationValueFragment import com.anytypeio.anytype.ui.page.cover.DocCoverAction import com.anytypeio.anytype.ui.page.cover.DocCoverSliderFragment import com.anytypeio.anytype.ui.page.gallery.FullScreenPictureFragment @@ -87,6 +89,9 @@ import com.anytypeio.anytype.ui.page.modals.actions.DocumentIconActionMenuFragme import com.anytypeio.anytype.ui.page.modals.actions.ProfileIconActionMenuFragment import com.anytypeio.anytype.ui.page.sheets.DocMenuBottomSheet import com.anytypeio.anytype.ui.page.sheets.DocMenuBottomSheet.DocumentMenuActionReceiver +import com.anytypeio.anytype.ui.relations.ObjectRelationDateValueFragment +import com.anytypeio.anytype.ui.relations.ObjectRelationListFragment +import com.anytypeio.anytype.ui.relations.ObjectRelationTextValueFragment import com.bumptech.glide.Glide import com.google.android.material.bottomsheet.BottomSheetBehavior import com.hbisoft.pickit.PickiT @@ -111,11 +116,15 @@ open class PageFragment : AddBlockFragment.AddBlockActionReceiver, TurnIntoActionReceiver, SelectProgrammingLanguageReceiver, + ObjectRelationTextValueFragment.EditObjectRelationTextValueReceiver, + ObjectRelationDateValueFragment.EditObjectRelationDateValueReceiver, DocumentMenuActionReceiver, ClipboardInterceptor, DocCoverAction, PickiTCallbacks { + private val ctx get() = arg(ID_KEY) + private val screen: Point by lazy { screen() } private val scrollAndMoveStateChannel = Channel() @@ -262,6 +271,13 @@ open class PageFragment : } } + override fun onAddObjectClicked(url: String, layout: ObjectType.Layout) { + vm.onAddNewObjectClicked( + type = url, + layout = layout + ) + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (resultCode == Activity.RESULT_OK) { when (requestCode) { @@ -537,6 +553,7 @@ open class PageFragment : UiBlock.LINE_DIVIDER -> vm.onAddDividerBlockClicked(Block.Content.Divider.Style.LINE) UiBlock.THREE_DOTS -> vm.onAddDividerBlockClicked(Block.Content.Divider.Style.DOTS) UiBlock.LINK_TO_OBJECT -> vm.onAddLinkToObjectClicked() + UiBlock.RELATION -> vm.onAddRelationBlockClicked() else -> toast(NOT_IMPLEMENTED_MESSAGE) } } @@ -604,6 +621,10 @@ open class PageFragment : vm.onActionMenuItemClicked(id, action) } + override fun onSetRelationKeyClicked(blockId: Id, key: Id) { + vm.onSetRelationKeyClicked(blockId = blockId, key = key) + } + private fun execute(event: EventWrapper) { event.getContentIfNotHandled()?.let { command -> when (command) { @@ -616,6 +637,7 @@ open class PageFragment : y = shared.y + dimen(R.dimen.dp_48), emoji = command.emoji, target = command.target, + ctx = ctx, image = command.image ).apply { enterTransition = Fade() @@ -636,6 +658,7 @@ open class PageFragment : val fr = ProfileIconActionMenuFragment.new( y = shared.y + dimen(R.dimen.dp_48), target = command.target, + ctx = ctx, image = command.image, name = command.name ).apply { @@ -657,7 +680,7 @@ open class PageFragment : } is Command.OpenAddBlockPanel -> { hideKeyboard() - AddBlockFragment.newInstance().show(childFragmentManager, null) + AddBlockFragment.newInstance(command.ctx).show(childFragmentManager, null) } is Command.OpenTurnIntoPanel -> { TurnIntoFragment.single( @@ -782,6 +805,53 @@ open class PageFragment : SelectProgrammingLanguageFragment.new(command.target) .show(childFragmentManager, null) } + is Command.OpenObjectRelationScreen.Add -> { + hideKeyboard() + ObjectRelationListFragment + .new( + ctx = command.ctx, + target = command.target, + mode = ObjectRelationListFragment.MODE_ADD + ) + .show(childFragmentManager, null) + } + is Command.OpenObjectRelationScreen.List -> { + hideKeyboard() + ObjectRelationListFragment + .new( + ctx = command.ctx, + target = command.target, + mode = ObjectRelationListFragment.MODE_LIST + ) + .show(childFragmentManager, null) + } + is Command.OpenObjectRelationScreen.Value.Default -> { + hideKeyboard() + val fr = ObjectObjectRelationValueFragment.new( + ctx = command.ctx, + target = command.target, + relation = command.relation + ) + fr.show(childFragmentManager, null) + } + is Command.OpenObjectRelationScreen.Value.Text -> { + hideKeyboard() + val fr = ObjectRelationTextValueFragment.new( + ctx = command.ctx, + objectId = command.target, + relationId = command.relation + ) + fr.show(childFragmentManager, null) + } + is Command.OpenObjectRelationScreen.Value.Date -> { + hideKeyboard() + val fr = ObjectRelationDateValueFragment.new( + ctx = command.ctx, + objectId = command.target, + relationId = command.relation + ) + fr.show(childFragmentManager, null) + } } } } @@ -902,8 +972,10 @@ open class PageFragment : topToolbar.invisible() lifecycleScope.launch { delay(DELAY_BEFORE_INIT_SAM_SEARCH) - bottomMenu.showWithAnimation() - showSelectButton() + activity?.runOnUiThread { + bottomMenu.showWithAnimation() + showSelectButton() + } } } } else { @@ -1244,6 +1316,10 @@ open class PageFragment : vm.onEnterSearchModeClicked() } + override fun onDocRelationsClicked() { + vm.onDocRelationsClicked() + } + override fun onAddCoverClicked() { vm.onAddCoverClicked() } @@ -1265,6 +1341,35 @@ open class PageFragment : vm.onDismissBlockActionMenu(childFragmentManager.backStackEntryCount > 0) } + override fun onRelationTextValueChanged(ctx: Id, text: String, objectId: Id, relationId: Id) { + vm.onRelationTextValueChanged( + ctx = ctx, + value = text, + relationId = relationId + ) + } + + override fun onRelationTextNumberValueChanged(ctx: Id, number: Number, objectId: Id, relationId: Id) { + vm.onRelationTextValueChanged( + ctx = ctx, + value = number, + relationId = relationId + ) + } + + override fun onRelationDateValueChanged( + ctx: Id, + timeInSeconds: Number?, + objectId: Id, + relationId: Id + ) { + vm.onRelationTextValueChanged( + ctx = ctx, + relationId = relationId, + value = timeInSeconds + ) + } + //------------ End of Anytype Custom Context Menu ------------ companion object { @@ -1298,4 +1403,5 @@ interface OnFragmentInteractionListener { fun onDismissBlockActionToolbar() fun onAddBookmarkUrlClicked(target: String, url: String) fun onExitToDesktopClicked() + fun onSetRelationKeyClicked(blockId: Id, key: Id) } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/cover/DocCoverGalleryFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/cover/DocCoverGalleryFragment.kt index 9adc973fb3..af824c3961 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/page/cover/DocCoverGalleryFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/cover/DocCoverGalleryFragment.kt @@ -16,7 +16,7 @@ import com.anytypeio.anytype.core_utils.ext.subscribe import com.anytypeio.anytype.core_utils.ext.withParent import com.anytypeio.anytype.core_utils.ui.BaseFragment import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.domain.common.Id +import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.presentation.page.cover.SelectDocCoverViewModel import com.google.android.material.bottomsheet.BottomSheetDialogFragment import kotlinx.android.synthetic.main.fragment_doc_cover_gallery.* diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/cover/DocCoverSliderFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/cover/DocCoverSliderFragment.kt index 982849a8ce..3738b37cb6 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/page/cover/DocCoverSliderFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/cover/DocCoverSliderFragment.kt @@ -14,7 +14,7 @@ import com.anytypeio.anytype.core_utils.ext.arg import com.anytypeio.anytype.core_utils.ext.subscribe import com.anytypeio.anytype.core_utils.ext.withParent import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment -import com.anytypeio.anytype.domain.common.Id +import com.anytypeio.anytype.core_models.Id import com.google.android.material.tabs.TabLayoutMediator import kotlinx.android.synthetic.main.fragment_doc_cover_slider.* diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/cover/UploadCoverImageFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/cover/UploadCoverImageFragment.kt index b0aad68a8c..e1259dd3fb 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/page/cover/UploadCoverImageFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/cover/UploadCoverImageFragment.kt @@ -18,7 +18,7 @@ import com.anytypeio.anytype.core_utils.ext.subscribe import com.anytypeio.anytype.core_utils.ext.withParent import com.anytypeio.anytype.core_utils.ui.BaseFragment import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.domain.common.Id +import com.anytypeio.anytype.core_models.Id import kotlinx.android.synthetic.main.fragment_upload_cover_image.* import timber.log.Timber diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/AddBlockFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/AddBlockFragment.kt index 6f1cc7f08c..efaeb99e66 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/AddBlockFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/AddBlockFragment.kt @@ -5,24 +5,48 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_ui.features.page.modal.AddBlockOrTurnIntoAdapter +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ext.withParent import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager import com.anytypeio.anytype.presentation.page.editor.model.UiBlock +import com.anytypeio.anytype.presentation.page.picker.AddBlockView +import com.anytypeio.anytype.presentation.page.picker.DocumentAddBlockViewModel +import com.anytypeio.anytype.presentation.page.picker.DocumentAddBlockViewModelFactory import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import kotlinx.android.synthetic.main.fragment_add_block.* +import javax.inject.Inject class AddBlockFragment : BaseBottomSheetFragment() { companion object { - fun newInstance(): AddBlockFragment = AddBlockFragment() + fun newInstance(ctx: Id): AddBlockFragment = AddBlockFragment().apply { + arguments = bundleOf(CONTEXT_ID to ctx) + } + + const val CONTEXT_ID = "arg.add-new-block.ctx" } + @Inject + lateinit var factory: DocumentAddBlockViewModelFactory + private val vm by viewModels { factory } + + private val ctx get() = argString(CONTEXT_ID) + private val addBlockOrTurnIntoAdapter = AddBlockOrTurnIntoAdapter( - views = AddBlockOrTurnIntoAdapter.addBlockAdapterData(), - onUiBlockClicked = { type -> dispatchAndExit(type) } + views = mutableListOf(), + onUiBlockClicked = { type -> dispatchAndExit(type) }, + onObjectClicked = { vm.onObjectTypeClicked(it) } ) override fun onCreateView( @@ -38,6 +62,33 @@ class AddBlockFragment : BaseBottomSheetFragment() { skipCollapsedState() } + override fun onStart() { + with(lifecycleScope) { + jobs += subscribe(vm.views) { observeViews(it) } + jobs += subscribe(vm.commands) { observeCommands(it) } + } + super.onStart() + vm.onStart() + } + + private fun observeViews(views: List) { + addBlockOrTurnIntoAdapter.update(views) + } + + private fun observeCommands(commands: DocumentAddBlockViewModel.Commands) { + when (commands) { + is DocumentAddBlockViewModel.Commands.NotifyOnObjectTypeClicked -> { + withParent { + onAddObjectClicked( + commands.url, + commands.layout + ) + } + dismiss() + } + } + } + private fun skipCollapsedState() { dialog?.setOnShowListener { dg -> val bottomSheet = (dg as? BottomSheetDialog)?.findViewById( @@ -63,10 +114,16 @@ class AddBlockFragment : BaseBottomSheetFragment() { dismiss() } - override fun injectDependencies() {} - override fun releaseDependencies() {} + override fun injectDependencies() { + componentManager().documentAddNewBlockComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().documentAddNewBlockComponent.release(ctx) + } interface AddBlockActionReceiver { fun onAddBlockClicked(block: UiBlock) + fun onAddObjectClicked(url: String, layout: ObjectType.Layout) } } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/DocumentEmojiIconPickerFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/DocumentEmojiIconPickerFragment.kt index fdd38067f4..5599cfde60 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/DocumentEmojiIconPickerFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/DocumentEmojiIconPickerFragment.kt @@ -125,11 +125,11 @@ open class DocumentEmojiIconPickerFragment : BaseBottomSheetFragment() { } override fun injectDependencies() { - componentManager().documentEmojiIconPickerComponent.get().inject(this) + componentManager().documentEmojiIconPickerComponent.get(context).inject(this) } override fun releaseDependencies() { - componentManager().documentEmojiIconPickerComponent.release() + componentManager().documentEmojiIconPickerComponent.release(context) } private fun setModalToFullScreenState(dialog: BottomSheetDialog) = diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/SelectProgrammingLanguageFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/SelectProgrammingLanguageFragment.kt index 386cb004bd..ef6452d929 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/SelectProgrammingLanguageFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/SelectProgrammingLanguageFragment.kt @@ -11,7 +11,7 @@ import com.anytypeio.anytype.R import com.anytypeio.anytype.core_ui.extensions.color import com.anytypeio.anytype.core_ui.features.page.modal.SelectProgrammingLanguageAdapter import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment -import com.anytypeio.anytype.domain.common.Id +import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.library_syntax_highlighter.obtainLanguages import com.google.android.material.bottomsheet.BottomSheetDialog import kotlinx.android.synthetic.main.fragment_select_programming_language.* diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/TurnIntoFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/TurnIntoFragment.kt index ad567e174d..2cfc14c0ed 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/TurnIntoFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/TurnIntoFragment.kt @@ -8,10 +8,10 @@ import android.widget.FrameLayout import androidx.core.os.bundleOf import androidx.recyclerview.widget.LinearLayoutManager import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_ui.features.page.TurnIntoActionReceiver import com.anytypeio.anytype.core_ui.features.page.modal.AddBlockOrTurnIntoAdapter import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment -import com.anytypeio.anytype.domain.common.Id import com.anytypeio.anytype.presentation.page.editor.model.UiBlock import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog @@ -47,7 +47,8 @@ class TurnIntoFragment : BaseBottomSheetFragment() { excludedCategories = excludedCategories, excludedTypes = excludedTypes ), - onUiBlockClicked = { type -> dispatchAndExit(type) } + onUiBlockClicked = { type -> dispatchAndExit(type) }, + onObjectClicked = { /*TODO Turn into objects will be implemented later */ } ) } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/BlockActionToolbar.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/BlockActionToolbar.kt index d09d7018d5..837a7b25e9 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/BlockActionToolbar.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/BlockActionToolbar.kt @@ -119,6 +119,8 @@ abstract class BlockActionToolbar : Fragment() { is BlockView.MediaPlaceholder.Picture -> addButtons(view, ACTIONS.VIDEO_PICTURE) is BlockView.Error.Picture -> addButtons(view, ACTIONS.VIDEO_PICTURE) is BlockView.Upload.Picture -> addButtons(view, ACTIONS.VIDEO_PICTURE) + is BlockView.Relation.Related -> addButtons(view, ACTIONS.RELATION) + is BlockView.Relation.Placeholder -> addButtons(view, ACTIONS.RELATION_PLACEHOLDER) } if (BuildConfig.DEBUG) { log(getBlock()) @@ -186,7 +188,7 @@ abstract class BlockActionToolbar : Fragment() { } private fun setBlockBackgroundColor(root: View, color: String? = null) { - if (color != null) { + if (!color.isNullOrEmpty()) { root.setBackgroundColor( ThemeColor.values().first { value -> value.title == color @@ -570,5 +572,27 @@ abstract class BlockActionToolbar : Fragment() { ActionItemType.Divider, ActionItemType.SAM ) + + val RELATION_PLACEHOLDER = listOf( + ActionItemType.AddBelow, + ActionItemType.DividerExtended, + ActionItemType.Delete, + ActionItemType.Divider, + ActionItemType.Duplicate, + ActionItemType.Divider, + ActionItemType.SAM + ) + + val RELATION = listOf( + ActionItemType.AddBelow, + ActionItemType.DividerExtended, + ActionItemType.Delete, + ActionItemType.Divider, + ActionItemType.Duplicate, + ActionItemType.Divider, + ActionItemType.SAM, + ActionItemType.DividerExtended, + ActionItemType.Style + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/BlockActionToolbarFactory.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/BlockActionToolbarFactory.kt index 1c0788a570..a51b54abe0 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/BlockActionToolbarFactory.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/BlockActionToolbarFactory.kt @@ -3,6 +3,7 @@ package com.anytypeio.anytype.ui.page.modals.actions import androidx.core.os.bundleOf import com.anytypeio.anytype.presentation.page.editor.BlockDimensions import com.anytypeio.anytype.presentation.page.editor.model.BlockView +import com.anytypeio.anytype.presentation.page.editor.model.Types object BlockActionToolbarFactory { @@ -39,8 +40,58 @@ object BlockActionToolbarFactory { is BlockView.Title.Document -> TODO() is BlockView.Title.Profile -> TODO() is BlockView.Title.Archive -> TODO() + is BlockView.Relation.Placeholder -> newInstance(block, dimensions) + is BlockView.Relation.Related -> newInstance(block, dimensions) } + fun newInstance( + block: BlockView.Relation.Related, + dimensions: BlockDimensions + ) = when (block.getViewType()) { + Types.HOLDER_RELATION_DEFAULT -> RelationDefaultActionToolbar().apply { + arguments = bundleOf( + BlockActionToolbar.ARG_BLOCK to block, + BlockActionToolbar.ARG_BLOCK_DIMENSIONS to dimensions + ) + } + Types.HOLDER_RELATION_STATUS -> RelationStatusActionToolbar().apply { + arguments = bundleOf( + BlockActionToolbar.ARG_BLOCK to block, + BlockActionToolbar.ARG_BLOCK_DIMENSIONS to dimensions + ) + } + Types.HOLDER_RELATION_TAGS -> RelationTagActionToolbar().apply { + arguments = bundleOf( + BlockActionToolbar.ARG_BLOCK to block, + BlockActionToolbar.ARG_BLOCK_DIMENSIONS to dimensions + ) + } + Types.HOLDER_RELATION_OBJECT -> RelationObjectActionToolbar().apply { + arguments = bundleOf( + BlockActionToolbar.ARG_BLOCK to block, + BlockActionToolbar.ARG_BLOCK_DIMENSIONS to dimensions + ) + } + Types.HOLDER_RELATION_FILE -> RelationFileActionToolbar().apply { + arguments = bundleOf( + BlockActionToolbar.ARG_BLOCK to block, + BlockActionToolbar.ARG_BLOCK_DIMENSIONS to dimensions + ) + } + else -> throw IllegalArgumentException("Wrong BlockView.Relation type :${block.getViewType()}") + } + + fun newInstance( + block: BlockView.Relation.Placeholder, + dimensions: BlockDimensions + ): RelationPlaceholderActionToolbar = + RelationPlaceholderActionToolbar().apply { + arguments = bundleOf( + BlockActionToolbar.ARG_BLOCK to block, + BlockActionToolbar.ARG_BLOCK_DIMENSIONS to dimensions + ) + } + fun newInstance(block: BlockView.Page, dimensions: BlockDimensions): PageBlockActionToolbar = PageBlockActionToolbar().apply { arguments = bundleOf( diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/DocumentIconActionMenuFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/DocumentIconActionMenuFragment.kt index 70c4277c7e..1f5c337d19 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/DocumentIconActionMenuFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/DocumentIconActionMenuFragment.kt @@ -18,6 +18,8 @@ import com.anytypeio.anytype.R import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.analytics.base.EventsDictionary import com.anytypeio.anytype.analytics.base.sendEvent +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_utils.ext.arg import com.anytypeio.anytype.core_utils.ext.parsePath import com.anytypeio.anytype.core_utils.ext.toast import com.anytypeio.anytype.core_utils.ext.visible @@ -44,6 +46,8 @@ import javax.inject.Inject class DocumentIconActionMenuFragment : BaseFragment(R.layout.action_toolbar_page_icon), Observer { + private val ctx get() = arg(ARG_CONTEXT_ID_KEY) + private val target: String get() = requireArguments() .getString(ARG_TARGET_ID_KEY) @@ -250,11 +254,11 @@ class DocumentIconActionMenuFragment : BaseFragment(R.layout.action_toolbar_page } override fun injectDependencies() { - componentManager().documentIconActionMenuComponent.get().inject(this) + componentManager().documentIconActionMenuComponent.get(ctx).inject(this) } override fun releaseDependencies() { - componentManager().documentIconActionMenuComponent.release() + componentManager().documentIconActionMenuComponent.release(ctx) } companion object { @@ -262,13 +266,15 @@ class DocumentIconActionMenuFragment : BaseFragment(R.layout.action_toolbar_page y: Float?, emoji: String?, image: String?, - target: String + target: String, + ctx: Id ): DocumentIconActionMenuFragment = DocumentIconActionMenuFragment().apply { arguments = bundleOf( Y_KEY to y, EMOJI_KEY to emoji, IMAGE_KEY to image, - ARG_TARGET_ID_KEY to target + ARG_TARGET_ID_KEY to target, + ARG_CONTEXT_ID_KEY to ctx ) } @@ -280,6 +286,7 @@ class DocumentIconActionMenuFragment : BaseFragment(R.layout.action_toolbar_page private const val ANIM_START_DELAY = 200L private const val ANIM_DURATION = 200L private const val ARG_TARGET_ID_KEY = "arg.picker.target.id" + private const val ARG_CONTEXT_ID_KEY = "arg.picker.target.id" private const val MISSING_TARGET_ERROR = "Missing target id" private const val COULD_NOT_PARSE_PATH_ERROR = "Could not parse path to your image" } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/ProfileIconActionMenuFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/ProfileIconActionMenuFragment.kt index dab21982b2..5e64ace9e5 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/ProfileIconActionMenuFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/ProfileIconActionMenuFragment.kt @@ -15,14 +15,15 @@ import androidx.core.os.bundleOf import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Url import com.anytypeio.anytype.core_ui.extensions.avatarColor +import com.anytypeio.anytype.core_utils.ext.arg import com.anytypeio.anytype.core_utils.ext.firstDigitByHash import com.anytypeio.anytype.core_utils.ext.parsePath import com.anytypeio.anytype.core_utils.ext.toast import com.anytypeio.anytype.core_utils.ui.BaseFragment import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.domain.common.Id -import com.anytypeio.anytype.domain.common.Url import com.anytypeio.anytype.library_page_icon_picker_widget.ui.ActionMenuAdapter import com.anytypeio.anytype.library_page_icon_picker_widget.ui.ActionMenuDivider import com.anytypeio.anytype.presentation.page.picker.DocumentIconActionMenuViewModel @@ -35,6 +36,8 @@ import javax.inject.Inject class ProfileIconActionMenuFragment : BaseFragment(R.layout.action_toolbar_profile_icon), Observer { + private val ctx get() = arg(ARG_CONTEXT_ID_KEY) + /** * avatar image url */ @@ -208,11 +211,11 @@ class ProfileIconActionMenuFragment : BaseFragment(R.layout.action_toolbar_profi } override fun injectDependencies() { - componentManager().documentIconActionMenuComponent.get().inject(this) + componentManager().documentIconActionMenuComponent.get(ctx).inject(this) } override fun releaseDependencies() { - componentManager().documentIconActionMenuComponent.release() + componentManager().documentIconActionMenuComponent.release(ctx) } private fun exit() { @@ -224,13 +227,15 @@ class ProfileIconActionMenuFragment : BaseFragment(R.layout.action_toolbar_profi y: Float?, image: String?, name: String?, - target: String + target: String, + ctx: Id ): ProfileIconActionMenuFragment = ProfileIconActionMenuFragment().apply { arguments = bundleOf( Y_KEY to y, IMAGE_KEY to image, NAME_KEY to name, - ARG_TARGET_ID_KEY to target + ARG_TARGET_ID_KEY to target, + ARG_CONTEXT_ID_KEY to ctx ) } @@ -242,6 +247,7 @@ class ProfileIconActionMenuFragment : BaseFragment(R.layout.action_toolbar_profi private const val ANIM_START_DELAY = 200L private const val ANIM_DURATION = 200L private const val ARG_TARGET_ID_KEY = "arg.picker.target.id" + private const val ARG_CONTEXT_ID_KEY = "arg.picker.target.context" private const val MISSING_TARGET_ERROR = "Missing target id" } } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/RelationDefaultActionToolbar.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/RelationDefaultActionToolbar.kt new file mode 100644 index 0000000000..3b7c230558 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/RelationDefaultActionToolbar.kt @@ -0,0 +1,258 @@ +package com.anytypeio.anytype.ui.page.modals.actions + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.extensions.color +import com.anytypeio.anytype.core_ui.widgets.GridCellFileItem +import com.anytypeio.anytype.core_ui.widgets.RelationObjectItem +import com.anytypeio.anytype.core_ui.widgets.text.TagWidget +import com.anytypeio.anytype.core_utils.ext.dimen +import com.anytypeio.anytype.core_utils.ext.visible +import com.anytypeio.anytype.presentation.page.editor.ThemeColor +import com.anytypeio.anytype.presentation.page.editor.model.BlockView +import com.anytypeio.anytype.presentation.relations.DocumentRelationView + +class RelationDefaultActionToolbar : BlockActionToolbar() { + + lateinit var block: BlockView.Relation.Related + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + block = arguments?.getParcelable(ARG_BLOCK)!! + } + + override fun initUi(view: View, colorView: ImageView?, backgroundView: ImageView?) { + view.findViewById(R.id.root).apply { + updateLayoutParams { + topMargin = 0 + bottomMargin = 0 + } + setPadding(0, 0, 0, 0) + processBackgroundColor( + root = this, + color = block.background, + bgImage = backgroundView + ) + } + view.findViewById(R.id.content).apply { + val paddingStart = context.dimen(R.dimen.default_document_content_padding_start) + val paddingEnd = context.dimen(R.dimen.default_document_content_padding_end) + updatePadding(left = paddingStart.toInt(), right = paddingEnd.toInt()) + } + val item = block.view as DocumentRelationView.Default + view.findViewById(R.id.tvRelationTitle).text = item.name + view.findViewById(R.id.tvRelationValue).text = item.value + setConstraints() + } + + override fun getBlock(): BlockView = block + override fun blockLayout(): Int = R.layout.item_block_relation_default +} + +class RelationStatusActionToolbar : BlockActionToolbar() { + + lateinit var block: BlockView.Relation.Related + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + block = arguments?.getParcelable(ARG_BLOCK)!! + } + + override fun initUi(view: View, colorView: ImageView?, backgroundView: ImageView?) { + view.findViewById(R.id.root).apply { + updateLayoutParams { + topMargin = 0 + bottomMargin = 0 + } + setPadding(0, 0, 0, 0) + processBackgroundColor( + root = this, + color = block.background, + bgImage = backgroundView + ) + } + view.findViewById(R.id.content).apply { + val paddingStart = context.dimen(R.dimen.default_document_content_padding_start) + val paddingEnd = context.dimen(R.dimen.default_document_content_padding_end) + updatePadding(left = paddingStart.toInt(), right = paddingEnd.toInt()) + } + val item = block.view as DocumentRelationView.Status + view.findViewById(R.id.tvRelationTitle).text = item.name + view.findViewById(R.id.tvRelationValue).apply { + if (item.status.isNotEmpty()) { + val status = item.status.first() + text = status.status + val color = ThemeColor.values().find { v -> v.title == status.color } + if (color != null) { + setTextColor(color.text) + } else { + setTextColor(context.color(R.color.default_filter_status_text_color)) + } + } else { + text = null + } + } + setConstraints() + } + + override fun getBlock(): BlockView = block + override fun blockLayout(): Int = R.layout.item_block_relation_status +} + +class RelationTagActionToolbar : BlockActionToolbar() { + + lateinit var block: BlockView.Relation.Related + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + block = arguments?.getParcelable(ARG_BLOCK)!! + } + + override fun initUi(view: View, colorView: ImageView?, backgroundView: ImageView?) { + view.findViewById(R.id.root).apply { + updateLayoutParams { + topMargin = 0 + bottomMargin = 0 + } + setPadding(0, 0, 0, 0) + processBackgroundColor( + root = this, + color = block.background, + bgImage = backgroundView + ) + } + view.findViewById(R.id.content).apply { + val paddingStart = context.dimen(R.dimen.default_document_content_padding_start) + val paddingEnd = context.dimen(R.dimen.default_document_content_padding_end) + updatePadding(left = paddingStart.toInt(), right = paddingEnd.toInt()) + } + val item = block.view as DocumentRelationView.Tags + view.findViewById(R.id.tvRelationTitle).text = item.name + item.tags.forEachIndexed { index, tag -> + when (index) { + 0 -> view.findViewById(R.id.tag0).apply { setup(tag.tag, tag.color) } + 1 -> view.findViewById(R.id.tag1).apply { setup(tag.tag, tag.color) } + 2 -> view.findViewById(R.id.tag2).apply { setup(tag.tag, tag.color) } + 3 -> view.findViewById(R.id.tag3).apply { setup(tag.tag, tag.color) } + 4 -> view.findViewById(R.id.tag4).apply { setup(tag.tag, tag.color) } + 5 -> view.findViewById(R.id.tag5).apply { setup(tag.tag, tag.color) } + } + } + setConstraints() + } + + override fun getBlock(): BlockView = block + override fun blockLayout(): Int = R.layout.item_block_relation_tag +} + +class RelationObjectActionToolbar : BlockActionToolbar() { + + lateinit var block: BlockView.Relation.Related + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + block = arguments?.getParcelable(ARG_BLOCK)!! + } + + override fun initUi(view: View, colorView: ImageView?, backgroundView: ImageView?) { + view.findViewById(R.id.root).apply { + updateLayoutParams { + topMargin = 0 + bottomMargin = 0 + } + setPadding(0, 0, 0, 0) + processBackgroundColor( + root = this, + color = block.background, + bgImage = backgroundView + ) + } + view.findViewById(R.id.content).apply { + val paddingStart = context.dimen(R.dimen.default_document_content_padding_start) + val paddingEnd = context.dimen(R.dimen.default_document_content_padding_end) + updatePadding(left = paddingStart.toInt(), right = paddingEnd.toInt()) + } + val item = block.view as DocumentRelationView.Object + view.findViewById(R.id.tvRelationTitle).text = item.name + item.objects.forEachIndexed { index, objectView -> + when (index) { + 0 -> view.findViewById(R.id.obj0).apply { + visible() + setup(objectView.name, objectView.emoji, objectView.image) + } + 1 -> view.findViewById(R.id.obj1).apply { + visible() + setup(objectView.name, objectView.emoji, objectView.image) + } + 2 -> view.findViewById(R.id.obj2).apply { + visible() + setup(objectView.name, objectView.emoji, objectView.image) + } + 3 -> view.findViewById(R.id.obj3).apply { + visible() + setup(objectView.name, objectView.emoji, objectView.image) + } + } + } + setConstraints() + } + + override fun getBlock(): BlockView = block + override fun blockLayout(): Int = R.layout.item_block_relation_object +} + +class RelationFileActionToolbar : BlockActionToolbar() { + + lateinit var block: BlockView.Relation.Related + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + block = arguments?.getParcelable(ARG_BLOCK)!! + } + + override fun initUi(view: View, colorView: ImageView?, backgroundView: ImageView?) { + view.findViewById(R.id.root).apply { + updateLayoutParams { + topMargin = 0 + bottomMargin = 0 + } + setPadding(0, 0, 0, 0) + processBackgroundColor( + root = this, + color = block.background, + bgImage = backgroundView + ) + } + view.findViewById(R.id.content).apply { + val paddingStart = context.dimen(R.dimen.default_document_content_padding_start) + val paddingEnd = context.dimen(R.dimen.default_document_content_padding_end) + updatePadding(left = paddingStart.toInt(), right = paddingEnd.toInt()) + } + val item = block.view as DocumentRelationView.File + view.findViewById(R.id.tvRelationTitle).text = item.name + item.files.forEachIndexed { index, file -> + when (index) { + 0 -> view.findViewById(R.id.file0).apply { + setup(name = file.name, mime = file.mime) + } + 1 -> view.findViewById(R.id.file1).apply { + setup(name = file.name, mime = file.mime) + } + 2 -> view.findViewById(R.id.file2).apply { + setup(name = file.name, mime = file.mime) + } + } + } + setConstraints() + } + + override fun getBlock(): BlockView = block + override fun blockLayout(): Int = R.layout.item_block_relation_file +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/RelationPlaceholderActionToolbar.kt b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/RelationPlaceholderActionToolbar.kt new file mode 100644 index 0000000000..1e41592b4d --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/page/modals/actions/RelationPlaceholderActionToolbar.kt @@ -0,0 +1,36 @@ +package com.anytypeio.anytype.ui.page.modals.actions + +import android.os.Bundle +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.core.view.updateLayoutParams +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.presentation.page.editor.model.BlockView + +class RelationPlaceholderActionToolbar : BlockActionToolbar() { + + lateinit var block: BlockView.Relation.Placeholder + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + block = arguments?.getParcelable(ARG_BLOCK)!! + } + + override fun initUi(view: View, colorView: ImageView?, backgroundView: ImageView?) { + view.findViewById(R.id.root).apply { + updateLayoutParams { + topMargin = 0 + bottomMargin = 0 + leftMargin = 0 + rightMargin = 0 + } + setPadding(0, 0, 0, 0) + } + setConstraints() + } + + override fun getBlock(): BlockView = block + + override fun blockLayout(): Int = R.layout.item_block_relation_placeholder +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/relations/AddObjectRelationObjectValueFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/relations/AddObjectRelationObjectValueFragment.kt new file mode 100644 index 0000000000..d75ceacffc --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/relations/AddObjectRelationObjectValueFragment.kt @@ -0,0 +1,148 @@ +package com.anytypeio.anytype.ui.relations + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.features.relations.RelationObjectValueAdapter +import com.anytypeio.anytype.core_ui.reactive.textChanges +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ext.withParent +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.relations.AddObjectRelationObjectValueViewModel +import com.anytypeio.anytype.presentation.relations.AddObjectValueCommand +import com.anytypeio.anytype.presentation.relations.AddObjectValueView +import kotlinx.android.synthetic.main.fragment_relation_value_object_add.* +import javax.inject.Inject + +class AddObjectRelationObjectValueFragment : BaseBottomSheetFragment() { + + @Inject + lateinit var factory: AddObjectRelationObjectValueViewModel.Factory + val vm: AddObjectRelationObjectValueViewModel by viewModels { factory } + + private val ctx get() = argString(CONTEXT_ID) + private val objectId get() = argString(OBJECT_ID) + private val relationId get() = argString(RELATION_ID) + private val flow get() = arg(FLOW_KEY) + + private val adapter by lazy { + RelationObjectValueAdapter( + onObjectClick = { objectId -> vm.onObjectClicked(objectId) } + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setTransparentBackground() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_relation_value_object_add, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + rvObjects.layoutManager = LinearLayoutManager(requireContext()) + rvObjects.adapter = adapter + btnBottomAction.setOnClickListener { vm.onActionButtonClicked() } + } + + override fun onStart() { + jobs += lifecycleScope.subscribe(vm.viewsFiltered) { observeState(it) } + jobs += lifecycleScope.subscribe(vm.commands) { observeCommands(it) } + jobs += lifecycleScope.subscribe(searchRelationInput.textChanges()) + { vm.onFilterTextChanged(it.toString()) } + super.onStart() + vm.onStart(objectId = objectId, relationId = relationId) + } + + private fun observeState(state: AddObjectValueView) { + adapter.update(state.objects) + tvObjectsCount.text = state.count + } + + private fun observeCommands(command: AddObjectValueCommand) { + when (command) { + is AddObjectValueCommand.DispatchResult -> { + dispatchResultAndDismiss(command.ids) + } + } + } + + private fun dispatchResultAndDismiss(ids: List) { + withParent { + onRelationObjectValueChanged( + ctx = ctx, + objectId = objectId, + relationId = relationId, + ids = ids + ) + } + dismiss() + } + + private fun setTransparentBackground() { + dialog?.window?.setBackgroundDrawableResource(android.R.color.transparent) + } + + override fun injectDependencies() { + if (flow == FLOW_DEFAULT) { + componentManager().addObjectRelationObjectValueComponent.get(ctx).inject(this) + } else { + componentManager().addObjectSetObjectRelationObjectValueComponent.get(ctx).inject(this) + } + } + + override fun releaseDependencies() { + if (flow == FLOW_DEFAULT) { + componentManager().addObjectRelationObjectValueComponent.release(ctx) + } else { + componentManager().addObjectSetObjectRelationObjectValueComponent.release(ctx) + } + } + + companion object { + + fun new( + ctx: Id, + objectId: Id, + relationId: Id, + flow: Int = FLOW_DEFAULT + ) = AddObjectRelationObjectValueFragment().apply { + arguments = bundleOf( + CONTEXT_ID to ctx, + OBJECT_ID to objectId, + RELATION_ID to relationId, + FLOW_KEY to flow + ) + } + + const val CONTEXT_ID = "arg.relation.add.object.context" + const val RELATION_ID = "arg.relation.add.object.relation.id" + const val OBJECT_ID = "arg.relation.add.object.object.id" + const val FLOW_KEY = "arg.relation.add.object.flow" + const val FLOW_DEFAULT = 0 + const val FLOW_DATAVIEW = 1 + } + + interface AddObjectRelationObjectValueReceiver { + fun onRelationObjectValueChanged( + ctx: Id, + objectId: Id, + relationId: Id, + ids: List + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/relations/AddObjectRelationValueFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/relations/AddObjectRelationValueFragment.kt new file mode 100644 index 0000000000..d5974dfa4e --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/relations/AddObjectRelationValueFragment.kt @@ -0,0 +1,247 @@ +package com.anytypeio.anytype.ui.relations + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.features.sets.ObjectRelationValueAdapter +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_ui.reactive.textChanges +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.relations.AddObjectObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.relations.AddObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.relations.AddObjectSetObjectRelationValueViewModel +import com.anytypeio.anytype.presentation.sets.ObjectRelationValueViewModel +import kotlinx.android.synthetic.main.add_object_relation_value_fragment.* +import kotlinx.android.synthetic.main.object_relation_value_fragment.filterInput +import kotlinx.android.synthetic.main.object_relation_value_fragment.recycler +import javax.inject.Inject + +abstract class AddObjectRelationValueFragment : BaseBottomSheetFragment() { + + val ctx get() = argString(CTX_KEY) + val relation get() = argString(RELATION_KEY) + val target get() = argString(TARGET_KEY) + val flow get() = arg(FLOW_KEY) + val dataview get() = argString(DATAVIEW_KEY) + val viewer get() = argString(VIEWER_KEY) + + abstract val vm: AddObjectRelationValueViewModel + + private val editCellTagAdapter by lazy { + ObjectRelationValueAdapter( + onCreateOptionClicked = { name -> onCreateOptionClicked(name) }, + onTagClicked = { tag -> vm.onTagClicked(tag) }, + onStatusClicked = { status -> onStatusClicked(status) }, + onRemoveTagClicked = {}, + onRemoveStatusClicked = {}, + onObjectClicked = {}, + onRemoveObjectClicked = {}, + onFileClicked = {}, + onRemoveFileClicked = {} + ) + } + + abstract fun onStatusClicked(status: ObjectRelationValueViewModel.ObjectRelationValueView.Status) + abstract fun onCreateOptionClicked(name: String) + abstract fun onAddButtonClicked() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.add_object_relation_value_fragment, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + recycler.apply { + layoutManager = LinearLayoutManager(context) + adapter = editCellTagAdapter + addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply { + setDrawable(drawable(R.drawable.divider_relations)) + } + ) + } + with(lifecycleScope) { + subscribe(btnAdd.clicks()) { + onAddButtonClicked() + } + subscribe(filterInput.textChanges()) { vm.onFilterInputChanged(it.toString()) } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.views) { editCellTagAdapter.update(it) } + subscribe(vm.counter) { tvSelectionCounter.text = it.toString() } + subscribe(vm.isAddButtonVisible) { isVisible -> + if (!isVisible) btnAddContainer.gone() else btnAddContainer.visible() + } + subscribe(vm.isDimissed) { isDismissed -> + if (isDismissed) { + proceedWithExiting() + } + } + } + } + + open fun proceedWithExiting() { + filterInput.apply { + clearFocus() + hideKeyboard() + } + dismiss() + } + + override fun onStart() { + super.onStart() + vm.onStart( + target = target, + relationId = relation + ) + } + + override fun onStop() { + super.onStop() + vm.onStop() + } + + companion object { + const val CTX_KEY = "arg.add-object-relation-value.ctx" + const val RELATION_KEY = "arg.add-object-relation-value.relation" + const val TARGET_KEY = "arg.add-object-relation-value.target" + const val FLOW_KEY = "arg.add-object-relation-value.flow" + const val DATAVIEW_KEY = "arg.add-object-relation-value.data-view" + const val VIEWER_KEY = "arg.add-object-relation-value.viewer" + } +} + +open class AddObjectSetObjectRelationValueFragment : AddObjectRelationValueFragment() { + + @Inject + lateinit var factory: AddObjectSetObjectRelationValueViewModel.Factory + override val vm: AddObjectSetObjectRelationValueViewModel by viewModels { factory } + + override fun onStatusClicked(status: ObjectRelationValueViewModel.ObjectRelationValueView.Status) { + vm.onAddObjectSetStatusClicked( + ctx = ctx, + obj = target, + dataview = dataview, + viewer = viewer, + relation = relation, + status = status + ) + } + + override fun onCreateOptionClicked(name: String) { + vm.onCreateDataViewRelationOptionClicked( + ctx = ctx, + relation = relation, + name = name, + dataview = dataview, + viewer = viewer, + target = target + ) + } + + override fun onAddButtonClicked() { + vm.onAddSelectedValuesToDataViewClicked( + ctx = ctx, + viewer = viewer, + target = target, + relation = relation, + dataview = dataview + ) + } + + override fun injectDependencies() { + componentManager().addObjectSetObjectRelationValueComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().addObjectSetObjectRelationValueComponent.release(ctx) + } + + companion object { + fun new( + ctx: Id, + target: Id, + relation: Id, + dataview: Id, + viewer: Id + ) = AddObjectSetObjectRelationValueFragment().apply { + arguments = bundleOf( + CTX_KEY to ctx, + TARGET_KEY to target, + RELATION_KEY to relation, + DATAVIEW_KEY to dataview, + VIEWER_KEY to viewer + ) + } + } +} + +class AddObjectObjectRelationValueFragment : AddObjectRelationValueFragment() { + + @Inject + lateinit var factory: AddObjectObjectRelationValueViewModel.Factory + override val vm: AddObjectObjectRelationValueViewModel by viewModels { factory } + + override fun onStatusClicked(status: ObjectRelationValueViewModel.ObjectRelationValueView.Status) { + vm.onAddObjectStatusClicked( + ctx = ctx, + relation = relation, + status = status + ) + } + + override fun onCreateOptionClicked(name: String) { + vm.onCreateObjectRelationOptionClicked( + ctx = ctx, + relation = relation, + obj = target, + name = name + ) + } + + override fun onAddButtonClicked() { + vm.onAddSelectedValuesToObjectClicked( + ctx = ctx, + obj = target, + relation = relation, + ) + } + + override fun injectDependencies() { + componentManager().addObjectObjectRelationValueComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().addObjectObjectRelationValueComponent.release(ctx) + } + + companion object { + fun new( + ctx: Id, + objectId: Id, + relationId: Id + ) = AddObjectObjectRelationValueFragment().apply { + arguments = bundleOf( + CTX_KEY to ctx, + TARGET_KEY to objectId, + RELATION_KEY to relationId + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/relations/CreateDataViewRelationFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/relations/CreateDataViewRelationFragment.kt new file mode 100644 index 0000000000..7a9b2ba53b --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/relations/CreateDataViewRelationFragment.kt @@ -0,0 +1,98 @@ +package com.anytypeio.anytype.ui.relations + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.core_ui.features.relations.create.RelationFormatAdapter +import com.anytypeio.anytype.core_ui.reactive.editorActionEvents +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.relations.CreateDataViewRelationViewModel +import com.anytypeio.anytype.presentation.relations.CreateDataViewRelationViewModelFactory +import kotlinx.android.synthetic.main.fragment_create_data_view_relation.* +import javax.inject.Inject + +class CreateDataViewRelationFragment : BaseBottomSheetFragment() { + + private val actionHandler: (Int) -> Boolean = { action -> + action == EditorInfo.IME_ACTION_DONE + } + + private val formatAdapter by lazy { + RelationFormatAdapter { vm.onFormatClicked(it) } + } + + private val ctx: String get() = argString(CONTEXT_KEY) + private val target: String get() = argString(TARGET_KEY) + + private val vm by viewModels { factory } + + @Inject + lateinit var factory: CreateDataViewRelationViewModelFactory + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_create_data_view_relation, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + formatRecycler.adapter = formatAdapter + lifecycleScope.subscribe(nameInputField.editorActionEvents(actionHandler)) { + if (nameInputField.text.isNotEmpty()) { + nameInputField.hideKeyboard() + dispatchResult() + } else { + toast("Relation name is required") + } + } + } + + private fun dispatchResult() { + withParent { + onAddDataViewRelationRequest( + context = ctx, + target = target, + name = nameInputField.text.toString(), + format = vm.formats.value.first { it.isSelected }.format + ) + } + dismiss() + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + lifecycleScope.subscribe(vm.formats) { formatAdapter.update(it) } + } + + override fun injectDependencies() { + componentManager().createDataViewRelationComponent.get().inject(this) + } + + override fun releaseDependencies() { + componentManager().createDataViewRelationComponent.release() + } + + interface OnAddDataViewRelationRequestReceiver { + fun onAddDataViewRelationRequest(context: String, target: Id, name: String, format: Relation.Format) + } + + companion object { + fun new(ctx: Id, target: Id) = CreateDataViewRelationFragment().apply { + arguments = bundleOf(CONTEXT_KEY to ctx, TARGET_KEY to target) + } + + private const val CONTEXT_KEY = "arg.create-dv-relation.context" + private const val TARGET_KEY = "arg.create-dv-relation.target" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationDateValueFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationDateValueFragment.kt new file mode 100644 index 0000000000..0c87600e65 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationDateValueFragment.kt @@ -0,0 +1,166 @@ +package com.anytypeio.anytype.ui.relations + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.DateValueCommand +import com.anytypeio.anytype.presentation.sets.DateValueView +import com.anytypeio.anytype.presentation.sets.ObjectRelationDateValueViewModel +import com.anytypeio.anytype.ui.sets.modals.DatePickerFragment +import kotlinx.android.synthetic.main.fragment_object_relation_date_value.* +import javax.inject.Inject + +open class ObjectRelationDateValueFragment : BaseBottomSheetFragment(), + DatePickerFragment.DatePickerReceiver { + + @Inject + lateinit var factory: ObjectRelationDateValueViewModel.Factory + val vm: ObjectRelationDateValueViewModel by viewModels { factory } + + private val ctx get() = argString(CONTEXT_ID) + private val objectId get() = argString(OBJECT_ID) + private val relationId get() = argString(RELATION_ID) + private val flow get() = arg(FLOW_KEY) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_object_relation_date_value, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setTransparentBackground() + btnBottomAction.setOnClickListener { vm.onActionClicked() } + btnClear.setOnClickListener { vm.onClearClicked() } + ivExactDayCheck.setOnClickListener { vm.onExactDayClicked() } + tvExactDay.setOnClickListener { vm.onExactDayClicked() } + tvDate.setOnClickListener { vm.onExactDayClicked() } + tvToday.setOnClickListener { vm.onTodayClicked() } + ivTodayCheck.setOnClickListener { vm.onTodayClicked() } + tvTomorrow.setOnClickListener { vm.onTomorrowClicked() } + ivTomorrowCheck.setOnClickListener { vm.onTomorrowClicked() } + tvYesterday.setOnClickListener { vm.onYesterdayClicked() } + ivYesterdayCheck.setOnClickListener { vm.onYesterdayClicked() } + } + + override fun onStart() { + jobs += lifecycleScope.subscribe(vm.views) { observeState(it) } + jobs += lifecycleScope.subscribe(vm.commands) { observeCommands(it) } + super.onStart() + vm.onStart(objectId = objectId, relationId = relationId) + } + + private fun observeState(state: DateValueView) { + tvRelationHeader.text = state.title + ivTodayCheck.invisible() + ivYesterdayCheck.invisible() + ivTomorrowCheck.invisible() + ivExactDayCheck.invisible() + tvDate.text = null + if (state.isToday) { + ivTodayCheck.visible() + } + if (state.isYesterday) { + ivYesterdayCheck.visible() + } + if (state.isTomorrow) { + ivTomorrowCheck.visible() + } + if (state.exactDayFormat != null) { + tvDate.text = state.exactDayFormat + ivExactDayCheck.visible() + } + } + + private fun observeCommands(command: DateValueCommand) { + when (command) { + is DateValueCommand.DispatchResult -> { + dispatchResultAndDismiss(command.timeInSeconds) + } + is DateValueCommand.OpenDatePicker -> { + DatePickerFragment.new(command.timeInSeconds) + .show(childFragmentManager, null) + } + } + } + + private fun dispatchResultAndDismiss(timeInSeconds: Double?) { + withParent { + onRelationDateValueChanged( + ctx = ctx, + objectId = objectId, + relationId = relationId, + timeInSeconds = timeInSeconds + ) + } + dismiss() + } + + private fun setTransparentBackground() { + dialog?.window?.setBackgroundDrawableResource(android.R.color.transparent) + } + + override fun onPickDate(timeInSeconds: Long) { + vm.setDate(timeInSeconds) + } + + override fun injectDependencies() { + if (flow == FLOW_DATAVIEW) { + componentManager().objectSetObjectRelationDataValueComponent.get(ctx).inject(this) + } else { + componentManager().objectObjectRelationDateValueComponet.get(ctx).inject(this) + } + } + + override fun releaseDependencies() { + if (flow == FLOW_DATAVIEW) { + componentManager().objectSetObjectRelationDataValueComponent.release(ctx) + } else { + componentManager().objectObjectRelationDateValueComponet.release(ctx) + } + } + + companion object { + + fun new( + ctx: Id, + relationId: Id, + objectId: Id, + flow: Int = FLOW_DEFAULT + ) = ObjectRelationDateValueFragment().apply { + arguments = bundleOf( + CONTEXT_ID to ctx, + RELATION_ID to relationId, + OBJECT_ID to objectId, + FLOW_KEY to flow + ) + } + + const val CONTEXT_ID = "arg.relation.date.context" + const val RELATION_ID = "arg.relation.date.relation.id" + const val OBJECT_ID = "arg.relation.date.object.id" + + const val FLOW_KEY = "arg.relation.date.flow" + const val FLOW_DEFAULT = 0 + const val FLOW_DATAVIEW = 1 + } + + interface EditObjectRelationDateValueReceiver { + fun onRelationDateValueChanged( + ctx: Id, + timeInSeconds: Number?, + objectId: Id, + relationId: Id + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationListFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationListFragment.kt new file mode 100644 index 0000000000..0e9a3c0c8e --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationListFragment.kt @@ -0,0 +1,201 @@ +package com.anytypeio.anytype.ui.relations + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.features.relations.DocumentRelationAdapter +import com.anytypeio.anytype.core_ui.reactive.textChanges +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.relations.ObjectRelationListViewModel +import com.anytypeio.anytype.presentation.relations.ObjectRelationListViewModel.Command +import com.anytypeio.anytype.presentation.relations.ObjectRelationListViewModelFactory +import com.anytypeio.anytype.ui.database.modals.ObjectObjectRelationValueFragment +import com.anytypeio.anytype.ui.page.OnFragmentInteractionListener +import kotlinx.android.synthetic.main.fragment_object_relation_list.* +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onStart +import javax.inject.Inject + +open class ObjectRelationListFragment : BaseBottomSheetFragment(), + ObjectRelationTextValueFragment.EditObjectRelationTextValueReceiver, + ObjectRelationDateValueFragment.EditObjectRelationDateValueReceiver { + + private val vm by viewModels { factory } + + @Inject + lateinit var factory: ObjectRelationListViewModelFactory + + private val ctx: String get() = argString(ARG_CTX) + private val target: String? get() = argStringOrNull(ARG_TARGET) + private val mode: Int get() = argInt(ARG_MODE) + + private val docRelationAdapter by lazy { + DocumentRelationAdapter(emptyList()) { + vm.onRelationClicked( + ctx = ctx, + target = target, + view = it + ) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_object_relation_list, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + recycler.apply { + adapter = docRelationAdapter + layoutManager = LinearLayoutManager(context) + addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply { + setDrawable(drawable(R.drawable.divider_relations)) + } + ) + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + if (mode == MODE_ADD) { + searchBar.visible() + val queries = searchRelationInput.textChanges() + .onStart { emit(searchRelationInput.text.toString()) } + val views = vm.views.combine(queries) { views, query -> + if (views.isEmpty()) { + views + } else { + views.filter { it.name.contains(query, true) } + } + } + subscribe(views) { docRelationAdapter.update(it) } + } else { + searchBar.gone() + subscribe(vm.views) { docRelationAdapter.update(it) } + } + subscribe(vm.commands) { command -> execute(command) } + } + } + + private fun execute(command: Command) { + when (command) { + is Command.EditTextRelationValue -> { + val fr = ObjectRelationTextValueFragment.new( + ctx = ctx, + relationId = command.relation, + objectId = command.target + ) + fr.show(childFragmentManager, null) + } + is Command.EditDateRelationValue -> { + val fr = ObjectRelationDateValueFragment.new( + ctx = ctx, + relationId = command.relation, + objectId = command.target + ) + fr.show(childFragmentManager, null) + } + is Command.EditRelationValue -> { + val fr = ObjectObjectRelationValueFragment.new( + ctx = ctx, + relation = command.relation, + target = command.target + ) + fr.show(childFragmentManager, null) + } + is Command.SetRelationKey -> { + withParent { + onSetRelationKeyClicked( + blockId = command.blockId, + key = command.key + ) + } + dismiss() + } + } + } + + override fun onStart() { + super.onStart() + if (mode == MODE_LIST) { + vm.onStartListMode(ctx) + } else { + vm.onStartAddMode(ctx) + } + } + + override fun onStop() { + super.onStop() + vm.onStop() + } + + override fun onRelationTextValueChanged(ctx: Id, text: String, objectId: Id, relationId: Id) { + vm.onRelationTextValueChanged( + ctx = ctx, + value = text, + objectId = objectId, + relationId = relationId + ) + } + + override fun onRelationTextNumberValueChanged(ctx: Id, number: Number, objectId: Id, relationId: Id) { + vm.onRelationTextValueChanged( + ctx = ctx, + value = number, + objectId = objectId, + relationId = relationId + ) + } + + override fun onRelationDateValueChanged( + ctx: Id, + timeInSeconds: Number?, + objectId: Id, + relationId: Id + ) { + vm.onRelationTextValueChanged( + ctx = ctx, + objectId = objectId, + relationId = relationId, + value = timeInSeconds + ) + } + + override fun injectDependencies() { + componentManager().documentRelationComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().documentRelationComponent.release(ctx) + } + + companion object { + fun new(ctx: String, target: String?, mode: Int) = ObjectRelationListFragment().apply { + arguments = bundleOf( + ARG_CTX to ctx, + ARG_TARGET to target, + ARG_MODE to mode + ) + } + + const val ARG_CTX = "arg.document-relation.ctx" + const val ARG_MODE = "arg.document-relation.mode" + const val ARG_TARGET = "arg.document-relation.target" + const val MODE_ADD = 1 + const val MODE_LIST = 2 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationTextValueFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationTextValueFragment.kt new file mode 100644 index 0000000000..61f19c7fcc --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationTextValueFragment.kt @@ -0,0 +1,204 @@ +package com.anytypeio.anytype.ui.relations + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.features.relations.ObjectRelationTextValueAdapter +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.EditGridCellAction +import com.anytypeio.anytype.presentation.sets.ObjectRelationTextValueView +import com.anytypeio.anytype.presentation.sets.ObjectRelationTextValueViewModel +import kotlinx.android.synthetic.main.fragment_object_relation_text_value.* +import javax.inject.Inject + +open class ObjectRelationTextValueFragment : BaseBottomSheetFragment() { + + @Inject + lateinit var factory: ObjectRelationTextValueViewModel.Factory + + private val vm: ObjectRelationTextValueViewModel by viewModels { factory } + + private val ctx get() = arg(CONTEXT_ID) + private val relationId get() = arg(RELATION_ID) + private val objectId get() = arg(OBJECT_ID) + private val flow get() = arg(FLOW_KEY) + + private val relationValueAdapter by lazy { + ObjectRelationTextValueAdapter( + items = emptyList(), + actionClick = { handleActions(it) }, + onEditCompleted = { view, txt -> + if (view is ObjectRelationTextValueView.Number) { + try { + dispatchNumberResultAndExit(txt.toDouble()) + } catch (e: NumberFormatException) { + toast("Invalid number format. Please try again.") + } + } else { + dispatchTextResultAndExit(txt) + } + } + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_object_relation_text_value, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + recycler.apply { + layoutManager = LinearLayoutManager(context) + adapter = relationValueAdapter + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.views) { relationValueAdapter.update(it) } + subscribe(vm.title) { tvRelationHeader.text = it } + } + } + + override fun onStart() { + super.onStart() + vm.onStart(relationId = relationId, recordId = objectId) + } + + private fun handleActions(action: EditGridCellAction) { + when (action) { + is EditGridCellAction.Url -> { + try { + Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(action.url) + }.let { + startActivity(it) + } + } catch (e: Exception) { + toast("An error occurred. Url may be invalid: ${e.message}") + } + } + is EditGridCellAction.Email -> { + try { + Intent(Intent.ACTION_SENDTO).apply { + data = Uri.parse("mailto:" + action.email) + }.let { + startActivity(it) + } + } catch (e: Exception) { + toast("An error occurred. Email address may be invalid: ${e.message}") + } + } + is EditGridCellAction.Phone -> { + try { + Intent(Intent.ACTION_DIAL).apply { + data = Uri.parse("tel:${action.phone}") + }.let { + startActivity(it) + } + } catch (e: Exception) { + toast("An error occurred. Phone number may be invalid: ${e.message}") + } + } + else -> { + toast("Unexpected action") + } + } + } + + private fun dispatchTextResultAndExit(txt: String) { + recycler.hideKeyboard() + withParent { + onRelationTextValueChanged( + ctx = ctx, + relationId = relationId, + objectId = objectId, + text = txt + ) + } + dismiss() + } + + private fun dispatchNumberResultAndExit(number: Number) { + recycler.hideKeyboard() + withParent { + onRelationTextNumberValueChanged( + ctx = ctx, + relationId = relationId, + objectId = objectId, + number = number + ) + } + dismiss() + } + + override fun injectDependencies() { + if (flow == FLOW_DATAVIEW) { + componentManager().editGridCellComponent.get(ctx).inject(this) + } else { + componentManager().editRelationCellComponent.get(ctx).inject(this) + } + } + + override fun releaseDependencies() { + if (flow == FLOW_DATAVIEW) { + componentManager().editGridCellComponent.release(ctx) + } else { + componentManager().editRelationCellComponent.get(ctx).inject(this) + } + } + + companion object { + + fun new( + ctx: Id, + relationId: Id, + objectId: Id, + flow: Int = FLOW_DEFAULT + ) = ObjectRelationTextValueFragment().apply { + arguments = bundleOf( + CONTEXT_ID to ctx, + RELATION_ID to relationId, + OBJECT_ID to objectId, + FLOW_KEY to flow + ) + } + + const val CONTEXT_ID = "arg.edit-relation-value.context" + const val RELATION_ID = "arg.edit-relation-value.relation.id" + const val OBJECT_ID = "arg.edit-relation-value.object.id" + const val FLOW_KEY = "arg.edit-relation-value.flow" + + const val FLOW_DEFAULT = 0 + const val FLOW_DATAVIEW = 1 + } + + interface EditObjectRelationTextValueReceiver { + fun onRelationTextValueChanged( + ctx: Id, + text: String, + objectId: Id, + relationId: Id + ) + fun onRelationTextNumberValueChanged( + ctx: Id, + number: Number, + objectId: Id, + relationId: Id + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/relations/RelationFileValueActionsFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/relations/RelationFileValueActionsFragment.kt new file mode 100644 index 0000000000..6123db3530 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/relations/RelationFileValueActionsFragment.kt @@ -0,0 +1,59 @@ +package com.anytypeio.anytype.ui.relations + +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_utils.ext.withParent +import com.anytypeio.anytype.core_utils.ui.BaseDialogFragment +import kotlinx.android.synthetic.main.fragment_relation_file_value_action.* + +class RelationFileValueActionsFragment : BaseDialogFragment() { + + override fun injectDependencies() {} + override fun releaseDependencies() {} + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_relation_file_value_action, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + btnAdd.setOnClickListener { + withParent { onFileValueActionAdd() } + dismiss() + } + btnUploadFromGallery.setOnClickListener { + withParent { onFileValueActionUploadFromGallery() } + dismiss() + } + btnUploadFromStorage.setOnClickListener { + withParent { onFileValueActionUploadFromStorage() } + dismiss() + } + } + + override fun onStart() { + super.onStart() + setupAppearance() + } + + private fun setupAppearance() { + dialog?.window?.apply { + setGravity(Gravity.BOTTOM) + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + setBackgroundDrawableResource(android.R.color.transparent) + setWindowAnimations(R.style.DefaultBottomDialogAnimation) + } + } + + interface RelationFileValueActionReceiver { + fun onFileValueActionAdd() + fun onFileValueActionUploadFromGallery() + fun onFileValueActionUploadFromStorage() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/relations/RelationFileValueAddFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/relations/RelationFileValueAddFragment.kt new file mode 100644 index 0000000000..ccd5186d39 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/relations/RelationFileValueAddFragment.kt @@ -0,0 +1,148 @@ +package com.anytypeio.anytype.ui.relations + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.features.relations.RelationFileValueAdapter +import com.anytypeio.anytype.core_ui.reactive.textChanges +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ext.withParent +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.relations.AddFileValueCommand +import com.anytypeio.anytype.presentation.relations.AddFileValueView +import com.anytypeio.anytype.presentation.relations.RelationFileValueAddViewModel +import kotlinx.android.synthetic.main.fragment_relation_value_file_add.* +import javax.inject.Inject + +class RelationFileValueAddFragment : BaseBottomSheetFragment() { + + @Inject + lateinit var factory: RelationFileValueAddViewModel.Factory + val vm: RelationFileValueAddViewModel by viewModels { factory } + + private val ctx get() = argString(CONTEXT_ID) + private val objectId get() = argString(OBJECT_ID) + private val relationId get() = argString(RELATION_ID) + private val flow get() = arg(FLOW_KEY) + + private val adapter by lazy { + RelationFileValueAdapter( + onFileClick = { vm.onFileClicked(it) } + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_relation_value_file_add, container, false) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setTransparentBackground() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + rvFiles.layoutManager = LinearLayoutManager(requireContext()) + rvFiles.adapter = adapter + btnBottomAction.setOnClickListener { vm.onActionButtonClicked() } + } + + override fun onStart() { + jobs += lifecycleScope.subscribe(vm.viewsFiltered) { observeState(it) } + jobs += lifecycleScope.subscribe(vm.commands) { observeCommands(it) } + jobs += lifecycleScope.subscribe(searchRelationInput.textChanges()) + { vm.onFilterTextChanged(it.toString()) } + super.onStart() + vm.onStart(objectId = objectId, relationId = relationId) + } + + private fun observeState(state: AddFileValueView) { + adapter.update(state.files) + tvFilesCount.text = state.count + } + + private fun observeCommands(command: AddFileValueCommand) { + when (command) { + is AddFileValueCommand.DispatchResult -> { + dispatchResultAndDismiss(command.ids) + } + } + } + + private fun dispatchResultAndDismiss(ids: List) { + withParent { + onRelationFileValueChanged( + ctx = ctx, + objectId = objectId, + relationId = relationId, + ids = ids + ) + } + dismiss() + } + + private fun setTransparentBackground() { + dialog?.window?.setBackgroundDrawableResource(android.R.color.transparent) + } + + override fun injectDependencies() { + if (flow == FLOW_DEFAULT) { + componentManager().relationFileValueComponent.get(ctx).inject(this) + } else { + componentManager().relationFileValueDVComponent.get(ctx).inject(this) + } + } + + override fun releaseDependencies() { + if (flow == FLOW_DEFAULT) { + componentManager().relationFileValueComponent.release(ctx) + } else { + componentManager().relationFileValueDVComponent.release(ctx) + } + } + + companion object { + + fun new( + ctx: Id, + objectId: Id, + relationId: Id, + flow: Int = FLOW_DEFAULT + ) = RelationFileValueAddFragment().apply { + arguments = bundleOf( + CONTEXT_ID to ctx, + OBJECT_ID to objectId, + RELATION_ID to relationId, + FLOW_KEY to flow + ) + } + + const val CONTEXT_ID = "arg.relation.add.file.context" + const val RELATION_ID = "arg.relation.add.file.relation.id" + const val OBJECT_ID = "arg.relation.add.file.object.id" + const val FLOW_KEY = "arg.relation.add.file.flow" + const val FLOW_DEFAULT = 0 + const val FLOW_DATAVIEW = 1 + } + + interface AddRelationFileValueReceiver { + fun onRelationFileValueChanged( + ctx: Id, + objectId: Id, + relationId: Id, + ids: List + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/CreateObjectSetFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/CreateObjectSetFragment.kt new file mode 100644 index 0000000000..be7bfca51b --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/CreateObjectSetFragment.kt @@ -0,0 +1,94 @@ +package com.anytypeio.anytype.ui.sets + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.observe +import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_ui.features.sets.CreateSetHeaderAdapter +import com.anytypeio.anytype.core_ui.features.sets.CreateSetObjectTypeAdapter +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.CreateObjectSetViewModel +import com.anytypeio.anytype.presentation.sets.CreateObjectTypeView +import com.anytypeio.anytype.presentation.sets.CreateSetViewState +import com.anytypeio.anytype.ui.base.NavigationFragment +import kotlinx.android.synthetic.main.fragment_create_set.* +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import javax.inject.Inject + +class CreateObjectSetFragment : NavigationFragment(R.layout.fragment_create_set), + CreateObjectTypeCallback { + + private val ctx: String get() = argString(CONTEXT_ID_KEY) + + private val vm: CreateObjectSetViewModel by viewModels { factory } + + @Inject + lateinit var factory: CreateObjectSetViewModel.Factory + + private val headerAdapter by lazy { + CreateSetHeaderAdapter { vm.onCreateNewObjectType() } + } + + private val typeAdapter by lazy { + CreateSetObjectTypeAdapter { type -> vm.onObjectTypeSelected(type, ctx) } + } + + private val createSetAdapter by lazy { + ConcatAdapter(headerAdapter, typeAdapter) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + vm.state.observe(viewLifecycleOwner) { observe(it) } + recycler.apply { + layoutManager = LinearLayoutManager(context) + adapter = createSetAdapter + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + vm.objectTypeViews.onEach { + Timber.d("Receiving new views: $it") + typeAdapter.views = it + typeAdapter.notifyDataSetChanged() + createSetAdapter.notifyDataSetChanged() + }.launchIn(lifecycleScope) + } + + override fun onCreateObjectTypeClicked(type: CreateObjectTypeView, name: String) { + vm.onCreateObjectTypeClicked(type, name) + Timber.d("CreateObjectType:$type, $name") + } + + override fun injectDependencies() { + componentManager().createSetComponent.get().inject(this) + } + + override fun releaseDependencies() { + componentManager().createSetComponent.release() + } + + private fun observe(state: CreateSetViewState) { + when (state) { + is CreateSetViewState.AddObjectType -> { + CreateObjectTypeFragment.newInstance(state.data).show(childFragmentManager, null) + } + } + } + + companion object { + const val CONTEXT_ID_KEY = "arg.create_object_set.context" + } +} + +interface CreateObjectTypeCallback { + fun onCreateObjectTypeClicked(type: CreateObjectTypeView, name: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/CreateObjectTypeFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/CreateObjectTypeFragment.kt new file mode 100644 index 0000000000..19a7154ce4 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/CreateObjectTypeFragment.kt @@ -0,0 +1,89 @@ +package com.anytypeio.anytype.ui.sets + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.observe +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_ui.features.sets.CreateObjectTypeAdapter +import com.anytypeio.anytype.core_utils.ext.hideKeyboard +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.CreateObjectTypeView +import com.anytypeio.anytype.presentation.sets.CreateObjectTypeViewModel +import com.anytypeio.anytype.presentation.sets.CreateObjectTypeViewState +import kotlinx.android.synthetic.main.fragment_create_object_type.* +import javax.inject.Inject + +class CreateObjectTypeFragment : BaseBottomSheetFragment() { + + @Inject + lateinit var factory: CreateObjectTypeViewModel.Factory + private val vm by viewModels { factory } + private val typesAdapter by lazy { CreateObjectTypeAdapter { vm.onSelectType(it) } } + private val types: List + get() { + val args = requireArguments() + val list = + args.getParcelableArrayList(ARG_TYPES) + checkNotNull(list) + return list + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_create_object_type, container, false) + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + btnCreate.setOnClickListener { vm.onCreateClicked(edtTypeName.text.toString()) } + vm.state.observe(viewLifecycleOwner) { observeData(it) } + rvTypes.layoutManager = LinearLayoutManager(requireContext()) + rvTypes.adapter = typesAdapter + vm.init(types) + } + + private fun observeData(state: CreateObjectTypeViewState) { + when (state) { + CreateObjectTypeViewState.Loading -> TODO() + is CreateObjectTypeViewState.Success -> { + typesAdapter.types = state.data + typesAdapter.notifyDataSetChanged() + } + is CreateObjectTypeViewState.Exit -> { + (parentFragment as CreateObjectTypeCallback).onCreateObjectTypeClicked( + state.type, + state.name + ) + edtTypeName.hideKeyboard() + dismiss() + } + } + } + + override fun injectDependencies() { + componentManager().createObjectTypeComponent.get().inject(this) + } + + override fun releaseDependencies() { + componentManager().createObjectTypeComponent.release() + } + + companion object { + + private const val ARG_TYPES = "arg.create.object.types" + + fun newInstance(types: ArrayList): CreateObjectTypeFragment = + CreateObjectTypeFragment().apply { + arguments = Bundle().apply { + putParcelableArrayList(ARG_TYPES, types) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt new file mode 100644 index 0000000000..79a9875f0c --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt @@ -0,0 +1,443 @@ +package com.anytypeio.anytype.ui.sets + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.GestureDetector +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator +import android.view.inputmethod.EditorInfo.IME_ACTION_DONE +import android.view.inputmethod.EditorInfo.IME_ACTION_GO +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.activity.addCallback +import androidx.constraintlayout.motion.widget.MotionLayout +import androidx.core.view.marginBottom +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.core_ui.extensions.setImageOrNull +import com.anytypeio.anytype.core_ui.features.dataview.* +import com.anytypeio.anytype.core_ui.reactive.* +import com.anytypeio.anytype.core_utils.OnSwipeListener +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ext.hideKeyboard +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ext.toast +import com.anytypeio.anytype.core_utils.ui.NonScrollLinearLayoutManager +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.page.editor.model.BlockView +import com.anytypeio.anytype.presentation.sets.ObjectSetCommand +import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel +import com.anytypeio.anytype.presentation.sets.ObjectSetViewModelFactory +import com.anytypeio.anytype.presentation.sets.model.FilterExpression +import com.anytypeio.anytype.presentation.sets.model.SortingExpression +import com.anytypeio.anytype.presentation.sets.model.Viewer +import com.anytypeio.anytype.ui.base.NavigationFragment +import com.anytypeio.anytype.ui.database.modals.ObjectSetObjectRelationValueFragment +import com.anytypeio.anytype.ui.relations.CreateDataViewRelationFragment +import com.anytypeio.anytype.ui.relations.CreateDataViewRelationFragment.OnAddDataViewRelationRequestReceiver +import com.anytypeio.anytype.ui.relations.ObjectRelationDateValueFragment +import com.anytypeio.anytype.ui.relations.ObjectRelationDateValueFragment.EditObjectRelationDateValueReceiver +import com.anytypeio.anytype.ui.relations.ObjectRelationTextValueFragment +import com.anytypeio.anytype.ui.relations.ObjectRelationTextValueFragment.EditObjectRelationTextValueReceiver +import com.anytypeio.anytype.ui.sets.modals.* +import com.anytypeio.anytype.ui.sets.modals.sort.ViewerSortFragment +import kotlinx.android.synthetic.main.fragment_object_set.* +import kotlinx.coroutines.flow.filterNotNull +import javax.inject.Inject + +open class ObjectSetFragment : + NavigationFragment(R.layout.fragment_object_set), + OnAddDataViewRelationRequestReceiver, + EditObjectRelationTextValueReceiver, + EditObjectRelationDateValueReceiver{ + + private val bottomPanelTranslationDelta: Float + get() = (bottomPanel.height + bottomPanel.marginBottom).toFloat() + + private val actionHandler: (Int) -> Boolean = { action -> + action == IME_ACTION_GO || action == IME_ACTION_DONE + } + + private val swipeListener = object : OnSwipeListener() { + override fun onSwipe(direction: Direction?): Boolean { + if (direction == Direction.DOWN) hideBottomPanel() + return true + } + } + + private val swipeDetector by lazy { + GestureDetector(context, swipeListener) + } + + private val viewerTabAdapter by lazy { + ViewerTabItemAdapter( + onAddNewViewerClicked = { vm.onCreateNewViewerClicked() }, + onViewerTabClicked = { vm.onViewerTabClicked(it) } + ) + } + private val viewerGridHeaderAdapter by lazy { + ViewerGridHeaderAdapter( + onCreateNewColumnClicked = { vm.onAddNewDataViewRelation() } + ) + } + private val viewerGridAdapter by lazy { + ViewerGridAdapter( + onCellClicked = vm::onGridCellClicked, + onCellAction = vm::onCellAction, + onObjectHeaderClicked = vm::onObjectHeaderClicked + ) + } + private val viewerListAdapter by lazy { ViewerListAdapter() } + private val viewerTypeAdapter by lazy { + ViewerTypeAdapter( + gridHeaderAdapter = viewerGridHeaderAdapter, + gridAdapter = viewerGridAdapter, + listAdapter = viewerListAdapter + ) + } + + private val ctx: String get() = argString(CONTEXT_ID_KEY) + + @Inject + lateinit var factory: ObjectSetViewModelFactory + private val vm: ObjectSetViewModel by viewModels { factory } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setupOnBackPressedDispatcher() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + topRecycler.apply { + layoutManager = NonScrollLinearLayoutManager(requireContext()) + adapter = viewerTypeAdapter + setHasFixedSize(true) + } + title.clearFocus() + root.setTransitionListener(transitionListener) + with(lifecycleScope) { + subscribe(addNewButton.clicks()) { vm.onCreateNewRecord() } + subscribe(title.afterTextChanges()) { vm.onTitleChanged(it.toString()) } + subscribe(title.editorActionEvents(actionHandler)) { + title.apply { + hideKeyboard() + clearFocus() + } + } + subscribe(objectSetIcon.clicks()) { vm.onIconClicked() } + subscribe(customizeViewButton.clicks()) { + showBottomPanel() + } + subscribe(icBack.clicks()) { vm.onBackButtonPressed() } + subscribe(tvCurrentViewerName.clicks()) { vm.onExpandViewerMenuClicked() } + subscribe(bottomPanel.findViewById(R.id.btnFilter).clicks()) { + vm.onViewerFiltersClicked() + } + subscribe(bottomPanel.findViewById(R.id.btnRelations).clicks()) { + vm.onViewerRelationsClicked() + } + subscribe(bottomPanel.findViewById(R.id.btnSort).clicks()) { + val fr = ViewerSortFragment.new(ctx) + fr.show(childFragmentManager, EMPTY_TAG) + } + subscribe(bottomPanel.findViewById(R.id.btnGroup).clicks()) { + toast(getString(R.string.coming_soon)) + } + + subscribe(bottomPanel.touches()) { swipeDetector.onTouchEvent(it) } + } + } + + private fun showBottomPanel() { + val animation = bottomPanel.animate().translationY(-bottomPanelTranslationDelta).apply { + duration = BOTTOM_PANEL_ANIM_DURATION + interpolator = AccelerateDecelerateInterpolator() + } + animation.start() + } + + private fun hideBottomPanel() { + val animation = bottomPanel.animate().translationY(0f).apply { + duration = BOTTOM_PANEL_ANIM_DURATION + interpolator = AccelerateDecelerateInterpolator() + } + animation.start() + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + vm.navigation.observe(viewLifecycleOwner, navObserver) + lifecycleScope.subscribe(vm.toasts.stream()) { toast(it) } + } + + private fun observeGrid(viewer: Viewer) { + tvCurrentViewerName.text = viewer.title + when (viewer) { + is Viewer.GridView -> { + viewerTypeAdapter.update(viewer) + } + is Viewer.ListView -> { + // TODO + } + } + } + + private fun observeHeader(title: BlockView.Title.Document) { + checkMainThread() + this.title.setText(title.text) + this.titleSmall.text = title.text + if (title.emoji != null) { + objectSetIcon.scaleType = ImageView.ScaleType.CENTER + objectSetIcon.setEmoji(unicode = title.emoji) + } else { + objectSetIcon.scaleType = ImageView.ScaleType.CENTER_CROP + objectSetIcon.setImageOrNull(image = title.image) + } + } + + private fun observeCommands(command: ObjectSetCommand) { + when (command) { + is ObjectSetCommand.Modal.CreateDataViewRelation -> { + val fr = CreateDataViewRelationFragment.new( + ctx = command.ctx, + target = command.target + ) + fr.show(childFragmentManager, EMPTY_TAG) + } + is ObjectSetCommand.Modal.EditGridTextCell -> { + val fr = ObjectRelationTextValueFragment.new( + ctx = ctx, + relationId = command.relationId, + objectId = command.recordId, + flow = ObjectRelationTextValueFragment.FLOW_DATAVIEW + ) + fr.show(childFragmentManager, EMPTY_TAG) + } + is ObjectSetCommand.Modal.EditGridDateCell -> { + val fr = ObjectRelationDateValueFragment.new( + ctx = ctx, + relationId = command.relationId, + objectId = command.objectId, + flow = ObjectRelationDateValueFragment.FLOW_DATAVIEW + ) + fr.show(childFragmentManager, EMPTY_TAG) + } + is ObjectSetCommand.Modal.EditRelationCell -> { + val fr = ObjectSetObjectRelationValueFragment.new( + ctx = command.ctx, + target = command.target, + dataview = command.dataview, + relation = command.relation, + viewer = command.viewer + ) + fr.show(childFragmentManager, EMPTY_TAG) + } + is ObjectSetCommand.Modal.ViewerCustomizeScreen -> { + val fr = ViewerBottomSheetRootFragment.new( + ctx = command.ctx, + viewer = command.viewer + ) + fr.show(childFragmentManager, EMPTY_TAG) + } + is ObjectSetCommand.Modal.CreateViewer -> { + val fr = CreateDataViewViewerFragment.new( + ctx = command.ctx, + target = command.target + ) + fr.show(childFragmentManager, EMPTY_TAG) + } + is ObjectSetCommand.Modal.EditDataViewViewer -> { + val fr = EditDataViewViewerFragment.new( + ctx = command.ctx, + viewer = command.viewer, + name = command.name + ) + fr.show(childFragmentManager, EMPTY_TAG) + } + is ObjectSetCommand.Modal.ManageViewer -> { + val fr = ManageViewerFragment.new(ctx = command.ctx, dataview = command.dataview) + fr.show(childFragmentManager, EMPTY_TAG) + } + is ObjectSetCommand.Modal.ModifyViewerRelationOrder -> { + val fr = ViewerRelationsFragment.new( + ctx = command.ctx + ) + fr.show(childFragmentManager, EMPTY_TAG) + } + is ObjectSetCommand.Modal.SetNameForCreatedRecord -> { + val dialog = SetObjectSetRecordNameFragment.new(command.ctx) + dialog.show(childFragmentManager, EMPTY_TAG) + } + is ObjectSetCommand.Intent.MailTo -> { + try { + Intent(Intent.ACTION_SENDTO).apply { + data = Uri.parse("mailto:" + command.email) + }.let { + startActivity(it) + } + } catch (e: Exception) { + toast("An error occurred. Email address may be invalid: ${e.message}") + } + } + is ObjectSetCommand.Intent.GoTo -> { + try { + Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(command.url) + }.let { + startActivity(it) + } + } catch (e: Exception) { + toast("An error occurred. Url may be invalid: ${e.message}") + } + } + is ObjectSetCommand.Modal.OpenIconActionMenu -> { + //todo show edit icon widget + } + is ObjectSetCommand.Intent.Call -> { + try { + Intent(Intent.ACTION_DIAL).apply { + data = Uri.parse("tel:${command.phone}") + }.let { + startActivity(it) + } + } catch (e: Exception) { + toast("An error occurred. Phone number may be invalid: ${e.message}") + } + } + is ObjectSetCommand.Modal.ModifyViewerFilters -> { + val fr = ViewerFilterFragment.new( + ctx = command.ctx + ) + fr.show(childFragmentManager, EMPTY_TAG) + } + } + } + + private val transitionListener = object : MotionLayout.TransitionListener { + override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {} + override fun onTransitionChange( + view: MotionLayout?, + start: Int, + end: Int, + progress: Float + ) { + } + + override fun onTransitionTrigger(view: MotionLayout?, id: Int, pos: Boolean, prog: Float) {} + + override fun onTransitionCompleted(motionLayout: MotionLayout?, id: Int) { + if (id == R.id.start) { + title.enableEditMode() + } + if (id == R.id.end) { + title.enableReadMode() + } + } + } + + override fun onStart() { + super.onStart() + jobs += lifecycleScope.subscribe(vm.commands) { observeCommands(it) } + jobs += lifecycleScope.subscribe(vm.header.filterNotNull()) { observeHeader(it) } + jobs += lifecycleScope.subscribe(vm.viewerTabs) { viewerTabAdapter.update(it) } + jobs += lifecycleScope.subscribe(vm.viewerGrid) { observeGrid(it) } + vm.onStart(ctx) + } + + override fun onStop() { + super.onStop() + vm.onStop() + } + + override fun onDestroyView() { + viewerGridAdapter.clear() + super.onDestroyView() + } + + private fun setupOnBackPressedDispatcher() { + requireActivity().onBackPressedDispatcher.addCallback(this) { + if (childFragmentManager.backStackEntryCount > 0) { + childFragmentManager.popBackStack() + } else { + vm.onSystemBackPressed() + } + } + } + + override fun onAddDataViewRelationRequest( + context: String, + target: Id, + name: String, + format: Relation.Format + ) = vm.onRelationPrototypeCreated( + context = context, + target = target, + name = name, + format = format + ) + + fun onViewerNewSortsRequest(sorts: List) { + vm.onUpdateViewerSorting(sorts) + } + + fun onViewerNewFiltersRequest(filters: List) { + vm.onUpdateViewerFilters(filters) + } + + override fun onRelationTextValueChanged( + ctx: String, + text: String, + objectId: String, + relationId: String + ) = vm.onRelationTextValueChanged( + ctx = ctx, + value = text, + objectId = objectId, + relationKey = relationId + ) + + override fun onRelationTextNumberValueChanged( + ctx: Id, + number: Number, + objectId: Id, + relationId: Id + ) = vm.onRelationTextValueChanged( + ctx = ctx, + value = number, + objectId = objectId, + relationKey = relationId + ) + + override fun onRelationDateValueChanged( + ctx: Id, + timeInSeconds: Number?, + objectId: Id, + relationId: Id + ) { + vm.onRelationTextValueChanged( + ctx = ctx, + value = timeInSeconds, + objectId = objectId, + relationKey = relationId + ) + } + + override fun injectDependencies() { + componentManager().objectSetComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().objectSetComponent.release(ctx) + } + + companion object { + const val CONTEXT_ID_KEY = "arg.object_set.context" + val EMPTY_TAG = null + const val BOTTOM_PANEL_ANIM_DURATION = 150L + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/SetObjectSetRecordNameFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/SetObjectSetRecordNameFragment.kt new file mode 100644 index 0000000000..a1f2fd531f --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/SetObjectSetRecordNameFragment.kt @@ -0,0 +1,70 @@ +package com.anytypeio.anytype.ui.sets + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_ui.reactive.editorActionEvents +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ext.hideKeyboard +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.presentation.sets.ObjectSetRecordViewModel +import kotlinx.android.synthetic.main.fragment_set_object_set_record_name.* +import javax.inject.Inject + +class SetObjectSetRecordNameFragment : BaseBottomSheetFragment() { + + private val ctx: String get() = argString(CONTEXT_KEY) + + @Inject + lateinit var factory: ObjectSetRecordViewModel.Factory + private val vm: ObjectSetRecordViewModel by viewModels { factory } + + private val handler: (Int) -> Boolean = { action -> + action == EditorInfo.IME_ACTION_DONE + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_set_object_set_record_name, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + lifecycleScope.subscribe(textInputField.editorActionEvents(handler)) { + textInputField.clearFocus() + textInputField.hideKeyboard() + vm.onComplete(ctx, textInputField.text.toString()) + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + lifecycleScope.subscribe(vm.isCompleted) { isCompleted -> if (isCompleted) dismiss() } + } + + override fun injectDependencies() { + componentManager().objectSetRecordComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().objectSetRecordComponent.release(ctx) + } + + companion object { + const val CONTEXT_KEY = "arg.object-set-record.context" + + fun new(ctx: Id) = SetObjectSetRecordNameFragment().apply { + arguments = bundleOf(CONTEXT_KEY to ctx) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/ViewerFilterFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/ViewerFilterFragment.kt new file mode 100644 index 0000000000..da5aac2495 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/ViewerFilterFragment.kt @@ -0,0 +1,154 @@ +package com.anytypeio.anytype.ui.sets + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.extensions.drawable +import com.anytypeio.anytype.core_ui.features.dataview.modals.FilterByAdapter +import com.anytypeio.anytype.core_ui.layout.DividerVerticalItemDecoration +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.filter.ViewerFilterCommand +import com.anytypeio.anytype.presentation.sets.filter.ViewerFilterViewModel +import com.anytypeio.anytype.ui.sets.modals.ViewerBottomSheetRootFragment +import com.anytypeio.anytype.ui.sets.modals.filter.CreateFilterFlowRootFragment +import com.anytypeio.anytype.ui.sets.modals.filter.ModifyFilterFromInputFieldValueFragment +import com.anytypeio.anytype.ui.sets.modals.filter.ModifyFilterFromSelectedValueFragment +import kotlinx.android.synthetic.main.fragment_filter.* +import javax.inject.Inject + +class ViewerFilterFragment : BaseBottomSheetFragment() { + + private val ctx get() = argString(CONTEXT_ID_KEY) + + private val filterAdapter by lazy { + FilterByAdapter( + click = { click -> vm.onFilterClicked(ctx, click) } + ) + } + + @Inject + lateinit var factory: ViewerFilterViewModel.Factory + private val vm: ViewerFilterViewModel by viewModels { factory } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_filter, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + recycler.layoutManager = LinearLayoutManager(requireContext()) + recycler.adapter = filterAdapter + with(lifecycleScope) { + subscribe(vm.commands) { observeCommands(it) } + subscribe(resetBtn.clicks()) { vm.onResetButtonClicked(ctx) } + subscribe(doneBtn.clicks()) { vm.onDoneButtonClicked() } + subscribe(editBtn.clicks()) { vm.onEditButtonClicked() } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.views) { filterAdapter.update(it) } + subscribe(vm.screenState) { render(it) } + } + } + + private fun render(state: ViewerFilterViewModel.ScreenState) { + when (state) { + ViewerFilterViewModel.ScreenState.LIST -> { + editBtn.visible() + resetBtn.visible() + doneBtn.invisible() + removeDivider() + recycler.addItemDecoration( + DividerVerticalItemDecoration( + divider = requireContext().drawable(R.drawable.divider_filter_list), + isShowInLastItem = false + ), + 0 + ) + } + ViewerFilterViewModel.ScreenState.EDIT -> { + doneBtn.visible() + editBtn.invisible() + resetBtn.invisible() + removeDivider() + recycler.addItemDecoration( + DividerVerticalItemDecoration( + divider = requireContext().drawable(R.drawable.divider_filter_edit), + isShowInLastItem = false + ), + 0 + ) + } + } + } + + private fun removeDivider() { + if (recycler.itemDecorationCount > 0) recycler.removeItemDecorationAt(0) + } + + private fun observeCommands(command: ViewerFilterCommand) { + when (command) { + is ViewerFilterCommand.Modal.ShowRelationList -> { + val fr = CreateFilterFlowRootFragment.new(ctx) + fr.show(parentFragmentManager, null) + } + is ViewerFilterCommand.Apply -> dispatchResultAndDismiss(command) + is ViewerFilterCommand.BackToCustomize -> exitToCustomizeScreen() + is ViewerFilterCommand.Modal.UpdateInputValueFilter -> { + val fr = ModifyFilterFromInputFieldValueFragment.new( + ctx = ctx, + relation = command.relation, + index = command.filterIndex + ) + fr.show(childFragmentManager, fr.javaClass.canonicalName) + } + is ViewerFilterCommand.Modal.UpdateSelectValueFilter -> { + val fr = ModifyFilterFromSelectedValueFragment.new( + ctx = ctx, + relation = command.relation, + index = command.filterIndex + ) + fr.show(childFragmentManager, fr.javaClass.canonicalName) + } + } + } + + private fun exitToCustomizeScreen() { + withParent { transitToCustomize() } + } + + private fun dispatchResultAndDismiss(command: ViewerFilterCommand.Apply) { + withParent { dispatchResultFiltersAndDismiss(command.filters) } + } + + override fun injectDependencies() { + componentManager().viewerFilterComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().viewerFilterComponent.release(ctx) + } + + companion object { + const val CONTEXT_ID_KEY = "arg.viewer.filters.context" + + fun new(ctx: Id) = ViewerFilterFragment().apply { + arguments = bundleOf(CONTEXT_ID_KEY to ctx) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/ViewerSortByFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/ViewerSortByFragment.kt new file mode 100644 index 0000000000..fbab2487fb --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/ViewerSortByFragment.kt @@ -0,0 +1,134 @@ +package com.anytypeio.anytype.ui.sets + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_ui.features.dataview.modals.SortByAdapter +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ext.withParent +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.presentation.sets.ViewerSortByCommand +import com.anytypeio.anytype.presentation.sets.ViewerSortByViewModel +import com.anytypeio.anytype.presentation.sets.ViewerSortByViewState +import com.anytypeio.anytype.presentation.sets.model.Viewer +import com.anytypeio.anytype.ui.sets.modals.PickSortingKeyFragment +import com.anytypeio.anytype.ui.sets.modals.PickSortingTypeFragment +import com.anytypeio.anytype.ui.sets.modals.ViewerBottomSheetRootFragment +import kotlinx.android.synthetic.main.fragment_sorting.* +import javax.inject.Inject + +class ViewerSortByFragment : BaseBottomSheetFragment() { + + private val sortingAdapter by lazy { + SortByAdapter(click = vm::itemClicked) + } + + private val ctx get() = argString(CONTEXT_ID_KEY) + private val viewer get() = argString(VIEWER_ID_KEY) + + @Inject + lateinit var factory: ViewerSortByViewModel.Factory + private val vm: ViewerSortByViewModel by viewModels { factory } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_sorting, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + with(recyclerView) { + adapter = sortingAdapter + } + lifecycleScope.subscribe(vm.viewState) { observeState(it) } + lifecycleScope.subscribe(vm.commands.stream()) { observeCommands(it) } + lifecycleScope.subscribe(ivBack.clicks()) { vm.onBackClicked() } + vm.onViewCreated(viewer) + } + + fun onPickSortType(key: String, type: Viewer.SortType) { + vm.onPickSortType(key, type) + } + + fun onReplaceSortKey(keySelected: String, keyNew: String) { + vm.onReplaceSortKey(keySelected = keySelected, keyNew = keyNew) + } + + fun onAddSortKey(key: String) { + vm.onAddSortKey(key) + } + + private fun observeState(state: ViewerSortByViewState) { + when (state) { + ViewerSortByViewState.Init -> { + } + is ViewerSortByViewState.Success -> { + sortingAdapter.update(state.items) + } + } + } + + private fun observeCommands(command: ViewerSortByCommand) { + when (command) { + is ViewerSortByCommand.Modal.ShowSortingKeyList -> { + PickSortingKeyFragment.new( + selected = command.old, + relations = command.relations, + sorts = command.sortingExpression + ) + .show(childFragmentManager, null) + } + is ViewerSortByCommand.Modal.ShowSortingTypeList -> { + PickSortingTypeFragment.new( + key = command.key, + type = command.selected + ) + .show(childFragmentManager, null) + } + is ViewerSortByCommand.Apply -> dispatchResultAndDismiss(command) + is ViewerSortByCommand.BackToCustomize -> exitToCustomizeScreen() + } + } + + private fun dispatchResultAndDismiss(command: ViewerSortByCommand.Apply) { + withParent { + dispatchResultSortsAndDismiss(command.sorts) + } + } + + private fun exitToCustomizeScreen() { + withParent { + transitToCustomize() + } + } + + override fun injectDependencies() { + componentManager().viewerSortByComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().viewerSortByComponent.release(ctx) + } + + companion object { + const val CONTEXT_ID_KEY = "arg.viewer.sorts.context" + const val VIEWER_ID_KEY = "arg.viewer.sorts.viewer_id" + + fun new(ctx: Id, viewer: Id) = ViewerSortByFragment().apply { + arguments = bundleOf( + CONTEXT_ID_KEY to ctx, + VIEWER_ID_KEY to viewer + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/BaseDialogListFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/BaseDialogListFragment.kt new file mode 100644 index 0000000000..8018ccf43f --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/BaseDialogListFragment.kt @@ -0,0 +1,46 @@ +package com.anytypeio.anytype.ui.sets.modals + +import android.app.Dialog +import android.os.Bundle +import android.view.* +import androidx.fragment.app.DialogFragment +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.R +import kotlinx.android.synthetic.main.fragment_list.* + +abstract class BaseDialogListFragment : DialogFragment() { + + abstract fun setAdapter(recyclerView: RecyclerView) + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.window?.requestFeature(Window.FEATURE_NO_TITLE) + return dialog + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_list, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setAdapter(recycler) + } + + override fun onStart() { + super.onStart() + setDialogWidthAndGravity() + } + + private fun setDialogWidthAndGravity() { + dialog?.window?.apply { + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + setGravity(Gravity.BOTTOM) + setBackgroundDrawableResource(android.R.color.transparent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/CreateDataViewViewerFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/CreateDataViewViewerFragment.kt new file mode 100644 index 0000000000..5cf7db5352 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/CreateDataViewViewerFragment.kt @@ -0,0 +1,74 @@ +package com.anytypeio.anytype.ui.sets.modals + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.CreateDataViewViewerViewModel +import com.anytypeio.anytype.presentation.sets.CreateDataViewViewerViewModel.Companion.STATE_COMPLETED +import kotlinx.android.synthetic.main.fragment_create_data_view_viewer.* +import javax.inject.Inject + +class CreateDataViewViewerFragment : BaseBottomSheetFragment() { + + val ctx get() = arg(CTX_KEY) + val target get() = arg(TARGET_KEY) + + @Inject + lateinit var factory: CreateDataViewViewerViewModel.Factory + private val vm: CreateDataViewViewerViewModel by viewModels { factory } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_create_data_view_viewer, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + with(lifecycleScope) { + subscribe(btnCreateViewer.clicks()) { + vm.onAddViewer( + name = viewerNameInput.text.toString(), + ctx = ctx, + target = target + ) + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + lifecycleScope.subscribe(vm.state) { state -> + if (state == STATE_COMPLETED) dismiss() + } + } + + override fun injectDependencies() { + componentManager().createDataViewViewerComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().createDataViewViewerComponent.release(ctx) + } + + companion object { + fun new(ctx: String, target: String) = CreateDataViewViewerFragment().apply { + arguments = bundleOf( + CTX_KEY to ctx, TARGET_KEY to target + ) + } + + private const val CTX_KEY = "arg.create-data-view-viewer.context" + private const val TARGET_KEY = "arg.create-data-view-viewer.target" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/DataViewViewerActionFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/DataViewViewerActionFragment.kt new file mode 100644 index 0000000000..cab4d2f395 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/DataViewViewerActionFragment.kt @@ -0,0 +1,98 @@ +package com.anytypeio.anytype.ui.sets.modals + +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ui.BaseDialogFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.DataViewViewerActionViewModel +import kotlinx.android.synthetic.main.fragment_data_view_viewer_actions.* +import javax.inject.Inject + +class DataViewViewerActionFragment : BaseDialogFragment() { + + private val ctx: String get() = arg(CTX_KEY) + private val viewer: String get() = arg(VIEWER_KEY) + private val title: String get() = arg(TITLE_KEY) + + @Inject + lateinit var factory: DataViewViewerActionViewModel.Factory + + private val vm: DataViewViewerActionViewModel by viewModels { factory } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_data_view_viewer_actions, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + tvTitle.text = title + with(lifecycleScope) { + subscribe(duplicateViewContainer.clicks()) { + vm.onDuplicateClicked(ctx = ctx, viewer = viewer) + } + subscribe(deleteViewContainer.clicks()) { + vm.onDeleteClicked(ctx = ctx, viewer = viewer) + } + subscribe(editViewContainer.clicks()) { + val fr = EditDataViewViewerFragment.new( + ctx = ctx, + viewer = viewer, + name = title + ) + fr.show(parentFragmentManager, null) + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.isDismissed) { isDismissed -> if (isDismissed) dismiss() } + } + } + + override fun onStart() { + super.onStart() + setupAppearance() + } + + override fun injectDependencies() { + componentManager().dataviewViewerActionComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().dataviewViewerActionComponent.release(ctx) + } + + private fun setupAppearance() { + dialog?.window?.apply { + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + setGravity(Gravity.BOTTOM) + setBackgroundDrawableResource(android.R.color.transparent) + setWindowAnimations(R.style.DefaultBottomDialogAnimation) + } + } + + companion object { + fun new(ctx: Id, title: String, viewer: Id) = DataViewViewerActionFragment().apply { + arguments = bundleOf(CTX_KEY to ctx, TITLE_KEY to title, VIEWER_KEY to viewer) + } + + const val CTX_KEY = "arg.dialog.viewer-action.ctx" + const val VIEWER_KEY = "arg.dialog.viewer-action.viewer" + const val TITLE_KEY = "arg.dialog.viewer-action.title" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/DatePickerFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/DatePickerFragment.kt new file mode 100644 index 0000000000..f8922a3a4f --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/DatePickerFragment.kt @@ -0,0 +1,73 @@ +package com.anytypeio.anytype.ui.sets.modals + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_utils.ext.argOrNull +import com.anytypeio.anytype.core_utils.ext.timeInSeconds +import com.anytypeio.anytype.core_utils.ext.withParent +import kotlinx.android.synthetic.main.fragment_date_picker.* +import java.util.* + +class DatePickerFragment : DialogFragment() { + + private val timeInSeconds get() = argOrNull(TIMESTAMP_KEY) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_date_picker, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setTransparentBackground() + initializePicker() + saveButton.setOnClickListener { + dispatchResultAndDismiss() + } + } + + private fun dispatchResultAndDismiss() { + val calendar = Calendar.getInstance().apply { + set(Calendar.YEAR, picker.year) + set(Calendar.MONTH, picker.month) + set(Calendar.DAY_OF_MONTH, picker.dayOfMonth) + } + withParent { onPickDate(calendar.timeInSeconds()) } + dismiss() + } + + private fun initializePicker() { + val calendar = Calendar.getInstance().apply { + time = timeInSeconds?.let { Date(it * 1000) } ?: Date(System.currentTimeMillis()) + } + picker.init( + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH), + null + ) + } + + private fun setTransparentBackground() { + dialog?.window?.setBackgroundDrawableResource(android.R.color.transparent) + } + + companion object { + + fun new(timeInSeconds: Long?) = DatePickerFragment().apply { + arguments = bundleOf(TIMESTAMP_KEY to timeInSeconds) + } + + private const val TIMESTAMP_KEY = "arg.date-picker.timestamp" + } + + interface DatePickerReceiver { + fun onPickDate(timeInSeconds: Long) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/EditDataViewViewerFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/EditDataViewViewerFragment.kt new file mode 100644 index 0000000000..94c982b5ef --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/EditDataViewViewerFragment.kt @@ -0,0 +1,96 @@ +package com.anytypeio.anytype.ui.sets.modals + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_ui.reactive.textChanges +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.hideKeyboard +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ext.toast +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.EditDataViewViewerViewModel +import kotlinx.android.synthetic.main.fragment_edit_data_view_viewer.* +import javax.inject.Inject + +class EditDataViewViewerFragment : BaseBottomSheetFragment() { + + private val ctx: Id get() = arg(CTX_KEY) + private val viewer: Id get() = arg(VIEWER_KEY) + private val name: Id get() = arg(NAME_KEY) + + @Inject + lateinit var factory: EditDataViewViewerViewModel.Factory + private val vm: EditDataViewViewerViewModel by viewModels { factory } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_edit_data_view_viewer, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewerNameInput.setText(name) + with(lifecycleScope) { + subscribe(viewerNameInput.textChanges()) { name -> + vm.onViewerNameChanged( + ctx = ctx, + viewer = viewer, + name = name.toString() + ) + } + subscribe(btnDone.clicks()) { vm.onDoneClicked() } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.isDismissed) { isDismissed -> + if (isDismissed) { + viewerNameInput.apply { + clearFocus() + hideKeyboard() + } + dismiss() + } + } + subscribe(vm.toasts) { toast(it) } + } + } + + override fun injectDependencies() { + componentManager().editDataViewViewerComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().editDataViewViewerComponent.release(ctx) + } + + companion object { + const val CTX_KEY = "arg.edit-data-view-viewer.ctx" + const val VIEWER_KEY = "arg.edit-data-view-viewer.viewer" + const val NAME_KEY = "arg.edit-data-view-viewer.name" + + fun new( + ctx: Id, + viewer: Id, + name: String, + ): EditDataViewViewerFragment = EditDataViewViewerFragment().apply { + arguments = bundleOf( + CTX_KEY to ctx, + VIEWER_KEY to viewer, + NAME_KEY to name + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ManageViewerFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ManageViewerFragment.kt new file mode 100644 index 0000000000..75f6e5f600 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ManageViewerFragment.kt @@ -0,0 +1,106 @@ +package com.anytypeio.anytype.ui.sets.modals + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.features.sets.ManageViewerAdapter +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.ManageViewerViewModel +import kotlinx.android.synthetic.main.fragment_manage_viewer.* +import javax.inject.Inject + +class ManageViewerFragment : BaseBottomSheetFragment() { + + private val manageViewerAdapter by lazy { + ManageViewerAdapter( + onViewerClicked = { view -> vm.onViewerClicked(ctx = ctx, view = view) }, + onViewerActionClicked = { view -> + val dialog = DataViewViewerActionFragment.new( + ctx = ctx, + viewer = view.id, + title = view.name + ) + dialog.show(parentFragmentManager, null) + } + ) + } + + private val ctx: String get() = arg(CTX_KEY) + private val dataview: String get() = arg(DATA_VIEW_KEY) + + @Inject + lateinit var factory: ManageViewerViewModel.Factory + + private val vm: ManageViewerViewModel by viewModels { factory } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_manage_viewer, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + dataViewViewerRecycler.apply { + layoutManager = LinearLayoutManager(context) + adapter = manageViewerAdapter + } + with(lifecycleScope) { + subscribe(btnEditViewers.clicks()) { vm.onViewerEditClicked() } + subscribe(btnAddNewViewer.clicks()) { navigateToCreateDataViewViewerScreen() } + } + } + + private fun navigateToCreateDataViewViewerScreen() { + val dialog = CreateDataViewViewerFragment.new( + ctx = ctx, + target = dataview + ) + dialog.show(parentFragmentManager, null) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.toasts) { toast(it) } + subscribe(vm.views) { manageViewerAdapter.update(it) } + subscribe(vm.isDismissed) { isDismissed -> if (isDismissed) dismiss() } + subscribe(vm.isEditEnabled) { isEditEnabled -> + if (isEditEnabled) { + btnEditViewers.setText(R.string.done) + btnAddNewViewer.invisible() + } else { + btnEditViewers.setText(R.string.edit) + btnAddNewViewer.visible() + } + } + } + } + + override fun injectDependencies() { + componentManager().manageViewerComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().manageViewerComponent.release(ctx) + } + + companion object { + fun new(ctx: Id, dataview: Id): ManageViewerFragment = ManageViewerFragment().apply { + arguments = bundleOf(CTX_KEY to ctx, DATA_VIEW_KEY to dataview) + } + + const val CTX_KEY = "arg.manage-data-view-viewer.ctx" + const val DATA_VIEW_KEY = "arg.manage-data-view-viewer.dataview" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/PickFilterConditionFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/PickFilterConditionFragment.kt new file mode 100644 index 0000000000..733d355077 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/PickFilterConditionFragment.kt @@ -0,0 +1,139 @@ +package com.anytypeio.anytype.ui.sets.modals + +import android.app.Dialog +import android.os.Bundle +import android.view.* +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.features.sets.PickFilterConditionAdapter +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.argInt +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ext.withParent +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.filter.PickFilterConditionViewModel +import com.anytypeio.anytype.presentation.sets.model.Viewer +import com.anytypeio.anytype.ui.sets.modals.filter.UpdateConditionActionReceiver +import kotlinx.android.synthetic.main.fragment_list.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class PickFilterConditionFragment : DialogFragment() { + + private val ctx: String get() = arg(CTX_KEY) + private val mode: Int get() = argInt(ARG_MODE) + private val type: Viewer.Filter.Type get() = requireArguments().getParcelable(TYPE_KEY)!! + private val index: Int get() = argInt(INDEX_KEY) + + @Inject + lateinit var factory: PickFilterConditionViewModel.Factory + private val vm: PickFilterConditionViewModel by viewModels { factory } + + val isDismissed = MutableSharedFlow(replay = 0) + + override fun onCreate(savedInstanceState: Bundle?) { + injectDependencies() + super.onCreate(savedInstanceState) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.window?.requestFeature(Window.FEATURE_NO_TITLE) + return dialog + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_list, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(isDismissed) { isDismissed -> if (isDismissed) dismiss() } + subscribe(vm.views) { screenState -> + recycler.adapter = PickFilterConditionAdapter( + picked = screenState.picked, + conditions = screenState.conditions, + click = this@PickFilterConditionFragment::click + ) + } + } + } + + override fun onStart() { + super.onStart() + setDialogWidthAndGravity() + vm.onStart(type, index) + } + + private fun click(condition: Viewer.Filter.Condition) { + withParent { update(condition) } + lifecycleScope.launch { isDismissed.emit(true) } + } + + private fun setDialogWidthAndGravity() { + dialog?.window?.apply { + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + setGravity(Gravity.BOTTOM) + setBackgroundDrawableResource(android.R.color.transparent) + } + } + + override fun onDestroyView() { + releaseDependencies() + super.onDestroyView() + } + + fun injectDependencies() { + when (mode) { + MODE_CREATE -> componentManager().pickFilterConditionComponentCreate.get(ctx) + .inject(this) + MODE_MODIFY -> componentManager().pickFilterConditionComponentModify.get(ctx) + .inject(this) + else -> throw RuntimeException("Wrong mode") + } + } + + fun releaseDependencies() { + when (mode) { + MODE_CREATE -> componentManager().pickFilterConditionComponentCreate.release(ctx) + MODE_MODIFY -> componentManager().pickFilterConditionComponentModify.release(ctx) + else -> throw RuntimeException("Wrong mode") + } + } + + + companion object { + + const val MODE_CREATE = 1 + const val MODE_MODIFY = 2 + + private const val CTX_KEY = "arg.create-filter-relation.ctx" + private const val ARG_MODE = "arg.create-filter-relation.mode" + private const val TYPE_KEY = "arg.create-filter-relation.type" + private const val INDEX_KEY = "arg.create-filter-relation.index" + + fun new( + ctx: Id, + mode: Int, + type: Viewer.Filter.Type, + index: Int + ) = PickFilterConditionFragment().apply { + arguments = bundleOf( + CTX_KEY to ctx, + ARG_MODE to mode, + TYPE_KEY to type, + INDEX_KEY to index + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/PickSortingKeyFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/PickSortingKeyFragment.kt new file mode 100644 index 0000000000..ddfc9d45f5 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/PickSortingKeyFragment.kt @@ -0,0 +1,59 @@ +package com.anytypeio.anytype.ui.sets.modals + +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.core_ui.features.sets.SortingRelationPickerAdapter +import com.anytypeio.anytype.core_utils.ext.argList +import com.anytypeio.anytype.core_utils.ext.argStringOrNull +import com.anytypeio.anytype.core_utils.ext.withParent +import com.anytypeio.anytype.presentation.sets.model.SimpleRelationView +import com.anytypeio.anytype.presentation.sets.model.SortingExpression +import com.anytypeio.anytype.ui.sets.ViewerSortByFragment + +class PickSortingKeyFragment : BaseDialogListFragment() { + + private val selected get() = argStringOrNull(ARG_SELECTED) + + private val relations get() = argList(ARG_RELATIONS) + private val sorts get() = argList(ARG_SORTS) + + override fun setAdapter(recyclerView: RecyclerView) { + recyclerView.adapter = SortingRelationPickerAdapter( + sorts = sorts, + relations = relations, + relationSelectedKey = selected, + click = { keySelected, keyNew -> + withParent { + if (keySelected == null) { + onAddSortKey(keyNew) + } else { + onReplaceSortKey( + keySelected = keySelected, + keyNew = keyNew + ) + } + } + dismiss() + } + ) + } + + companion object { + const val ARG_SELECTED = "arg.viewer.sorts.key.selected" + const val ARG_RELATIONS = "arg.viewer.sorts.key.relations" + const val ARG_SORTS = "arg.viewer.sorts.key.sorts" + + fun new( + selected: String?, + relations: List, + sorts: ArrayList + ) = + PickSortingKeyFragment().apply { + arguments = bundleOf( + ARG_SELECTED to selected, + ARG_RELATIONS to relations, + ARG_SORTS to sorts + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/PickSortingTypeFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/PickSortingTypeFragment.kt new file mode 100644 index 0000000000..939ff2325c --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/PickSortingTypeFragment.kt @@ -0,0 +1,43 @@ +package com.anytypeio.anytype.ui.sets.modals + +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.core_ui.features.sets.PickSortingTypeAdapter +import com.anytypeio.anytype.core_utils.ext.argInt +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ext.withParent +import com.anytypeio.anytype.presentation.sets.model.Viewer +import com.anytypeio.anytype.ui.sets.ViewerSortByFragment + +class PickSortingTypeFragment : BaseDialogListFragment() { + + private val keySelected get() = argString(ARG_KEY_SELECTED) + private val typeSelected get() = argInt(ARG_TYPE_SELECTED) + private val types = listOf(Viewer.SortType.ASC, Viewer.SortType.DESC) + + override fun setAdapter(recyclerView: RecyclerView) { + recyclerView.adapter = PickSortingTypeAdapter( + items = types, + typeSelected = typeSelected, + keySelected = keySelected, + click = { key, type -> + withParent { + onPickSortType(key, type) + } + dismiss() + } + ) + } + + companion object { + const val ARG_KEY_SELECTED = "arg.viewer.sorts.type.key" + const val ARG_TYPE_SELECTED = "arg.viewer.sorts.type.selected" + + fun new(key: String, type: Int) = PickSortingTypeFragment().apply { + arguments = bundleOf( + ARG_KEY_SELECTED to key, + ARG_TYPE_SELECTED to type + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerBottomSheetRootFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerBottomSheetRootFragment.kt new file mode 100644 index 0000000000..369b0205ab --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerBottomSheetRootFragment.kt @@ -0,0 +1,125 @@ +package com.anytypeio.anytype.ui.sets.modals + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_ui.tools.BottomSheetSharedTransition +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ext.withParent +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.presentation.sets.model.FilterExpression +import com.anytypeio.anytype.presentation.sets.model.SortingExpression +import com.anytypeio.anytype.ui.sets.ObjectSetFragment +import com.anytypeio.anytype.ui.sets.ViewerFilterFragment +import com.anytypeio.anytype.ui.sets.ViewerSortByFragment +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +class ViewerBottomSheetRootFragment : BottomSheetDialogFragment() { + + private val ctx get() = argString(CONTEXT_ID_KEY) + private val viewer get() = argString(VIEWER_ID_KEY) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(DialogFragment.STYLE_NORMAL, R.style.AppBottomSheetDialogTheme) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_viewer_bottom_sheet_root, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + childFragmentManager + .beginTransaction() + .add(R.id.container, ViewerCustomizeFragment.new(ctx, viewer)) + .addToBackStack(TAG_ROOT) + .commit() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState) + .also { dialog -> + dialog.setOnShowListener { + val viewBehavior = BottomSheetBehavior.from( + dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) + ) + setupBehavior(viewBehavior) + } + } + } + + fun transitToFilter() = {} + + fun transitToCustomize() = transitToFragment( + ViewerCustomizeFragment.new( + ctx = ctx, + viewer = viewer + ) + ) + + fun transitToSorting() = transitToFragment( + ViewerSortByFragment.new( + ctx = ctx, + viewer = viewer + ) + ) + + fun transitToRelations() {} + + fun dispatchResultSortsAndDismiss(sorts: List) { + withParent { onViewerNewSortsRequest(sorts) } + dismiss() + } + + fun dispatchResultFiltersAndDismiss(filters: List) { + withParent { onViewerNewFiltersRequest(filters) } + dismiss() + } + + private fun transitToFragment(newFragment: Fragment) { + val currentFragmentRoot = childFragmentManager.fragments[0].requireView() + + childFragmentManager + .beginTransaction() + .apply { + addSharedElement(currentFragmentRoot, currentFragmentRoot.transitionName) + setReorderingAllowed(true) + newFragment.sharedElementEnterTransition = BottomSheetSharedTransition() + } + .replace(R.id.container, newFragment) + .addToBackStack(newFragment.javaClass.name) + .commit() + } + + private fun setupBehavior(bottomSheetBehavior: BottomSheetBehavior) { + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + bottomSheetBehavior.isHideable = true + bottomSheetBehavior.skipCollapsed = true + } + + // INSTANTIATION + + companion object { + + fun new(ctx: Id, viewer: Id) = ViewerBottomSheetRootFragment().apply { + arguments = bundleOf( + CONTEXT_ID_KEY to ctx, + VIEWER_ID_KEY to viewer + ) + } + + const val VIEWER_ID_KEY = "arg.viewer.root.viewer_id" + const val CONTEXT_ID_KEY = "arg.viewer.root.context_id" + const val TAG_ROOT = "tag.root" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerCustomizeFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerCustomizeFragment.kt new file mode 100644 index 0000000000..325233473d --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerCustomizeFragment.kt @@ -0,0 +1,97 @@ +package com.anytypeio.anytype.ui.sets.modals + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.presentation.sets.ViewerCustomizeViewModel +import com.anytypeio.anytype.presentation.sets.ViewerCustomizeViewState +import kotlinx.android.synthetic.main.fragment_viewer_customize.* +import javax.inject.Inject + +class ViewerCustomizeFragment : BaseBottomSheetFragment() { + + private val ctx get() = argString(CONTEXT_ID_KEY) + private val viewer get() = argString(VIEWER_ID_KEY) + + @Inject + lateinit var factory: ViewerCustomizeViewModel.Factory + private val vm: ViewerCustomizeViewModel by viewModels { factory } + + lateinit var filtersCount: TextView + lateinit var sortsCount: TextView + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_viewer_customize, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + filtersCount = view.findViewById(R.id.filterCount) + sortsCount = view.findViewById(R.id.sortsCount) + lifecycleScope.subscribe(vm.viewState) { + observeViewState(it) + } + itemFilter.setOnClickListener { + withParent { transitToFilter() } + } + itemSort.setOnClickListener { + withParent { transitToSorting() } + } + itemRelations.setOnClickListener { + withParent { transitToRelations() } + } + vm.onViewCreated(viewerId = viewer) + } + + private fun observeViewState(viewState: ViewerCustomizeViewState) { + when (viewState) { + ViewerCustomizeViewState.Init -> { + } + is ViewerCustomizeViewState.InitGrid -> { + itemTable.show() + if (viewState.isShowFilterSize) { + filtersCount.text = viewState.filterSize + } else { + filtersCount.invisible() + } + if (viewState.isShowSortsSize) { + sortsCount.text = viewState.sortsSize + } else { + sortsCount.invisible() + } + } + } + } + + override fun injectDependencies() { + componentManager().viewerCustomizeComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().viewerCustomizeComponent.release(ctx) + } + + companion object { + const val CONTEXT_ID_KEY = "arg.viewer.customize.context" + const val VIEWER_ID_KEY = "arg.viewer.customize.viewer_id" + + fun new(ctx: Id, viewer: Id) = ViewerCustomizeFragment().apply { + arguments = bundleOf( + CONTEXT_ID_KEY to ctx, + VIEWER_ID_KEY to viewer + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerRelationOptionFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerRelationOptionFragment.kt new file mode 100644 index 0000000000..306055aede --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerRelationOptionFragment.kt @@ -0,0 +1,84 @@ +package com.anytypeio.anytype.ui.sets.modals + +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.extensions.relationIcon +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ext.toast +import com.anytypeio.anytype.core_utils.ui.BaseDialogFragment +import com.anytypeio.anytype.presentation.sets.model.ColumnView +import kotlinx.android.synthetic.main.fragment_viewer_relation_option.* +import java.util.* + +class ViewerRelationOptionFragment : BaseDialogFragment() { + + private val ctx: String get() = arg(CTX_KEY) + private val relation: String get() = arg(RELATION_KEY) + private val format: ColumnView.Format? get() = requireArguments().getParcelable(FORMAT_KEY) + private val title: String get() = arg(TITLE_KEY) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_viewer_relation_option, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + tvTitle.text = title + format?.let { + tvFormat.text = it.name.toLowerCase(Locale.ROOT).capitalize(Locale.ROOT) + iconFormat.setBackgroundResource(it.relationIcon()) + } + with(lifecycleScope) { + subscribe(openToEditViewContainer.clicks()) { toast("Not implemented yet") } + subscribe(removeViewContainer.clicks()) { toast("Not implemented yet") } + } + } + + override fun onStart() { + super.onStart() + setupAppearance() + } + + override fun injectDependencies() { + } + + override fun releaseDependencies() { + } + + private fun setupAppearance() { + dialog?.window?.apply { + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + setGravity(Gravity.BOTTOM) + setBackgroundDrawableResource(android.R.color.transparent) + setWindowAnimations(R.style.DefaultBottomDialogAnimation) + } + } + + companion object { + fun new(ctx: Id, title: String, relation: Id, format: ColumnView.Format) = + ViewerRelationOptionFragment().apply { + arguments = bundleOf( + CTX_KEY to ctx, + TITLE_KEY to title, + RELATION_KEY to relation, + FORMAT_KEY to format + ) + } + + const val CTX_KEY = "arg.dialog.relation-option.ctx" + const val RELATION_KEY = "arg.dialog.relation-option.relation" + const val FORMAT_KEY = "arg.dialog.relation-option.format" + const val TITLE_KEY = "arg.dialog.relation-option.title" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerRelationsFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerRelationsFragment.kt new file mode 100644 index 0000000000..4f2697e25a --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/ViewerRelationsFragment.kt @@ -0,0 +1,147 @@ +package com.anytypeio.anytype.ui.sets.modals + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.extensions.drawable +import com.anytypeio.anytype.core_ui.features.dataview.ViewerModifyOrderAdapter +import com.anytypeio.anytype.core_ui.features.dataview.ViewerRelationsAdapter +import com.anytypeio.anytype.core_ui.layout.DividerVerticalItemDecoration +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_ui.tools.DefaultDragAndDropBehavior +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.core_utils.ui.OnStartDragListener +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.relations.ViewerRelationsViewModel +import com.anytypeio.anytype.presentation.sets.model.SimpleRelationView +import kotlinx.android.synthetic.main.fragment_viewer_relations_list.* +import javax.inject.Inject + +class ViewerRelationsFragment : BaseBottomSheetFragment(), OnStartDragListener { + + @Inject + lateinit var factory: ViewerRelationsViewModel.Factory + private val vm: ViewerRelationsViewModel by viewModels { factory } + + private val ctx get() = arg(CONTEXT_KEY) + + private val listAdapter by lazy { + ViewerRelationsAdapter( + onSwitchClick = { vm.onSwitchClicked(ctx, it) } + ) + } + private val editAdapter by lazy { + ViewerModifyOrderAdapter( + dragListener = this, + onMoreClick = { navigateToOptions(it) } + ) + } + private val dndItemTouchHelper: ItemTouchHelper by lazy { ItemTouchHelper(dndBehavior) } + private val dndBehavior by lazy { + DefaultDragAndDropBehavior( + onItemMoved = { from, to -> editAdapter.onItemMove(from, to) }, + onItemDropped = { vm.onOrderChanged(ctx, editAdapter.order) } + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_viewer_relations_list, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + recycler.apply { + layoutManager = LinearLayoutManager(requireContext()) + addItemDecoration( + DividerVerticalItemDecoration( + divider = requireContext().drawable(R.drawable.divider_relation_layer), + isShowInLastItem = false + ) + ) + dndItemTouchHelper.attachToRecyclerView(this) + } + with(lifecycleScope) { + subscribe(editBtn.clicks()) { vm.onEditButtonClicked() } + subscribe(doneBtn.clicks()) { vm.onDoneButtonClicked() } + subscribe(iconAdd.clicks()) { toast("Not implemented yet")} + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.toasts) { toast(it) } + subscribe(vm.views) { + listAdapter.update(it) + editAdapter.update(it) + } + subscribe(vm.screenState) { render(it) } + } + } + + private fun render(state: ViewerRelationsViewModel.ScreenState) { + when (state) { + ViewerRelationsViewModel.ScreenState.LIST -> { + iconAdd.visible() + editBtn.visible() + doneBtn.invisible() + recycler.apply { + adapter = listAdapter + } + } + ViewerRelationsViewModel.ScreenState.EDIT -> { + iconAdd.invisible() + doneBtn.visible() + editBtn.invisible() + recycler.apply { + adapter = editAdapter + } + } + } + } + + private fun navigateToOptions(item: SimpleRelationView) { + val dialog = ViewerRelationOptionFragment.new( + ctx = ctx, + relation = item.key, + title = item.title, + format = item.format + ) + dialog.show(parentFragmentManager, null) + } + + override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) { + dndItemTouchHelper.startDrag(viewHolder) + } + + override fun injectDependencies() { + componentManager().viewerRelationsComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().viewerRelationsComponent.release(ctx) + } + + companion object { + + fun new(ctx: Id) = ViewerRelationsFragment().apply { + arguments = bundleOf( + CONTEXT_KEY to ctx + ) + } + + private const val CONTEXT_KEY = "arg.viewer-relation-list.ctx" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFlow.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFlow.kt new file mode 100644 index 0000000000..cff80b373e --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFlow.kt @@ -0,0 +1,9 @@ +package com.anytypeio.anytype.ui.sets.modals.filter + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.presentation.sets.model.SimpleRelationView + +interface CreateFilterFlow { + fun onRelationSelected(ctx: Id, relation: SimpleRelationView) + fun onFilterCreated() +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFlowRootFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFlowRootFragment.kt new file mode 100644 index 0000000000..a4b826b8a2 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFlowRootFragment.kt @@ -0,0 +1,97 @@ +package com.anytypeio.anytype.ui.sets.modals.filter + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.tools.BottomSheetSharedTransition +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.presentation.sets.filter.CreateFilterFlowViewModel +import com.anytypeio.anytype.presentation.sets.filter.CreateFilterFlowViewModel.Step +import com.anytypeio.anytype.presentation.sets.model.SimpleRelationView +import com.anytypeio.anytype.ui.sets.modals.ViewerBottomSheetRootFragment + +class CreateFilterFlowRootFragment : BaseBottomSheetFragment(), CreateFilterFlow { + + private val ctx: String get() = arg(CTX_KEY) + + val vm by lazy { CreateFilterFlowViewModel() } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return inflater.inflate(R.layout.fragment_viewer_bottom_sheet_root, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + lifecycleScope.subscribe(vm.step) { step -> + when (step) { + is Step.SelectRelation -> transitToSelection() + is Step.CreateFilter -> transitToCreation(step) + } + } + } + + override fun onRelationSelected(ctx: Id, relation: SimpleRelationView) { + vm.onRelationSelected(ctx = ctx, relation = relation.key, format = relation.format) + } + + override fun onFilterCreated() { + dismiss() + } + + private fun transitToCreation(step: Step.CreateFilter) { + val fr = when (step.type) { + Step.CreateFilter.Type.INPUT_FIELD -> { + CreateFilterFromInputFieldValueFragment.new( + ctx = step.ctx, + relation = step.relation + ) + } + else -> { + CreateFilterFromSelectedValueFragment.new( + ctx = step.ctx, + relation = step.relation + ) + } + } + + val currentFragmentRoot = childFragmentManager.fragments[0].requireView() + + childFragmentManager + .beginTransaction() + .apply { + addSharedElement(currentFragmentRoot, currentFragmentRoot.transitionName) + setReorderingAllowed(true) + fr.sharedElementEnterTransition = BottomSheetSharedTransition() + } + .replace(R.id.container, fr) + .addToBackStack(fr.javaClass.name) + .commit() + } + + private fun transitToSelection() { + val fr = SelectFilterRelationFragment.new(ctx) + childFragmentManager + .beginTransaction() + .add(R.id.container, fr) + .addToBackStack(ViewerBottomSheetRootFragment.TAG_ROOT) + .commit() + } + + override fun injectDependencies() {} + override fun releaseDependencies() {} + + companion object { + fun new(ctx: Id): CreateFilterFlowRootFragment = CreateFilterFlowRootFragment().apply { + arguments = bundleOf(CTX_KEY to ctx) + } + + private const val CTX_KEY = "arg.create-filter-flow-root.ctx" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFromInputFieldValueFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFromInputFieldValueFragment.kt new file mode 100644 index 0000000000..f27aaa7338 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFromInputFieldValueFragment.kt @@ -0,0 +1,115 @@ +package com.anytypeio.anytype.ui.sets.modals.filter + +import android.os.Bundle +import android.view.View +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.extensions.relationIcon +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.filter.FilterViewModel +import com.anytypeio.anytype.presentation.sets.model.Viewer +import com.anytypeio.anytype.ui.sets.modals.PickFilterConditionFragment +import kotlinx.android.synthetic.main.fragment_create_or_update_filter.btnBottomAction +import kotlinx.android.synthetic.main.fragment_create_or_update_filter.ivRelationIcon +import kotlinx.android.synthetic.main.fragment_create_or_update_filter.tvRelationName +import kotlinx.android.synthetic.main.fragment_create_or_update_filter_input_field_value.* +import kotlinx.coroutines.flow.filterNotNull +import javax.inject.Inject + +class CreateFilterFromInputFieldValueFragment : + BaseFragment(R.layout.fragment_create_or_update_filter_input_field_value), UpdateConditionActionReceiver { + + private val ctx: String get() = arg(CTX_KEY) + private val relation: String get() = arg(RELATION_KEY) + + @Inject + lateinit var factory: FilterViewModel.Factory + private val vm: FilterViewModel by viewModels { factory } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + btnBottomAction.setText(R.string.create) + with(lifecycleScope) { + subscribe(btnBottomAction.clicks()) { + vm.onCreateInputValueFilterClicked( + ctx = ctx, + relation = relation, + input = enterTextValueInputField.text.toString() + ) + } + subscribe(tvFilterCondition.clicks()) { + vm.onConditionClicked() + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.relationState.filterNotNull()) { + tvRelationName.text = it.title + ivRelationIcon.setImageResource(it.format.relationIcon(true)) + } + subscribe(vm.isCompleted) { isCompleted -> + if (isCompleted) withParent { onFilterCreated() } + } + subscribe(vm.conditionState) { + tvFilterCondition.text = it?.condition?.title + if (it?.isFilterValueEnabled == true) { + enterTextValueInputField.enable() + } else { + enterTextValueInputField.text = null + enterTextValueInputField.disable() + } + } + subscribe(vm.commands) { observeCommands(it) } + } + } + + private fun observeCommands(commands: FilterViewModel.Commands) { + when (commands) { + is FilterViewModel.Commands.OpenConditionPicker -> { + PickFilterConditionFragment.new( + ctx = ctx, + mode = PickFilterConditionFragment.MODE_CREATE, + type = commands.type, + index = commands.index + ).show(childFragmentManager, null) + } + is FilterViewModel.Commands.OpenDatePicker -> {} + } + } + + override fun onStart() { + super.onStart() + vm.onStart(relation, FILTER_INDEX_EMPTY) + } + + override fun update(condition: Viewer.Filter.Condition) { + vm.onConditionUpdate(condition) + } + + override fun injectDependencies() { + componentManager().createFilterComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().createFilterComponent.release(ctx) + } + + companion object { + fun new(ctx: Id, relation: Id) = CreateFilterFromInputFieldValueFragment().apply { + arguments = bundleOf(CTX_KEY to ctx, RELATION_KEY to relation) + } + + private const val CTX_KEY = "arg.create-filter-relation.ctx" + private const val RELATION_KEY = "arg.create-filter-relation.relation" + val FILTER_INDEX_EMPTY: Int? = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFromSelectedValueFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFromSelectedValueFragment.kt new file mode 100644 index 0000000000..d587a237b0 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/CreateFilterFromSelectedValueFragment.kt @@ -0,0 +1,158 @@ +package com.anytypeio.anytype.ui.sets.modals.filter + +import android.os.Bundle +import android.view.View +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.extensions.relationIcon +import com.anytypeio.anytype.core_ui.features.sets.CreateFilterAdapter +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_ui.reactive.textChanges +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.filter.FilterViewModel +import com.anytypeio.anytype.presentation.sets.model.ColumnView +import com.anytypeio.anytype.presentation.sets.model.Viewer +import com.anytypeio.anytype.ui.sets.modals.DatePickerFragment +import com.anytypeio.anytype.ui.sets.modals.DatePickerFragment.DatePickerReceiver +import com.anytypeio.anytype.ui.sets.modals.PickFilterConditionFragment +import com.anytypeio.anytype.ui.sets.modals.filter.CreateFilterFromInputFieldValueFragment.Companion.FILTER_INDEX_EMPTY +import kotlinx.android.synthetic.main.fragment_create_or_update_filter.* +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.onStart +import javax.inject.Inject + +open class CreateFilterFromSelectedValueFragment : + BaseFragment(R.layout.fragment_create_or_update_filter), UpdateConditionActionReceiver, + DatePickerReceiver { + + private val ctx: String get() = arg(CTX_KEY) + private val relation: String get() = arg(RELATION_KEY) + + @Inject + lateinit var factory: FilterViewModel.Factory + + private val vm: FilterViewModel by viewModels { factory } + + private val createFilterAdapter by lazy { + CreateFilterAdapter( + onItemClicked = vm::onFilterItemClicked + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + btnBottomAction.setText(R.string.create) + rvViewerFilterRecycler.apply { + layoutManager = LinearLayoutManager(context) + adapter = createFilterAdapter + addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply { + setDrawable(drawable(R.drawable.divider_relations)) + } + ) + } + with(lifecycleScope) { + subscribe(tvFilterCondition.clicks()) { + vm.onConditionClicked() + } + subscribe(btnBottomAction.clicks()) { + vm.onCreateFilterFromSelectedValueClicked(ctx = ctx, relation = relation) + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.relationState.filterNotNull()) { + if (it.format == ColumnView.Format.DATE) { + searchBar.gone() + tvOptionCount.gone() + } + tvRelationName.text = it.title + ivRelationIcon.setImageResource(it.format.relationIcon(true)) + } + subscribe(vm.optionCountState) { tvOptionCount.text = it.toString() } + subscribe(vm.isCompleted) { isCompleted -> + if (isCompleted) withParent { onFilterCreated() } + } + subscribe(vm.conditionState) { + tvFilterCondition.text = it?.condition?.title + if (it?.isFilterValueEnabled == true) { + searchBar.visible() + tvOptionCount.visible() + } else { + searchBar.gone() + tvOptionCount.gone() + } + } + val queries = searchRelationInput.textChanges() + .onStart { emit(searchRelationInput.text.toString()) } + val views = + vm.filterValueListState.combine(queries) { views, query -> + if (views.isEmpty()) { + views + } else { + views.filter { it.text.contains(query, true) } + } + } + subscribe(views) { createFilterAdapter.update(it) } + subscribe(vm.commands) { observeCommands(it) } + } + } + + private fun observeCommands(commands: FilterViewModel.Commands) { + when (commands) { + is FilterViewModel.Commands.OpenDatePicker -> { + DatePickerFragment.new(commands.timeInMillis) + .show(childFragmentManager, null) + } + is FilterViewModel.Commands.OpenConditionPicker -> { + PickFilterConditionFragment.new( + ctx = ctx, + mode = PickFilterConditionFragment.MODE_CREATE, + type = commands.type, + index = commands.index + ).show(childFragmentManager, null) + } + } + } + + override fun update(condition: Viewer.Filter.Condition) { + vm.onConditionUpdate(condition) + } + + override fun onPickDate(timeInSeconds: Long) { + vm.onExactDayPicked(timeInSeconds) + } + + override fun onStart() { + super.onStart() + vm.onStart(relationId = relation, filterIndex = FILTER_INDEX_EMPTY) + } + + override fun injectDependencies() { + componentManager().createFilterComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().createFilterComponent.release(ctx) + } + + companion object { + fun new(ctx: Id, relation: Id): CreateFilterFromSelectedValueFragment = CreateFilterFromSelectedValueFragment().apply { + arguments = bundleOf(CTX_KEY to ctx, RELATION_KEY to relation) + } + + const val CTX_KEY = "arg.create-filter-relation.ctx" + const val RELATION_KEY = "arg.create-filter-relation.relation" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/ModifyFilterFromInputFieldValueFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/ModifyFilterFromInputFieldValueFragment.kt new file mode 100644 index 0000000000..e9b45d7bf7 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/ModifyFilterFromInputFieldValueFragment.kt @@ -0,0 +1,140 @@ +package com.anytypeio.anytype.ui.sets.modals.filter + +import android.os.Bundle +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.extensions.relationIcon +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.disable +import com.anytypeio.anytype.core_utils.ext.enable +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.extension.getTextValue +import com.anytypeio.anytype.presentation.sets.filter.FilterViewModel +import com.anytypeio.anytype.presentation.sets.model.ColumnView +import com.anytypeio.anytype.presentation.sets.model.Viewer +import com.anytypeio.anytype.ui.sets.modals.PickFilterConditionFragment +import kotlinx.android.synthetic.main.fragment_create_or_update_filter_input_field_value.* +import kotlinx.coroutines.flow.filterNotNull +import javax.inject.Inject + +open class ModifyFilterFromInputFieldValueFragment : BaseBottomSheetFragment(), UpdateConditionActionReceiver { + + private val ctx: String get() = arg(CTX_KEY) + private val relation: String get() = arg(RELATION_KEY) + private val index: Int get() = arg(IDX_KEY) + + @Inject + lateinit var factory: FilterViewModel.Factory + + private val vm: FilterViewModel by viewModels { factory } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_create_or_update_filter_input_field_value, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + btnBottomAction.setText(R.string.apply) + with(lifecycleScope) { + subscribe(btnBottomAction.clicks()) { + vm.onModifyApplyClicked( + ctx = ctx, + input = enterTextValueInputField.text.toString() + ) + } + subscribe(tvFilterCondition.clicks()) { + vm.onConditionClicked() + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.relationState.filterNotNull()) { + tvRelationName.text = it.title + ivRelationIcon.setImageResource(it.format.relationIcon(true)) + setupFormat(it.format) + } + subscribe(vm.filterValueState) { value -> + enterTextValueInputField.setText(value.getTextValue()) + } + subscribe(vm.isCompleted) { isCompleted -> + if (isCompleted) dismiss() + } + subscribe(vm.conditionState) { + tvFilterCondition.text = it?.condition?.title + if (it?.isFilterValueEnabled == true) { + enterTextValueInputField.enable() + } else { + enterTextValueInputField.text = null + enterTextValueInputField.disable() + } + } + subscribe(vm.commands) { observeCommands(it) } + } + } + + private fun observeCommands(commands: FilterViewModel.Commands) { + when (commands) { + is FilterViewModel.Commands.OpenDatePicker -> { + } + is FilterViewModel.Commands.OpenConditionPicker -> { + PickFilterConditionFragment.new( + ctx = ctx, + mode = PickFilterConditionFragment.MODE_MODIFY, + type = commands.type, + index = commands.index + ).show(childFragmentManager, null) + } + } + } + + private fun setupFormat(format: ColumnView.Format) { + val type = when (format) { + ColumnView.Format.NUMBER -> InputType.TYPE_CLASS_NUMBER + ColumnView.Format.URL -> InputType.TYPE_TEXT_VARIATION_URI + ColumnView.Format.EMAIL -> InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + ColumnView.Format.PHONE -> InputType.TYPE_CLASS_PHONE + else -> InputType.TYPE_CLASS_TEXT + } + enterTextValueInputField.inputType = type + } + + override fun onStart() { + super.onStart() + vm.onStart(relation, index) + } + + override fun update(condition: Viewer.Filter.Condition) { + vm.onConditionUpdate(condition) + } + + override fun injectDependencies() { + componentManager().modifyFilterComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().modifyFilterComponent.release(ctx) + } + + companion object { + fun new(ctx: Id, relation: Id, index: Int) = ModifyFilterFromInputFieldValueFragment().apply { + arguments = bundleOf(CTX_KEY to ctx, RELATION_KEY to relation, IDX_KEY to index) + } + + const val CTX_KEY = "arg.modify-filter-relation.ctx" + const val RELATION_KEY = "arg.modify-filter-relation.relation" + const val IDX_KEY = "arg.modify-filter-relation.index" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/ModifyFilterFromSelectedValueFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/ModifyFilterFromSelectedValueFragment.kt new file mode 100644 index 0000000000..c53725a778 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/ModifyFilterFromSelectedValueFragment.kt @@ -0,0 +1,162 @@ +package com.anytypeio.anytype.ui.sets.modals.filter + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.extensions.relationIcon +import com.anytypeio.anytype.core_ui.features.sets.CreateFilterAdapter +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_ui.reactive.textChanges +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.filter.FilterViewModel +import com.anytypeio.anytype.presentation.sets.model.ColumnView +import com.anytypeio.anytype.presentation.sets.model.Viewer +import com.anytypeio.anytype.ui.sets.modals.DatePickerFragment +import com.anytypeio.anytype.ui.sets.modals.DatePickerFragment.DatePickerReceiver +import com.anytypeio.anytype.ui.sets.modals.PickFilterConditionFragment +import kotlinx.android.synthetic.main.fragment_create_or_update_filter.* +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.onStart +import javax.inject.Inject + +open class ModifyFilterFromSelectedValueFragment : BaseBottomSheetFragment(), + UpdateConditionActionReceiver, DatePickerReceiver { + + private val ctx: String get() = arg(CTX_KEY) + private val relation: String get() = arg(RELATION_KEY) + private val index: Int get() = arg(IDX_KEY) + + @Inject + lateinit var factory: FilterViewModel.Factory + + private val vm: FilterViewModel by viewModels { factory } + + private val createFilterAdapter by lazy { + CreateFilterAdapter( + onItemClicked = vm::onFilterItemClicked + ) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_create_or_update_filter, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + btnBottomAction.setText(R.string.apply) + rvViewerFilterRecycler.apply { + layoutManager = LinearLayoutManager(context) + adapter = createFilterAdapter + addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply { + setDrawable(drawable(R.drawable.divider_relations)) + } + ) + } + with(lifecycleScope) { + subscribe(tvFilterCondition.clicks()) { + vm.onConditionClicked() + } + subscribe(btnBottomAction.clicks()) { + vm.onModifyApplyClicked(ctx = ctx) + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.relationState.filterNotNull()) { + if (it.format == ColumnView.Format.DATE) { + searchBar.gone() + tvOptionCount.gone() + } + tvRelationName.text = it.title + ivRelationIcon.setImageResource(it.format.relationIcon(true)) + } + subscribe(vm.optionCountState) { tvOptionCount.text = it.toString() } + subscribe(vm.isCompleted) { isCompleted -> if (isCompleted) dismiss() } + subscribe(vm.conditionState) { + tvFilterCondition.text = it?.condition?.title + if (it?.isFilterValueEnabled == true) { + searchBar.visible() + tvOptionCount.visible() + } else { + searchBar.gone() + tvOptionCount.gone() + } + } + val queries = searchRelationInput.textChanges() + .onStart { emit(searchRelationInput.text.toString()) } + val views = vm.filterValueListState + .combine(queries) { views, query -> + views.filter { + it.text.contains(query, true) + } + } + subscribe(views) { createFilterAdapter.update(it) } + subscribe(vm.commands) { observeCommands(it) } + } + } + + private fun observeCommands(commands: FilterViewModel.Commands) { + when (commands) { + is FilterViewModel.Commands.OpenDatePicker -> { + DatePickerFragment.new(commands.timeInMillis) + .show(childFragmentManager, null) + } + is FilterViewModel.Commands.OpenConditionPicker -> { + PickFilterConditionFragment.new( + ctx = ctx, + mode = PickFilterConditionFragment.MODE_MODIFY, + type = commands.type, + index = commands.index + ) + .show(childFragmentManager, null) + } + } + } + + override fun update(condition: Viewer.Filter.Condition) { + vm.onConditionUpdate(condition) + } + + override fun onPickDate(timeInSeconds: Long) { + vm.onExactDayPicked(timeInSeconds) + } + + override fun onStart() { + super.onStart() + vm.onStart(relation, index) + expand() + } + + override fun injectDependencies() { + componentManager().modifyFilterComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().modifyFilterComponent.release(ctx) + } + + companion object { + fun new(ctx: Id, relation: Id, index: Int): ModifyFilterFromSelectedValueFragment = ModifyFilterFromSelectedValueFragment().apply { + arguments = bundleOf(CTX_KEY to ctx, RELATION_KEY to relation, IDX_KEY to index) + } + + const val CTX_KEY = "arg.modify-filter-relation.ctx" + const val RELATION_KEY = "arg.modify-filter-relation.relation" + const val IDX_KEY = "arg.modify-filter-relation.index" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/SelectFilterRelationFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/SelectFilterRelationFragment.kt new file mode 100644 index 0000000000..d83705b65a --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/SelectFilterRelationFragment.kt @@ -0,0 +1,43 @@ +package com.anytypeio.anytype.ui.sets.modals.filter + +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.withParent +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.SearchRelationViewModel +import com.anytypeio.anytype.presentation.sets.SelectFilterRelationViewModel +import com.anytypeio.anytype.presentation.sets.model.SimpleRelationView +import com.anytypeio.anytype.ui.sets.modals.search.SearchRelationFragment +import javax.inject.Inject + +class SelectFilterRelationFragment : SearchRelationFragment() { + + override val ctx: String get() = arg(CTX_KEY) + + @Inject + lateinit var factory: SelectFilterRelationViewModel.Factory + + override val vm: SearchRelationViewModel by viewModels { factory } + + override fun onRelationClicked(ctx: Id, relation: SimpleRelationView) { + withParent { onRelationSelected(ctx, relation) } + } + + override fun injectDependencies() { + componentManager().selectFilterRelationComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().selectFilterRelationComponent.release(ctx) + } + + companion object { + fun new(ctx: Id): SelectFilterRelationFragment = SelectFilterRelationFragment().apply { + arguments = bundleOf(CTX_KEY to ctx) + } + + const val CTX_KEY = "arg.select-filter-relation.ctx" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/UpdateConditionActionReceiver.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/UpdateConditionActionReceiver.kt new file mode 100644 index 0000000000..369da1a046 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/filter/UpdateConditionActionReceiver.kt @@ -0,0 +1,7 @@ +package com.anytypeio.anytype.ui.sets.modals.filter + +import com.anytypeio.anytype.presentation.sets.model.Viewer + +interface UpdateConditionActionReceiver { + fun update(condition: Viewer.Filter.Condition) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/search/SearchRelationFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/search/SearchRelationFragment.kt new file mode 100644 index 0000000000..7d4ba01779 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/search/SearchRelationFragment.kt @@ -0,0 +1,61 @@ +package com.anytypeio.anytype.ui.sets.modals.search + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.features.sets.SearchRelationAdapter +import com.anytypeio.anytype.core_ui.reactive.textChanges +import com.anytypeio.anytype.core_utils.ext.drawable +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.presentation.sets.SearchRelationViewModel +import com.anytypeio.anytype.presentation.sets.model.SimpleRelationView +import kotlinx.android.synthetic.main.fragment_select_sort_or_filter_relation.* + +abstract class SearchRelationFragment : BaseBottomSheetFragment() { + + abstract val ctx: String + abstract val vm: SearchRelationViewModel + + private val searchRelationAdapter by lazy { + SearchRelationAdapter { relation -> onRelationClicked(ctx = ctx, relation = relation) } + } + + abstract fun onRelationClicked(ctx: Id, relation: SimpleRelationView) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_select_sort_or_filter_relation, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + searchRelationRecycler.apply { + layoutManager = LinearLayoutManager(context) + adapter = searchRelationAdapter + addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply { + setDrawable(drawable(R.drawable.divider_sort_or_filter_relation)) + } + ) + } + with(lifecycleScope) { + subscribe(searchRelationInput.textChanges()) { vm.onSearchQueryChanged(it.toString()) } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.views) { searchRelationAdapter.update(it) } + subscribe(vm.isDismissed) { isDismissed -> if (isDismissed) dismiss() } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/sort/ModifyViewerSortFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/sort/ModifyViewerSortFragment.kt new file mode 100644 index 0000000000..ba496ae0bb --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/sort/ModifyViewerSortFragment.kt @@ -0,0 +1,93 @@ +package com.anytypeio.anytype.ui.sets.modals.sort + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.DVSortType +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.extensions.text +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.invisible +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ext.visible +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.sort.ModifyViewerSortViewModel +import kotlinx.android.synthetic.main.fragment_modify_viewer_sort.* +import kotlinx.coroutines.flow.filterNotNull +import javax.inject.Inject + +class ModifyViewerSortFragment : BaseBottomSheetFragment() { + + private val ctx: String get() = arg(CTX_KEY) + private val relation: String get() = arg(RELATION_KEY) + + @Inject + lateinit var factory: ModifyViewerSortViewModel.Factory + + private val vm: ModifyViewerSortViewModel by viewModels { factory } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_modify_viewer_sort, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + with(lifecycleScope) { + subscribe(tvSortAsc.clicks()) { vm.onSortAscSelected(ctx, relation) } + subscribe(tvSortDesc.clicks()) { vm.onSortDescSelected(ctx, relation) } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.isDismissed) { isDismissed -> if (isDismissed) dismiss() } + subscribe(vm.viewState.filterNotNull()) { state -> + tvSortAsc.setText(DVSortType.ASC.text(state.format)) + tvSortDesc.setText(DVSortType.DESC.text(state.format)) + when (state.type) { + Block.Content.DataView.Sort.Type.ASC -> { + ivAscSelected.visible() + ivDescSelected.invisible() + } + Block.Content.DataView.Sort.Type.DESC -> { + ivAscSelected.invisible() + ivDescSelected.visible() + } + } + } + } + } + + override fun onStart() { + super.onStart() + vm.onStart(relation) + } + + override fun injectDependencies() { + componentManager().modifyViewerSortComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().modifyViewerSortComponent.release(ctx) + } + + companion object { + fun new(ctx: Id, relation: Id): ModifyViewerSortFragment = ModifyViewerSortFragment().apply { + arguments = bundleOf(CTX_KEY to ctx, RELATION_KEY to relation) + } + + private const val CTX_KEY = "arg.modify-viewer-sort.ctx" + private const val RELATION_KEY = "arg.modify-viewer-sort.relation" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/sort/SelectSortRelationFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/sort/SelectSortRelationFragment.kt new file mode 100644 index 0000000000..d34145dcf9 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/sort/SelectSortRelationFragment.kt @@ -0,0 +1,86 @@ +package com.anytypeio.anytype.ui.sets.modals.sort + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.features.sets.SearchRelationAdapter +import com.anytypeio.anytype.core_ui.reactive.textChanges +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.drawable +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.SelectSortRelationViewModel +import kotlinx.android.synthetic.main.fragment_select_sort_or_filter_relation.* +import javax.inject.Inject + +class SelectSortRelationFragment : BaseBottomSheetFragment() { + + private val ctx: String get() = arg(CTX_KEY) + + private val searchRelationAdapter by lazy { + SearchRelationAdapter { relation -> + vm.onRelationClicked(ctx = ctx, relation = relation) + } + } + + @Inject + lateinit var factory: SelectSortRelationViewModel.Factory + + private val vm: SelectSortRelationViewModel by viewModels { factory } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_select_sort_or_filter_relation, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + searchRelationRecycler.apply { + layoutManager = LinearLayoutManager(context) + adapter = searchRelationAdapter + addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply { + setDrawable(drawable(R.drawable.divider_sort_or_filter_relation)) + } + ) + } + with(lifecycleScope) { + subscribe(searchRelationInput.textChanges()) { vm.onSearchQueryChanged(it.toString()) } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.views) { searchRelationAdapter.update(it) } + subscribe(vm.isDismissed) { isDismissed -> if (isDismissed) dismiss() } + } + } + + override fun injectDependencies() { + componentManager().selectSortRelationComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().selectSortRelationComponent.release(ctx) + } + + companion object { + + fun new(ctx: Id): SelectSortRelationFragment = SelectSortRelationFragment().apply { + arguments = bundleOf(CTX_KEY to ctx) + } + + const val CTX_KEY = "arg.select-sort-relation.ctx" + } +} diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/sort/ViewerSortFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/sort/ViewerSortFragment.kt new file mode 100644 index 0000000000..18e22c20eb --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/modals/sort/ViewerSortFragment.kt @@ -0,0 +1,116 @@ +package com.anytypeio.anytype.ui.sets.modals.sort + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.features.sets.ViewerSortAdapter +import com.anytypeio.anytype.core_ui.reactive.clicks +import com.anytypeio.anytype.core_utils.ext.* +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.sets.sort.ViewerSortViewModel +import com.anytypeio.anytype.presentation.sets.sort.ViewerSortViewModel.Mode +import kotlinx.android.synthetic.main.fragment_viewer_sort.* +import javax.inject.Inject + +open class ViewerSortFragment : BaseBottomSheetFragment() { + + private val ctx: String get() = arg(CTX_KEY) + + @Inject + lateinit var factory: ViewerSortViewModel.Factory + + private val vm: ViewerSortViewModel by viewModels { factory } + + private val viewerSortAdapter by lazy { + ViewerSortAdapter( + onAddViewerSortClicked = { navigateToSelectSort() }, + onViewerSortClicked = { view -> + if (view.mode == Mode.READ) navigateToChangeSort(view.relation) + }, + onRemoveViewerSortClicked = { vm.onRemoveViewerSortClicked(ctx, it) } + ) + } + + private fun navigateToSelectSort() { + val fr = SelectSortRelationFragment.new(ctx) + fr.show(parentFragmentManager, null) + } + + private fun navigateToChangeSort(relation: Id) { + val fr = ModifyViewerSortFragment.new(ctx = ctx, relation = relation) + fr.show(parentFragmentManager, null) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_viewer_sort, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewerSortRecycler.apply { + layoutManager = LinearLayoutManager(context) + adapter = viewerSortAdapter + addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply { + setDrawable(drawable(R.drawable.decoration_viewer_sort)) + } + ) + } + with(lifecycleScope) { + subscribe(btnResetSort.clicks()) { vm.onResetClicked(ctx) } + subscribe(btnEditSortOrDone.clicks()) { + if (btnEditSortOrDone.text == getString(R.string.edit)) + vm.onEditClicked() + else if (btnEditSortOrDone.text == getString(R.string.done)) + vm.onDoneClicked() + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + with(lifecycleScope) { + subscribe(vm.mode) { mode -> + when (mode) { + Mode.READ -> { + btnEditSortOrDone.setText(R.string.edit) + btnResetSort.visible() + } + Mode.EDIT -> { + btnEditSortOrDone.setText(R.string.done) + btnResetSort.invisible() + } + } + } + subscribe(vm.views) { viewerSortAdapter.update(it) } + subscribe(vm.isDismissed) { isDismissed -> if (isDismissed) dismiss() } + } + } + + override fun injectDependencies() { + componentManager().viewerSortComponent.get(ctx).inject(this) + } + + override fun releaseDependencies() { + componentManager().viewerSortComponent.release(ctx) + } + + companion object { + fun new(ctx: Id): ViewerSortFragment = ViewerSortFragment().apply { + arguments = bundleOf(CTX_KEY to ctx) + } + + const val CTX_KEY = "arg.viewer-sort.ctx" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/DebugSettingsFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/DebugSettingsFragment.kt index c211f307b6..dcbfe923b4 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/settings/DebugSettingsFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/DebugSettingsFragment.kt @@ -1,13 +1,18 @@ package com.anytypeio.anytype.ui.settings +import android.content.ClipboardManager +import android.content.Context.CLIPBOARD_SERVICE import android.os.Bundle import android.view.View import androidx.lifecycle.lifecycleScope import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_utils.ext.toast +import com.anytypeio.anytype.core_utils.ext.visible import com.anytypeio.anytype.core_utils.ui.BaseFragment import com.anytypeio.anytype.di.common.componentManager import com.anytypeio.anytype.domain.config.GetDebugSettings import com.anytypeio.anytype.domain.config.UseCustomContextMenu +import com.anytypeio.anytype.domain.dataview.interactor.DebugSync import kotlinx.android.synthetic.main.fragment_debug_settings.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -20,6 +25,9 @@ class DebugSettingsFragment : BaseFragment(R.layout.fragment_debug_settings) { @Inject lateinit var getDebugSettings: GetDebugSettings + @Inject + lateinit var debugSync: DebugSync + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -30,6 +38,21 @@ class DebugSettingsFragment : BaseFragment(R.layout.fragment_debug_settings) { ) } + btnSync.setOnClickListener { + viewLifecycleOwner.lifecycleScope.launch { + debugSync.invoke(Unit).proceed( + failure = {}, + success = { status -> setSyncStatus(status) } + ) + } + } + + tvSync.setOnClickListener { + val cm = activity?.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + cm.text = tvSync.text + requireContext().toast("Sync status is copied to the clipboard") + } + anytypeContextMenuToggle.setOnCheckedChangeListener { _, isChecked -> viewLifecycleOwner.lifecycleScope.launch { useCustomContextMenu.invoke(UseCustomContextMenu.Params(isChecked)) @@ -37,6 +60,11 @@ class DebugSettingsFragment : BaseFragment(R.layout.fragment_debug_settings) { } } + private fun setSyncStatus(status: String) { + scrollContainer.visible() + tvSync.text = status + } + private fun setContextMenuToggle(value: Boolean) { anytypeContextMenuToggle.isChecked = value } diff --git a/app/src/main/res/anim/slide_bottom.xml b/app/src/main/res/anim/slide_bottom.xml new file mode 100644 index 0000000000..da49a71290 --- /dev/null +++ b/app/src/main/res/anim/slide_bottom.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_up.xml b/app/src/main/res/anim/slide_up.xml new file mode 100644 index 0000000000..cc0e322959 --- /dev/null +++ b/app/src/main/res/anim/slide_up.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/divider_sort_or_filter_relation.xml b/app/src/main/res/drawable/divider_sort_or_filter_relation.xml new file mode 100644 index 0000000000..8510407cc3 --- /dev/null +++ b/app/src/main/res/drawable/divider_sort_or_filter_relation.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_expand_dv_viewer.xml b/app/src/main/res/drawable/ic_arrow_expand_dv_viewer.xml new file mode 100644 index 0000000000..3fadbaccda --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_expand_dv_viewer.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_create_viewer_gallery.xml b/app/src/main/res/drawable/ic_create_viewer_gallery.xml new file mode 100644 index 0000000000..a256184e47 --- /dev/null +++ b/app/src/main/res/drawable/ic_create_viewer_gallery.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_create_viewer_grid.xml b/app/src/main/res/drawable/ic_create_viewer_grid.xml new file mode 100644 index 0000000000..6989730562 --- /dev/null +++ b/app/src/main/res/drawable/ic_create_viewer_grid.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_create_viewer_kanban.xml b/app/src/main/res/drawable/ic_create_viewer_kanban.xml new file mode 100644 index 0000000000..d3a338f4f5 --- /dev/null +++ b/app/src/main/res/drawable/ic_create_viewer_kanban.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_create_viewer_list.xml b/app/src/main/res/drawable/ic_create_viewer_list.xml new file mode 100644 index 0000000000..d88d93950a --- /dev/null +++ b/app/src/main/res/drawable/ic_create_viewer_list.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_data_view_viewer_delete.xml b/app/src/main/res/drawable/ic_data_view_viewer_delete.xml new file mode 100644 index 0000000000..52b23fcc9c --- /dev/null +++ b/app/src/main/res/drawable/ic_data_view_viewer_delete.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_data_view_viewer_duplicate.xml b/app/src/main/res/drawable/ic_data_view_viewer_duplicate.xml new file mode 100644 index 0000000000..cf72709e2a --- /dev/null +++ b/app/src/main/res/drawable/ic_data_view_viewer_duplicate.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_data_view_viewer_edit.xml b/app/src/main/res/drawable/ic_data_view_viewer_edit.xml new file mode 100644 index 0000000000..307063d60a --- /dev/null +++ b/app/src/main/res/drawable/ic_data_view_viewer_edit.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_dv_relation_search.xml b/app/src/main/res/drawable/ic_dv_relation_search.xml new file mode 100644 index 0000000000..ab36367703 --- /dev/null +++ b/app/src/main/res/drawable/ic_dv_relation_search.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_viewer_chosen.xml b/app/src/main/res/drawable/ic_viewer_chosen.xml new file mode 100644 index 0000000000..5c08e14631 --- /dev/null +++ b/app/src/main/res/drawable/ic_viewer_chosen.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/rect_create_viewer_icon_background.xml b/app/src/main/res/drawable/rect_create_viewer_icon_background.xml new file mode 100644 index 0000000000..aa5271a785 --- /dev/null +++ b/app/src/main/res/drawable/rect_create_viewer_icon_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rect_dv_search_relation.xml b/app/src/main/res/drawable/rect_dv_search_relation.xml new file mode 100644 index 0000000000..ee97351fe7 --- /dev/null +++ b/app/src/main/res/drawable/rect_dv_search_relation.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rectangle_default_button.xml b/app/src/main/res/drawable/rectangle_default_button.xml index 27da31d861..bf40333659 100644 --- a/app/src/main/res/drawable/rectangle_default_button.xml +++ b/app/src/main/res/drawable/rectangle_default_button.xml @@ -1,8 +1,6 @@ - - - - + + \ No newline at end of file diff --git a/app/src/main/res/layout/add_object_relation_value_fragment.xml b/app/src/main/res/layout/add_object_relation_value_fragment.xml new file mode 100644 index 0000000000..5d6369b687 --- /dev/null +++ b/app/src/main/res/layout/add_object_relation_value_fragment.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_create_data_view_relation.xml b/app/src/main/res/layout/fragment_create_data_view_relation.xml new file mode 100644 index 0000000000..a912f0d781 --- /dev/null +++ b/app/src/main/res/layout/fragment_create_data_view_relation.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_create_data_view_viewer.xml b/app/src/main/res/layout/fragment_create_data_view_viewer.xml new file mode 100644 index 0000000000..2d2a849c2c --- /dev/null +++ b/app/src/main/res/layout/fragment_create_data_view_viewer.xml @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_create_filter_flow.xml b/app/src/main/res/layout/fragment_create_filter_flow.xml new file mode 100644 index 0000000000..beba12a857 --- /dev/null +++ b/app/src/main/res/layout/fragment_create_filter_flow.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_create_object_type.xml b/app/src/main/res/layout/fragment_create_object_type.xml new file mode 100644 index 0000000000..58258901ad --- /dev/null +++ b/app/src/main/res/layout/fragment_create_object_type.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_create_or_update_filter.xml b/app/src/main/res/layout/fragment_create_or_update_filter.xml new file mode 100644 index 0000000000..6c469b1a0a --- /dev/null +++ b/app/src/main/res/layout/fragment_create_or_update_filter.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_create_or_update_filter_input_field_value.xml b/app/src/main/res/layout/fragment_create_or_update_filter_input_field_value.xml new file mode 100644 index 0000000000..f54ec5f89b --- /dev/null +++ b/app/src/main/res/layout/fragment_create_or_update_filter_input_field_value.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_create_set.xml b/app/src/main/res/layout/fragment_create_set.xml new file mode 100644 index 0000000000..e715989e97 --- /dev/null +++ b/app/src/main/res/layout/fragment_create_set.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_data_view_viewer_actions.xml b/app/src/main/res/layout/fragment_data_view_viewer_actions.xml new file mode 100644 index 0000000000..a8c2c533c0 --- /dev/null +++ b/app/src/main/res/layout/fragment_data_view_viewer_actions.xml @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_date_picker.xml b/app/src/main/res/layout/fragment_date_picker.xml new file mode 100644 index 0000000000..59e9b75927 --- /dev/null +++ b/app/src/main/res/layout/fragment_date_picker.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_debug_settings.xml b/app/src/main/res/layout/fragment_debug_settings.xml index 1ac892d030..fcec3227c5 100644 --- a/app/src/main/res/layout/fragment_debug_settings.xml +++ b/app/src/main/res/layout/fragment_debug_settings.xml @@ -1,11 +1,13 @@ +