From f527e6e1855f30cf5ca5cb834b2d20438299a70e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 20 Mar 2018 14:12:48 +0000 Subject: Move IDE to CE This also makes the IDE generally available --- .../ide/components/changed_file_icon.vue | 31 ++ .../ide/components/commit_sidebar/actions.vue | 65 +++ .../ide/components/commit_sidebar/list.vue | 66 +++ .../components/commit_sidebar/list_collapsed.vue | 35 ++ .../ide/components/commit_sidebar/list_item.vue | 60 +++ .../ide/components/commit_sidebar/radio_group.vue | 94 +++++ .../ide/components/editor_mode_dropdown.vue | 91 +++++ app/assets/javascripts/ide/components/ide.vue | 111 +++++ .../javascripts/ide/components/ide_context_bar.vue | 84 ++++ .../ide/components/ide_external_links.vue | 43 ++ .../ide/components/ide_project_branches_tree.vue | 47 +++ .../ide/components/ide_project_tree.vue | 54 +++ .../javascripts/ide/components/ide_repo_tree.vue | 41 ++ .../javascripts/ide/components/ide_side_bar.vue | 51 +++ .../javascripts/ide/components/ide_status_bar.vue | 60 +++ .../ide/components/new_dropdown/index.vue | 111 +++++ .../ide/components/new_dropdown/modal.vue | 99 +++++ .../ide/components/new_dropdown/upload.vue | 75 ++++ .../ide/components/repo_commit_section.vue | 174 ++++++++ .../javascripts/ide/components/repo_editor.vue | 161 ++++++++ .../javascripts/ide/components/repo_file.vue | 127 ++++++ .../ide/components/repo_file_buttons.vue | 61 +++ .../ide/components/repo_file_status_icon.vue | 39 ++ .../ide/components/repo_loading_file.vue | 42 ++ app/assets/javascripts/ide/components/repo_tab.vue | 98 +++++ .../javascripts/ide/components/repo_tabs.vue | 61 +++ .../javascripts/ide/components/resizable_panel.vue | 88 ++++ app/assets/javascripts/ide/eventhub.js | 3 + app/assets/javascripts/ide/ide_router.js | 97 +++++ app/assets/javascripts/ide/index.js | 33 ++ .../javascripts/ide/lib/common/disposable.js | 14 + app/assets/javascripts/ide/lib/common/model.js | 90 +++++ .../javascripts/ide/lib/common/model_manager.js | 51 +++ .../javascripts/ide/lib/decorations/controller.js | 45 +++ app/assets/javascripts/ide/lib/diff/controller.js | 72 ++++ app/assets/javascripts/ide/lib/diff/diff.js | 30 ++ app/assets/javascripts/ide/lib/diff/diff_worker.js | 10 + app/assets/javascripts/ide/lib/editor.js | 164 ++++++++ app/assets/javascripts/ide/lib/editor_options.js | 15 + app/assets/javascripts/ide/lib/themes/gl_theme.js | 14 + app/assets/javascripts/ide/monaco_loader.js | 16 + app/assets/javascripts/ide/services/index.js | 55 +++ app/assets/javascripts/ide/stores/actions.js | 121 ++++++ app/assets/javascripts/ide/stores/actions/file.js | 146 +++++++ .../javascripts/ide/stores/actions/project.js | 49 +++ app/assets/javascripts/ide/stores/actions/tree.js | 93 +++++ app/assets/javascripts/ide/stores/getters.js | 30 ++ app/assets/javascripts/ide/stores/index.js | 19 + .../ide/stores/modules/commit/actions.js | 218 ++++++++++ .../ide/stores/modules/commit/constants.js | 3 + .../ide/stores/modules/commit/getters.js | 24 ++ .../javascripts/ide/stores/modules/commit/index.js | 12 + .../ide/stores/modules/commit/mutation_types.js | 4 + .../ide/stores/modules/commit/mutations.js | 24 ++ .../javascripts/ide/stores/modules/commit/state.js | 6 + .../javascripts/ide/stores/mutation_types.js | 43 ++ app/assets/javascripts/ide/stores/mutations.js | 106 +++++ .../javascripts/ide/stores/mutations/branch.js | 26 ++ .../javascripts/ide/stores/mutations/file.js | 83 ++++ .../javascripts/ide/stores/mutations/project.js | 23 ++ .../javascripts/ide/stores/mutations/tree.js | 38 ++ app/assets/javascripts/ide/stores/state.js | 19 + app/assets/javascripts/ide/stores/utils.js | 125 ++++++ .../ide/stores/workers/files_decorator_worker.js | 91 +++++ app/assets/stylesheets/framework/images.scss | 34 +- app/assets/stylesheets/pages/repo.scss | 365 +++++++++++++---- app/controllers/ide_controller.rb | 6 + app/helpers/ide_helper.rb | 14 + app/views/ide/index.html.haml | 12 + app/views/projects/tree/_tree_header.html.haml | 4 + config/routes.rb | 3 + config/webpack.config.js | 123 +++--- .../projects/tree/create_directory_spec.rb | 53 +++ spec/features/projects/tree/create_file_spec.rb | 43 ++ spec/features/projects/tree/upload_file_spec.rb | 51 +++ .../ide/components/changed_file_icon_spec.js | 45 +++ .../ide/components/commit_sidebar/actions_spec.js | 35 ++ .../commit_sidebar/list_collapsed_spec.js | 28 ++ .../components/commit_sidebar/list_item_spec.js | 83 ++++ .../ide/components/commit_sidebar/list_spec.js | 53 +++ .../components/commit_sidebar/radio_group_spec.js | 130 ++++++ .../ide/components/ide_context_bar_spec.js | 37 ++ .../ide/components/ide_external_links_spec.js | 43 ++ .../ide/components/ide_repo_tree_spec.js | 41 ++ .../ide/components/ide_side_bar_spec.js | 36 ++ spec/javascripts/ide/components/ide_spec.js | 41 ++ .../ide/components/new_dropdown/index_spec.js | 84 ++++ .../ide/components/new_dropdown/modal_spec.js | 72 ++++ .../ide/components/new_dropdown/upload_spec.js | 87 ++++ .../ide/components/repo_commit_section_spec.js | 154 +++++++ .../javascripts/ide/components/repo_editor_spec.js | 137 +++++++ .../ide/components/repo_file_buttons_spec.js | 45 +++ spec/javascripts/ide/components/repo_file_spec.js | 80 ++++ .../ide/components/repo_loading_file_spec.js | 63 +++ spec/javascripts/ide/components/repo_tab_spec.js | 163 ++++++++ spec/javascripts/ide/components/repo_tabs_spec.js | 81 ++++ spec/javascripts/ide/helpers.js | 21 + spec/javascripts/ide/lib/common/disposable_spec.js | 44 ++ .../ide/lib/common/model_manager_spec.js | 123 ++++++ spec/javascripts/ide/lib/common/model_spec.js | 107 +++++ .../ide/lib/decorations/controller_spec.js | 120 ++++++ spec/javascripts/ide/lib/diff/controller_spec.js | 176 ++++++++ spec/javascripts/ide/lib/diff/diff_spec.js | 80 ++++ spec/javascripts/ide/lib/editor_options_spec.js | 11 + spec/javascripts/ide/lib/editor_spec.js | 197 +++++++++ spec/javascripts/ide/monaco_loader_spec.js | 13 + spec/javascripts/ide/stores/actions/file_spec.js | 421 +++++++++++++++++++ spec/javascripts/ide/stores/actions/tree_spec.js | 145 +++++++ spec/javascripts/ide/stores/actions_spec.js | 306 ++++++++++++++ spec/javascripts/ide/stores/getters_spec.js | 55 +++ .../ide/stores/modules/commit/actions_spec.js | 450 +++++++++++++++++++++ .../ide/stores/modules/commit/getters_spec.js | 114 ++++++ .../ide/stores/modules/commit/mutations_spec.js | 42 ++ .../ide/stores/mutations/branch_spec.js | 18 + spec/javascripts/ide/stores/mutations/file_spec.js | 157 +++++++ spec/javascripts/ide/stores/mutations/tree_spec.js | 67 +++ spec/javascripts/ide/stores/mutations_spec.js | 79 ++++ spec/javascripts/ide/stores/utils_spec.js | 60 +++ 118 files changed, 8988 insertions(+), 145 deletions(-) create mode 100644 app/assets/javascripts/ide/components/changed_file_icon.vue create mode 100644 app/assets/javascripts/ide/components/commit_sidebar/actions.vue create mode 100644 app/assets/javascripts/ide/components/commit_sidebar/list.vue create mode 100644 app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue create mode 100644 app/assets/javascripts/ide/components/commit_sidebar/list_item.vue create mode 100644 app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue create mode 100644 app/assets/javascripts/ide/components/editor_mode_dropdown.vue create mode 100644 app/assets/javascripts/ide/components/ide.vue create mode 100644 app/assets/javascripts/ide/components/ide_context_bar.vue create mode 100644 app/assets/javascripts/ide/components/ide_external_links.vue create mode 100644 app/assets/javascripts/ide/components/ide_project_branches_tree.vue create mode 100644 app/assets/javascripts/ide/components/ide_project_tree.vue create mode 100644 app/assets/javascripts/ide/components/ide_repo_tree.vue create mode 100644 app/assets/javascripts/ide/components/ide_side_bar.vue create mode 100644 app/assets/javascripts/ide/components/ide_status_bar.vue create mode 100644 app/assets/javascripts/ide/components/new_dropdown/index.vue create mode 100644 app/assets/javascripts/ide/components/new_dropdown/modal.vue create mode 100644 app/assets/javascripts/ide/components/new_dropdown/upload.vue create mode 100644 app/assets/javascripts/ide/components/repo_commit_section.vue create mode 100644 app/assets/javascripts/ide/components/repo_editor.vue create mode 100644 app/assets/javascripts/ide/components/repo_file.vue create mode 100644 app/assets/javascripts/ide/components/repo_file_buttons.vue create mode 100644 app/assets/javascripts/ide/components/repo_file_status_icon.vue create mode 100644 app/assets/javascripts/ide/components/repo_loading_file.vue create mode 100644 app/assets/javascripts/ide/components/repo_tab.vue create mode 100644 app/assets/javascripts/ide/components/repo_tabs.vue create mode 100644 app/assets/javascripts/ide/components/resizable_panel.vue create mode 100644 app/assets/javascripts/ide/eventhub.js create mode 100644 app/assets/javascripts/ide/ide_router.js create mode 100644 app/assets/javascripts/ide/index.js create mode 100644 app/assets/javascripts/ide/lib/common/disposable.js create mode 100644 app/assets/javascripts/ide/lib/common/model.js create mode 100644 app/assets/javascripts/ide/lib/common/model_manager.js create mode 100644 app/assets/javascripts/ide/lib/decorations/controller.js create mode 100644 app/assets/javascripts/ide/lib/diff/controller.js create mode 100644 app/assets/javascripts/ide/lib/diff/diff.js create mode 100644 app/assets/javascripts/ide/lib/diff/diff_worker.js create mode 100644 app/assets/javascripts/ide/lib/editor.js create mode 100644 app/assets/javascripts/ide/lib/editor_options.js create mode 100644 app/assets/javascripts/ide/lib/themes/gl_theme.js create mode 100644 app/assets/javascripts/ide/monaco_loader.js create mode 100644 app/assets/javascripts/ide/services/index.js create mode 100644 app/assets/javascripts/ide/stores/actions.js create mode 100644 app/assets/javascripts/ide/stores/actions/file.js create mode 100644 app/assets/javascripts/ide/stores/actions/project.js create mode 100644 app/assets/javascripts/ide/stores/actions/tree.js create mode 100644 app/assets/javascripts/ide/stores/getters.js create mode 100644 app/assets/javascripts/ide/stores/index.js create mode 100644 app/assets/javascripts/ide/stores/modules/commit/actions.js create mode 100644 app/assets/javascripts/ide/stores/modules/commit/constants.js create mode 100644 app/assets/javascripts/ide/stores/modules/commit/getters.js create mode 100644 app/assets/javascripts/ide/stores/modules/commit/index.js create mode 100644 app/assets/javascripts/ide/stores/modules/commit/mutation_types.js create mode 100644 app/assets/javascripts/ide/stores/modules/commit/mutations.js create mode 100644 app/assets/javascripts/ide/stores/modules/commit/state.js create mode 100644 app/assets/javascripts/ide/stores/mutation_types.js create mode 100644 app/assets/javascripts/ide/stores/mutations.js create mode 100644 app/assets/javascripts/ide/stores/mutations/branch.js create mode 100644 app/assets/javascripts/ide/stores/mutations/file.js create mode 100644 app/assets/javascripts/ide/stores/mutations/project.js create mode 100644 app/assets/javascripts/ide/stores/mutations/tree.js create mode 100644 app/assets/javascripts/ide/stores/state.js create mode 100644 app/assets/javascripts/ide/stores/utils.js create mode 100644 app/assets/javascripts/ide/stores/workers/files_decorator_worker.js create mode 100644 app/controllers/ide_controller.rb create mode 100644 app/helpers/ide_helper.rb create mode 100644 app/views/ide/index.html.haml create mode 100644 spec/features/projects/tree/create_directory_spec.rb create mode 100644 spec/features/projects/tree/create_file_spec.rb create mode 100644 spec/features/projects/tree/upload_file_spec.rb create mode 100644 spec/javascripts/ide/components/changed_file_icon_spec.js create mode 100644 spec/javascripts/ide/components/commit_sidebar/actions_spec.js create mode 100644 spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js create mode 100644 spec/javascripts/ide/components/commit_sidebar/list_item_spec.js create mode 100644 spec/javascripts/ide/components/commit_sidebar/list_spec.js create mode 100644 spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js create mode 100644 spec/javascripts/ide/components/ide_context_bar_spec.js create mode 100644 spec/javascripts/ide/components/ide_external_links_spec.js create mode 100644 spec/javascripts/ide/components/ide_repo_tree_spec.js create mode 100644 spec/javascripts/ide/components/ide_side_bar_spec.js create mode 100644 spec/javascripts/ide/components/ide_spec.js create mode 100644 spec/javascripts/ide/components/new_dropdown/index_spec.js create mode 100644 spec/javascripts/ide/components/new_dropdown/modal_spec.js create mode 100644 spec/javascripts/ide/components/new_dropdown/upload_spec.js create mode 100644 spec/javascripts/ide/components/repo_commit_section_spec.js create mode 100644 spec/javascripts/ide/components/repo_editor_spec.js create mode 100644 spec/javascripts/ide/components/repo_file_buttons_spec.js create mode 100644 spec/javascripts/ide/components/repo_file_spec.js create mode 100644 spec/javascripts/ide/components/repo_loading_file_spec.js create mode 100644 spec/javascripts/ide/components/repo_tab_spec.js create mode 100644 spec/javascripts/ide/components/repo_tabs_spec.js create mode 100644 spec/javascripts/ide/helpers.js create mode 100644 spec/javascripts/ide/lib/common/disposable_spec.js create mode 100644 spec/javascripts/ide/lib/common/model_manager_spec.js create mode 100644 spec/javascripts/ide/lib/common/model_spec.js create mode 100644 spec/javascripts/ide/lib/decorations/controller_spec.js create mode 100644 spec/javascripts/ide/lib/diff/controller_spec.js create mode 100644 spec/javascripts/ide/lib/diff/diff_spec.js create mode 100644 spec/javascripts/ide/lib/editor_options_spec.js create mode 100644 spec/javascripts/ide/lib/editor_spec.js create mode 100644 spec/javascripts/ide/monaco_loader_spec.js create mode 100644 spec/javascripts/ide/stores/actions/file_spec.js create mode 100644 spec/javascripts/ide/stores/actions/tree_spec.js create mode 100644 spec/javascripts/ide/stores/actions_spec.js create mode 100644 spec/javascripts/ide/stores/getters_spec.js create mode 100644 spec/javascripts/ide/stores/modules/commit/actions_spec.js create mode 100644 spec/javascripts/ide/stores/modules/commit/getters_spec.js create mode 100644 spec/javascripts/ide/stores/modules/commit/mutations_spec.js create mode 100644 spec/javascripts/ide/stores/mutations/branch_spec.js create mode 100644 spec/javascripts/ide/stores/mutations/file_spec.js create mode 100644 spec/javascripts/ide/stores/mutations/tree_spec.js create mode 100644 spec/javascripts/ide/stores/mutations_spec.js create mode 100644 spec/javascripts/ide/stores/utils_spec.js diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue new file mode 100644 index 00000000000..0c54c992e51 --- /dev/null +++ b/app/assets/javascripts/ide/components/changed_file_icon.vue @@ -0,0 +1,31 @@ + + + diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue new file mode 100644 index 00000000000..2cbd982af19 --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -0,0 +1,65 @@ + + + diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue new file mode 100644 index 00000000000..453208f3f19 --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -0,0 +1,66 @@ + + + diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue new file mode 100644 index 00000000000..15918ac9631 --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue @@ -0,0 +1,35 @@ + + + diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue new file mode 100644 index 00000000000..18934af004a --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -0,0 +1,60 @@ + + + diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue new file mode 100644 index 00000000000..4310d762c78 --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -0,0 +1,94 @@ + + + diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue new file mode 100644 index 00000000000..170347881e0 --- /dev/null +++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue @@ -0,0 +1,91 @@ + + + diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue new file mode 100644 index 00000000000..015e750525a --- /dev/null +++ b/app/assets/javascripts/ide/components/ide.vue @@ -0,0 +1,111 @@ + + + diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue new file mode 100644 index 00000000000..79a83b47994 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_context_bar.vue @@ -0,0 +1,84 @@ + + + diff --git a/app/assets/javascripts/ide/components/ide_external_links.vue b/app/assets/javascripts/ide/components/ide_external_links.vue new file mode 100644 index 00000000000..c6f6e0d2348 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_external_links.vue @@ -0,0 +1,43 @@ + + + diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue new file mode 100644 index 00000000000..eb2749e6151 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue @@ -0,0 +1,47 @@ + + + diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue new file mode 100644 index 00000000000..220db1abfb0 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_project_tree.vue @@ -0,0 +1,54 @@ + + + diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue new file mode 100644 index 00000000000..e6af88e04bc --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue @@ -0,0 +1,41 @@ + + + diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue new file mode 100644 index 00000000000..8cf1ccb4fce --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -0,0 +1,51 @@ + + + diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue new file mode 100644 index 00000000000..9c386896448 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -0,0 +1,60 @@ + + + diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue new file mode 100644 index 00000000000..769e9b79cad --- /dev/null +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -0,0 +1,111 @@ + + + diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue new file mode 100644 index 00000000000..5723891d130 --- /dev/null +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -0,0 +1,99 @@ + + + diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue new file mode 100644 index 00000000000..c165af5ce52 --- /dev/null +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -0,0 +1,75 @@ + + + diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue new file mode 100644 index 00000000000..d772cab2d0e --- /dev/null +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -0,0 +1,174 @@ + + + diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue new file mode 100644 index 00000000000..e73d1ce839f --- /dev/null +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -0,0 +1,161 @@ + + + diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue new file mode 100644 index 00000000000..03a40096bb0 --- /dev/null +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -0,0 +1,127 @@ + + + diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue new file mode 100644 index 00000000000..4ea8cf7504b --- /dev/null +++ b/app/assets/javascripts/ide/components/repo_file_buttons.vue @@ -0,0 +1,61 @@ + + + diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue new file mode 100644 index 00000000000..25d311142d5 --- /dev/null +++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue @@ -0,0 +1,39 @@ + + + diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue new file mode 100644 index 00000000000..79af8c0b0c7 --- /dev/null +++ b/app/assets/javascripts/ide/components/repo_loading_file.vue @@ -0,0 +1,42 @@ + + + diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue new file mode 100644 index 00000000000..c337bc813e6 --- /dev/null +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -0,0 +1,98 @@ + + + diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue new file mode 100644 index 00000000000..8ea64ddf84a --- /dev/null +++ b/app/assets/javascripts/ide/components/repo_tabs.vue @@ -0,0 +1,61 @@ + + + diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue new file mode 100644 index 00000000000..faa690ecba0 --- /dev/null +++ b/app/assets/javascripts/ide/components/resizable_panel.vue @@ -0,0 +1,88 @@ + + + diff --git a/app/assets/javascripts/ide/eventhub.js b/app/assets/javascripts/ide/eventhub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/ide/eventhub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js new file mode 100644 index 00000000000..048d5316922 --- /dev/null +++ b/app/assets/javascripts/ide/ide_router.js @@ -0,0 +1,97 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import flash from '~/flash'; +import store from './stores'; + +Vue.use(VueRouter); + +/** + * Routes below /-/ide/: + +/project/h5bp/html5-boilerplate/blob/master +/project/h5bp/html5-boilerplate/blob/master/app/js/test.js + +/project/h5bp/html5-boilerplate/mr/123 +/project/h5bp/html5-boilerplate/mr/123/app/js/test.js + +/workspace/123 +/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch +/workspace/project/h5bp/html5-boilerplate/mr/123 + +/ = /workspace + +/settings +*/ + +// Unfortunately Vue Router doesn't work without at least a fake component +// If you do only data handling +const EmptyRouterComponent = { + render(createElement) { + return createElement('div'); + }, +}; + +const router = new VueRouter({ + mode: 'history', + base: `${gon.relative_url_root}/-/ide/`, + routes: [ + { + path: '/project/:namespace/:project', + component: EmptyRouterComponent, + children: [ + { + path: ':targetmode/:branch/*', + component: EmptyRouterComponent, + }, + { + path: 'mr/:mrid', + component: EmptyRouterComponent, + }, + ], + }, + ], +}); + +router.beforeEach((to, from, next) => { + if (to.params.namespace && to.params.project) { + store.dispatch('getProjectData', { + namespace: to.params.namespace, + projectId: to.params.project, + }) + .then(() => { + const fullProjectId = `${to.params.namespace}/${to.params.project}`; + + if (to.params.branch) { + store.dispatch('getBranchData', { + projectId: fullProjectId, + branchId: to.params.branch, + }); + + store.dispatch('getFiles', { + projectId: fullProjectId, + branchId: to.params.branch, + }) + .then(() => { + if (to.params[0]) { + const treeEntry = store.state.entries[to.params[0]]; + if (treeEntry) { + store.dispatch('handleTreeEntryAction', treeEntry); + } + } + }) + .catch((e) => { + flash('Error while loading the branch files. Please try again.', 'alert', document, null, false, true); + throw e; + }); + } + }) + .catch((e) => { + flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true); + throw e; + }); + } + + next(); +}); + +export default router; diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js new file mode 100644 index 00000000000..cbfb3dc54f2 --- /dev/null +++ b/app/assets/javascripts/ide/index.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import ide from './components/ide.vue'; +import store from './stores'; +import router from './ide_router'; + +function initIde(el) { + if (!el) return null; + + return new Vue({ + el, + store, + router, + components: { + ide, + }, + render(createElement) { + return createElement('ide', { + props: { + emptyStateSvgPath: el.dataset.emptyStateSvgPath, + noChangesStateSvgPath: el.dataset.noChangesStateSvgPath, + committedStateSvgPath: el.dataset.committedStateSvgPath, + }, + }); + }, + }); +} + +const ideElement = document.getElementById('ide'); + +Vue.use(Translate); + +initIde(ideElement); diff --git a/app/assets/javascripts/ide/lib/common/disposable.js b/app/assets/javascripts/ide/lib/common/disposable.js new file mode 100644 index 00000000000..84b29bdb600 --- /dev/null +++ b/app/assets/javascripts/ide/lib/common/disposable.js @@ -0,0 +1,14 @@ +export default class Disposable { + constructor() { + this.disposers = new Set(); + } + + add(...disposers) { + disposers.forEach(disposer => this.disposers.add(disposer)); + } + + dispose() { + this.disposers.forEach(disposer => disposer.dispose()); + this.disposers.clear(); + } +} diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js new file mode 100644 index 00000000000..73cd684351c --- /dev/null +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -0,0 +1,90 @@ +/* global monaco */ +import Disposable from './disposable'; +import eventHub from '../../eventhub'; + +export default class Model { + constructor(monaco, file) { + this.monaco = monaco; + this.disposable = new Disposable(); + this.file = file; + this.content = file.content !== '' ? file.content : file.raw; + + this.disposable.add( + (this.originalModel = this.monaco.editor.createModel( + this.file.raw, + undefined, + new this.monaco.Uri(null, null, `original/${this.file.path}`), + )), + (this.model = this.monaco.editor.createModel( + this.content, + undefined, + new this.monaco.Uri(null, null, this.file.path), + )), + ); + + this.events = new Map(); + + this.updateContent = this.updateContent.bind(this); + this.dispose = this.dispose.bind(this); + + eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose); + eventHub.$on( + `editor.update.model.content.${this.file.path}`, + this.updateContent, + ); + } + + get url() { + return this.model.uri.toString(); + } + + get language() { + return this.model.getModeId(); + } + + get eol() { + return this.model.getEOL() === '\n' ? 'LF' : 'CRLF'; + } + + get path() { + return this.file.path; + } + + getModel() { + return this.model; + } + + getOriginalModel() { + return this.originalModel; + } + + setValue(value) { + this.getModel().setValue(value); + } + + onChange(cb) { + this.events.set( + this.path, + this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))), + ); + } + + updateContent(content) { + this.getOriginalModel().setValue(content); + this.getModel().setValue(content); + } + + dispose() { + this.disposable.dispose(); + this.events.clear(); + + eventHub.$off( + `editor.update.model.dispose.${this.file.path}`, + this.dispose, + ); + eventHub.$off( + `editor.update.model.content.${this.file.path}`, + this.updateContent, + ); + } +} diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js new file mode 100644 index 00000000000..57d5e59a88b --- /dev/null +++ b/app/assets/javascripts/ide/lib/common/model_manager.js @@ -0,0 +1,51 @@ +import eventHub from '../../eventhub'; +import Disposable from './disposable'; +import Model from './model'; + +export default class ModelManager { + constructor(monaco) { + this.monaco = monaco; + this.disposable = new Disposable(); + this.models = new Map(); + } + + hasCachedModel(path) { + return this.models.has(path); + } + + getModel(path) { + return this.models.get(path); + } + + addModel(file) { + if (this.hasCachedModel(file.path)) { + return this.getModel(file.path); + } + + const model = new Model(this.monaco, file); + this.models.set(model.path, model); + this.disposable.add(model); + + eventHub.$on( + `editor.update.model.dispose.${file.path}`, + this.removeCachedModel.bind(this, file), + ); + + return model; + } + + removeCachedModel(file) { + this.models.delete(file.path); + + eventHub.$off( + `editor.update.model.dispose.${file.path}`, + this.removeCachedModel, + ); + } + + dispose() { + // dispose of all the models + this.disposable.dispose(); + this.models.clear(); + } +} diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js new file mode 100644 index 00000000000..42904774747 --- /dev/null +++ b/app/assets/javascripts/ide/lib/decorations/controller.js @@ -0,0 +1,45 @@ +export default class DecorationsController { + constructor(editor) { + this.editor = editor; + this.decorations = new Map(); + this.editorDecorations = new Map(); + } + + getAllDecorationsForModel(model) { + if (!this.decorations.has(model.url)) return []; + + const modelDecorations = this.decorations.get(model.url); + const decorations = []; + + modelDecorations.forEach(val => decorations.push(...val)); + + return decorations; + } + + addDecorations(model, decorationsKey, decorations) { + const decorationMap = this.decorations.get(model.url) || new Map(); + + decorationMap.set(decorationsKey, decorations); + + this.decorations.set(model.url, decorationMap); + + this.decorate(model); + } + + decorate(model) { + if (!this.editor.instance) return; + + const decorations = this.getAllDecorationsForModel(model); + const oldDecorations = this.editorDecorations.get(model.url) || []; + + this.editorDecorations.set( + model.url, + this.editor.instance.deltaDecorations(oldDecorations, decorations), + ); + } + + dispose() { + this.decorations.clear(); + this.editorDecorations.clear(); + } +} diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js new file mode 100644 index 00000000000..b136545ad11 --- /dev/null +++ b/app/assets/javascripts/ide/lib/diff/controller.js @@ -0,0 +1,72 @@ +/* global monaco */ +import { throttle } from 'underscore'; +import DirtyDiffWorker from './diff_worker'; +import Disposable from '../common/disposable'; + +export const getDiffChangeType = (change) => { + if (change.modified) { + return 'modified'; + } else if (change.added) { + return 'added'; + } else if (change.removed) { + return 'removed'; + } + + return ''; +}; + +export const getDecorator = change => ({ + range: new monaco.Range( + change.lineNumber, + 1, + change.endLineNumber, + 1, + ), + options: { + isWholeLine: true, + linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`, + }, +}); + +export default class DirtyDiffController { + constructor(modelManager, decorationsController) { + this.disposable = new Disposable(); + this.editorSimpleWorker = null; + this.modelManager = modelManager; + this.decorationsController = decorationsController; + this.dirtyDiffWorker = new DirtyDiffWorker(); + this.throttledComputeDiff = throttle(this.computeDiff, 250); + this.decorate = this.decorate.bind(this); + + this.dirtyDiffWorker.addEventListener('message', this.decorate); + } + + attachModel(model) { + model.onChange(() => this.throttledComputeDiff(model)); + } + + computeDiff(model) { + this.dirtyDiffWorker.postMessage({ + path: model.path, + originalContent: model.getOriginalModel().getValue(), + newContent: model.getModel().getValue(), + }); + } + + reDecorate(model) { + this.decorationsController.decorate(model); + } + + decorate({ data }) { + const decorations = data.changes.map(change => getDecorator(change)); + const model = this.modelManager.getModel(data.path); + this.decorationsController.addDecorations(model, 'dirtyDiff', decorations); + } + + dispose() { + this.disposable.dispose(); + + this.dirtyDiffWorker.removeEventListener('message', this.decorate); + this.dirtyDiffWorker.terminate(); + } +} diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js new file mode 100644 index 00000000000..0e37f5c4704 --- /dev/null +++ b/app/assets/javascripts/ide/lib/diff/diff.js @@ -0,0 +1,30 @@ +import { diffLines } from 'diff'; + +// eslint-disable-next-line import/prefer-default-export +export const computeDiff = (originalContent, newContent) => { + const changes = diffLines(originalContent, newContent); + + let lineNumber = 1; + return changes.reduce((acc, change) => { + const findOnLine = acc.find(c => c.lineNumber === lineNumber); + + if (findOnLine) { + Object.assign(findOnLine, change, { + modified: true, + endLineNumber: (lineNumber + change.count) - 1, + }); + } else if ('added' in change || 'removed' in change) { + acc.push(Object.assign({}, change, { + lineNumber, + modified: undefined, + endLineNumber: (lineNumber + change.count) - 1, + })); + } + + if (!change.removed) { + lineNumber += change.count; + } + + return acc; + }, []); +}; diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js new file mode 100644 index 00000000000..e74c4046330 --- /dev/null +++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js @@ -0,0 +1,10 @@ +import { computeDiff } from './diff'; + +self.addEventListener('message', (e) => { + const data = e.data; + + self.postMessage({ + path: data.path, + changes: computeDiff(data.originalContent, data.newContent), + }); +}); diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js new file mode 100644 index 00000000000..38de2fe2b27 --- /dev/null +++ b/app/assets/javascripts/ide/lib/editor.js @@ -0,0 +1,164 @@ +import _ from 'underscore'; +import DecorationsController from './decorations/controller'; +import DirtyDiffController from './diff/controller'; +import Disposable from './common/disposable'; +import ModelManager from './common/model_manager'; +import editorOptions, { defaultEditorOptions } from './editor_options'; +import gitlabTheme from './themes/gl_theme'; + +export const clearDomElement = el => { + if (!el || !el.firstChild) return; + + while (el.firstChild) { + el.removeChild(el.firstChild); + } +}; + +export default class Editor { + static create(monaco) { + if (this.editorInstance) return this.editorInstance; + + this.editorInstance = new Editor(monaco); + + return this.editorInstance; + } + + constructor(monaco) { + this.monaco = monaco; + this.currentModel = null; + this.instance = null; + this.dirtyDiffController = null; + this.disposable = new Disposable(); + this.modelManager = new ModelManager(this.monaco); + this.decorationsController = new DecorationsController(this); + + this.setupMonacoTheme(); + + this.debouncedUpdate = _.debounce(() => { + this.updateDimensions(); + }, 200); + } + + createInstance(domElement) { + if (!this.instance) { + clearDomElement(domElement); + + this.disposable.add( + (this.instance = this.monaco.editor.create(domElement, { + ...defaultEditorOptions, + })), + (this.dirtyDiffController = new DirtyDiffController( + this.modelManager, + this.decorationsController, + )), + ); + + window.addEventListener('resize', this.debouncedUpdate, false); + } + } + + createDiffInstance(domElement) { + if (!this.instance) { + clearDomElement(domElement); + + this.disposable.add( + (this.instance = this.monaco.editor.createDiffEditor(domElement, { + ...defaultEditorOptions, + readOnly: true, + })), + ); + + window.addEventListener('resize', this.debouncedUpdate, false); + } + } + + createModel(file) { + return this.modelManager.addModel(file); + } + + attachModel(model) { + if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') { + this.instance.setModel({ + original: model.getOriginalModel(), + modified: model.getModel(), + }); + + return; + } + + this.instance.setModel(model.getModel()); + if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model); + + this.currentModel = model; + + this.instance.updateOptions( + editorOptions.reduce((acc, obj) => { + Object.keys(obj).forEach(key => { + Object.assign(acc, { + [key]: obj[key](model), + }); + }); + return acc; + }, {}), + ); + + if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model); + } + + setupMonacoTheme() { + this.monaco.editor.defineTheme( + gitlabTheme.themeName, + gitlabTheme.monacoTheme, + ); + + this.monaco.editor.setTheme('gitlab'); + } + + clearEditor() { + if (this.instance) { + this.instance.setModel(null); + } + } + + dispose() { + window.removeEventListener('resize', this.debouncedUpdate); + + // catch any potential errors with disposing the error + // this is mainly for tests caused by elements not existing + try { + this.disposable.dispose(); + + this.instance = null; + } catch (e) { + this.instance = null; + + if (process.env.NODE_ENV !== 'test') { + // eslint-disable-next-line no-console + console.error(e); + } + } + } + + updateDimensions() { + this.instance.layout(); + } + + setPosition({ lineNumber, column }) { + this.instance.revealPositionInCenter({ + lineNumber, + column, + }); + this.instance.setPosition({ + lineNumber, + column, + }); + } + + onPositionChange(cb) { + if (!this.instance.onDidChangeCursorPosition) return; + + this.disposable.add( + this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)), + ); + } +} diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js new file mode 100644 index 00000000000..d69d4b8c615 --- /dev/null +++ b/app/assets/javascripts/ide/lib/editor_options.js @@ -0,0 +1,15 @@ +export const defaultEditorOptions = { + model: null, + readOnly: false, + contextmenu: true, + scrollBeyondLastLine: false, + minimap: { + enabled: false, + }, +}; + +export default [ + { + readOnly: model => !!model.file.file_lock, + }, +]; diff --git a/app/assets/javascripts/ide/lib/themes/gl_theme.js b/app/assets/javascripts/ide/lib/themes/gl_theme.js new file mode 100644 index 00000000000..2fc96250c7d --- /dev/null +++ b/app/assets/javascripts/ide/lib/themes/gl_theme.js @@ -0,0 +1,14 @@ +export default { + themeName: 'gitlab', + monacoTheme: { + base: 'vs', + inherit: true, + rules: [], + colors: { + 'editorLineNumber.foreground': '#CCCCCC', + 'diffEditor.insertedTextBackground': '#ddfbe6', + 'diffEditor.removedTextBackground': '#f9d7dc', + 'editor.selectionBackground': '#aad6f8', + }, + }, +}; diff --git a/app/assets/javascripts/ide/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js new file mode 100644 index 00000000000..142a220097b --- /dev/null +++ b/app/assets/javascripts/ide/monaco_loader.js @@ -0,0 +1,16 @@ +import monacoContext from 'monaco-editor/dev/vs/loader'; + +monacoContext.require.config({ + paths: { + vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase + }, +}); + +// ignore CDN config and use local assets path for service worker which cannot be cross-domain +const relativeRootPath = (gon && gon.relative_url_root) || ''; +const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`; +window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` }; + +// eslint-disable-next-line no-underscore-dangle +window.__monaco_context__ = monacoContext; +export default monacoContext.require; diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js new file mode 100644 index 00000000000..5f1fb6cf843 --- /dev/null +++ b/app/assets/javascripts/ide/services/index.js @@ -0,0 +1,55 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import Api from '~/api'; + +Vue.use(VueResource); + +export default { + getTreeData(endpoint) { + return Vue.http.get(endpoint, { params: { format: 'json' } }); + }, + getFileData(endpoint) { + return Vue.http.get(endpoint, { params: { format: 'json' } }); + }, + getRawFileData(file) { + if (file.tempFile) { + return Promise.resolve(file.content); + } + + if (file.raw) { + return Promise.resolve(file.raw); + } + + return Vue.http.get(file.rawPath, { params: { format: 'json' } }) + .then(res => res.text()); + }, + getProjectData(namespace, project) { + return Api.project(`${namespace}/${project}`); + }, + getBranchData(projectId, currentBranchId) { + return Api.branchSingle(projectId, currentBranchId); + }, + createBranch(projectId, payload) { + const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId); + + return Vue.http.post(url, payload); + }, + commit(projectId, payload) { + return Api.commitMultiple(projectId, payload); + }, + getTreeLastCommit(endpoint) { + return Vue.http.get(endpoint, { + params: { + format: 'json', + }, + }); + }, + getFiles(projectUrl, branchId) { + const url = `${projectUrl}/files/${branchId}`; + return Vue.http.get(url, { + params: { + format: 'json', + }, + }); + }, +}; diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js new file mode 100644 index 00000000000..7e920aa9f30 --- /dev/null +++ b/app/assets/javascripts/ide/stores/actions.js @@ -0,0 +1,121 @@ +import Vue from 'vue'; +import { visitUrl } from '~/lib/utils/url_utility'; +import flash from '~/flash'; +import * as types from './mutation_types'; +import FilesDecoratorWorker from './workers/files_decorator_worker'; + +export const redirectToUrl = (_, url) => visitUrl(url); + +export const setInitialData = ({ commit }, data) => + commit(types.SET_INITIAL_DATA, data); + +export const discardAllChanges = ({ state, commit, dispatch }) => { + state.changedFiles.forEach(file => { + commit(types.DISCARD_FILE_CHANGES, file.path); + + if (file.tempFile) { + dispatch('closeFile', file.path); + } + }); + + commit(types.REMOVE_ALL_CHANGES_FILES); +}; + +export const closeAllFiles = ({ state, dispatch }) => { + state.openFiles.forEach(file => dispatch('closeFile', file.path)); +}; + +export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => { + if (side === 'left') { + commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed); + } else { + commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed); + } +}; + +export const setResizingStatus = ({ commit }, resizing) => { + commit(types.SET_RESIZING_STATUS, resizing); +}; + +export const createTempEntry = ( + { state, commit, dispatch }, + { branchId, name, type, content = '', base64 = false }, +) => + new Promise(resolve => { + const worker = new FilesDecoratorWorker(); + const fullName = + name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name; + + if (state.entries[name]) { + flash( + `The name "${name + .split('/') + .pop()}" is already taken in this directory.`, + 'alert', + document, + null, + false, + true, + ); + + resolve(); + + return null; + } + + worker.addEventListener('message', ({ data }) => { + const { file } = data; + + worker.terminate(); + + commit(types.CREATE_TMP_ENTRY, { + data, + projectId: state.currentProjectId, + branchId, + }); + + if (type === 'blob') { + commit(types.TOGGLE_FILE_OPEN, file.path); + commit(types.ADD_FILE_TO_CHANGED, file.path); + dispatch('setFileActive', file.path); + } + + resolve(file); + }); + + worker.postMessage({ + data: [fullName], + projectId: state.currentProjectId, + branchId, + type, + tempFile: true, + base64, + content, + }); + + return null; + }); + +export const scrollToTab = () => { + Vue.nextTick(() => { + const tabs = document.getElementById('tabs'); + + if (tabs) { + const tabEl = tabs.querySelector('.active .repo-tab'); + + tabEl.focus(); + } + }); +}; + +export const updateViewer = ({ commit }, viewer) => { + commit(types.UPDATE_VIEWER, viewer); +}; + +export const updateDelayViewerUpdated = ({ commit }, delay) => { + commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay); +}; + +export * from './actions/tree'; +export * from './actions/file'; +export * from './actions/project'; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js new file mode 100644 index 00000000000..ddc4b757bf9 --- /dev/null +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -0,0 +1,146 @@ +import { normalizeHeaders } from '~/lib/utils/common_utils'; +import flash from '~/flash'; +import eventHub from '../../eventhub'; +import service from '../../services'; +import * as types from '../mutation_types'; +import router from '../../ide_router'; +import { setPageTitle } from '../utils'; + +export const closeFile = ({ commit, state, getters, dispatch }, path) => { + const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path); + const file = state.entries[path]; + const fileWasActive = file.active; + + commit(types.TOGGLE_FILE_OPEN, path); + commit(types.SET_FILE_ACTIVE, { path, active: false }); + + if (state.openFiles.length > 0 && fileWasActive) { + const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1; + const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path]; + + router.push(`/project${nextFileToOpen.url}`); + } else if (!state.openFiles.length) { + router.push(`/project/${file.projectId}/tree/${file.branchId}/`); + } + + eventHub.$emit(`editor.update.model.dispose.${file.path}`); +}; + +export const setFileActive = ({ commit, state, getters, dispatch }, path) => { + const file = state.entries[path]; + const currentActiveFile = getters.activeFile; + + if (file.active) return; + + if (currentActiveFile) { + commit(types.SET_FILE_ACTIVE, { + path: currentActiveFile.path, + active: false, + }); + } + + commit(types.SET_FILE_ACTIVE, { path, active: true }); + dispatch('scrollToTab'); + + commit(types.SET_CURRENT_PROJECT, file.projectId); + commit(types.SET_CURRENT_BRANCH, file.branchId); +}; + +export const getFileData = ({ state, commit, dispatch }, file) => { + commit(types.TOGGLE_LOADING, { entry: file }); + + return service + .getFileData(file.url) + .then(res => { + const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); + + setPageTitle(pageTitle); + + return res.json(); + }) + .then(data => { + commit(types.SET_FILE_DATA, { data, file }); + commit(types.TOGGLE_FILE_OPEN, file.path); + dispatch('setFileActive', file.path); + commit(types.TOGGLE_LOADING, { entry: file }); + }) + .catch(() => { + commit(types.TOGGLE_LOADING, { entry: file }); + flash( + 'Error loading file data. Please try again.', + 'alert', + document, + null, + false, + true, + ); + }); +}; + +export const getRawFileData = ({ commit, dispatch }, file) => + service + .getRawFileData(file) + .then(raw => { + commit(types.SET_FILE_RAW_DATA, { file, raw }); + }) + .catch(() => + flash( + 'Error loading file content. Please try again.', + 'alert', + document, + null, + false, + true, + ), + ); + +export const changeFileContent = ({ state, commit }, { path, content }) => { + const file = state.entries[path]; + commit(types.UPDATE_FILE_CONTENT, { path, content }); + + const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path); + + if (file.changed && indexOfChangedFile === -1) { + commit(types.ADD_FILE_TO_CHANGED, path); + } else if (!file.changed && indexOfChangedFile !== -1) { + commit(types.REMOVE_FILE_FROM_CHANGED, path); + } +}; + +export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => { + if (getters.activeFile) { + commit(types.SET_FILE_LANGUAGE, { file: getters.activeFile, fileLanguage }); + } +}; + +export const setFileEOL = ({ getters, commit }, { eol }) => { + if (getters.activeFile) { + commit(types.SET_FILE_EOL, { file: getters.activeFile, eol }); + } +}; + +export const setEditorPosition = ( + { getters, commit }, + { editorRow, editorColumn }, +) => { + if (getters.activeFile) { + commit(types.SET_FILE_POSITION, { + file: getters.activeFile, + editorRow, + editorColumn, + }); + } +}; + +export const discardFileChanges = ({ state, commit }, path) => { + const file = state.entries[path]; + + commit(types.DISCARD_FILE_CHANGES, path); + commit(types.REMOVE_FILE_FROM_CHANGED, path); + + if (file.tempFile && file.opened) { + commit(types.TOGGLE_FILE_OPEN, path); + } + + eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw); +}; diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js new file mode 100644 index 00000000000..b3882cb8d21 --- /dev/null +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -0,0 +1,49 @@ +import flash from '~/flash'; +import service from '../../services'; +import * as types from '../mutation_types'; + +export const getProjectData = ( + { commit, state, dispatch }, + { namespace, projectId, force = false } = {}, +) => new Promise((resolve, reject) => { + if (!state.projects[`${namespace}/${projectId}`] || force) { + commit(types.TOGGLE_LOADING, { entry: state }); + service.getProjectData(namespace, projectId) + .then(res => res.data) + .then((data) => { + commit(types.TOGGLE_LOADING, { entry: state }); + commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); + if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); + resolve(data); + }) + .catch(() => { + flash('Error loading project data. Please try again.', 'alert', document, null, false, true); + reject(new Error(`Project not loaded ${namespace}/${projectId}`)); + }); + } else { + resolve(state.projects[`${namespace}/${projectId}`]); + } +}); + +export const getBranchData = ( + { commit, state, dispatch }, + { projectId, branchId, force = false } = {}, +) => new Promise((resolve, reject) => { + if ((typeof state.projects[`${projectId}`] === 'undefined' || + !state.projects[`${projectId}`].branches[branchId]) + || force) { + service.getBranchData(`${projectId}`, branchId) + .then(({ data }) => { + const { id } = data.commit; + commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data }); + commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); + resolve(data); + }) + .catch(() => { + flash('Error loading branch data. Please try again.', 'alert', document, null, false, true); + reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); + }); + } else { + resolve(state.projects[`${projectId}`].branches[branchId]); + } +}); diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js new file mode 100644 index 00000000000..70a969a0325 --- /dev/null +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -0,0 +1,93 @@ +import { normalizeHeaders } from '~/lib/utils/common_utils'; +import flash from '~/flash'; +import service from '../../services'; +import * as types from '../mutation_types'; +import { + findEntry, +} from '../utils'; +import FilesDecoratorWorker from '../workers/files_decorator_worker'; + +export const toggleTreeOpen = ({ commit, dispatch }, path) => { + commit(types.TOGGLE_TREE_OPEN, path); +}; + +export const handleTreeEntryAction = ({ commit, dispatch }, row) => { + if (row.type === 'tree') { + dispatch('toggleTreeOpen', row.path); + } else if (row.type === 'blob' && (row.opened || row.changed)) { + if (row.changed && !row.opened) { + commit(types.TOGGLE_FILE_OPEN, row.path); + } + + dispatch('setFileActive', row.path); + } else { + dispatch('getFileData', row); + } +}; + +export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { + if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return; + + service.getTreeLastCommit(tree.lastCommitPath) + .then((res) => { + const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null; + + commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath }); + + return res.json(); + }) + .then((data) => { + data.forEach((lastCommit) => { + const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name); + + if (entry) { + commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit }); + } + }); + + dispatch('getLastCommitData', tree); + }) + .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); +}; + +export const getFiles = ( + { state, commit, dispatch }, + { projectId, branchId } = {}, +) => new Promise((resolve, reject) => { + if (!state.trees[`${projectId}/${branchId}`]) { + const selectedProject = state.projects[projectId]; + commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` }); + + service + .getFiles(selectedProject.web_url, branchId) + .then(res => res.json()) + .then((data) => { + const worker = new FilesDecoratorWorker(); + worker.addEventListener('message', (e) => { + const { entries, treeList } = e.data; + const selectedTree = state.trees[`${projectId}/${branchId}`]; + + commit(types.SET_ENTRIES, entries); + commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList }); + commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false }); + + worker.terminate(); + + resolve(); + }); + + worker.postMessage({ + data, + projectId, + branchId, + }); + }) + .catch((e) => { + flash('Error loading tree data. Please try again.', 'alert', document, null, false, true); + reject(e); + }); + } else { + resolve(); + } +}); + diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js new file mode 100644 index 00000000000..eba325a31df --- /dev/null +++ b/app/assets/javascripts/ide/stores/getters.js @@ -0,0 +1,30 @@ +export const activeFile = state => + state.openFiles.find(file => file.active) || null; + +export const addedFiles = state => state.changedFiles.filter(f => f.tempFile); + +export const modifiedFiles = state => + state.changedFiles.filter(f => !f.tempFile); + +export const projectsWithTrees = state => + Object.keys(state.projects).map(projectId => { + const project = state.projects[projectId]; + + return { + ...project, + branches: Object.keys(project.branches).map(branchId => { + const branch = project.branches[branchId]; + + return { + ...branch, + tree: state.trees[branch.treeId], + }; + }), + }; + }); + +// eslint-disable-next-line no-confusing-arrow +export const currentIcon = state => + state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; + +export const hasChanges = state => !!state.changedFiles.length; diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js new file mode 100644 index 00000000000..7c82ce7976b --- /dev/null +++ b/app/assets/javascripts/ide/stores/index.js @@ -0,0 +1,19 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import state from './state'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; +import commitModule from './modules/commit'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + state: state(), + actions, + mutations, + getters, + modules: { + commit: commitModule, + }, +}); diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js new file mode 100644 index 00000000000..2e1aea9a399 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -0,0 +1,218 @@ +import $ from 'jquery'; +import { sprintf, __ } from '~/locale'; +import flash from '~/flash'; +import { stripHtml } from '~/lib/utils/text_utility'; +import * as rootTypes from '../../mutation_types'; +import { createCommitPayload, createNewMergeRequestUrl } from '../../utils'; +import router from '../../../ide_router'; +import service from '../../../services'; +import * as types from './mutation_types'; +import * as consts from './constants'; +import eventHub from '../../../eventhub'; + +export const updateCommitMessage = ({ commit }, message) => { + commit(types.UPDATE_COMMIT_MESSAGE, message); +}; + +export const discardDraft = ({ commit }) => { + commit(types.UPDATE_COMMIT_MESSAGE, ''); +}; + +export const updateCommitAction = ({ commit }, commitAction) => { + commit(types.UPDATE_COMMIT_ACTION, commitAction); +}; + +export const updateBranchName = ({ commit }, branchName) => { + commit(types.UPDATE_NEW_BRANCH_NAME, branchName); +}; + +export const setLastCommitMessage = ({ rootState, commit }, data) => { + const currentProject = rootState.projects[rootState.currentProjectId]; + const commitStats = data.stats + ? sprintf(__('with %{additions} additions, %{deletions} deletions.'), { + additions: data.stats.additions, + deletions: data.stats.deletions, + }) + : ''; + const commitMsg = sprintf( + __('Your changes have been committed. Commit %{commitId} %{commitStats}'), + { + commitId: `${data.short_id}`, + commitStats, + }, + false, + ); + + commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true }); +}; + +export const checkCommitStatus = ({ rootState }) => + service + .getBranchData(rootState.currentProjectId, rootState.currentBranchId) + .then(({ data }) => { + const { id } = data.commit; + const selectedBranch = + rootState.projects[rootState.currentProjectId].branches[ + rootState.currentBranchId + ]; + + if (selectedBranch.workingReference !== id) { + return true; + } + + return false; + }) + .catch(() => + flash( + __('Error checking branch data. Please try again.'), + 'alert', + document, + null, + false, + true, + ), + ); + +export const updateFilesAfterCommit = ( + { commit, dispatch, state, rootState, rootGetters }, + { data, branch }, +) => { + const selectedProject = rootState.projects[rootState.currentProjectId]; + const lastCommit = { + commit_path: `${selectedProject.web_url}/commit/${data.id}`, + commit: { + id: data.id, + message: data.message, + authored_date: data.committed_date, + author_name: data.committer_name, + }, + }; + + commit( + rootTypes.SET_BRANCH_WORKING_REFERENCE, + { + projectId: rootState.currentProjectId, + branchId: rootState.currentBranchId, + reference: data.id, + }, + { root: true }, + ); + + rootState.changedFiles.forEach(entry => { + commit( + rootTypes.SET_LAST_COMMIT_DATA, + { + entry, + lastCommit, + }, + { root: true }, + ); + + eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content); + + commit( + rootTypes.SET_FILE_RAW_DATA, + { + file: entry, + raw: entry.content, + }, + { root: true }, + ); + + commit( + rootTypes.TOGGLE_FILE_CHANGED, + { + file: entry, + changed: false, + }, + { root: true }, + ); + }); + + commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true }); + + if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) { + router.push( + `/project/${rootState.currentProjectId}/blob/${branch}/${ + rootGetters.activeFile.path + }`, + ); + } + + dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH); +}; + +export const commitChanges = ({ + commit, + state, + getters, + dispatch, + rootState, +}) => { + const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; + const payload = createCommitPayload( + getters.branchName, + newBranch, + state, + rootState, + ); + const getCommitStatus = newBranch + ? Promise.resolve(false) + : dispatch('checkCommitStatus'); + + commit(types.UPDATE_LOADING, true); + + return getCommitStatus + .then( + branchChanged => + new Promise(resolve => { + if (branchChanged) { + // show the modal with a Bootstrap call + $('#ide-create-branch-modal').modal('show'); + } else { + resolve(); + } + }), + ) + .then(() => service.commit(rootState.currentProjectId, payload)) + .then(({ data }) => { + commit(types.UPDATE_LOADING, false); + + if (!data.short_id) { + flash(data.message, 'alert', document, null, false, true); + return; + } + + dispatch('setLastCommitMessage', data); + dispatch('updateCommitMessage', ''); + + if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) { + dispatch( + 'redirectToUrl', + createNewMergeRequestUrl( + rootState.projects[rootState.currentProjectId].web_url, + getters.branchName, + rootState.currentBranchId, + ), + { root: true }, + ); + } else { + dispatch('updateFilesAfterCommit', { + data, + branch: getters.branchName, + }); + } + }) + .catch(err => { + let errMsg = __('Error committing changes. Please try again.'); + if (err.response.data && err.response.data.message) { + errMsg += ` (${stripHtml(err.response.data.message)})`; + } + flash(errMsg, 'alert', document, null, false, true); + window.dispatchEvent(new Event('resize')); + + commit(types.UPDATE_LOADING, false); + }); +}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/constants.js b/app/assets/javascripts/ide/stores/modules/commit/constants.js new file mode 100644 index 00000000000..230b0a3d9b5 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/constants.js @@ -0,0 +1,3 @@ +export const COMMIT_TO_CURRENT_BRANCH = '1'; +export const COMMIT_TO_NEW_BRANCH = '2'; +export const COMMIT_TO_NEW_BRANCH_MR = '3'; diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js new file mode 100644 index 00000000000..f7cdd6adb0c --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -0,0 +1,24 @@ +import * as consts from './constants'; + +export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading; + +export const commitButtonDisabled = (state, getters, rootState) => + getters.discardDraftButtonDisabled || !rootState.changedFiles.length; + +export const newBranchName = (state, _, rootState) => + `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`; + +export const branchName = (state, getters, rootState) => { + if ( + state.commitAction === consts.COMMIT_TO_NEW_BRANCH || + state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR + ) { + if (state.newBranchName === '') { + return getters.newBranchName; + } + + return state.newBranchName; + } + + return rootState.currentBranchId; +}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/index.js b/app/assets/javascripts/ide/stores/modules/commit/index.js new file mode 100644 index 00000000000..3bf65b02847 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/index.js @@ -0,0 +1,12 @@ +import state from './state'; +import mutations from './mutations'; +import * as actions from './actions'; +import * as getters from './getters'; + +export default { + namespaced: true, + state: state(), + mutations, + actions, + getters, +}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js new file mode 100644 index 00000000000..9221f054e9f --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js @@ -0,0 +1,4 @@ +export const UPDATE_COMMIT_MESSAGE = 'UPDATE_COMMIT_MESSAGE'; +export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION'; +export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME'; +export const UPDATE_LOADING = 'UPDATE_LOADING'; diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js new file mode 100644 index 00000000000..797357e3df9 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js @@ -0,0 +1,24 @@ +import * as types from './mutation_types'; + +export default { + [types.UPDATE_COMMIT_MESSAGE](state, commitMessage) { + Object.assign(state, { + commitMessage, + }); + }, + [types.UPDATE_COMMIT_ACTION](state, commitAction) { + Object.assign(state, { + commitAction, + }); + }, + [types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) { + Object.assign(state, { + newBranchName, + }); + }, + [types.UPDATE_LOADING](state, submitCommitLoading) { + Object.assign(state, { + submitCommitLoading, + }); + }, +}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js new file mode 100644 index 00000000000..8dae50961b0 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/commit/state.js @@ -0,0 +1,6 @@ +export default () => ({ + commitMessage: '', + commitAction: '1', + newBranchName: '', + submitCommitLoading: false, +}); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js new file mode 100644 index 00000000000..e28f190897c --- /dev/null +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -0,0 +1,43 @@ +export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; +export const TOGGLE_LOADING = 'TOGGLE_LOADING'; +export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA'; +export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG'; +export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED'; +export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED'; +export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS'; + +// Project Mutation Types +export const SET_PROJECT = 'SET_PROJECT'; +export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT'; +export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN'; + +// Branch Mutation Types +export const SET_BRANCH = 'SET_BRANCH'; +export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; +export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; + +// Tree mutation types +export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; +export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN'; +export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL'; +export const CREATE_TREE = 'CREATE_TREE'; +export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES'; + +// File mutation types +export const SET_FILE_DATA = 'SET_FILE_DATA'; +export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN'; +export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE'; +export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA'; +export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; +export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE'; +export const SET_FILE_POSITION = 'SET_FILE_POSITION'; +export const SET_FILE_EOL = 'SET_FILE_EOL'; +export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES'; +export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED'; +export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED'; +export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED'; +export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; +export const SET_ENTRIES = 'SET_ENTRIES'; +export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY'; +export const UPDATE_VIEWER = 'UPDATE_VIEWER'; +export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js new file mode 100644 index 00000000000..da41fc9285c --- /dev/null +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -0,0 +1,106 @@ +import * as types from './mutation_types'; +import projectMutations from './mutations/project'; +import fileMutations from './mutations/file'; +import treeMutations from './mutations/tree'; +import branchMutations from './mutations/branch'; + +export default { + [types.SET_INITIAL_DATA](state, data) { + Object.assign(state, data); + }, + [types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) { + if (entry.path) { + Object.assign(state.entries[entry.path], { + loading: + forceValue !== undefined + ? forceValue + : !state.entries[entry.path].loading, + }); + } else { + Object.assign(entry, { + loading: forceValue !== undefined ? forceValue : !entry.loading, + }); + } + }, + [types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) { + Object.assign(state, { + leftPanelCollapsed: collapsed, + }); + }, + [types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) { + Object.assign(state, { + rightPanelCollapsed: collapsed, + }); + }, + [types.SET_RESIZING_STATUS](state, resizing) { + Object.assign(state, { + panelResizing: resizing, + }); + }, + [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) { + Object.assign(entry.lastCommit, { + id: lastCommit.commit.id, + url: lastCommit.commit_path, + message: lastCommit.commit.message, + author: lastCommit.commit.author_name, + updatedAt: lastCommit.commit.authored_date, + }); + }, + [types.SET_LAST_COMMIT_MSG](state, lastCommitMsg) { + Object.assign(state, { + lastCommitMsg, + }); + }, + [types.SET_ENTRIES](state, entries) { + Object.assign(state, { + entries, + }); + }, + [types.CREATE_TMP_ENTRY](state, { data, projectId, branchId }) { + Object.keys(data.entries).reduce((acc, key) => { + const entry = data.entries[key]; + const foundEntry = state.entries[key]; + + if (!foundEntry) { + Object.assign(state.entries, { + [key]: entry, + }); + } else { + const tree = entry.tree.filter( + f => foundEntry.tree.find(e => e.path === f.path) === undefined, + ); + Object.assign(foundEntry, { + tree: foundEntry.tree.concat(tree), + }); + } + + return acc.concat(key); + }, []); + + const foundEntry = state.trees[`${projectId}/${branchId}`].tree.find( + e => e.path === data.treeList[0].path, + ); + + if (!foundEntry) { + Object.assign(state.trees[`${projectId}/${branchId}`], { + tree: state.trees[`${projectId}/${branchId}`].tree.concat( + data.treeList, + ), + }); + } + }, + [types.UPDATE_VIEWER](state, viewer) { + Object.assign(state, { + viewer, + }); + }, + [types.UPDATE_DELAY_VIEWER_CHANGE](state, delayViewerUpdated) { + Object.assign(state, { + delayViewerUpdated, + }); + }, + ...projectMutations, + ...fileMutations, + ...treeMutations, + ...branchMutations, +}; diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js new file mode 100644 index 00000000000..2972ba5e38e --- /dev/null +++ b/app/assets/javascripts/ide/stores/mutations/branch.js @@ -0,0 +1,26 @@ +import * as types from '../mutation_types'; + +export default { + [types.SET_CURRENT_BRANCH](state, currentBranchId) { + Object.assign(state, { + currentBranchId, + }); + }, + [types.SET_BRANCH](state, { projectPath, branchName, branch }) { + Object.assign(state.projects[projectPath], { + branches: { + [branchName]: { + ...branch, + treeId: `${projectPath}/${branchName}`, + active: true, + workingReference: '', + }, + }, + }); + }, + [types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) { + Object.assign(state.projects[projectId].branches[branchId], { + workingReference: reference, + }); + }, +}; diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js new file mode 100644 index 00000000000..2500f13db7c --- /dev/null +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -0,0 +1,83 @@ +import * as types from '../mutation_types'; + +export default { + [types.SET_FILE_ACTIVE](state, { path, active }) { + Object.assign(state.entries[path], { + active, + }); + }, + [types.TOGGLE_FILE_OPEN](state, path) { + Object.assign(state.entries[path], { + opened: !state.entries[path].opened, + }); + + if (state.entries[path].opened) { + state.openFiles.push(state.entries[path]); + } else { + Object.assign(state, { + openFiles: state.openFiles.filter(f => f.path !== path), + }); + } + }, + [types.SET_FILE_DATA](state, { data, file }) { + Object.assign(state.entries[file.path], { + id: data.id, + blamePath: data.blame_path, + commitsPath: data.commits_path, + permalink: data.permalink, + rawPath: data.raw_path, + binary: data.binary, + renderError: data.render_error, + }); + }, + [types.SET_FILE_RAW_DATA](state, { file, raw }) { + Object.assign(state.entries[file.path], { + raw, + }); + }, + [types.UPDATE_FILE_CONTENT](state, { path, content }) { + const changed = content !== state.entries[path].raw; + + Object.assign(state.entries[path], { + content, + changed, + }); + }, + [types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) { + Object.assign(state.entries[file.path], { + fileLanguage, + }); + }, + [types.SET_FILE_EOL](state, { file, eol }) { + Object.assign(state.entries[file.path], { + eol, + }); + }, + [types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) { + Object.assign(state.entries[file.path], { + editorRow, + editorColumn, + }); + }, + [types.DISCARD_FILE_CHANGES](state, path) { + Object.assign(state.entries[path], { + content: state.entries[path].raw, + changed: false, + }); + }, + [types.ADD_FILE_TO_CHANGED](state, path) { + Object.assign(state, { + changedFiles: state.changedFiles.concat(state.entries[path]), + }); + }, + [types.REMOVE_FILE_FROM_CHANGED](state, path) { + Object.assign(state, { + changedFiles: state.changedFiles.filter(f => f.path !== path), + }); + }, + [types.TOGGLE_FILE_CHANGED](state, { file, changed }) { + Object.assign(state.entries[file.path], { + changed, + }); + }, +}; diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js new file mode 100644 index 00000000000..2816562a919 --- /dev/null +++ b/app/assets/javascripts/ide/stores/mutations/project.js @@ -0,0 +1,23 @@ +import * as types from '../mutation_types'; + +export default { + [types.SET_CURRENT_PROJECT](state, currentProjectId) { + Object.assign(state, { + currentProjectId, + }); + }, + [types.SET_PROJECT](state, { projectPath, project }) { + // Add client side properties + Object.assign(project, { + tree: [], + branches: {}, + active: true, + }); + + Object.assign(state, { + projects: Object.assign({}, state.projects, { + [projectPath]: project, + }), + }); + }, +}; diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js new file mode 100644 index 00000000000..7f7e470c9bb --- /dev/null +++ b/app/assets/javascripts/ide/stores/mutations/tree.js @@ -0,0 +1,38 @@ +import * as types from '../mutation_types'; + +export default { + [types.TOGGLE_TREE_OPEN](state, path) { + Object.assign(state.entries[path], { + opened: !state.entries[path].opened, + }); + }, + [types.CREATE_TREE](state, { treePath }) { + Object.assign(state, { + trees: Object.assign({}, state.trees, { + [treePath]: { + tree: [], + loading: true, + }, + }), + }); + }, + [types.SET_DIRECTORY_DATA](state, { data, treePath }) { + Object.assign(state, { + trees: Object.assign(state.trees, { + [treePath]: { + tree: data, + }, + }), + }); + }, + [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) { + Object.assign(tree, { + lastCommitPath: url, + }); + }, + [types.REMOVE_ALL_CHANGES_FILES](state) { + Object.assign(state, { + changedFiles: [], + }); + }, +}; diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js new file mode 100644 index 00000000000..6110f54951c --- /dev/null +++ b/app/assets/javascripts/ide/stores/state.js @@ -0,0 +1,19 @@ +export default () => ({ + currentProjectId: '', + currentBranchId: '', + changedFiles: [], + endpoints: {}, + lastCommitMsg: '', + lastCommitPath: '', + loading: false, + openFiles: [], + parentTreeUrl: '', + trees: {}, + projects: {}, + leftPanelCollapsed: false, + rightPanelCollapsed: false, + panelResizing: false, + entries: {}, + viewer: 'editor', + delayViewerUpdated: false, +}); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js new file mode 100644 index 00000000000..487ea1ead8e --- /dev/null +++ b/app/assets/javascripts/ide/stores/utils.js @@ -0,0 +1,125 @@ +export const dataStructure = () => ({ + id: '', + key: '', + type: '', + projectId: '', + branchId: '', + name: '', + url: '', + path: '', + tempFile: false, + tree: [], + loading: false, + opened: false, + active: false, + changed: false, + lastCommitPath: '', + lastCommit: { + id: '', + url: '', + message: '', + updatedAt: '', + author: '', + }, + blamePath: '', + commitsPath: '', + permalink: '', + rawPath: '', + binary: false, + html: '', + raw: '', + content: '', + parentTreeUrl: '', + renderError: false, + base64: false, + editorRow: 1, + editorColumn: 1, + fileLanguage: '', + eol: '', +}); + +export const decorateData = (entity) => { + const { + id, + projectId, + branchId, + type, + url, + name, + path, + renderError, + content = '', + tempFile = false, + active = false, + opened = false, + changed = false, + parentTreeUrl = '', + base64 = false, + + file_lock, + + } = entity; + + return { + ...dataStructure(), + id, + projectId, + branchId, + key: `${name}-${type}-${id}`, + type, + name, + url, + path, + tempFile, + opened, + active, + parentTreeUrl, + changed, + renderError, + content, + base64, + + file_lock, + + }; +}; + +export const findEntry = (tree, type, name, prop = 'name') => tree.find( + f => f.type === type && f[prop] === name, +); + +export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path); + +export const setPageTitle = (title) => { + document.title = title; +}; + +export const createCommitPayload = (branch, newBranch, state, rootState) => ({ + branch, + commit_message: state.commitMessage, + actions: rootState.changedFiles.map(f => ({ + action: f.tempFile ? 'create' : 'update', + file_path: f.path, + content: f.content, + encoding: f.base64 ? 'base64' : 'text', + })), + start_branch: newBranch ? rootState.currentBranchId : undefined, +}); + +export const createNewMergeRequestUrl = (projectUrl, source, target) => + `${projectUrl}/merge_requests/new?merge_request[source_branch]=${source}&merge_request[target_branch]=${target}`; + +const sortTreesByTypeAndName = (a, b) => { + if (a.type === 'tree' && b.type === 'blob') { + return -1; + } else if (a.type === 'blob' && b.type === 'tree') { + return 1; + } + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; + return 0; +}; + +export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, { + tree: entity.tree.length ? sortTree(entity.tree) : [], +})).sort(sortTreesByTypeAndName); diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js new file mode 100644 index 00000000000..e959130300b --- /dev/null +++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js @@ -0,0 +1,91 @@ +import { + decorateData, + sortTree, +} from '../utils'; + +self.addEventListener('message', (e) => { + const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data; + + const treeList = []; + let file; + const entries = data.reduce((acc, path) => { + const pathSplit = path.split('/'); + const blobName = pathSplit.pop().trim(); + + if (pathSplit.length > 0) { + pathSplit.reduce((pathAcc, folderName) => { + const parentFolder = acc[pathAcc[pathAcc.length - 1]]; + const folderPath = `${(parentFolder ? `${parentFolder.path}/` : '')}${folderName}`; + const foundEntry = acc[folderPath]; + + if (!foundEntry) { + const tree = decorateData({ + projectId, + branchId, + id: folderPath, + name: folderName, + path: folderPath, + url: `/${projectId}/tree/${branchId}/${folderPath}`, + type: 'tree', + parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`, + tempFile, + changed: tempFile, + opened: tempFile, + }); + + Object.assign(acc, { + [folderPath]: tree, + }); + + if (parentFolder) { + parentFolder.tree.push(tree); + } else { + treeList.push(tree); + } + + pathAcc.push(tree.path); + } else { + pathAcc.push(foundEntry.path); + } + + return pathAcc; + }, []); + } + + if (blobName !== '') { + const fileFolder = acc[pathSplit.join('/')]; + file = decorateData({ + projectId, + branchId, + id: path, + name: blobName, + path, + url: `/${projectId}/blob/${branchId}/${path}`, + type: 'blob', + parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`, + tempFile, + changed: tempFile, + content, + base64, + }); + + Object.assign(acc, { + [path]: file, + }); + + if (fileFolder) { + fileFolder.tree.push(file); + } else { + treeList.push(file); + } + } + + return acc; + }, {}); + + self.postMessage({ + entries, + treeList: sortTree(treeList), + file, + }); +}); diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 2d015ef086b..93cb83b3a4c 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -20,7 +20,7 @@ width: 100%; } - $image-widths: 250 306 394 430; + $image-widths: 80 250 306 394 430; @each $width in $image-widths { &.svg-#{$width} { img, @@ -39,12 +39,28 @@ svg { fill: currentColor; - &.s8 { @include svg-size(8px); } - &.s12 { @include svg-size(12px); } - &.s16 { @include svg-size(16px); } - &.s18 { @include svg-size(18px); } - &.s24 { @include svg-size(24px); } - &.s32 { @include svg-size(32px); } - &.s48 { @include svg-size(48px); } - &.s72 { @include svg-size(72px); } + &.s8 { + @include svg-size(8px); + } + &.s12 { + @include svg-size(12px); + } + &.s16 { + @include svg-size(16px); + } + &.s18 { + @include svg-size(18px); + } + &.s24 { + @include svg-size(24px); + } + &.s32 { + @include svg-size(32px); + } + &.s48 { + @include svg-size(48px); + } + &.s72 { + @include svg-size(72px); + } } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 8265b8370f7..7a8fbfc517d 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -19,6 +19,7 @@ .ide-view { display: flex; height: calc(100vh - #{$header-height}); + margin-top: 40px; color: $almost-black; border-top: 1px solid $white-dark; border-bottom: 1px solid $white-dark; @@ -28,6 +29,11 @@ max-width: 250px; } } + + .file-status-icon { + width: 10px; + height: 10px; + } } .ide-file-list { @@ -40,31 +46,41 @@ background: $white-normal; } - .repo-file-name { + .ide-file-name { + flex: 1; white-space: nowrap; text-overflow: ellipsis; + + svg { + vertical-align: middle; + margin-right: 2px; + } + + .loading-container { + margin-right: 4px; + display: inline-block; + } } - .unsaved-icon { - color: $indigo-700; - float: right; - font-size: smaller; - line-height: 20px; + .ide-file-changed-icon { + margin-left: auto; } - .repo-new-btn { + .ide-new-btn { display: none; - margin-top: -4px; margin-bottom: -4px; + margin-right: -8px; } &:hover { - .repo-new-btn { + .ide-new-btn { display: block; } + } - .unsaved-icon { - display: none; + &.folder { + svg { + fill: $gl-text-color-secondary; } } } @@ -79,10 +95,10 @@ } } -.multi-file-table-name, -.multi-file-table-col-commit-message { +.file-name, +.file-col-commit-message { + display: flex; overflow: visible; - max-width: 0; padding: 6px 12px; } @@ -99,21 +115,6 @@ } } -table.table tr td.multi-file-table-name { - width: 350px; - padding: 6px 12px; - - svg { - vertical-align: middle; - margin-right: 2px; - } - - .loading-container { - margin-right: 4px; - display: inline-block; - } -} - .multi-file-table-col-commit-message { white-space: nowrap; width: 50%; @@ -129,13 +130,35 @@ table.table tr td.multi-file-table-name { .multi-file-tabs { display: flex; - overflow-x: auto; background-color: $white-normal; box-shadow: inset 0 -1px $white-dark; - > li { + > ul { + display: flex; + overflow-x: auto; + } + + li { position: relative; } + + .dropdown { + display: flex; + margin-left: auto; + margin-bottom: 1px; + padding: 0 $grid-size; + border-left: 1px solid $white-dark; + background-color: $white-light; + + &.shadow { + box-shadow: 0 0 10px $dropdown-shadow-color; + } + + .btn { + margin-top: auto; + margin-bottom: auto; + } + } } .multi-file-tab { @@ -160,20 +183,32 @@ table.table tr td.multi-file-table-name { position: absolute; right: 8px; top: 50%; + width: 16px; + height: 16px; padding: 0; background: none; border: 0; - font-size: $gl-font-size; - color: $gray-darkest; + border-radius: $border-radius-default; + color: $theme-gray-900; transform: translateY(-50%); - &:not(.modified):hover, - &:not(.modified):focus { - color: $hint-color; + svg { + position: relative; + top: -1px; } - &.modified { - color: $indigo-700; + &:hover { + background-color: $theme-gray-200; + } + + &:focus { + background-color: $blue-500; + color: $white-light; + outline: 0; + + svg { + fill: currentColor; + } } } @@ -192,6 +227,70 @@ table.table tr td.multi-file-table-name { .vertical-center { min-height: auto; } + + .monaco-editor .lines-content .cigr { + display: none; + } + + .monaco-diff-editor.vs { + .editor.modified { + box-shadow: none; + } + + .diagonal-fill { + display: none !important; + } + + .diffOverview { + background-color: $white-light; + border-left: 1px solid $white-dark; + cursor: ns-resize; + } + + .diffViewport { + display: none; + } + + .char-insert { + background-color: $line-added-dark; + } + + .char-delete { + background-color: $line-removed-dark; + } + + .line-numbers { + color: $black-transparent; + } + + .view-overlays { + .line-insert { + background-color: $line-added; + } + + .line-delete { + background-color: $line-removed; + } + } + + .margin { + background-color: $gray-light; + border-right: 1px solid $white-normal; + + .line-insert { + border-right: 1px solid $line-added-dark; + } + + .line-delete { + border-right: 1px solid $line-removed-dark; + } + } + + .margin-view-overlays .insert-sign, + .margin-view-overlays .delete-sign { + opacity: 0.4; + } + } } .multi-file-editor-holder { @@ -252,7 +351,7 @@ table.table tr td.multi-file-table-name { display: flex; position: relative; flex-direction: column; - width: 290px; + width: 340px; padding: 0; background-color: $gray-light; padding-right: 3px; @@ -350,6 +449,11 @@ table.table tr td.multi-file-table-name { flex: 1; } +.multi-file-commit-empty-state-container { + align-items: center; + justify-content: center; +} + .multi-file-commit-panel-header { display: flex; align-items: center; @@ -376,7 +480,7 @@ table.table tr td.multi-file-table-name { .multi-file-commit-panel-header-title { display: flex; flex: 1; - padding: $gl-btn-padding; + padding: 0 $gl-btn-padding; svg { margin-right: $gl-btn-padding; @@ -390,12 +494,34 @@ table.table tr td.multi-file-table-name { .multi-file-commit-list { flex: 1; overflow: auto; - padding: $gl-padding; + padding: $gl-padding 0; + min-height: 60px; } .multi-file-commit-list-item { display: flex; + padding: 0; align-items: center; + + .multi-file-discard-btn { + display: none; + margin-left: auto; + color: $gl-link-color; + padding: 0 2px; + + &:focus, + &:hover { + text-decoration: underline; + } + } + + &:hover { + background: $white-normal; + + .multi-file-discard-btn { + display: block; + } + } } .multi-file-addition { @@ -414,29 +540,58 @@ table.table tr td.multi-file-table-name { margin-left: auto; margin-right: auto; } + + .file-status-icon { + width: 10px; + height: 10px; + margin-left: 3px; + } } .multi-file-commit-list-path { + padding: $grid-size / 2; + padding-left: $gl-padding; + background: none; + border: 0; + text-align: left; + width: 100%; + min-width: 0; + + svg { + min-width: 16px; + vertical-align: middle; + display: inline-block; + } + + &:hover, + &:focus { + outline: 0; + } +} + +.multi-file-commit-list-file-path { @include str-truncated(100%); + + &:hover { + text-decoration: underline; + } + + &:active { + text-decoration: none; + } } .multi-file-commit-form { padding: $gl-padding; border-top: 1px solid $white-dark; -} - -.multi-file-commit-fieldset { - display: flex; - align-items: center; - padding-bottom: 12px; .btn { - flex: 1; + font-size: $gl-font-size; } } .multi-file-commit-message.form-control { - height: 80px; + height: 160px; resize: none; } @@ -468,7 +623,7 @@ table.table tr td.multi-file-table-name { top: 0; width: 100px; height: 1px; - background-color: rgba($red-500, .5); + background-color: rgba($red-500, 0.5); } } } @@ -487,7 +642,7 @@ table.table tr td.multi-file-table-name { justify-content: center; } -.repo-new-btn { +.ide-new-btn { .dropdown-toggle svg { margin-top: -2px; margin-bottom: 2px; @@ -505,36 +660,39 @@ table.table tr td.multi-file-table-name { } } -.ide.nav-only { - .flash-container { - margin-top: $header-height; - margin-bottom: 0; - } - - .alert-wrapper .flash-container .flash-alert:last-child, - .alert-wrapper .flash-container .flash-notice:last-child { - margin-bottom: 0; - } +.ide { + overflow: hidden; - .content { - margin-top: $header-height; - } + &.nav-only { + .flash-container { + margin-top: $header-height; + margin-bottom: 0; + } - .multi-file-commit-panel .multi-file-commit-panel-inner-scroll { - max-height: calc(100vh - #{$header-height + $context-header-height}); - } + .alert-wrapper .flash-container .flash-alert:last-child, + .alert-wrapper .flash-container .flash-notice:last-child { + margin-bottom: 0; + } - &.flash-shown { - .content { - margin-top: 0; + .content-wrapper { + margin-top: $header-height; + padding-bottom: 0; } - .ide-view { - height: calc(100vh - #{$header-height + $flash-height}); + &.flash-shown { + .content-wrapper { + margin-top: 0; + } + + .ide-view { + height: calc(100vh - #{$header-height + $flash-height}); + } } - .multi-file-commit-panel .multi-file-commit-panel-inner-scroll { - max-height: calc(100vh - #{$header-height + $flash-height + $context-header-height}); + .projects-sidebar { + .multi-file-commit-panel-inner-scroll { + flex: 1; + } } } } @@ -544,34 +702,28 @@ table.table tr td.multi-file-table-name { margin-top: #{$header-height + $performance-bar-height}; } - .content { + .content-wrapper { margin-top: #{$header-height + $performance-bar-height}; + padding-bottom: 0; } .ide-view { height: calc(100vh - #{$header-height + $performance-bar-height}); } - .multi-file-commit-panel .multi-file-commit-panel-inner-scroll { - max-height: calc(100vh - #{$header-height + $performance-bar-height + 60}); - } - &.flash-shown { - .content { + .content-wrapper { margin-top: 0; } .ide-view { - height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height}); - } - - .multi-file-commit-panel .multi-file-commit-panel-inner-scroll { - max-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height}); + height: calc( + 100vh - #{$header-height + $performance-bar-height + $flash-height} + ); } } } - .dragHandle { position: absolute; top: 0; @@ -587,3 +739,44 @@ table.table tr td.multi-file-table-name { left: 0; } } + +.ide-commit-radios { + label { + font-weight: normal; + } + + .help-block { + margin-top: 0; + line-height: 0; + } +} + +.ide-commit-new-branch { + margin-left: 25px; +} + +.ide-external-links { + p { + margin: 0; + } +} + +.ide-sidebar-link { + padding: $gl-padding-8 $gl-padding; + background: $indigo-700; + color: $white-light; + text-decoration: none; + display: flex; + align-items: center; + + &:focus, + &:hover { + color: $white-light; + text-decoration: underline; + background: $indigo-500; + } + + &:active { + background: $indigo-800; + } +} diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb new file mode 100644 index 00000000000..1ff25a45398 --- /dev/null +++ b/app/controllers/ide_controller.rb @@ -0,0 +1,6 @@ +class IdeController < ApplicationController + layout 'nav_only' + + def index + end +end diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb new file mode 100644 index 00000000000..f090ae71269 --- /dev/null +++ b/app/helpers/ide_helper.rb @@ -0,0 +1,14 @@ +module IdeHelper + def ide_edit_button(project = @project, ref = @ref, path = @path, options = {}) + return unless blob = readable_blob(options, path, project, ref) + + common_classes = "btn js-edit-ide #{options[:extra_class]}" + + edit_button_tag(blob, + common_classes, + _('Web IDE'), + ide_edit_path(project, ref, path, options), + project, + ref) + end +end diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml new file mode 100644 index 00000000000..e0e8fe548d0 --- /dev/null +++ b/app/views/ide/index.html.haml @@ -0,0 +1,12 @@ +- @body_class = 'ide' +- page_title 'IDE' + +- content_for :page_specific_javascripts do + = webpack_bundle_tag 'ide', force_same_domain: true + +#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'), + "no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'), + "committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg') } } + .text-center + = icon('spinner spin 2x') + %h2.clgray= _('Loading the GitLab IDE...') diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 06bce52e709..67613949b7d 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -76,4 +76,8 @@ = render 'projects/find_file_link' + = succeed " " do + = link_to ide_edit_path(@project, @id), class: 'btn btn-default' do + = _('Web IDE') + = render 'projects/buttons/download', project: @project, ref: @ref diff --git a/config/routes.rb b/config/routes.rb index 8769f433c39..52726f94753 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -61,6 +61,9 @@ Rails.application.routes.draw do # UserCallouts resources :user_callouts, only: [:create] + + get 'ide' => 'ide#index' + get 'ide/*vueroute' => 'ide#index', format: false end # Koding route diff --git a/config/webpack.config.js b/config/webpack.config.js index 3403c0c207d..f5fb7de6176 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -9,12 +9,14 @@ const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin; const CopyWebpackPlugin = require('copy-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); const NameAllModulesPlugin = require('name-all-modules-plugin'); -const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') + .BundleAnalyzerPlugin; const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); const ROOT_PATH = path.resolve(__dirname, '..'); const IS_PRODUCTION = process.env.NODE_ENV === 'production'; -const IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1; +const IS_DEV_SERVER = + process.argv.join(' ').indexOf('webpack-dev-server') !== -1; const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; const DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; @@ -27,10 +29,10 @@ let watchAutoEntries = []; function generateEntries() { // generate automatic entry points const autoEntries = {}; - const pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') }); - watchAutoEntries = [ - path.join(ROOT_PATH, 'app/assets/javascripts/pages/'), - ]; + const pageEntries = glob.sync('pages/**/index.js', { + cwd: path.join(ROOT_PATH, 'app/assets/javascripts'), + }); + watchAutoEntries = [path.join(ROOT_PATH, 'app/assets/javascripts/pages/')]; function generateAutoEntries(path, prefix = '.') { const chunkPath = path.replace(/\/index\.js$/, ''); @@ -38,15 +40,16 @@ function generateEntries() { autoEntries[chunkName] = `${prefix}/${path}`; } - pageEntries.forEach(( path ) => generateAutoEntries(path)); + pageEntries.forEach(path => generateAutoEntries(path)); autoEntriesCount = Object.keys(autoEntries).length; const manualEntries = { - common: './commons/index.js', - main: './main.js', - raven: './raven/index.js', - webpack_runtime: './webpack.js', + common: './commons/index.js', + main: './main.js', + raven: './raven/index.js', + webpack_runtime: './webpack.js', + ide: './ide/index.js', }; return Object.assign(manualEntries, autoEntries); @@ -60,8 +63,12 @@ const config = { output: { path: path.join(ROOT_PATH, 'public/assets/webpack'), publicPath: '/assets/webpack/', - filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js', - chunkFilename: IS_PRODUCTION ? '[name].[chunkhash].chunk.js' : '[name].chunk.js', + filename: IS_PRODUCTION + ? '[name].[chunkhash].bundle.js' + : '[name].bundle.js', + chunkFilename: IS_PRODUCTION + ? '[name].[chunkhash].chunk.js' + : '[name].chunk.js', }, module: { @@ -90,8 +97,8 @@ const config = { { loader: 'worker-loader', options: { - inline: true - } + inline: true, + }, }, { loader: 'babel-loader' }, ], @@ -102,7 +109,7 @@ const config = { loader: 'file-loader', options: { name: '[name].[hash].[ext]', - } + }, }, { test: /katex.css$/, @@ -112,8 +119,8 @@ const config = { { loader: 'css-loader', options: { - name: '[name].[hash].[ext]' - } + name: '[name].[hash].[ext]', + }, }, ], }, @@ -123,15 +130,18 @@ const config = { loader: 'file-loader', options: { name: '[name].[hash].[ext]', - } + }, }, { test: /monaco-editor\/\w+\/vs\/loader\.js$/, use: [ { loader: 'exports-loader', options: 'l.global' }, - { loader: 'imports-loader', options: 'l=>{},this=>l,AMDLoader=>this,module=>undefined' }, + { + loader: 'imports-loader', + options: 'l=>{},this=>l,AMDLoader=>this,module=>undefined', + }, ], - } + }, ], noParse: [/monaco-editor\/\w+\/vs\//], @@ -149,10 +159,10 @@ const config = { source: false, chunks: false, modules: false, - assets: true + assets: true, }); return JSON.stringify(stats, null, 2); - } + }, }), // prevent pikaday from including moment.js @@ -169,7 +179,7 @@ const config = { new NameAllModulesPlugin(), // assign deterministic chunk ids - new webpack.NamedChunksPlugin((chunk) => { + new webpack.NamedChunksPlugin(chunk => { if (chunk.name) { return chunk.name; } @@ -186,9 +196,12 @@ const config = { const pagesBase = path.join(ROOT_PATH, 'app/assets/javascripts/pages'); if (m.resource.indexOf(pagesBase) === 0) { - moduleNames.push(path.relative(pagesBase, m.resource) - .replace(/\/index\.[a-z]+$/, '') - .replace(/\//g, '__')); + moduleNames.push( + path + .relative(pagesBase, m.resource) + .replace(/\/index\.[a-z]+$/, '') + .replace(/\//g, '__'), + ); } else { moduleNames.push(path.relative(m.context, m.resource)); } @@ -196,7 +209,8 @@ const config = { chunk.forEachModule(collectModuleNames); - const hash = crypto.createHash('sha256') + const hash = crypto + .createHash('sha256') .update(moduleNames.join('_')) .digest('hex'); @@ -214,10 +228,17 @@ const config = { // copy pre-compiled vendor libraries verbatim new CopyWebpackPlugin([ { - from: path.join(ROOT_PATH, `node_modules/monaco-editor/${IS_PRODUCTION ? 'min' : 'dev'}/vs`), + from: path.join( + ROOT_PATH, + `node_modules/monaco-editor/${IS_PRODUCTION ? 'min' : 'dev'}/vs`, + ), to: 'monaco-editor/vs', transform: function(content, path) { - if (/\.js$/.test(path) && !/worker/i.test(path) && !/typescript/i.test(path)) { + if ( + /\.js$/.test(path) && + !/worker/i.test(path) && + !/typescript/i.test(path) + ) { return ( '(function(){\n' + 'var define = this.define, require = this.require;\n' + @@ -227,23 +248,23 @@ const config = { ); } return content; - } - } + }, + }, ]), ], resolve: { extensions: ['.js'], alias: { - '~': path.join(ROOT_PATH, 'app/assets/javascripts'), - 'emojis': path.join(ROOT_PATH, 'fixtures/emojis'), - 'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'), - 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'), - 'images': path.join(ROOT_PATH, 'app/assets/images'), - 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), - 'vue$': 'vue/dist/vue.esm.js', - 'spec': path.join(ROOT_PATH, 'spec/javascripts'), - } + '~': path.join(ROOT_PATH, 'app/assets/javascripts'), + emojis: path.join(ROOT_PATH, 'fixtures/emojis'), + empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'), + icons: path.join(ROOT_PATH, 'app/views/shared/icons'), + images: path.join(ROOT_PATH, 'app/assets/images'), + vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'), + vue$: 'vue/dist/vue.esm.js', + spec: path.join(ROOT_PATH, 'spec/javascripts'), + }, }, // sqljs requires fs @@ -258,14 +279,14 @@ if (IS_PRODUCTION) { new webpack.NoEmitOnErrorsPlugin(), new webpack.LoaderOptionsPlugin({ minimize: true, - debug: false + debug: false, }), new webpack.optimize.UglifyJsPlugin({ - sourceMap: true + sourceMap: true, }), new webpack.DefinePlugin({ - 'process.env': { NODE_ENV: JSON.stringify('production') } - }) + 'process.env': { NODE_ENV: JSON.stringify('production') }, + }), ); // compression can require a lot of compute time and is disabled in CI @@ -283,7 +304,7 @@ if (IS_DEV_SERVER) { headers: { 'Access-Control-Allow-Origin': '*' }, stats: 'errors-only', hot: DEV_SERVER_LIVERELOAD, - inline: DEV_SERVER_LIVERELOAD + inline: DEV_SERVER_LIVERELOAD, }; config.plugins.push( // watch node_modules for changes if we encounter a missing module compile error @@ -299,12 +320,14 @@ if (IS_DEV_SERVER) { ]; // report our auto-generated bundle count - console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`); + console.log( + `${autoEntriesCount} entries from '/pages' automatically added to webpack output.`, + ); callback(); - }) + }); }, - } + }, ); if (DEV_SERVER_LIVERELOAD) { config.plugins.push(new webpack.HotModuleReplacementPlugin()); @@ -319,7 +342,7 @@ if (WEBPACK_REPORT) { openAnalyzer: false, reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'), statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'), - }) + }), ); } diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb new file mode 100644 index 00000000000..d96c7e655ba --- /dev/null +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +feature 'Multi-file editor new directory', :js do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + + before do + project.add_master(user) + sign_in(user) + + visit project_tree_path(project, :master) + + wait_for_requests + + click_link('Web IDE') + + wait_for_requests + end + + after do + set_cookie('new_repo', 'false') + end + + it 'creates directory in current directory' do + find('.add-to-tree').click + + click_link('New directory') + + page.within('.modal') do + find('.form-control').set('folder name') + + click_button('Create directory') + end + + find('.add-to-tree').click + + click_link('New file') + + page.within('.modal-dialog') do + find('.form-control').set('file name') + + click_button('Create file') + end + + wait_for_requests + + fill_in('commit-message', with: 'commit message ide') + + click_button('Commit') + + expect(page).to have_content('folder name') + end +end diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb new file mode 100644 index 00000000000..a4cbd5cf766 --- /dev/null +++ b/spec/features/projects/tree/create_file_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +feature 'Multi-file editor new file', :js do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + + before do + project.add_master(user) + sign_in(user) + + visit project_path(project) + + wait_for_requests + + click_link('Web IDE') + + wait_for_requests + end + + after do + set_cookie('new_repo', 'false') + end + + it 'creates file in current directory' do + find('.add-to-tree').click + + click_link('New file') + + page.within('.modal') do + find('.form-control').set('file name') + + click_button('Create file') + end + + wait_for_requests + + fill_in('commit-message', with: 'commit message ide') + + click_button('Commit') + + expect(page).to have_content('file name') + end +end diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb new file mode 100644 index 00000000000..8e53ae15700 --- /dev/null +++ b/spec/features/projects/tree/upload_file_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +feature 'Multi-file editor upload file', :js do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') } + let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') } + + before do + project.add_master(user) + sign_in(user) + + visit project_tree_path(project, :master) + + wait_for_requests + + click_link('Web IDE') + + wait_for_requests + end + + after do + set_cookie('new_repo', 'false') + end + + it 'uploads text file' do + find('.add-to-tree').click + + # make the field visible so capybara can use it + execute_script('document.querySelector("#file-upload").classList.remove("hidden")') + attach_file('file-upload', txt_file) + + find('.add-to-tree').click + + expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt') + expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline)) + end + + it 'uploads image file' do + find('.add-to-tree').click + + # make the field visible so capybara can use it + execute_script('document.querySelector("#file-upload").classList.remove("hidden")') + attach_file('file-upload', img_file) + + find('.add-to-tree').click + + expect(page).to have_selector('.multi-file-tab', text: 'dk.png') + expect(page).not_to have_selector('.monaco-editor') + end +end diff --git a/spec/javascripts/ide/components/changed_file_icon_spec.js b/spec/javascripts/ide/components/changed_file_icon_spec.js new file mode 100644 index 00000000000..8f796b2f7f5 --- /dev/null +++ b/spec/javascripts/ide/components/changed_file_icon_spec.js @@ -0,0 +1,45 @@ +import Vue from 'vue'; +import changedFileIcon from 'ee/ide/components/changed_file_icon.vue'; +import createComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('IDE changed file icon', () => { + let vm; + + beforeEach(() => { + const component = Vue.extend(changedFileIcon); + + vm = createComponent(component, { + file: { + tempFile: false, + }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('changedIcon', () => { + it('equals file-modified when not a temp file', () => { + expect(vm.changedIcon).toBe('file-modified'); + }); + + it('equals file-addition when a temp file', () => { + vm.file.tempFile = true; + + expect(vm.changedIcon).toBe('file-addition'); + }); + }); + + describe('changedIconClass', () => { + it('includes multi-file-modified when not a temp file', () => { + expect(vm.changedIconClass).toContain('multi-file-modified'); + }); + + it('includes multi-file-addition when a temp file', () => { + vm.file.tempFile = true; + + expect(vm.changedIconClass).toContain('multi-file-addition'); + }); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js new file mode 100644 index 00000000000..47a9007a8d0 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import commitActions from 'ee/ide/components/commit_sidebar/actions.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { resetStore } from 'spec/ide/helpers'; + +describe('IDE commit sidebar actions', () => { + let vm; + + beforeEach((done) => { + const Component = Vue.extend(commitActions); + + vm = createComponentWithStore(Component, store); + + vm.$store.state.currentBranchId = 'master'; + + vm.$mount(); + + Vue.nextTick(done); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders 3 groups', () => { + expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(3); + }); + + it('renders current branch text', () => { + expect(vm.$el.textContent).toContain('Commit to master branch'); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js new file mode 100644 index 00000000000..f6789bc861f --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js @@ -0,0 +1,28 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import listCollapsed from 'ee/ide/components/commit_sidebar/list_collapsed.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { file } from '../../helpers'; + +describe('Multi-file editor commit sidebar list collapsed', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(listCollapsed); + + vm = createComponentWithStore(Component, store); + + vm.$store.state.changedFiles.push(file('file1'), file('file2')); + vm.$store.state.changedFiles[0].tempFile = true; + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders added & modified files count', () => { + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1'); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js new file mode 100644 index 00000000000..543299950ea --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js @@ -0,0 +1,83 @@ +import Vue from 'vue'; +import listItem from 'ee/ide/components/commit_sidebar/list_item.vue'; +import router from 'ee/ide/ide_router'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { file } from '../../helpers'; + +describe('Multi-file editor commit sidebar list item', () => { + let vm; + let f; + + beforeEach(() => { + const Component = Vue.extend(listItem); + + f = file('test-file'); + + vm = mountComponent(Component, { + file: f, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders file path', () => { + expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path); + }); + + it('calls discardFileChanges when clicking discard button', () => { + spyOn(vm, 'discardFileChanges'); + + vm.$el.querySelector('.multi-file-discard-btn').click(); + + expect(vm.discardFileChanges).toHaveBeenCalled(); + }); + + it('opens a closed file in the editor when clicking the file path', () => { + spyOn(vm, 'openFileInEditor').and.callThrough(); + spyOn(vm, 'updateViewer'); + spyOn(router, 'push'); + + vm.$el.querySelector('.multi-file-commit-list-path').click(); + + expect(vm.openFileInEditor).toHaveBeenCalled(); + expect(router.push).toHaveBeenCalled(); + }); + + it('calls updateViewer with diff when clicking file', () => { + spyOn(vm, 'openFileInEditor').and.callThrough(); + spyOn(vm, 'updateViewer'); + spyOn(router, 'push'); + + vm.$el.querySelector('.multi-file-commit-list-path').click(); + + expect(vm.updateViewer).toHaveBeenCalledWith('diff'); + }); + + describe('computed', () => { + describe('iconName', () => { + it('returns modified when not a tempFile', () => { + expect(vm.iconName).toBe('file-modified'); + }); + + it('returns addition when not a tempFile', () => { + f.tempFile = true; + + expect(vm.iconName).toBe('file-addition'); + }); + }); + + describe('iconClass', () => { + it('returns modified when not a tempFile', () => { + expect(vm.iconClass).toContain('multi-file-modified'); + }); + + it('returns addition when not a tempFile', () => { + f.tempFile = true; + + expect(vm.iconClass).toContain('multi-file-addition'); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js new file mode 100644 index 00000000000..f02d055e38c --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js @@ -0,0 +1,53 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import commitSidebarList from 'ee/ide/components/commit_sidebar/list.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { file } from '../../helpers'; + +describe('Multi-file editor commit sidebar list', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(commitSidebarList); + + vm = createComponentWithStore(Component, store, { + title: 'Staged', + fileList: [], + }); + + vm.$store.state.rightPanelCollapsed = false; + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('with a list of files', () => { + beforeEach((done) => { + const f = file('file name'); + f.changed = true; + vm.fileList.push(f); + + Vue.nextTick(done); + }); + + it('renders list', () => { + expect(vm.$el.querySelectorAll('li').length).toBe(1); + }); + }); + + describe('collapsed', () => { + beforeEach((done) => { + vm.$store.state.rightPanelCollapsed = true; + + Vue.nextTick(done); + }); + + it('hides list', () => { + expect(vm.$el.querySelector('.list-unstyled')).toBeNull(); + expect(vm.$el.querySelector('.help-block')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js new file mode 100644 index 00000000000..1058cc28de2 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js @@ -0,0 +1,130 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import radioGroup from 'ee/ide/components/commit_sidebar/radio_group.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { resetStore } from 'spec/ide/helpers'; + +describe('IDE commit sidebar radio group', () => { + let vm; + + beforeEach((done) => { + const Component = Vue.extend(radioGroup); + + store.state.commit.commitAction = '2'; + + vm = createComponentWithStore(Component, store, { + value: '1', + label: 'test', + checked: true, + }); + + vm.$mount(); + + Vue.nextTick(done); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('uses label if present', () => { + expect(vm.$el.textContent).toContain('test'); + }); + + it('uses slot if label is not present', (done) => { + vm.$destroy(); + + vm = new Vue({ + components: { + radioGroup, + }, + store, + template: ` + + Testing slot + + `, + }); + + vm.$mount(); + + Vue.nextTick(() => { + expect(vm.$el.textContent).toContain('Testing slot'); + + done(); + }); + }); + + it('updates store when changing radio button', (done) => { + vm.$el.querySelector('input').dispatchEvent(new Event('change')); + + Vue.nextTick(() => { + expect(store.state.commit.commitAction).toBe('1'); + + done(); + }); + }); + + it('renders helpText tooltip', (done) => { + vm.helpText = 'help text'; + + Vue.nextTick(() => { + const help = vm.$el.querySelector('.help-block'); + + expect(help).not.toBeNull(); + expect(help.getAttribute('data-original-title')).toBe('help text'); + + done(); + }); + }); + + describe('with input', () => { + beforeEach((done) => { + vm.$destroy(); + + const Component = Vue.extend(radioGroup); + + store.state.commit.commitAction = '1'; + + vm = createComponentWithStore(Component, store, { + value: '1', + label: 'test', + checked: true, + showInput: true, + }); + + vm.$mount(); + + Vue.nextTick(done); + }); + + it('renders input box when commitAction matches value', () => { + expect(vm.$el.querySelector('.form-control')).not.toBeNull(); + }); + + it('hides input when commitAction doesnt match value', (done) => { + store.state.commit.commitAction = '2'; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.form-control')).toBeNull(); + done(); + }); + }); + + it('updates branch name in store on input', (done) => { + const input = vm.$el.querySelector('.form-control'); + input.value = 'testing-123'; + input.dispatchEvent(new Event('input')); + + Vue.nextTick(() => { + expect(store.state.commit.newBranchName).toBe('testing-123'); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_context_bar_spec.js b/spec/javascripts/ide/components/ide_context_bar_spec.js new file mode 100644 index 00000000000..9fa2e947db2 --- /dev/null +++ b/spec/javascripts/ide/components/ide_context_bar_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import ideContextBar from 'ee/ide/components/ide_context_bar.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; + +describe('Multi-file editor right context bar', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(ideContextBar); + + vm = createComponentWithStore(Component, store, { + noChangesStateSvgPath: 'svg', + committedStateSvgPath: 'svg', + }); + + vm.$store.state.rightPanelCollapsed = false; + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('collapsed', () => { + beforeEach((done) => { + vm.$store.state.rightPanelCollapsed = true; + + Vue.nextTick(done); + }); + + it('adds collapsed class', () => { + expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_external_links_spec.js b/spec/javascripts/ide/components/ide_external_links_spec.js new file mode 100644 index 00000000000..b8da6747653 --- /dev/null +++ b/spec/javascripts/ide/components/ide_external_links_spec.js @@ -0,0 +1,43 @@ +import Vue from 'vue'; +import ideExternalLinks from 'ee/ide/components/ide_external_links.vue'; +import createComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('ide external links component', () => { + let vm; + let fakeReferrer; + let Component; + + const fakeProjectUrl = '/project/'; + + beforeEach(() => { + Component = Vue.extend(ideExternalLinks); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('goBackUrl', () => { + it('renders the Go Back link with the referrer when present', () => { + fakeReferrer = '/example/README.md'; + spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer); + + vm = createComponent(Component, { + projectUrl: fakeProjectUrl, + }).$mount(); + + expect(vm.goBackUrl).toEqual(fakeReferrer); + }); + + it('renders the Go Back link with the project url when referrer is not present', () => { + fakeReferrer = ''; + spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer); + + vm = createComponent(Component, { + projectUrl: fakeProjectUrl, + }).$mount(); + + expect(vm.goBackUrl).toEqual(fakeProjectUrl); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_repo_tree_spec.js b/spec/javascripts/ide/components/ide_repo_tree_spec.js new file mode 100644 index 00000000000..e7188490f64 --- /dev/null +++ b/spec/javascripts/ide/components/ide_repo_tree_spec.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import ideRepoTree from 'ee/ide/components/ide_repo_tree.vue'; +import createComponent from '../../helpers/vue_mount_component_helper'; +import { file } from '../helpers'; + +describe('IdeRepoTree', () => { + let vm; + let tree; + + beforeEach(() => { + const IdeRepoTree = Vue.extend(ideRepoTree); + + tree = { + tree: [file()], + loading: false, + }; + + vm = createComponent(IdeRepoTree, { + tree, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a sidebar', () => { + expect(vm.$el.querySelector('.loading-file')).toBeNull(); + expect(vm.$el.querySelector('.file')).not.toBeNull(); + }); + + it('renders 3 loading files if tree is loading', (done) => { + tree.loading = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toEqual(3); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_side_bar_spec.js b/spec/javascripts/ide/components/ide_side_bar_spec.js new file mode 100644 index 00000000000..74afca280d1 --- /dev/null +++ b/spec/javascripts/ide/components/ide_side_bar_spec.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import ideSidebar from 'ee/ide/components/ide_side_bar.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { resetStore } from '../helpers'; + +describe('IdeSidebar', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(ideSidebar); + + vm = createComponentWithStore(Component, store).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders a sidebar', () => { + expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull(); + }); + + it('renders loading icon component', (done) => { + vm.$store.state.loading = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull(); + expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js new file mode 100644 index 00000000000..7f8dcd9049f --- /dev/null +++ b/spec/javascripts/ide/components/ide_spec.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import ide from 'ee/ide/components/ide.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { file, resetStore } from '../helpers'; + +describe('ide component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(ide); + + vm = createComponentWithStore(Component, store, { + emptyStateSvgPath: 'svg', + noChangesStateSvgPath: 'svg', + committedStateSvgPath: 'svg', + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('does not render panel right when no files open', () => { + expect(vm.$el.querySelector('.panel-right')).toBeNull(); + }); + + it('renders panel right when files are open', (done) => { + vm.$store.state.trees['abcproject/mybranch'] = { + tree: [file()], + }; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.panel-right')).toBeNull(); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js new file mode 100644 index 00000000000..cba27f94833 --- /dev/null +++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js @@ -0,0 +1,84 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import newDropdown from 'ee/ide/components/new_dropdown/index.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { resetStore } from '../../helpers'; + +describe('new dropdown component', () => { + let vm; + + beforeEach(() => { + const component = Vue.extend(newDropdown); + + vm = createComponentWithStore(component, store, { + branch: 'master', + path: '', + }); + + vm.$store.state.currentProjectId = 'abcproject'; + vm.$store.state.path = ''; + vm.$store.state.trees['abcproject/mybranch'] = { + tree: [], + }; + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders new file, upload and new directory links', () => { + expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file'); + expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe( + 'Upload file', + ); + expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe( + 'New directory', + ); + }); + + describe('createNewItem', () => { + it('sets modalType to blob when new file is clicked', () => { + vm.$el.querySelectorAll('a')[0].click(); + + expect(vm.modalType).toBe('blob'); + }); + + it('sets modalType to tree when new directory is clicked', () => { + vm.$el.querySelectorAll('a')[2].click(); + + expect(vm.modalType).toBe('tree'); + }); + + it('opens modal when link is clicked', done => { + vm.$el.querySelectorAll('a')[0].click(); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.modal')).not.toBeNull(); + + done(); + }); + }); + }); + + describe('hideModal', () => { + beforeAll(done => { + vm.openModal = true; + Vue.nextTick(done); + }); + + it('closes modal after toggling', done => { + vm.hideModal(); + + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.modal')).toBeNull(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js new file mode 100644 index 00000000000..1a9c96c64da --- /dev/null +++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js @@ -0,0 +1,72 @@ +import Vue from 'vue'; +import modal from 'ee/ide/components/new_dropdown/modal.vue'; +import createComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('new file modal component', () => { + const Component = Vue.extend(modal); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + ['tree', 'blob'].forEach((type) => { + describe(type, () => { + beforeEach(() => { + vm = createComponent(Component, { + type, + branchId: 'master', + path: '', + }); + + vm.entryName = 'testing'; + }); + + it(`sets modal title as ${type}`, () => { + const title = type === 'tree' ? 'directory' : 'file'; + + expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`); + }); + + it(`sets button label as ${type}`, () => { + const title = type === 'tree' ? 'directory' : 'file'; + + expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`); + }); + + it(`sets form label as ${type}`, () => { + const title = type === 'tree' ? 'Directory' : 'File'; + + expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(`${title} name`); + }); + + describe('createEntryInStore', () => { + it('$emits create', () => { + spyOn(vm, '$emit'); + + vm.createEntryInStore(); + + expect(vm.$emit).toHaveBeenCalledWith('create', { + branchId: 'master', + name: 'testing', + type, + }); + }); + }); + }); + }); + + it('focuses field on mount', () => { + document.body.innerHTML += '
'; + + vm = createComponent(Component, { + type: 'tree', + branchId: 'master', + path: '', + }, '.js-test'); + + expect(document.activeElement).toBe(vm.$refs.fieldName); + + vm.$el.remove(); + }); +}); diff --git a/spec/javascripts/ide/components/new_dropdown/upload_spec.js b/spec/javascripts/ide/components/new_dropdown/upload_spec.js new file mode 100644 index 00000000000..766e8b72360 --- /dev/null +++ b/spec/javascripts/ide/components/new_dropdown/upload_spec.js @@ -0,0 +1,87 @@ +import Vue from 'vue'; +import upload from 'ee/ide/components/new_dropdown/upload.vue'; +import createComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('new dropdown upload', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(upload); + + vm = createComponent(Component, { + branchId: 'master', + path: '', + }); + + vm.entryName = 'testing'; + + spyOn(vm, '$emit'); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('readFile', () => { + beforeEach(() => { + spyOn(FileReader.prototype, 'readAsText'); + spyOn(FileReader.prototype, 'readAsDataURL'); + }); + + it('calls readAsText for text files', () => { + const file = { + type: 'text/html', + }; + + vm.readFile(file); + + expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file); + }); + + it('calls readAsDataURL for non-text files', () => { + const file = { + type: 'images/png', + }; + + vm.readFile(file); + + expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file); + }); + }); + + describe('createFile', () => { + const target = { + result: 'content', + }; + const binaryTarget = { + result: 'base64,base64content', + }; + const file = { + name: 'file', + }; + + it('creates new file', () => { + vm.createFile(target, file, true); + + expect(vm.$emit).toHaveBeenCalledWith('create', { + name: file.name, + branchId: 'master', + type: 'blob', + content: target.result, + base64: false, + }); + }); + + it('splits content on base64 if binary', () => { + vm.createFile(binaryTarget, file, false); + + expect(vm.$emit).toHaveBeenCalledWith('create', { + name: file.name, + branchId: 'master', + type: 'blob', + content: binaryTarget.result.split('base64,')[1], + base64: true, + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js new file mode 100644 index 00000000000..8090e3664e0 --- /dev/null +++ b/spec/javascripts/ide/components/repo_commit_section_spec.js @@ -0,0 +1,154 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import service from 'ee/ide/services'; +import repoCommitSection from 'ee/ide/components/repo_commit_section.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; +import { file, resetStore } from '../helpers'; + +describe('RepoCommitSection', () => { + let vm; + + function createComponent() { + const Component = Vue.extend(repoCommitSection); + + vm = createComponentWithStore(Component, store, { + noChangesStateSvgPath: 'svg', + committedStateSvgPath: 'commitsvg', + }); + + vm.$store.state.currentProjectId = 'abcproject'; + vm.$store.state.currentBranchId = 'master'; + vm.$store.state.projects.abcproject = { + web_url: '', + branches: { + master: { + workingReference: '1', + }, + }, + }; + + vm.$store.state.rightPanelCollapsed = false; + vm.$store.state.currentBranch = 'master'; + vm.$store.state.changedFiles = [file('file1'), file('file2')]; + vm.$store.state.changedFiles.forEach(f => Object.assign(f, { + changed: true, + content: 'testing', + })); + + return vm.$mount(); + } + + beforeEach((done) => { + vm = createComponent(); + + spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({ + headers: { + 'page-title': 'test', + }, + json: () => Promise.resolve({ + last_commit_path: 'last_commit_path', + parent_tree_url: 'parent_tree_url', + path: '/', + trees: [{ name: 'tree' }], + blobs: [{ name: 'blob' }], + submodules: [{ name: 'submodule' }], + }), + })); + + Vue.nextTick(done); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + describe('empty Stage', () => { + it('renders no changes text', () => { + resetStore(vm.$store); + const Component = Vue.extend(repoCommitSection); + + vm = createComponentWithStore(Component, store, { + noChangesStateSvgPath: 'nochangessvg', + committedStateSvgPath: 'svg', + }).$mount(); + + expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toContain('No changes'); + expect(vm.$el.querySelector('.js-empty-state img').getAttribute('src')).toBe('nochangessvg'); + }); + }); + + it('renders a commit section', () => { + const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')]; + const submitCommit = vm.$el.querySelector('form .btn'); + + expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull(); + expect(changedFileElements.length).toEqual(2); + + changedFileElements.forEach((changedFile, i) => { + expect(changedFile.textContent.trim()).toContain(vm.$store.state.changedFiles[i].path); + }); + + expect(submitCommit.disabled).toBeTruthy(); + expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull(); + }); + + it('updates commitMessage in store on input', (done) => { + const textarea = vm.$el.querySelector('textarea'); + + textarea.value = 'testing commit message'; + + textarea.dispatchEvent(new Event('input')); + + getSetTimeoutPromise() + .then(() => { + expect(vm.$store.state.commit.commitMessage).toBe('testing commit message'); + }) + .then(done) + .catch(done.fail); + }); + + describe('discard draft button', () => { + it('hidden when commitMessage is empty', () => { + expect(vm.$el.querySelector('.multi-file-commit-form .btn-default')).toBeNull(); + }); + + it('resets commitMessage when clicking discard button', (done) => { + vm.$store.state.commit.commitMessage = 'testing commit message'; + + getSetTimeoutPromise() + .then(() => { + vm.$el.querySelector('.multi-file-commit-form .btn-default').click(); + }) + .then(Vue.nextTick) + .then(() => { + expect(vm.$store.state.commit.commitMessage).not.toBe('testing commit message'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('when submitting', () => { + beforeEach(() => { + spyOn(vm, 'commitChanges'); + }); + + it('calls commitChanges', (done) => { + vm.$store.state.commit.commitMessage = 'testing commit message'; + + getSetTimeoutPromise() + .then(() => { + vm.$el.querySelector('.multi-file-commit-form .btn-success').click(); + }) + .then(Vue.nextTick) + .then(() => { + expect(vm.commitChanges).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js new file mode 100644 index 00000000000..cda88623497 --- /dev/null +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -0,0 +1,137 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import repoEditor from 'ee/ide/components/repo_editor.vue'; +import monacoLoader from 'ee/ide/monaco_loader'; +import Editor from 'ee/ide/lib/editor'; +import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { file, resetStore } from '../helpers'; + +describe('RepoEditor', () => { + let vm; + + beforeEach((done) => { + const f = file(); + const RepoEditor = Vue.extend(repoEditor); + + vm = createComponentWithStore(RepoEditor, store, { + file: f, + }); + + f.active = true; + f.tempFile = true; + f.html = 'testing'; + vm.$store.state.openFiles.push(f); + vm.$store.state.entries[f.path] = f; + vm.monaco = true; + + vm.$mount(); + + monacoLoader(['vs/editor/editor.main'], () => { + setTimeout(done, 0); + }); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + + Editor.editorInstance.modelManager.dispose(); + }); + + it('renders an ide container', (done) => { + Vue.nextTick(() => { + expect(vm.shouldHideEditor).toBeFalsy(); + + done(); + }); + }); + + describe('when open file is binary and not raw', () => { + beforeEach((done) => { + vm.file.binary = true; + + vm.$nextTick(done); + }); + + it('does not render the IDE', () => { + expect(vm.shouldHideEditor).toBeTruthy(); + }); + + it('shows activeFile html', () => { + expect(vm.$el.textContent).toContain('testing'); + }); + }); + + describe('createEditorInstance', () => { + it('calls createInstance when viewer is editor', (done) => { + spyOn(vm.editor, 'createInstance'); + + vm.createEditorInstance(); + + vm.$nextTick(() => { + expect(vm.editor.createInstance).toHaveBeenCalled(); + + done(); + }); + }); + + it('calls createDiffInstance when viewer is diff', (done) => { + vm.$store.state.viewer = 'diff'; + + spyOn(vm.editor, 'createDiffInstance'); + + vm.createEditorInstance(); + + vm.$nextTick(() => { + expect(vm.editor.createDiffInstance).toHaveBeenCalled(); + + done(); + }); + }); + }); + + describe('setupEditor', () => { + it('creates new model', () => { + spyOn(vm.editor, 'createModel').and.callThrough(); + + Editor.editorInstance.modelManager.dispose(); + + vm.setupEditor(); + + expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file); + expect(vm.model).not.toBeNull(); + }); + + it('attaches model to editor', () => { + spyOn(vm.editor, 'attachModel').and.callThrough(); + + Editor.editorInstance.modelManager.dispose(); + + vm.setupEditor(); + + expect(vm.editor.attachModel).toHaveBeenCalledWith(vm.model); + }); + + it('adds callback methods', () => { + spyOn(vm.editor, 'onPositionChange').and.callThrough(); + + Editor.editorInstance.modelManager.dispose(); + + vm.setupEditor(); + + expect(vm.editor.onPositionChange).toHaveBeenCalled(); + expect(vm.model.events.size).toBe(1); + }); + + it('updates state when model content changed', (done) => { + vm.model.setValue('testing 123'); + + setTimeout(() => { + expect(vm.file.content).toBe('testing 123'); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/repo_file_buttons_spec.js b/spec/javascripts/ide/components/repo_file_buttons_spec.js new file mode 100644 index 00000000000..13b452b1936 --- /dev/null +++ b/spec/javascripts/ide/components/repo_file_buttons_spec.js @@ -0,0 +1,45 @@ +import Vue from 'vue'; +import repoFileButtons from 'ee/ide/components/repo_file_buttons.vue'; +import createVueComponent from '../../helpers/vue_mount_component_helper'; +import { file } from '../helpers'; + +describe('RepoFileButtons', () => { + const activeFile = file(); + let vm; + + function createComponent() { + const RepoFileButtons = Vue.extend(repoFileButtons); + + activeFile.rawPath = 'test'; + activeFile.blamePath = 'test'; + activeFile.commitsPath = 'test'; + + return createVueComponent(RepoFileButtons, { + file: activeFile, + }); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders Raw, Blame, History, Permalink and Preview toggle', (done) => { + vm = createComponent(); + + vm.$nextTick(() => { + const raw = vm.$el.querySelector('.raw'); + const blame = vm.$el.querySelector('.blame'); + const history = vm.$el.querySelector('.history'); + + expect(raw.href).toMatch(`/${activeFile.rawPath}`); + expect(raw.textContent.trim()).toEqual('Raw'); + expect(blame.href).toMatch(`/${activeFile.blamePath}`); + expect(blame.textContent.trim()).toEqual('Blame'); + expect(history.href).toMatch(`/${activeFile.commitsPath}`); + expect(history.textContent.trim()).toEqual('History'); + expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink'); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/repo_file_spec.js b/spec/javascripts/ide/components/repo_file_spec.js new file mode 100644 index 00000000000..3bd871544ea --- /dev/null +++ b/spec/javascripts/ide/components/repo_file_spec.js @@ -0,0 +1,80 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import repoFile from 'ee/ide/components/repo_file.vue'; +import router from 'ee/ide/ide_router'; +import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { file } from '../helpers'; + +describe('RepoFile', () => { + let vm; + + function createComponent(propsData) { + const RepoFile = Vue.extend(repoFile); + + vm = createComponentWithStore(RepoFile, store, propsData); + + vm.$mount(); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders link, icon and name', () => { + createComponent({ + file: file('t4'), + level: 0, + }); + + const name = vm.$el.querySelector('.ide-file-name'); + + expect(name.href).toMatch(''); + expect(name.textContent.trim()).toEqual(vm.file.name); + }); + + it('fires clickFile when the link is clicked', done => { + spyOn(router, 'push'); + createComponent({ + file: file('t3'), + level: 0, + }); + + vm.$el.querySelector('.file-name').click(); + + setTimeout(() => { + expect(router.push).toHaveBeenCalledWith(`/project${vm.file.url}`); + + done(); + }); + }); + + describe('locked file', () => { + let f; + + beforeEach(() => { + f = file('locked file'); + f.file_lock = { + user: { + name: 'testuser', + updated_at: new Date(), + }, + }; + + createComponent({ + file: f, + level: 0, + }); + }); + + it('renders lock icon', () => { + expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull(); + }); + + it('renders a tooltip', () => { + expect( + vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset + .originalTitle, + ).toContain('Locked by testuser'); + }); + }); +}); diff --git a/spec/javascripts/ide/components/repo_loading_file_spec.js b/spec/javascripts/ide/components/repo_loading_file_spec.js new file mode 100644 index 00000000000..dd267654289 --- /dev/null +++ b/spec/javascripts/ide/components/repo_loading_file_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import repoLoadingFile from 'ee/ide/components/repo_loading_file.vue'; +import { resetStore } from '../helpers'; + +describe('RepoLoadingFile', () => { + let vm; + + function createComponent() { + const RepoLoadingFile = Vue.extend(repoLoadingFile); + + return new RepoLoadingFile({ + store, + }).$mount(); + } + + function assertLines(lines) { + lines.forEach((line, n) => { + const index = n + 1; + expect(line.classList.contains(`skeleton-line-${index}`)).toBeTruthy(); + }); + } + + function assertColumns(columns) { + columns.forEach((column) => { + const container = column.querySelector('.animation-container'); + const lines = [...container.querySelectorAll(':scope > div')]; + + expect(container).toBeTruthy(); + expect(lines.length).toEqual(6); + assertLines(lines); + }); + } + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders 3 columns of animated LoC', () => { + vm = createComponent(); + const columns = [...vm.$el.querySelectorAll('td')]; + + expect(columns.length).toEqual(3); + assertColumns(columns); + }); + + it('renders 1 column of animated LoC if isMini', (done) => { + vm = createComponent(); + vm.$store.state.leftPanelCollapsed = true; + vm.$store.state.openFiles.push('test'); + + vm.$nextTick(() => { + const columns = [...vm.$el.querySelectorAll('td')]; + + expect(columns.length).toEqual(1); + assertColumns(columns); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js new file mode 100644 index 00000000000..c3246cd1f1f --- /dev/null +++ b/spec/javascripts/ide/components/repo_tab_spec.js @@ -0,0 +1,163 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import repoTab from 'ee/ide/components/repo_tab.vue'; +import router from 'ee/ide/ide_router'; +import { file, resetStore } from '../helpers'; + +describe('RepoTab', () => { + let vm; + + function createComponent(propsData) { + const RepoTab = Vue.extend(repoTab); + + return new RepoTab({ + store, + propsData, + }).$mount(); + } + + beforeEach(() => { + spyOn(router, 'push'); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders a close link and a name link', () => { + vm = createComponent({ + tab: file(), + }); + vm.$store.state.openFiles.push(vm.tab); + const close = vm.$el.querySelector('.multi-file-tab-close'); + const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`); + + expect(close.innerHTML).toContain('#close'); + expect(name.textContent.trim()).toEqual(vm.tab.name); + }); + + it('fires clickFile when the link is clicked', () => { + vm = createComponent({ + tab: file(), + }); + + spyOn(vm, 'clickFile'); + + vm.$el.click(); + + expect(vm.clickFile).toHaveBeenCalledWith(vm.tab); + }); + + it('calls closeFile when clicking close button', () => { + vm = createComponent({ + tab: file(), + }); + + spyOn(vm, 'closeFile'); + + vm.$el.querySelector('.multi-file-tab-close').click(); + + expect(vm.closeFile).toHaveBeenCalledWith(vm.tab.path); + }); + + it('changes icon on hover', (done) => { + const tab = file(); + tab.changed = true; + vm = createComponent({ + tab, + }); + + vm.$el.dispatchEvent(new Event('mouseover')); + + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.multi-file-modified')).toBeNull(); + + vm.$el.dispatchEvent(new Event('mouseout')); + }) + .then(Vue.nextTick) + .then(() => { + expect(vm.$el.querySelector('.multi-file-modified')).not.toBeNull(); + + done(); + }) + .catch(done.fail); + }); + + describe('locked file', () => { + let f; + + beforeEach(() => { + f = file('locked file'); + f.file_lock = { + user: { + name: 'testuser', + updated_at: new Date(), + }, + }; + + vm = createComponent({ + tab: f, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders lock icon', () => { + expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull(); + }); + + it('renders a tooltip', () => { + expect(vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle).toContain('Locked by testuser'); + }); + }); + + describe('methods', () => { + describe('closeTab', () => { + it('closes tab if file has changed', (done) => { + const tab = file(); + tab.changed = true; + tab.opened = true; + vm = createComponent({ + tab, + }); + vm.$store.state.openFiles.push(tab); + vm.$store.state.changedFiles.push(tab); + vm.$store.state.entries[tab.path] = tab; + vm.$store.dispatch('setFileActive', tab.path); + + vm.$el.querySelector('.multi-file-tab-close').click(); + + vm.$nextTick(() => { + expect(tab.opened).toBeFalsy(); + expect(vm.$store.state.changedFiles.length).toBe(1); + + done(); + }); + }); + + it('closes tab when clicking close btn', (done) => { + const tab = file('lose'); + tab.opened = true; + vm = createComponent({ + tab, + }); + vm.$store.state.openFiles.push(tab); + vm.$store.state.entries[tab.path] = tab; + vm.$store.dispatch('setFileActive', tab.path); + + vm.$el.querySelector('.multi-file-tab-close').click(); + + vm.$nextTick(() => { + expect(tab.opened).toBeFalsy(); + + done(); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js new file mode 100644 index 00000000000..40834f230a8 --- /dev/null +++ b/spec/javascripts/ide/components/repo_tabs_spec.js @@ -0,0 +1,81 @@ +import Vue from 'vue'; +import repoTabs from 'ee/ide/components/repo_tabs.vue'; +import createComponent from '../../helpers/vue_mount_component_helper'; +import { file } from '../helpers'; + +describe('RepoTabs', () => { + const openedFiles = [file('open1'), file('open2')]; + const RepoTabs = Vue.extend(repoTabs); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a list of tabs', done => { + vm = createComponent(RepoTabs, { + files: openedFiles, + viewer: 'editor', + hasChanges: false, + }); + openedFiles[0].active = true; + + vm.$nextTick(() => { + const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')]; + + expect(tabs.length).toEqual(2); + expect(tabs[0].classList.contains('active')).toEqual(true); + expect(tabs[1].classList.contains('active')).toEqual(false); + + done(); + }); + }); + + describe('updated', () => { + it('sets showShadow as true when scroll width is larger than width', done => { + const el = document.createElement('div'); + el.innerHTML = '
'; + document.body.appendChild(el); + + const style = document.createElement('style'); + style.innerText = ` + .multi-file-tabs { + width: 100px; + } + + .multi-file-tabs .list-unstyled { + display: flex; + overflow-x: auto; + } + `; + document.head.appendChild(style); + + vm = createComponent( + RepoTabs, + { + files: [], + viewer: 'editor', + hasChanges: false, + }, + '#test-app', + ); + + vm + .$nextTick() + .then(() => { + expect(vm.showShadow).toEqual(false); + + vm.files = openedFiles; + }) + .then(vm.$nextTick) + .then(() => { + expect(vm.showShadow).toEqual(true); + + style.remove(); + el.remove(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js new file mode 100644 index 00000000000..67f9eaff44a --- /dev/null +++ b/spec/javascripts/ide/helpers.js @@ -0,0 +1,21 @@ +import { decorateData } from 'ee/ide/stores/utils'; +import state from 'ee/ide/stores/state'; +import commitState from 'ee/ide/stores/modules/commit/state'; + +export const resetStore = (store) => { + const newState = { + ...state(), + commit: commitState(), + }; + store.replaceState(newState); +}; + +export const file = (name = 'name', id = name, type = '') => decorateData({ + id, + type, + icon: 'icon', + url: 'url', + name, + path: name, + lastCommit: {}, +}); diff --git a/spec/javascripts/ide/lib/common/disposable_spec.js b/spec/javascripts/ide/lib/common/disposable_spec.js new file mode 100644 index 00000000000..677986aff91 --- /dev/null +++ b/spec/javascripts/ide/lib/common/disposable_spec.js @@ -0,0 +1,44 @@ +import Disposable from 'ee/ide/lib/common/disposable'; + +describe('Multi-file editor library disposable class', () => { + let instance; + let disposableClass; + + beforeEach(() => { + instance = new Disposable(); + + disposableClass = { + dispose: jasmine.createSpy('dispose'), + }; + }); + + afterEach(() => { + instance.dispose(); + }); + + describe('add', () => { + it('adds disposable classes', () => { + instance.add(disposableClass); + + expect(instance.disposers.size).toBe(1); + }); + }); + + describe('dispose', () => { + beforeEach(() => { + instance.add(disposableClass); + }); + + it('calls dispose on all cached disposers', () => { + instance.dispose(); + + expect(disposableClass.dispose).toHaveBeenCalled(); + }); + + it('clears cached disposers', () => { + instance.dispose(); + + expect(instance.disposers.size).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/ide/lib/common/model_manager_spec.js b/spec/javascripts/ide/lib/common/model_manager_spec.js new file mode 100644 index 00000000000..7a1fab0f74d --- /dev/null +++ b/spec/javascripts/ide/lib/common/model_manager_spec.js @@ -0,0 +1,123 @@ +/* global monaco */ +import eventHub from 'ee/ide/eventhub'; +import monacoLoader from 'ee/ide/monaco_loader'; +import ModelManager from 'ee/ide/lib/common/model_manager'; +import { file } from '../../helpers'; + +describe('Multi-file editor library model manager', () => { + let instance; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + instance = new ModelManager(monaco); + + done(); + }); + }); + + afterEach(() => { + instance.dispose(); + }); + + describe('addModel', () => { + it('caches model', () => { + instance.addModel(file()); + + expect(instance.models.size).toBe(1); + }); + + it('caches model by file path', () => { + instance.addModel(file('path-name')); + + expect(instance.models.keys().next().value).toBe('path-name'); + }); + + it('adds model into disposable', () => { + spyOn(instance.disposable, 'add').and.callThrough(); + + instance.addModel(file()); + + expect(instance.disposable.add).toHaveBeenCalled(); + }); + + it('returns cached model', () => { + spyOn(instance.models, 'get').and.callThrough(); + + instance.addModel(file()); + instance.addModel(file()); + + expect(instance.models.get).toHaveBeenCalled(); + }); + + it('adds eventHub listener', () => { + const f = file(); + spyOn(eventHub, '$on').and.callThrough(); + + instance.addModel(f); + + expect(eventHub.$on).toHaveBeenCalledWith(`editor.update.model.dispose.${f.path}`, jasmine.anything()); + }); + }); + + describe('hasCachedModel', () => { + it('returns false when no models exist', () => { + expect(instance.hasCachedModel('path')).toBeFalsy(); + }); + + it('returns true when model exists', () => { + instance.addModel(file('path-name')); + + expect(instance.hasCachedModel('path-name')).toBeTruthy(); + }); + }); + + describe('getModel', () => { + it('returns cached model', () => { + instance.addModel(file('path-name')); + + expect(instance.getModel('path-name')).not.toBeNull(); + }); + }); + + describe('removeCachedModel', () => { + let f; + + beforeEach(() => { + f = file(); + + instance.addModel(f); + }); + + it('clears cached model', () => { + instance.removeCachedModel(f); + + expect(instance.models.size).toBe(0); + }); + + it('removes eventHub listener', () => { + spyOn(eventHub, '$off').and.callThrough(); + + instance.removeCachedModel(f); + + expect(eventHub.$off).toHaveBeenCalledWith(`editor.update.model.dispose.${f.path}`, jasmine.anything()); + }); + }); + + describe('dispose', () => { + it('clears cached models', () => { + instance.addModel(file()); + + instance.dispose(); + + expect(instance.models.size).toBe(0); + }); + + it('calls disposable dispose', () => { + spyOn(instance.disposable, 'dispose').and.callThrough(); + + instance.dispose(); + + expect(instance.disposable.dispose).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js new file mode 100644 index 00000000000..dd9e4946883 --- /dev/null +++ b/spec/javascripts/ide/lib/common/model_spec.js @@ -0,0 +1,107 @@ +/* global monaco */ +import eventHub from 'ee/ide/eventhub'; +import monacoLoader from 'ee/ide/monaco_loader'; +import Model from 'ee/ide/lib/common/model'; +import { file } from '../../helpers'; + +describe('Multi-file editor library model', () => { + let model; + + beforeEach((done) => { + spyOn(eventHub, '$on').and.callThrough(); + + monacoLoader(['vs/editor/editor.main'], () => { + model = new Model(monaco, file('path')); + + done(); + }); + }); + + afterEach(() => { + model.dispose(); + }); + + it('creates original model & new model', () => { + expect(model.originalModel).not.toBeNull(); + expect(model.model).not.toBeNull(); + }); + + it('adds eventHub listener', () => { + expect(eventHub.$on).toHaveBeenCalledWith(`editor.update.model.dispose.${model.file.path}`, jasmine.anything()); + }); + + describe('path', () => { + it('returns file path', () => { + expect(model.path).toBe('path'); + }); + }); + + describe('getModel', () => { + it('returns model', () => { + expect(model.getModel()).toBe(model.model); + }); + }); + + describe('getOriginalModel', () => { + it('returns original model', () => { + expect(model.getOriginalModel()).toBe(model.originalModel); + }); + }); + + describe('setValue', () => { + it('updates models value', () => { + model.setValue('testing 123'); + + expect(model.getModel().getValue()).toBe('testing 123'); + }); + }); + + describe('onChange', () => { + it('caches event by path', () => { + model.onChange(() => {}); + + expect(model.events.size).toBe(1); + expect(model.events.keys().next().value).toBe('path'); + }); + + it('calls callback on change', (done) => { + const spy = jasmine.createSpy(); + model.onChange(spy); + + model.getModel().setValue('123'); + + setTimeout(() => { + expect(spy).toHaveBeenCalledWith(model, jasmine.anything()); + done(); + }); + }); + }); + + describe('dispose', () => { + it('calls disposable dispose', () => { + spyOn(model.disposable, 'dispose').and.callThrough(); + + model.dispose(); + + expect(model.disposable.dispose).toHaveBeenCalled(); + }); + + it('clears events', () => { + model.onChange(() => {}); + + expect(model.events.size).toBe(1); + + model.dispose(); + + expect(model.events.size).toBe(0); + }); + + it('removes eventHub listener', () => { + spyOn(eventHub, '$off').and.callThrough(); + + model.dispose(); + + expect(eventHub.$off).toHaveBeenCalledWith(`editor.update.model.dispose.${model.file.path}`, jasmine.anything()); + }); + }); +}); diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js new file mode 100644 index 00000000000..63e4282d4df --- /dev/null +++ b/spec/javascripts/ide/lib/decorations/controller_spec.js @@ -0,0 +1,120 @@ +/* global monaco */ +import monacoLoader from 'ee/ide/monaco_loader'; +import editor from 'ee/ide/lib/editor'; +import DecorationsController from 'ee/ide/lib/decorations/controller'; +import Model from 'ee/ide/lib/common/model'; +import { file } from '../../helpers'; + +describe('Multi-file editor library decorations controller', () => { + let editorInstance; + let controller; + let model; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + editorInstance = editor.create(monaco); + editorInstance.createInstance(document.createElement('div')); + + controller = new DecorationsController(editorInstance); + model = new Model(monaco, file('path')); + + done(); + }); + }); + + afterEach(() => { + model.dispose(); + editorInstance.dispose(); + controller.dispose(); + }); + + describe('getAllDecorationsForModel', () => { + it('returns empty array when no decorations exist for model', () => { + const decorations = controller.getAllDecorationsForModel(model); + + expect(decorations).toEqual([]); + }); + + it('returns decorations by model URL', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + const decorations = controller.getAllDecorationsForModel(model); + + expect(decorations[0]).toEqual({ decoration: 'decorationValue' }); + }); + }); + + describe('addDecorations', () => { + it('caches decorations in a new map', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + expect(controller.decorations.size).toBe(1); + }); + + it('does not create new cache model', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]); + + expect(controller.decorations.size).toBe(1); + }); + + it('caches decorations by model URL', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + expect(controller.decorations.size).toBe(1); + expect(controller.decorations.keys().next().value).toBe('path'); + }); + + it('calls decorate method', () => { + spyOn(controller, 'decorate'); + + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + expect(controller.decorate).toHaveBeenCalled(); + }); + }); + + describe('decorate', () => { + it('sets decorations on editor instance', () => { + spyOn(controller.editor.instance, 'deltaDecorations'); + + controller.decorate(model); + + expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []); + }); + + it('caches decorations', () => { + spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]); + + controller.decorate(model); + + expect(controller.editorDecorations.size).toBe(1); + }); + + it('caches decorations by model URL', () => { + spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]); + + controller.decorate(model); + + expect(controller.editorDecorations.keys().next().value).toBe('path'); + }); + }); + + describe('dispose', () => { + it('clears cached decorations', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + controller.dispose(); + + expect(controller.decorations.size).toBe(0); + }); + + it('clears cached editorDecorations', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + controller.dispose(); + + expect(controller.editorDecorations.size).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js new file mode 100644 index 00000000000..90216f8b07e --- /dev/null +++ b/spec/javascripts/ide/lib/diff/controller_spec.js @@ -0,0 +1,176 @@ +/* global monaco */ +import monacoLoader from 'ee/ide/monaco_loader'; +import editor from 'ee/ide/lib/editor'; +import ModelManager from 'ee/ide/lib/common/model_manager'; +import DecorationsController from 'ee/ide/lib/decorations/controller'; +import DirtyDiffController, { getDiffChangeType, getDecorator } from 'ee/ide/lib/diff/controller'; +import { computeDiff } from 'ee/ide/lib/diff/diff'; +import { file } from '../../helpers'; + +describe('Multi-file editor library dirty diff controller', () => { + let editorInstance; + let controller; + let modelManager; + let decorationsController; + let model; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + editorInstance = editor.create(monaco); + editorInstance.createInstance(document.createElement('div')); + + modelManager = new ModelManager(monaco); + decorationsController = new DecorationsController(editorInstance); + + model = modelManager.addModel(file('path')); + + controller = new DirtyDiffController(modelManager, decorationsController); + + done(); + }); + }); + + afterEach(() => { + controller.dispose(); + model.dispose(); + decorationsController.dispose(); + editorInstance.dispose(); + }); + + describe('getDiffChangeType', () => { + ['added', 'removed', 'modified'].forEach((type) => { + it(`returns ${type}`, () => { + const change = { + [type]: true, + }; + + expect(getDiffChangeType(change)).toBe(type); + }); + }); + }); + + describe('getDecorator', () => { + ['added', 'removed', 'modified'].forEach((type) => { + it(`returns with linesDecorationsClassName for ${type}`, () => { + const change = { + [type]: true, + }; + + expect( + getDecorator(change).options.linesDecorationsClassName, + ).toBe(`dirty-diff dirty-diff-${type}`); + }); + + it('returns with line numbers', () => { + const change = { + lineNumber: 1, + endLineNumber: 2, + [type]: true, + }; + + const range = getDecorator(change).range; + + expect(range.startLineNumber).toBe(1); + expect(range.endLineNumber).toBe(2); + expect(range.startColumn).toBe(1); + expect(range.endColumn).toBe(1); + }); + }); + }); + + describe('attachModel', () => { + it('adds change event callback', () => { + spyOn(model, 'onChange'); + + controller.attachModel(model); + + expect(model.onChange).toHaveBeenCalled(); + }); + + it('calls throttledComputeDiff on change', () => { + spyOn(controller, 'throttledComputeDiff'); + + controller.attachModel(model); + + model.getModel().setValue('123'); + + expect(controller.throttledComputeDiff).toHaveBeenCalled(); + }); + }); + + describe('computeDiff', () => { + it('posts to worker', () => { + spyOn(controller.dirtyDiffWorker, 'postMessage'); + + controller.computeDiff(model); + + expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({ + path: model.path, + originalContent: '', + newContent: '', + }); + }); + }); + + describe('reDecorate', () => { + it('calls decorations controller decorate', () => { + spyOn(controller.decorationsController, 'decorate'); + + controller.reDecorate(model); + + expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model); + }); + }); + + describe('decorate', () => { + it('adds decorations into decorations controller', () => { + spyOn(controller.decorationsController, 'addDecorations'); + + controller.decorate({ data: { changes: [], path: 'path' } }); + + expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith(model, 'dirtyDiff', jasmine.anything()); + }); + + it('adds decorations into editor', () => { + const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations'); + + controller.decorate({ data: { changes: computeDiff('123', '1234'), path: 'path' } }); + + expect(spy).toHaveBeenCalledWith([], [{ + range: new monaco.Range( + 1, 1, 1, 1, + ), + options: { + isWholeLine: true, + linesDecorationsClassName: 'dirty-diff dirty-diff-modified', + }, + }]); + }); + }); + + describe('dispose', () => { + it('calls disposable dispose', () => { + spyOn(controller.disposable, 'dispose').and.callThrough(); + + controller.dispose(); + + expect(controller.disposable.dispose).toHaveBeenCalled(); + }); + + it('terminates worker', () => { + spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough(); + + controller.dispose(); + + expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled(); + }); + + it('removes worker event listener', () => { + spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough(); + + controller.dispose(); + + expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith('message', jasmine.anything()); + }); + }); +}); diff --git a/spec/javascripts/ide/lib/diff/diff_spec.js b/spec/javascripts/ide/lib/diff/diff_spec.js new file mode 100644 index 00000000000..3bdd0a77e40 --- /dev/null +++ b/spec/javascripts/ide/lib/diff/diff_spec.js @@ -0,0 +1,80 @@ +import { computeDiff } from 'ee/ide/lib/diff/diff'; + +describe('Multi-file editor library diff calculator', () => { + describe('computeDiff', () => { + it('returns empty array if no changes', () => { + const diff = computeDiff('123', '123'); + + expect(diff).toEqual([]); + }); + + describe('modified', () => { + it('', () => { + const diff = computeDiff('123', '1234')[0]; + + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeTruthy(); + expect(diff.removed).toBeUndefined(); + }); + + it('', () => { + const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0]; + + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeTruthy(); + expect(diff.removed).toBeUndefined(); + expect(diff.lineNumber).toBe(2); + }); + }); + + describe('added', () => { + it('', () => { + const diff = computeDiff('123', '123\n123')[0]; + + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeUndefined(); + expect(diff.removed).toBeUndefined(); + }); + + it('', () => { + const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0]; + + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeUndefined(); + expect(diff.removed).toBeUndefined(); + expect(diff.lineNumber).toBe(3); + }); + }); + + describe('removed', () => { + it('', () => { + const diff = computeDiff('123', '')[0]; + + expect(diff.added).toBeUndefined(); + expect(diff.modified).toBeUndefined(); + expect(diff.removed).toBeTruthy(); + }); + + it('', () => { + const diff = computeDiff('123\n123\n123', '123\n123')[0]; + + expect(diff.added).toBeUndefined(); + expect(diff.modified).toBeTruthy(); + expect(diff.removed).toBeTruthy(); + expect(diff.lineNumber).toBe(2); + }); + }); + + it('includes line number of change', () => { + const diff = computeDiff('123', '')[0]; + + expect(diff.lineNumber).toBe(1); + }); + + it('includes end line number of change', () => { + const diff = computeDiff('123', '')[0]; + + expect(diff.endLineNumber).toBe(1); + }); + }); +}); diff --git a/spec/javascripts/ide/lib/editor_options_spec.js b/spec/javascripts/ide/lib/editor_options_spec.js new file mode 100644 index 00000000000..b974a6befd3 --- /dev/null +++ b/spec/javascripts/ide/lib/editor_options_spec.js @@ -0,0 +1,11 @@ +import editorOptions from 'ee/ide/lib/editor_options'; + +describe('Multi-file editor library editor options', () => { + it('returns an array', () => { + expect(editorOptions).toEqual(jasmine.any(Array)); + }); + + it('contains readOnly option', () => { + expect(editorOptions[0].readOnly).toBeDefined(); + }); +}); diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js new file mode 100644 index 00000000000..76869bbc7ce --- /dev/null +++ b/spec/javascripts/ide/lib/editor_spec.js @@ -0,0 +1,197 @@ +/* global monaco */ +import monacoLoader from 'ee/ide/monaco_loader'; +import editor from 'ee/ide/lib/editor'; +import { file } from '../helpers'; + +describe('Multi-file editor library', () => { + let instance; + let el; + let holder; + + beforeEach(done => { + el = document.createElement('div'); + holder = document.createElement('div'); + el.appendChild(holder); + + document.body.appendChild(el); + + monacoLoader(['vs/editor/editor.main'], () => { + instance = editor.create(monaco); + + done(); + }); + }); + + afterEach(() => { + instance.dispose(); + + el.remove(); + }); + + it('creates instance of editor', () => { + expect(editor.editorInstance).not.toBeNull(); + }); + + it('creates instance returns cached instance', () => { + expect(editor.create(monaco)).toEqual(instance); + }); + + describe('createInstance', () => { + it('creates editor instance', () => { + spyOn(instance.monaco.editor, 'create').and.callThrough(); + + instance.createInstance(holder); + + expect(instance.monaco.editor.create).toHaveBeenCalled(); + }); + + it('creates dirty diff controller', () => { + instance.createInstance(holder); + + expect(instance.dirtyDiffController).not.toBeNull(); + }); + + it('creates model manager', () => { + instance.createInstance(holder); + + expect(instance.modelManager).not.toBeNull(); + }); + }); + + describe('createDiffInstance', () => { + it('creates editor instance', () => { + spyOn(instance.monaco.editor, 'createDiffEditor').and.callThrough(); + + instance.createDiffInstance(holder); + + expect(instance.monaco.editor.createDiffEditor).toHaveBeenCalledWith( + holder, + { + model: null, + contextmenu: true, + minimap: { + enabled: false, + }, + readOnly: true, + scrollBeyondLastLine: false, + }, + ); + }); + }); + + describe('createModel', () => { + it('calls model manager addModel', () => { + spyOn(instance.modelManager, 'addModel'); + + instance.createModel('FILE'); + + expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE'); + }); + }); + + describe('attachModel', () => { + let model; + + beforeEach(() => { + instance.createInstance(document.createElement('div')); + + model = instance.createModel(file()); + }); + + it('sets the current model on the instance', () => { + instance.attachModel(model); + + expect(instance.currentModel).toBe(model); + }); + + it('attaches the model to the current instance', () => { + spyOn(instance.instance, 'setModel'); + + instance.attachModel(model); + + expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel()); + }); + + it('sets original & modified when diff editor', () => { + spyOn(instance.instance, 'getEditorType').and.returnValue( + 'vs.editor.IDiffEditor', + ); + spyOn(instance.instance, 'setModel'); + + instance.attachModel(model); + + expect(instance.instance.setModel).toHaveBeenCalledWith({ + original: model.getOriginalModel(), + modified: model.getModel(), + }); + }); + + it('attaches the model to the dirty diff controller', () => { + spyOn(instance.dirtyDiffController, 'attachModel'); + + instance.attachModel(model); + + expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith( + model, + ); + }); + + it('re-decorates with the dirty diff controller', () => { + spyOn(instance.dirtyDiffController, 'reDecorate'); + + instance.attachModel(model); + + expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith( + model, + ); + }); + }); + + describe('clearEditor', () => { + it('resets the editor model', () => { + instance.createInstance(document.createElement('div')); + + spyOn(instance.instance, 'setModel'); + + instance.clearEditor(); + + expect(instance.instance.setModel).toHaveBeenCalledWith(null); + }); + }); + + describe('dispose', () => { + it('calls disposble dispose method', () => { + spyOn(instance.disposable, 'dispose').and.callThrough(); + + instance.dispose(); + + expect(instance.disposable.dispose).toHaveBeenCalled(); + }); + + it('resets instance', () => { + instance.createInstance(document.createElement('div')); + + expect(instance.instance).not.toBeNull(); + + instance.dispose(); + + expect(instance.instance).toBeNull(); + }); + + it('does not dispose modelManager', () => { + spyOn(instance.modelManager, 'dispose'); + + instance.dispose(); + + expect(instance.modelManager.dispose).not.toHaveBeenCalled(); + }); + + it('does not dispose decorationsController', () => { + spyOn(instance.decorationsController, 'dispose'); + + instance.dispose(); + + expect(instance.decorationsController.dispose).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/ide/monaco_loader_spec.js b/spec/javascripts/ide/monaco_loader_spec.js new file mode 100644 index 00000000000..43bc256e718 --- /dev/null +++ b/spec/javascripts/ide/monaco_loader_spec.js @@ -0,0 +1,13 @@ +import monacoContext from 'monaco-editor/dev/vs/loader'; +import monacoLoader from 'ee/ide/monaco_loader'; + +describe('MonacoLoader', () => { + it('calls require.config and exports require', () => { + expect(monacoContext.require.getConfig()).toEqual(jasmine.objectContaining({ + paths: { + vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase + }, + })); + expect(monacoLoader).toBe(monacoContext.require); + }); +}); diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js new file mode 100644 index 00000000000..55563e29d7f --- /dev/null +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -0,0 +1,421 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import service from 'ee/ide/services'; +import router from 'ee/ide/ide_router'; +import eventHub from 'ee/ide/eventhub'; +import { file, resetStore } from '../../helpers'; + +describe('Multi-file store file actions', () => { + beforeEach(() => { + spyOn(router, 'push'); + }); + + afterEach(() => { + resetStore(store); + }); + + describe('closeFile', () => { + let localFile; + + beforeEach(() => { + localFile = file('testFile'); + localFile.active = true; + localFile.opened = true; + localFile.parentTreeUrl = 'parentTreeUrl'; + + store.state.openFiles.push(localFile); + store.state.entries[localFile.path] = localFile; + }); + + it('closes open files', done => { + store + .dispatch('closeFile', localFile.path) + .then(() => { + expect(localFile.opened).toBeFalsy(); + expect(localFile.active).toBeFalsy(); + expect(store.state.openFiles.length).toBe(0); + + done(); + }) + .catch(done.fail); + }); + + it('closes file even if file has changes', done => { + store.state.changedFiles.push(localFile); + + store + .dispatch('closeFile', localFile.path) + .then(Vue.nextTick) + .then(() => { + expect(store.state.openFiles.length).toBe(0); + expect(store.state.changedFiles.length).toBe(1); + + done(); + }) + .catch(done.fail); + }); + + it('closes file & opens next available file', done => { + const f = { + ...file('newOpenFile'), + url: '/newOpenFile', + }; + + store.state.openFiles.push(f); + store.state.entries[f.path] = f; + + store + .dispatch('closeFile', localFile.path) + .then(Vue.nextTick) + .then(() => { + expect(router.push).toHaveBeenCalledWith(`/project${f.url}`); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('setFileActive', () => { + let localFile; + let scrollToTabSpy; + let oldScrollToTab; + + beforeEach(() => { + scrollToTabSpy = jasmine.createSpy('scrollToTab'); + oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line + store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line + + localFile = file('setThisActive'); + + store.state.entries[localFile.path] = localFile; + }); + + afterEach(() => { + store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line + }); + + it('calls scrollToTab', done => { + store + .dispatch('setFileActive', localFile.path) + .then(() => { + expect(scrollToTabSpy).toHaveBeenCalled(); + + done(); + }) + .catch(done.fail); + }); + + it('sets the file active', done => { + store + .dispatch('setFileActive', localFile.path) + .then(() => { + expect(localFile.active).toBeTruthy(); + + done(); + }) + .catch(done.fail); + }); + + it('returns early if file is already active', done => { + localFile.active = true; + + store + .dispatch('setFileActive', localFile.path) + .then(() => { + expect(scrollToTabSpy).not.toHaveBeenCalled(); + + done(); + }) + .catch(done.fail); + }); + + it('sets current active file to not active', done => { + const f = file('newActive'); + store.state.entries[f.path] = f; + localFile.active = true; + store.state.openFiles.push(localFile); + + store + .dispatch('setFileActive', f.path) + .then(() => { + expect(localFile.active).toBeFalsy(); + + done(); + }) + .catch(done.fail); + }); + + it('resets location.hash for line highlighting', done => { + location.hash = 'test'; + + store + .dispatch('setFileActive', localFile.path) + .then(() => { + expect(location.hash).not.toBe('test'); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('getFileData', () => { + let localFile; + + beforeEach(() => { + spyOn(service, 'getFileData').and.returnValue( + Promise.resolve({ + headers: { + 'page-title': 'testing getFileData', + }, + json: () => + Promise.resolve({ + blame_path: 'blame_path', + commits_path: 'commits_path', + permalink: 'permalink', + raw_path: 'raw_path', + binary: false, + html: '123', + render_error: '', + }), + }), + ); + + localFile = file(`newCreate-${Math.random()}`); + localFile.url = 'getFileDataURL'; + store.state.entries[localFile.path] = localFile; + }); + + it('calls the service', done => { + store + .dispatch('getFileData', localFile) + .then(() => { + expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL'); + + done(); + }) + .catch(done.fail); + }); + + it('sets the file data', done => { + store + .dispatch('getFileData', localFile) + .then(() => { + expect(localFile.blamePath).toBe('blame_path'); + + done(); + }) + .catch(done.fail); + }); + + it('sets document title', done => { + store + .dispatch('getFileData', localFile) + .then(() => { + expect(document.title).toBe('testing getFileData'); + + done(); + }) + .catch(done.fail); + }); + + it('sets the file as active', done => { + store + .dispatch('getFileData', localFile) + .then(() => { + expect(localFile.active).toBeTruthy(); + + done(); + }) + .catch(done.fail); + }); + + it('adds the file to open files', done => { + store + .dispatch('getFileData', localFile) + .then(() => { + expect(store.state.openFiles.length).toBe(1); + expect(store.state.openFiles[0].name).toBe(localFile.name); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('getRawFileData', () => { + let tmpFile; + + beforeEach(() => { + spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw')); + + tmpFile = file('tmpFile'); + store.state.entries[tmpFile.path] = tmpFile; + }); + + it('calls getRawFileData service method', done => { + store + .dispatch('getRawFileData', tmpFile) + .then(() => { + expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile); + + done(); + }) + .catch(done.fail); + }); + + it('updates file raw data', done => { + store + .dispatch('getRawFileData', tmpFile) + .then(() => { + expect(tmpFile.raw).toBe('raw'); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('changeFileContent', () => { + let tmpFile; + + beforeEach(() => { + tmpFile = file('tmpFile'); + store.state.entries[tmpFile.path] = tmpFile; + }); + + it('updates file content', done => { + store + .dispatch('changeFileContent', { + path: tmpFile.path, + content: 'content', + }) + .then(() => { + expect(tmpFile.content).toBe('content'); + + done(); + }) + .catch(done.fail); + }); + + it('adds file into changedFiles array', done => { + store + .dispatch('changeFileContent', { + path: tmpFile.path, + content: 'content', + }) + .then(() => { + expect(store.state.changedFiles.length).toBe(1); + + done(); + }) + .catch(done.fail); + }); + + it('adds file once into changedFiles array', done => { + store + .dispatch('changeFileContent', { + path: tmpFile.path, + content: 'content', + }) + .then(() => + store.dispatch('changeFileContent', { + path: tmpFile.path, + content: 'content 123', + }), + ) + .then(() => { + expect(store.state.changedFiles.length).toBe(1); + + done(); + }) + .catch(done.fail); + }); + + it('removes file from changedFiles array if not changed', done => { + store + .dispatch('changeFileContent', { + path: tmpFile.path, + content: 'content', + }) + .then(() => + store.dispatch('changeFileContent', { + path: tmpFile.path, + content: '', + }), + ) + .then(() => { + expect(store.state.changedFiles.length).toBe(0); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('discardFileChanges', () => { + let tmpFile; + + beforeEach(() => { + spyOn(eventHub, '$on'); + + tmpFile = file(); + tmpFile.content = 'testing'; + + store.state.changedFiles.push(tmpFile); + store.state.entries[tmpFile.path] = tmpFile; + }); + + it('resets file content', done => { + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(tmpFile.content).not.toBe('testing'); + + done(); + }) + .catch(done.fail); + }); + + it('removes file from changedFiles array', done => { + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(store.state.changedFiles.length).toBe(0); + + done(); + }) + .catch(done.fail); + }); + + it('closes temp file', done => { + tmpFile.tempFile = true; + tmpFile.opened = true; + + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(tmpFile.opened).toBeFalsy(); + + done(); + }) + .catch(done.fail); + }); + + it('does not re-open a closed temp file', done => { + tmpFile.tempFile = true; + + expect(tmpFile.opened).toBeFalsy(); + + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(tmpFile.opened).toBeFalsy(); + + done(); + }) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js new file mode 100644 index 00000000000..dba4bc10f9d --- /dev/null +++ b/spec/javascripts/ide/stores/actions/tree_spec.js @@ -0,0 +1,145 @@ +import Vue from 'vue'; +import store from 'ee/ide/stores'; +import service from 'ee/ide/services'; +import router from 'ee/ide/ide_router'; +import { file, resetStore } from '../../helpers'; + +describe('Multi-file store tree actions', () => { + let projectTree; + + const basicCallParameters = { + endpoint: 'rootEndpoint', + projectId: 'abcproject', + branch: 'master', + branchId: 'master', + }; + + beforeEach(() => { + spyOn(router, 'push'); + + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = { + web_url: '', + branches: { + master: { + workingReference: '1', + }, + }, + }; + }); + + afterEach(() => { + resetStore(store); + }); + + describe('getFiles', () => { + beforeEach(() => { + spyOn(service, 'getFiles').and.returnValue(Promise.resolve({ + json: () => Promise.resolve([ + 'file.txt', + 'folder/fileinfolder.js', + 'folder/subfolder/fileinsubfolder.js', + ]), + })); + }); + + it('calls service getFiles', (done) => { + store.dispatch('getFiles', basicCallParameters) + .then(() => { + expect(service.getFiles).toHaveBeenCalledWith('', 'master'); + + done(); + }).catch(done.fail); + }); + + it('adds data into tree', (done) => { + store.dispatch('getFiles', basicCallParameters) + .then(() => { + projectTree = store.state.trees['abcproject/master']; + expect(projectTree.tree.length).toBe(2); + expect(projectTree.tree[0].type).toBe('tree'); + expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js'); + expect(projectTree.tree[1].type).toBe('blob'); + expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob'); + expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js'); + + done(); + }).catch(done.fail); + }); + }); + + describe('toggleTreeOpen', () => { + let tree; + + beforeEach(() => { + tree = file('testing', '1', 'tree'); + store.state.entries[tree.path] = tree; + }); + + it('toggles the tree open', (done) => { + store.dispatch('toggleTreeOpen', tree.path).then(() => { + expect(tree.opened).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + }); + + describe('getLastCommitData', () => { + beforeEach(() => { + spyOn(service, 'getTreeLastCommit').and.returnValue(Promise.resolve({ + headers: { + 'more-logs-url': null, + }, + json: () => Promise.resolve([{ + type: 'tree', + file_name: 'testing', + commit: { + message: 'commit message', + authored_date: '123', + }, + }]), + })); + + store.state.trees['abcproject/mybranch'] = { + tree: [], + }; + + projectTree = store.state.trees['abcproject/mybranch']; + projectTree.tree.push(file('testing', '1', 'tree')); + projectTree.lastCommitPath = 'lastcommitpath'; + }); + + it('calls service with lastCommitPath', (done) => { + store.dispatch('getLastCommitData', projectTree) + .then(() => { + expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath'); + + done(); + }).catch(done.fail); + }); + + it('updates trees last commit data', (done) => { + store.dispatch('getLastCommitData', projectTree) + .then(Vue.nextTick) + .then(() => { + expect(projectTree.tree[0].lastCommit.message).toBe('commit message'); + + done(); + }).catch(done.fail); + }); + + it('does not update entry if not found', (done) => { + projectTree.tree[0].name = 'a'; + + store.dispatch('getLastCommitData', projectTree) + .then(Vue.nextTick) + .then(() => { + expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message'); + + done(); + }).catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js new file mode 100644 index 00000000000..0da1226c7aa --- /dev/null +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -0,0 +1,306 @@ +import * as urlUtils from '~/lib/utils/url_utility'; +import store from 'ee/ide/stores'; +import router from 'ee/ide/ide_router'; +import { resetStore, file } from '../helpers'; + +describe('Multi-file store actions', () => { + beforeEach(() => { + spyOn(router, 'push'); + }); + + afterEach(() => { + resetStore(store); + }); + + describe('redirectToUrl', () => { + it('calls visitUrl', done => { + spyOn(urlUtils, 'visitUrl'); + + store + .dispatch('redirectToUrl', 'test') + .then(() => { + expect(urlUtils.visitUrl).toHaveBeenCalledWith('test'); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('setInitialData', () => { + it('commits initial data', done => { + store + .dispatch('setInitialData', { canCommit: true }) + .then(() => { + expect(store.state.canCommit).toBeTruthy(); + done(); + }) + .catch(done.fail); + }); + }); + + describe('discardAllChanges', () => { + beforeEach(() => { + const f = file('discardAll'); + f.changed = true; + + store.state.openFiles.push(f); + store.state.changedFiles.push(f); + store.state.entries[f.path] = f; + }); + + it('discards changes in file', done => { + store + .dispatch('discardAllChanges') + .then(() => { + expect(store.state.openFiles.changed).toBeFalsy(); + }) + .then(done) + .catch(done.fail); + }); + + it('removes all files from changedFiles state', done => { + store + .dispatch('discardAllChanges') + .then(() => { + expect(store.state.changedFiles.length).toBe(0); + expect(store.state.openFiles.length).toBe(1); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('closeAllFiles', () => { + beforeEach(() => { + const f = file('closeAll'); + store.state.openFiles.push(f); + store.state.openFiles[0].opened = true; + store.state.entries[f.path] = f; + }); + + it('closes all open files', done => { + store + .dispatch('closeAllFiles') + .then(() => { + expect(store.state.openFiles.length).toBe(0); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('createTempEntry', () => { + beforeEach(() => { + document.body.innerHTML += '
'; + + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'mybranch'; + + store.state.trees['abcproject/mybranch'] = { + tree: [], + }; + store.state.projects.abcproject = { + web_url: '', + }; + }); + + afterEach(() => { + document.querySelector('.flash-container').remove(); + }); + + describe('tree', () => { + it('creates temp tree', done => { + store + .dispatch('createTempEntry', { + branchId: store.state.currentBranchId, + name: 'test', + type: 'tree', + }) + .then(() => { + const entry = store.state.entries.test; + + expect(entry).not.toBeNull(); + expect(entry.type).toBe('tree'); + + done(); + }) + .catch(done.fail); + }); + + it('creates new folder inside another tree', done => { + const tree = { + type: 'tree', + name: 'testing', + path: 'testing', + tree: [], + }; + + store.state.entries[tree.path] = tree; + + store + .dispatch('createTempEntry', { + branchId: store.state.currentBranchId, + name: 'testing/test', + type: 'tree', + }) + .then(() => { + expect(tree.tree[0].tempFile).toBeTruthy(); + expect(tree.tree[0].name).toBe('test'); + expect(tree.tree[0].type).toBe('tree'); + + done(); + }) + .catch(done.fail); + }); + + it('does not create new tree if already exists', done => { + const tree = { + type: 'tree', + path: 'testing', + tempFile: false, + tree: [], + }; + + store.state.entries[tree.path] = tree; + + store + .dispatch('createTempEntry', { + branchId: store.state.currentBranchId, + name: 'testing', + type: 'tree', + }) + .then(() => { + expect(store.state.entries[tree.path].tempFile).toEqual(false); + expect(document.querySelector('.flash-alert')).not.toBeNull(); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('blob', () => { + it('creates temp file', done => { + store + .dispatch('createTempEntry', { + name: 'test', + branchId: 'mybranch', + type: 'blob', + }) + .then(f => { + expect(f.tempFile).toBeTruthy(); + expect(store.state.trees['abcproject/mybranch'].tree.length).toBe( + 1, + ); + + done(); + }) + .catch(done.fail); + }); + + it('adds tmp file to open files', done => { + store + .dispatch('createTempEntry', { + name: 'test', + branchId: 'mybranch', + type: 'blob', + }) + .then(f => { + expect(store.state.openFiles.length).toBe(1); + expect(store.state.openFiles[0].name).toBe(f.name); + + done(); + }) + .catch(done.fail); + }); + + it('adds tmp file to changed files', done => { + store + .dispatch('createTempEntry', { + name: 'test', + branchId: 'mybranch', + type: 'blob', + }) + .then(f => { + expect(store.state.changedFiles.length).toBe(1); + expect(store.state.changedFiles[0].name).toBe(f.name); + + done(); + }) + .catch(done.fail); + }); + + it('sets tmp file as active', done => { + store + .dispatch('createTempEntry', { + name: 'test', + branchId: 'mybranch', + type: 'blob', + }) + .then(f => { + expect(f.active).toBeTruthy(); + + done(); + }) + .catch(done.fail); + }); + + it('creates flash message if file already exists', done => { + const f = file('test', '1', 'blob'); + store.state.trees['abcproject/mybranch'].tree = [f]; + store.state.entries[f.path] = f; + + store + .dispatch('createTempEntry', { + name: 'test', + branchId: 'mybranch', + type: 'blob', + }) + .then(() => { + expect(document.querySelector('.flash-alert')).not.toBeNull(); + + done(); + }) + .catch(done.fail); + }); + }); + }); + + describe('popHistoryState', () => {}); + + describe('scrollToTab', () => { + it('focuses the current active element', done => { + document.body.innerHTML += + '
'; + const el = document.querySelector('.repo-tab'); + spyOn(el, 'focus'); + + store + .dispatch('scrollToTab') + .then(() => { + setTimeout(() => { + expect(el.focus).toHaveBeenCalled(); + + document.getElementById('tabs').remove(); + + done(); + }); + }) + .catch(done.fail); + }); + }); + + describe('updateViewer', () => { + it('updates viewer state', done => { + store + .dispatch('updateViewer', 'diff') + .then(() => { + expect(store.state.viewer).toBe('diff'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js new file mode 100644 index 00000000000..2fb69339915 --- /dev/null +++ b/spec/javascripts/ide/stores/getters_spec.js @@ -0,0 +1,55 @@ +import * as getters from 'ee/ide/stores/getters'; +import state from 'ee/ide/stores/state'; +import { file } from '../helpers'; + +describe('Multi-file store getters', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('activeFile', () => { + it('returns the current active file', () => { + localState.openFiles.push(file()); + localState.openFiles.push(file('active')); + localState.openFiles[1].active = true; + + expect(getters.activeFile(localState).name).toBe('active'); + }); + + it('returns undefined if no active files are found', () => { + localState.openFiles.push(file()); + localState.openFiles.push(file('active')); + + expect(getters.activeFile(localState)).toBeNull(); + }); + }); + + describe('modifiedFiles', () => { + it('returns a list of modified files', () => { + localState.openFiles.push(file()); + localState.changedFiles.push(file('changed')); + localState.changedFiles[0].changed = true; + + const modifiedFiles = getters.modifiedFiles(localState); + + expect(modifiedFiles.length).toBe(1); + expect(modifiedFiles[0].name).toBe('changed'); + }); + }); + + describe('addedFiles', () => { + it('returns a list of added files', () => { + localState.openFiles.push(file()); + localState.changedFiles.push(file('added')); + localState.changedFiles[0].changed = true; + localState.changedFiles[0].tempFile = true; + + const modifiedFiles = getters.addedFiles(localState); + + expect(modifiedFiles.length).toBe(1); + expect(modifiedFiles[0].name).toBe('added'); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js new file mode 100644 index 00000000000..0aef29f77e3 --- /dev/null +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -0,0 +1,450 @@ +import store from 'ee/ide/stores'; +import service from 'ee/ide/services'; +import router from 'ee/ide/ide_router'; +import * as urlUtils from '~/lib/utils/url_utility'; +import eventHub from 'ee/ide/eventhub'; +import * as consts from 'ee/ide/stores/modules/commit/constants'; +import { resetStore, file } from 'spec/ide/helpers'; + +describe('IDE commit module actions', () => { + beforeEach(() => { + spyOn(router, 'push'); + }); + + afterEach(() => { + resetStore(store); + }); + + describe('updateCommitMessage', () => { + it('updates store with new commit message', (done) => { + store.dispatch('commit/updateCommitMessage', 'testing') + .then(() => { + expect(store.state.commit.commitMessage).toBe('testing'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('discardDraft', () => { + it('resets commit message to blank', (done) => { + store.state.commit.commitMessage = 'testing'; + + store.dispatch('commit/discardDraft') + .then(() => { + expect(store.state.commit.commitMessage).not.toBe('testing'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('updateCommitAction', () => { + it('updates store with new commit action', (done) => { + store.dispatch('commit/updateCommitAction', '1') + .then(() => { + expect(store.state.commit.commitAction).toBe('1'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('updateBranchName', () => { + it('updates store with new branch name', (done) => { + store.dispatch('commit/updateBranchName', 'branch-name') + .then(() => { + expect(store.state.commit.newBranchName).toBe('branch-name'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('setLastCommitMessage', () => { + beforeEach(() => { + Object.assign(store.state, { + currentProjectId: 'abcproject', + projects: { + abcproject: { + web_url: 'http://testing', + }, + }, + }); + }); + + it('updates commit message with short_id', (done) => { + store.dispatch('commit/setLastCommitMessage', { short_id: '123' }) + .then(() => { + expect(store.state.lastCommitMsg).toContain( + 'Your changes have been committed. Commit 123', + ); + }) + .then(done) + .catch(done.fail); + }); + + it('updates commit message with stats', (done) => { + store.dispatch('commit/setLastCommitMessage', { + short_id: '123', + stats: { + additions: '1', + deletions: '2', + }, + }) + .then(() => { + expect(store.state.lastCommitMsg).toBe('Your changes have been committed. Commit 123 with 1 additions, 2 deletions.'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('checkCommitStatus', () => { + beforeEach(() => { + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = { + branches: { + master: { + workingReference: '1', + }, + }, + }; + }); + + it('calls service', (done) => { + spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ + data: { + commit: { id: '123' }, + }, + })); + + store.dispatch('commit/checkCommitStatus') + .then(() => { + expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master'); + + done(); + }) + .catch(done.fail); + }); + + it('returns true if current ref does not equal returned ID', (done) => { + spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ + data: { + commit: { id: '123' }, + }, + })); + + store.dispatch('commit/checkCommitStatus') + .then((val) => { + expect(val).toBeTruthy(); + + done(); + }) + .catch(done.fail); + }); + + it('returns false if current ref equals returned ID', (done) => { + spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ + data: { + commit: { id: '1' }, + }, + })); + + store.dispatch('commit/checkCommitStatus') + .then((val) => { + expect(val).toBeFalsy(); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('updateFilesAfterCommit', () => { + const data = { + id: '123', + message: 'testing commit message', + committed_date: '123', + committer_name: 'root', + }; + const branch = 'master'; + let f; + + beforeEach(() => { + spyOn(eventHub, '$emit'); + + f = file('changedFile'); + Object.assign(f, { + active: true, + changed: true, + content: 'file content', + }); + + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = { + web_url: 'web_url', + branches: { + master: { + workingReference: '', + }, + }, + }; + store.state.changedFiles.push(f, { + ...file('changedFile2'), + changed: true, + }); + store.state.openFiles = store.state.changedFiles; + + store.state.changedFiles.forEach((changedFile) => { + store.state.entries[changedFile.path] = changedFile; + }); + }); + + it('updates stores working reference', (done) => { + store.dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) + .then(() => { + expect( + store.state.projects.abcproject.branches.master.workingReference, + ).toBe(data.id); + }) + .then(done) + .catch(done.fail); + }); + + it('resets all files changed status', (done) => { + store.dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) + .then(() => { + store.state.openFiles.forEach((entry) => { + expect(entry.changed).toBeFalsy(); + }); + }) + .then(done) + .catch(done.fail); + }); + + it('removes all changed files', (done) => { + store.dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) + .then(() => { + expect(store.state.changedFiles.length).toBe(0); + }) + .then(done) + .catch(done.fail); + }); + + it('sets files commit data', (done) => { + store.dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) + .then(() => { + expect(f.lastCommit.message).toBe(data.message); + }) + .then(done) + .catch(done.fail); + }); + + it('updates raw content for changed file', (done) => { + store.dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) + .then(() => { + expect(f.raw).toBe(f.content); + }) + .then(done) + .catch(done.fail); + }); + + it('emits changed event for file', (done) => { + store.dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) + .then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.content.${f.path}`, f.content); + }) + .then(done) + .catch(done.fail); + }); + + it('pushes route to new branch if commitAction is new branch', (done) => { + store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; + + store.dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) + .then(() => { + expect(router.push).toHaveBeenCalledWith( + `/project/abcproject/blob/master/${f.path}`, + ); + }) + .then(done) + .catch(done.fail); + }); + + it('resets stores commit actions', (done) => { + store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; + + store.dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) + .then(() => { + expect(store.state.commit.commitAction).not.toBe(consts.COMMIT_TO_NEW_BRANCH); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('commitChanges', () => { + beforeEach(() => { + spyOn(urlUtils, 'visitUrl'); + + document.body.innerHTML += '
'; + + store.state.currentProjectId = 'abcproject'; + store.state.currentBranchId = 'master'; + store.state.projects.abcproject = { + web_url: 'webUrl', + branches: { + master: { + workingReference: '1', + }, + }, + }; + store.state.changedFiles.push(file('changed')); + store.state.changedFiles[0].active = true; + store.state.openFiles = store.state.changedFiles; + + store.state.openFiles.forEach((f) => { + store.state.entries[f.path] = f; + }); + + store.state.commit.commitAction = '2'; + store.state.commit.commitMessage = 'testing 123'; + }); + + afterEach(() => { + document.querySelector('.flash-container').remove(); + }); + + describe('success', () => { + beforeEach(() => { + spyOn(service, 'commit').and.returnValue(Promise.resolve({ + data: { + id: '123456', + short_id: '123', + message: 'test message', + committed_date: 'date', + stats: { + additions: '1', + deletions: '2', + }, + }, + })); + }); + + it('calls service', (done) => { + store.dispatch('commit/commitChanges') + .then(() => { + expect(service.commit).toHaveBeenCalledWith('abcproject', { + branch: jasmine.anything(), + commit_message: 'testing 123', + actions: [{ + action: 'update', + file_path: jasmine.anything(), + content: jasmine.anything(), + encoding: jasmine.anything(), + }], + start_branch: 'master', + }); + + done(); + }).catch(done.fail); + }); + + it('pushes router to new route', (done) => { + store.dispatch('commit/commitChanges') + .then(() => { + expect(router.push).toHaveBeenCalledWith( + `/project/${store.state.currentProjectId}/blob/${store.getters['commit/newBranchName']}/changed`, + ); + + done(); + }).catch(done.fail); + }); + + it('sets last Commit Msg', (done) => { + store.dispatch('commit/commitChanges') + .then(() => { + expect(store.state.lastCommitMsg).toBe( + 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.', + ); + + done(); + }).catch(done.fail); + }); + + it('adds commit data to changed files', (done) => { + store.dispatch('commit/commitChanges') + .then(() => { + expect(store.state.openFiles[0].lastCommit.message).toBe('test message'); + + done(); + }).catch(done.fail); + }); + + it('redirects to new merge request page', (done) => { + spyOn(eventHub, '$on'); + + store.state.commit.commitAction = '3'; + + store.dispatch('commit/commitChanges') + .then(() => { + expect(urlUtils.visitUrl).toHaveBeenCalledWith( + `webUrl/merge_requests/new?merge_request[source_branch]=${store.getters['commit/newBranchName']}&merge_request[target_branch]=master`, + ); + + done(); + }).catch(done.fail); + }); + }); + + describe('failed', () => { + beforeEach(() => { + spyOn(service, 'commit').and.returnValue(Promise.resolve({ + data: { + message: 'failed message', + }, + })); + }); + + it('shows failed message', (done) => { + store.dispatch('commit/commitChanges') + .then(() => { + const alert = document.querySelector('.flash-container'); + + expect(alert.textContent.trim()).toBe( + 'failed message', + ); + + done(); + }).catch(done.fail); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js new file mode 100644 index 00000000000..b1467bcf3c7 --- /dev/null +++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js @@ -0,0 +1,114 @@ +import commitState from 'ee/ide/stores/modules/commit/state'; +import * as consts from 'ee/ide/stores/modules/commit/constants'; +import * as getters from 'ee/ide/stores/modules/commit/getters'; + +describe('IDE commit module getters', () => { + let state; + + beforeEach(() => { + state = commitState(); + }); + + describe('discardDraftButtonDisabled', () => { + it('returns true when commitMessage is empty', () => { + expect(getters.discardDraftButtonDisabled(state)).toBeTruthy(); + }); + + it('returns false when commitMessage is not empty & loading is false', () => { + state.commitMessage = 'test'; + state.submitCommitLoading = false; + + expect(getters.discardDraftButtonDisabled(state)).toBeFalsy(); + }); + + it('returns true when commitMessage is not empty & loading is true', () => { + state.commitMessage = 'test'; + state.submitCommitLoading = true; + + expect(getters.discardDraftButtonDisabled(state)).toBeTruthy(); + }); + }); + + describe('commitButtonDisabled', () => { + const localGetters = { + discardDraftButtonDisabled: false, + }; + const rootState = { + changedFiles: ['a'], + }; + + it('returns false when discardDraftButtonDisabled is false & changedFiles is not empty', () => { + expect(getters.commitButtonDisabled(state, localGetters, rootState)).toBeFalsy(); + }); + + it('returns true when discardDraftButtonDisabled is false & changedFiles is empty', () => { + rootState.changedFiles.length = 0; + + expect(getters.commitButtonDisabled(state, localGetters, rootState)).toBeTruthy(); + }); + + it('returns true when discardDraftButtonDisabled is true', () => { + localGetters.discardDraftButtonDisabled = true; + + expect(getters.commitButtonDisabled(state, localGetters, rootState)).toBeTruthy(); + }); + + it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => { + localGetters.discardDraftButtonDisabled = false; + rootState.changedFiles.length = 0; + + expect(getters.commitButtonDisabled(state, localGetters, rootState)).toBeTruthy(); + }); + }); + + describe('newBranchName', () => { + it('includes username, currentBranchId, patch & random number', () => { + gon.current_username = 'username'; + + const branch = getters.newBranchName(state, null, { currentBranchId: 'testing' }); + + expect(branch).toMatch(/username-testing-patch-\d{5}$/); + }); + }); + + describe('branchName', () => { + const rootState = { + currentBranchId: 'master', + }; + const localGetters = { + newBranchName: 'newBranchName', + }; + + beforeEach(() => { + Object.assign(state, { + newBranchName: 'state-newBranchName', + }); + }); + + it('defualts to currentBranchId', () => { + expect(getters.branchName(state, null, rootState)).toBe('master'); + }); + + ['COMMIT_TO_NEW_BRANCH', 'COMMIT_TO_NEW_BRANCH_MR'].forEach((type) => { + describe(type, () => { + beforeEach(() => { + Object.assign(state, { + commitAction: consts[type], + }); + }); + + it('uses newBranchName when not empty', () => { + expect(getters.branchName(state, localGetters, rootState)).toBe('state-newBranchName'); + }); + + it('uses getters newBranchName when state newBranchName is empty', () => { + Object.assign(state, { + newBranchName: '', + }); + + expect(getters.branchName(state, localGetters, rootState)).toBe('newBranchName'); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/commit/mutations_spec.js b/spec/javascripts/ide/stores/modules/commit/mutations_spec.js new file mode 100644 index 00000000000..fa43e3d9d02 --- /dev/null +++ b/spec/javascripts/ide/stores/modules/commit/mutations_spec.js @@ -0,0 +1,42 @@ +import commitState from 'ee/ide/stores/modules/commit/state'; +import mutations from 'ee/ide/stores/modules/commit/mutations'; + +describe('IDE commit module mutations', () => { + let state; + + beforeEach(() => { + state = commitState(); + }); + + describe('UPDATE_COMMIT_MESSAGE', () => { + it('updates commitMessage', () => { + mutations.UPDATE_COMMIT_MESSAGE(state, 'testing'); + + expect(state.commitMessage).toBe('testing'); + }); + }); + + describe('UPDATE_COMMIT_ACTION', () => { + it('updates commitAction', () => { + mutations.UPDATE_COMMIT_ACTION(state, 'testing'); + + expect(state.commitAction).toBe('testing'); + }); + }); + + describe('UPDATE_NEW_BRANCH_NAME', () => { + it('updates newBranchName', () => { + mutations.UPDATE_NEW_BRANCH_NAME(state, 'testing'); + + expect(state.newBranchName).toBe('testing'); + }); + }); + + describe('UPDATE_LOADING', () => { + it('updates submitCommitLoading', () => { + mutations.UPDATE_LOADING(state, true); + + expect(state.submitCommitLoading).toBeTruthy(); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/javascripts/ide/stores/mutations/branch_spec.js new file mode 100644 index 00000000000..1601769144a --- /dev/null +++ b/spec/javascripts/ide/stores/mutations/branch_spec.js @@ -0,0 +1,18 @@ +import mutations from 'ee/ide/stores/mutations/branch'; +import state from 'ee/ide/stores/state'; + +describe('Multi-file store branch mutations', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('SET_CURRENT_BRANCH', () => { + it('sets currentBranch', () => { + mutations.SET_CURRENT_BRANCH(localState, 'master'); + + expect(localState.currentBranchId).toBe('master'); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js new file mode 100644 index 00000000000..944639c3336 --- /dev/null +++ b/spec/javascripts/ide/stores/mutations/file_spec.js @@ -0,0 +1,157 @@ +import mutations from 'ee/ide/stores/mutations/file'; +import state from 'ee/ide/stores/state'; +import { file } from '../../helpers'; + +describe('Multi-file store file mutations', () => { + let localState; + let localFile; + + beforeEach(() => { + localState = state(); + localFile = file(); + + localState.entries[localFile.path] = localFile; + }); + + describe('SET_FILE_ACTIVE', () => { + it('sets the file active', () => { + mutations.SET_FILE_ACTIVE(localState, { + path: localFile.path, + active: true, + }); + + expect(localFile.active).toBeTruthy(); + }); + }); + + describe('TOGGLE_FILE_OPEN', () => { + beforeEach(() => { + mutations.TOGGLE_FILE_OPEN(localState, localFile.path); + }); + + it('adds into opened files', () => { + expect(localFile.opened).toBeTruthy(); + expect(localState.openFiles.length).toBe(1); + }); + + it('removes from opened files', () => { + mutations.TOGGLE_FILE_OPEN(localState, localFile.path); + + expect(localFile.opened).toBeFalsy(); + expect(localState.openFiles.length).toBe(0); + }); + }); + + describe('SET_FILE_DATA', () => { + it('sets extra file data', () => { + mutations.SET_FILE_DATA(localState, { + data: { + blame_path: 'blame', + commits_path: 'commits', + permalink: 'permalink', + raw_path: 'raw', + binary: true, + render_error: 'render_error', + }, + file: localFile, + }); + + expect(localFile.blamePath).toBe('blame'); + expect(localFile.commitsPath).toBe('commits'); + expect(localFile.permalink).toBe('permalink'); + expect(localFile.rawPath).toBe('raw'); + expect(localFile.binary).toBeTruthy(); + expect(localFile.renderError).toBe('render_error'); + }); + }); + + describe('SET_FILE_RAW_DATA', () => { + it('sets raw data', () => { + mutations.SET_FILE_RAW_DATA(localState, { + file: localFile, + raw: 'testing', + }); + + expect(localFile.raw).toBe('testing'); + }); + }); + + describe('UPDATE_FILE_CONTENT', () => { + beforeEach(() => { + localFile.raw = 'test'; + }); + + it('sets content', () => { + mutations.UPDATE_FILE_CONTENT(localState, { + path: localFile.path, + content: 'test', + }); + + expect(localFile.content).toBe('test'); + }); + + it('sets changed if content does not match raw', () => { + mutations.UPDATE_FILE_CONTENT(localState, { + path: localFile.path, + content: 'testing', + }); + + expect(localFile.content).toBe('testing'); + expect(localFile.changed).toBeTruthy(); + }); + + it('sets changed if file is a temp file', () => { + localFile.tempFile = true; + + mutations.UPDATE_FILE_CONTENT(localState, { + path: localFile.path, + content: '', + }); + + expect(localFile.changed).toBeTruthy(); + }); + }); + + describe('DISCARD_FILE_CHANGES', () => { + beforeEach(() => { + localFile.content = 'test'; + localFile.changed = true; + }); + + it('resets content and changed', () => { + mutations.DISCARD_FILE_CHANGES(localState, localFile.path); + + expect(localFile.content).toBe(''); + expect(localFile.changed).toBeFalsy(); + }); + }); + + describe('ADD_FILE_TO_CHANGED', () => { + it('adds file into changed files array', () => { + mutations.ADD_FILE_TO_CHANGED(localState, localFile.path); + + expect(localState.changedFiles.length).toBe(1); + }); + }); + + describe('REMOVE_FILE_FROM_CHANGED', () => { + it('removes files from changed files array', () => { + localState.changedFiles.push(localFile); + + mutations.REMOVE_FILE_FROM_CHANGED(localState, localFile.path); + + expect(localState.changedFiles.length).toBe(0); + }); + }); + + describe('TOGGLE_FILE_CHANGED', () => { + it('updates file changed status', () => { + mutations.TOGGLE_FILE_CHANGED(localState, { + file: localFile, + changed: true, + }); + + expect(localFile.changed).toBeTruthy(); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/mutations/tree_spec.js b/spec/javascripts/ide/stores/mutations/tree_spec.js new file mode 100644 index 00000000000..e321eff8749 --- /dev/null +++ b/spec/javascripts/ide/stores/mutations/tree_spec.js @@ -0,0 +1,67 @@ +import mutations from 'ee/ide/stores/mutations/tree'; +import state from 'ee/ide/stores/state'; +import { file } from '../../helpers'; + +describe('Multi-file store tree mutations', () => { + let localState; + let localTree; + + beforeEach(() => { + localState = state(); + localTree = file(); + + localState.entries[localTree.path] = localTree; + }); + + describe('TOGGLE_TREE_OPEN', () => { + it('toggles tree open', () => { + mutations.TOGGLE_TREE_OPEN(localState, localTree.path); + + expect(localTree.opened).toBeTruthy(); + + mutations.TOGGLE_TREE_OPEN(localState, localTree.path); + + expect(localTree.opened).toBeFalsy(); + }); + }); + + describe('SET_DIRECTORY_DATA', () => { + const data = [{ + name: 'tree', + }, + { + name: 'submodule', + }, + { + name: 'blob', + }]; + + it('adds directory data', () => { + localState.trees['project/master'] = { + tree: [], + }; + + mutations.SET_DIRECTORY_DATA(localState, { + data, + treePath: 'project/master', + }); + + const tree = localState.trees['project/master']; + + expect(tree.tree.length).toBe(3); + expect(tree.tree[0].name).toBe('tree'); + expect(tree.tree[1].name).toBe('submodule'); + expect(tree.tree[2].name).toBe('blob'); + }); + }); + + describe('REMOVE_ALL_CHANGES_FILES', () => { + it('removes all files from changedFiles state', () => { + localState.changedFiles.push(file('REMOVE_ALL_CHANGES_FILES')); + + mutations.REMOVE_ALL_CHANGES_FILES(localState); + + expect(localState.changedFiles.length).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js new file mode 100644 index 00000000000..e0d214010d5 --- /dev/null +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -0,0 +1,79 @@ +import mutations from 'ee/ide/stores/mutations'; +import state from 'ee/ide/stores/state'; +import { file } from '../helpers'; + +describe('Multi-file store mutations', () => { + let localState; + let entry; + + beforeEach(() => { + localState = state(); + entry = file(); + + localState.entries[entry.path] = entry; + }); + + describe('SET_INITIAL_DATA', () => { + it('sets all initial data', () => { + mutations.SET_INITIAL_DATA(localState, { + test: 'test', + }); + + expect(localState.test).toBe('test'); + }); + }); + + describe('TOGGLE_LOADING', () => { + it('toggles loading of entry', () => { + mutations.TOGGLE_LOADING(localState, { entry }); + + expect(entry.loading).toBeTruthy(); + + mutations.TOGGLE_LOADING(localState, { entry }); + + expect(entry.loading).toBeFalsy(); + }); + + it('toggles loading of entry and sets specific value', () => { + mutations.TOGGLE_LOADING(localState, { entry }); + + expect(entry.loading).toBeTruthy(); + + mutations.TOGGLE_LOADING(localState, { entry, forceValue: true }); + + expect(entry.loading).toBeTruthy(); + }); + }); + + describe('SET_LEFT_PANEL_COLLAPSED', () => { + it('sets left panel collapsed', () => { + mutations.SET_LEFT_PANEL_COLLAPSED(localState, true); + + expect(localState.leftPanelCollapsed).toBeTruthy(); + + mutations.SET_LEFT_PANEL_COLLAPSED(localState, false); + + expect(localState.leftPanelCollapsed).toBeFalsy(); + }); + }); + + describe('SET_RIGHT_PANEL_COLLAPSED', () => { + it('sets right panel collapsed', () => { + mutations.SET_RIGHT_PANEL_COLLAPSED(localState, true); + + expect(localState.rightPanelCollapsed).toBeTruthy(); + + mutations.SET_RIGHT_PANEL_COLLAPSED(localState, false); + + expect(localState.rightPanelCollapsed).toBeFalsy(); + }); + }); + + describe('UPDATE_VIEWER', () => { + it('sets viewer state', () => { + mutations.UPDATE_VIEWER(localState, 'diff'); + + expect(localState.viewer).toBe('diff'); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js new file mode 100644 index 00000000000..a473d3a4294 --- /dev/null +++ b/spec/javascripts/ide/stores/utils_spec.js @@ -0,0 +1,60 @@ +import * as utils from 'ee/ide/stores/utils'; + +describe('Multi-file store utils', () => { + describe('setPageTitle', () => { + it('sets the document page title', () => { + utils.setPageTitle('test'); + + expect(document.title).toBe('test'); + }); + }); + + describe('findIndexOfFile', () => { + let localState; + + beforeEach(() => { + localState = [{ + path: '1', + }, { + path: '2', + }]; + }); + + it('finds in the index of an entry by path', () => { + const index = utils.findIndexOfFile(localState, { + path: '2', + }); + + expect(index).toBe(1); + }); + }); + + describe('findEntry', () => { + let localState; + + beforeEach(() => { + localState = { + tree: [{ + type: 'tree', + name: 'test', + }, { + type: 'blob', + name: 'file', + }], + }; + }); + + it('returns an entry found by name', () => { + const foundEntry = utils.findEntry(localState.tree, 'tree', 'test'); + + expect(foundEntry.type).toBe('tree'); + expect(foundEntry.name).toBe('test'); + }); + + it('returns undefined when no entry found', () => { + const foundEntry = utils.findEntry(localState.tree, 'blob', 'test'); + + expect(foundEntry).toBeUndefined(); + }); + }); +}); -- cgit v1.2.1 From af22ddafe4e580850e183993ae276b1f0f565ca6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 20 Mar 2018 14:16:38 +0000 Subject: updated file references in specs --- .../ide/stores/modules/commit/actions.js | 6 +- .../ide/components/changed_file_icon_spec.js | 2 +- .../ide/components/commit_sidebar/actions_spec.js | 6 +- .../commit_sidebar/list_collapsed_spec.js | 4 +- .../components/commit_sidebar/list_item_spec.js | 8 +- .../ide/components/commit_sidebar/list_spec.js | 8 +- .../components/commit_sidebar/radio_group_spec.js | 18 +- .../ide/components/ide_context_bar_spec.js | 6 +- .../ide/components/ide_external_links_spec.js | 2 +- .../ide/components/ide_repo_tree_spec.js | 8 +- .../ide/components/ide_side_bar_spec.js | 18 +- spec/javascripts/ide/components/ide_spec.js | 6 +- .../ide/components/new_dropdown/index_spec.js | 4 +- .../ide/components/new_dropdown/modal_spec.js | 30 +- .../ide/components/new_dropdown/upload_spec.js | 2 +- .../ide/components/repo_commit_section_spec.js | 79 +++-- .../javascripts/ide/components/repo_editor_spec.js | 20 +- .../ide/components/repo_file_buttons_spec.js | 8 +- spec/javascripts/ide/components/repo_file_spec.js | 6 +- .../ide/components/repo_loading_file_spec.js | 8 +- spec/javascripts/ide/components/repo_tab_spec.js | 16 +- spec/javascripts/ide/components/repo_tabs_spec.js | 2 +- spec/javascripts/ide/helpers.js | 27 +- spec/javascripts/ide/lib/common/disposable_spec.js | 2 +- .../ide/lib/common/model_manager_spec.js | 18 +- spec/javascripts/ide/lib/common/model_spec.js | 20 +- .../ide/lib/decorations/controller_spec.js | 47 ++- spec/javascripts/ide/lib/diff/controller_spec.js | 78 +++-- spec/javascripts/ide/lib/diff/diff_spec.js | 2 +- spec/javascripts/ide/lib/editor_options_spec.js | 2 +- spec/javascripts/ide/lib/editor_spec.js | 4 +- spec/javascripts/ide/monaco_loader_spec.js | 14 +- spec/javascripts/ide/stores/actions/file_spec.js | 8 +- spec/javascripts/ide/stores/actions/tree_spec.js | 125 +++++--- spec/javascripts/ide/stores/actions_spec.js | 4 +- spec/javascripts/ide/stores/getters_spec.js | 4 +- .../ide/stores/modules/commit/actions_spec.js | 333 ++++++++++++--------- .../ide/stores/modules/commit/getters_spec.js | 36 ++- .../ide/stores/modules/commit/mutations_spec.js | 4 +- .../ide/stores/mutations/branch_spec.js | 4 +- spec/javascripts/ide/stores/mutations/file_spec.js | 4 +- spec/javascripts/ide/stores/mutations/tree_spec.js | 24 +- spec/javascripts/ide/stores/mutations_spec.js | 4 +- spec/javascripts/ide/stores/utils_spec.js | 32 +- 44 files changed, 632 insertions(+), 431 deletions(-) diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 2e1aea9a399..f536ce6344b 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -30,9 +30,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => { const currentProject = rootState.projects[rootState.currentProjectId]; const commitStats = data.stats ? sprintf(__('with %{additions} additions, %{deletions} deletions.'), { - additions: data.stats.additions, - deletions: data.stats.deletions, - }) + additions: data.stats.additions, // eslint-disable-line indent + deletions: data.stats.deletions, // eslint-disable-line indent + }) // eslint-disable-line indent : ''; const commitMsg = sprintf( __('Your changes have been committed. Commit %{commitId} %{commitStats}'), diff --git a/spec/javascripts/ide/components/changed_file_icon_spec.js b/spec/javascripts/ide/components/changed_file_icon_spec.js index 8f796b2f7f5..987aea7befc 100644 --- a/spec/javascripts/ide/components/changed_file_icon_spec.js +++ b/spec/javascripts/ide/components/changed_file_icon_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import changedFileIcon from 'ee/ide/components/changed_file_icon.vue'; +import changedFileIcon from '~/ide/components/changed_file_icon.vue'; import createComponent from 'spec/helpers/vue_mount_component_helper'; describe('IDE changed file icon', () => { diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js index 47a9007a8d0..144e78d14b5 100644 --- a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js @@ -1,13 +1,13 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import commitActions from 'ee/ide/components/commit_sidebar/actions.vue'; +import store from '~/ide/stores'; +import commitActions from '~/ide/components/commit_sidebar/actions.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from 'spec/ide/helpers'; describe('IDE commit sidebar actions', () => { let vm; - beforeEach((done) => { + beforeEach(done => { const Component = Vue.extend(commitActions); vm = createComponentWithStore(Component, store); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js index f6789bc861f..5b402886b55 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import listCollapsed from 'ee/ide/components/commit_sidebar/list_collapsed.vue'; +import store from '~/ide/stores'; +import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { file } from '../../helpers'; diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js index 543299950ea..15b66952d99 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import listItem from 'ee/ide/components/commit_sidebar/list_item.vue'; -import router from 'ee/ide/ide_router'; +import listItem from '~/ide/components/commit_sidebar/list_item.vue'; +import router from '~/ide/ide_router'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { file } from '../../helpers'; @@ -23,7 +23,9 @@ describe('Multi-file editor commit sidebar list item', () => { }); it('renders file path', () => { - expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path); + expect( + vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim(), + ).toBe(f.path); }); it('calls discardFileChanges when clicking discard button', () => { diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js index f02d055e38c..a62c0a28340 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import commitSidebarList from 'ee/ide/components/commit_sidebar/list.vue'; +import store from '~/ide/stores'; +import commitSidebarList from '~/ide/components/commit_sidebar/list.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { file } from '../../helpers'; @@ -25,7 +25,7 @@ describe('Multi-file editor commit sidebar list', () => { }); describe('with a list of files', () => { - beforeEach((done) => { + beforeEach(done => { const f = file('file name'); f.changed = true; vm.fileList.push(f); @@ -39,7 +39,7 @@ describe('Multi-file editor commit sidebar list', () => { }); describe('collapsed', () => { - beforeEach((done) => { + beforeEach(done => { vm.$store.state.rightPanelCollapsed = true; Vue.nextTick(done); diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js index 1058cc28de2..4e8243439f3 100644 --- a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js @@ -1,13 +1,13 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import radioGroup from 'ee/ide/components/commit_sidebar/radio_group.vue'; +import store from '~/ide/stores'; +import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from 'spec/ide/helpers'; describe('IDE commit sidebar radio group', () => { let vm; - beforeEach((done) => { + beforeEach(done => { const Component = Vue.extend(radioGroup); store.state.commit.commitAction = '2'; @@ -33,7 +33,7 @@ describe('IDE commit sidebar radio group', () => { expect(vm.$el.textContent).toContain('test'); }); - it('uses slot if label is not present', (done) => { + it('uses slot if label is not present', done => { vm.$destroy(); vm = new Vue({ @@ -59,7 +59,7 @@ describe('IDE commit sidebar radio group', () => { }); }); - it('updates store when changing radio button', (done) => { + it('updates store when changing radio button', done => { vm.$el.querySelector('input').dispatchEvent(new Event('change')); Vue.nextTick(() => { @@ -69,7 +69,7 @@ describe('IDE commit sidebar radio group', () => { }); }); - it('renders helpText tooltip', (done) => { + it('renders helpText tooltip', done => { vm.helpText = 'help text'; Vue.nextTick(() => { @@ -83,7 +83,7 @@ describe('IDE commit sidebar radio group', () => { }); describe('with input', () => { - beforeEach((done) => { + beforeEach(done => { vm.$destroy(); const Component = Vue.extend(radioGroup); @@ -106,7 +106,7 @@ describe('IDE commit sidebar radio group', () => { expect(vm.$el.querySelector('.form-control')).not.toBeNull(); }); - it('hides input when commitAction doesnt match value', (done) => { + it('hides input when commitAction doesnt match value', done => { store.state.commit.commitAction = '2'; Vue.nextTick(() => { @@ -115,7 +115,7 @@ describe('IDE commit sidebar radio group', () => { }); }); - it('updates branch name in store on input', (done) => { + it('updates branch name in store on input', done => { const input = vm.$el.querySelector('.form-control'); input.value = 'testing-123'; input.dispatchEvent(new Event('input')); diff --git a/spec/javascripts/ide/components/ide_context_bar_spec.js b/spec/javascripts/ide/components/ide_context_bar_spec.js index 9fa2e947db2..e17b051f137 100644 --- a/spec/javascripts/ide/components/ide_context_bar_spec.js +++ b/spec/javascripts/ide/components/ide_context_bar_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import ideContextBar from 'ee/ide/components/ide_context_bar.vue'; +import store from '~/ide/stores'; +import ideContextBar from '~/ide/components/ide_context_bar.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; describe('Multi-file editor right context bar', () => { @@ -24,7 +24,7 @@ describe('Multi-file editor right context bar', () => { }); describe('collapsed', () => { - beforeEach((done) => { + beforeEach(done => { vm.$store.state.rightPanelCollapsed = true; Vue.nextTick(done); diff --git a/spec/javascripts/ide/components/ide_external_links_spec.js b/spec/javascripts/ide/components/ide_external_links_spec.js index b8da6747653..9f6cb459f3b 100644 --- a/spec/javascripts/ide/components/ide_external_links_spec.js +++ b/spec/javascripts/ide/components/ide_external_links_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import ideExternalLinks from 'ee/ide/components/ide_external_links.vue'; +import ideExternalLinks from '~/ide/components/ide_external_links.vue'; import createComponent from 'spec/helpers/vue_mount_component_helper'; describe('ide external links component', () => { diff --git a/spec/javascripts/ide/components/ide_repo_tree_spec.js b/spec/javascripts/ide/components/ide_repo_tree_spec.js index e7188490f64..e0fbc90ca61 100644 --- a/spec/javascripts/ide/components/ide_repo_tree_spec.js +++ b/spec/javascripts/ide/components/ide_repo_tree_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import ideRepoTree from 'ee/ide/components/ide_repo_tree.vue'; +import ideRepoTree from '~/ide/components/ide_repo_tree.vue'; import createComponent from '../../helpers/vue_mount_component_helper'; import { file } from '../helpers'; @@ -29,11 +29,13 @@ describe('IdeRepoTree', () => { expect(vm.$el.querySelector('.file')).not.toBeNull(); }); - it('renders 3 loading files if tree is loading', (done) => { + it('renders 3 loading files if tree is loading', done => { tree.loading = true; vm.$nextTick(() => { - expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toEqual(3); + expect( + vm.$el.querySelectorAll('.multi-file-loading-container').length, + ).toEqual(3); done(); }); diff --git a/spec/javascripts/ide/components/ide_side_bar_spec.js b/spec/javascripts/ide/components/ide_side_bar_spec.js index 74afca280d1..699dae1ce2f 100644 --- a/spec/javascripts/ide/components/ide_side_bar_spec.js +++ b/spec/javascripts/ide/components/ide_side_bar_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import ideSidebar from 'ee/ide/components/ide_side_bar.vue'; +import store from '~/ide/stores'; +import ideSidebar from '~/ide/components/ide_side_bar.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../helpers'; @@ -20,15 +20,21 @@ describe('IdeSidebar', () => { }); it('renders a sidebar', () => { - expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull(); + expect( + vm.$el.querySelector('.multi-file-commit-panel-inner'), + ).not.toBeNull(); }); - it('renders loading icon component', (done) => { + it('renders loading icon component', done => { vm.$store.state.loading = true; vm.$nextTick(() => { - expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull(); - expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3); + expect( + vm.$el.querySelector('.multi-file-loading-container'), + ).not.toBeNull(); + expect( + vm.$el.querySelectorAll('.multi-file-loading-container').length, + ).toBe(3); done(); }); diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js index 7f8dcd9049f..5bd890094cc 100644 --- a/spec/javascripts/ide/components/ide_spec.js +++ b/spec/javascripts/ide/components/ide_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import ide from 'ee/ide/components/ide.vue'; +import store from '~/ide/stores'; +import ide from '~/ide/components/ide.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { file, resetStore } from '../helpers'; @@ -27,7 +27,7 @@ describe('ide component', () => { expect(vm.$el.querySelector('.panel-right')).toBeNull(); }); - it('renders panel right when files are open', (done) => { + it('renders panel right when files are open', done => { vm.$store.state.trees['abcproject/mybranch'] = { tree: [file()], }; diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js index cba27f94833..e08abe7d849 100644 --- a/spec/javascripts/ide/components/new_dropdown/index_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import newDropdown from 'ee/ide/components/new_dropdown/index.vue'; +import store from '~/ide/stores'; +import newDropdown from '~/ide/components/new_dropdown/index.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../../helpers'; diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js index 1a9c96c64da..a6e1e5a0d35 100644 --- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import modal from 'ee/ide/components/new_dropdown/modal.vue'; +import modal from '~/ide/components/new_dropdown/modal.vue'; import createComponent from 'spec/helpers/vue_mount_component_helper'; describe('new file modal component', () => { @@ -10,7 +10,7 @@ describe('new file modal component', () => { vm.$destroy(); }); - ['tree', 'blob'].forEach((type) => { + ['tree', 'blob'].forEach(type => { describe(type, () => { beforeEach(() => { vm = createComponent(Component, { @@ -25,19 +25,25 @@ describe('new file modal component', () => { it(`sets modal title as ${type}`, () => { const title = type === 'tree' ? 'directory' : 'file'; - expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`); + expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe( + `Create new ${title}`, + ); }); it(`sets button label as ${type}`, () => { const title = type === 'tree' ? 'directory' : 'file'; - expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`); + expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe( + `Create ${title}`, + ); }); it(`sets form label as ${type}`, () => { const title = type === 'tree' ? 'Directory' : 'File'; - expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(`${title} name`); + expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe( + `${title} name`, + ); }); describe('createEntryInStore', () => { @@ -59,11 +65,15 @@ describe('new file modal component', () => { it('focuses field on mount', () => { document.body.innerHTML += '
'; - vm = createComponent(Component, { - type: 'tree', - branchId: 'master', - path: '', - }, '.js-test'); + vm = createComponent( + Component, + { + type: 'tree', + branchId: 'master', + path: '', + }, + '.js-test', + ); expect(document.activeElement).toBe(vm.$refs.fieldName); diff --git a/spec/javascripts/ide/components/new_dropdown/upload_spec.js b/spec/javascripts/ide/components/new_dropdown/upload_spec.js index 766e8b72360..2bc5d701601 100644 --- a/spec/javascripts/ide/components/new_dropdown/upload_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/upload_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import upload from 'ee/ide/components/new_dropdown/upload.vue'; +import upload from '~/ide/components/new_dropdown/upload.vue'; import createComponent from 'spec/helpers/vue_mount_component_helper'; describe('new dropdown upload', () => { diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js index 8090e3664e0..113ade269e9 100644 --- a/spec/javascripts/ide/components/repo_commit_section_spec.js +++ b/spec/javascripts/ide/components/repo_commit_section_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import service from 'ee/ide/services'; -import repoCommitSection from 'ee/ide/components/repo_commit_section.vue'; +import store from '~/ide/stores'; +import service from '~/ide/services'; +import repoCommitSection from '~/ide/components/repo_commit_section.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; import { file, resetStore } from '../helpers'; @@ -31,30 +31,35 @@ describe('RepoCommitSection', () => { vm.$store.state.rightPanelCollapsed = false; vm.$store.state.currentBranch = 'master'; vm.$store.state.changedFiles = [file('file1'), file('file2')]; - vm.$store.state.changedFiles.forEach(f => Object.assign(f, { - changed: true, - content: 'testing', - })); + vm.$store.state.changedFiles.forEach(f => + Object.assign(f, { + changed: true, + content: 'testing', + }), + ); return vm.$mount(); } - beforeEach((done) => { + beforeEach(done => { vm = createComponent(); - spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({ - headers: { - 'page-title': 'test', - }, - json: () => Promise.resolve({ - last_commit_path: 'last_commit_path', - parent_tree_url: 'parent_tree_url', - path: '/', - trees: [{ name: 'tree' }], - blobs: [{ name: 'blob' }], - submodules: [{ name: 'submodule' }], + spyOn(service, 'getTreeData').and.returnValue( + Promise.resolve({ + headers: { + 'page-title': 'test', + }, + json: () => + Promise.resolve({ + last_commit_path: 'last_commit_path', + parent_tree_url: 'parent_tree_url', + path: '/', + trees: [{ name: 'tree' }], + blobs: [{ name: 'blob' }], + submodules: [{ name: 'submodule' }], + }), }), - })); + ); Vue.nextTick(done); }); @@ -75,27 +80,35 @@ describe('RepoCommitSection', () => { committedStateSvgPath: 'svg', }).$mount(); - expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toContain('No changes'); - expect(vm.$el.querySelector('.js-empty-state img').getAttribute('src')).toBe('nochangessvg'); + expect( + vm.$el.querySelector('.js-empty-state').textContent.trim(), + ).toContain('No changes'); + expect( + vm.$el.querySelector('.js-empty-state img').getAttribute('src'), + ).toBe('nochangessvg'); }); }); it('renders a commit section', () => { - const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')]; + const changedFileElements = [ + ...vm.$el.querySelectorAll('.multi-file-commit-list li'), + ]; const submitCommit = vm.$el.querySelector('form .btn'); expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull(); expect(changedFileElements.length).toEqual(2); changedFileElements.forEach((changedFile, i) => { - expect(changedFile.textContent.trim()).toContain(vm.$store.state.changedFiles[i].path); + expect(changedFile.textContent.trim()).toContain( + vm.$store.state.changedFiles[i].path, + ); }); expect(submitCommit.disabled).toBeTruthy(); expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull(); }); - it('updates commitMessage in store on input', (done) => { + it('updates commitMessage in store on input', done => { const textarea = vm.$el.querySelector('textarea'); textarea.value = 'testing commit message'; @@ -104,7 +117,9 @@ describe('RepoCommitSection', () => { getSetTimeoutPromise() .then(() => { - expect(vm.$store.state.commit.commitMessage).toBe('testing commit message'); + expect(vm.$store.state.commit.commitMessage).toBe( + 'testing commit message', + ); }) .then(done) .catch(done.fail); @@ -112,10 +127,12 @@ describe('RepoCommitSection', () => { describe('discard draft button', () => { it('hidden when commitMessage is empty', () => { - expect(vm.$el.querySelector('.multi-file-commit-form .btn-default')).toBeNull(); + expect( + vm.$el.querySelector('.multi-file-commit-form .btn-default'), + ).toBeNull(); }); - it('resets commitMessage when clicking discard button', (done) => { + it('resets commitMessage when clicking discard button', done => { vm.$store.state.commit.commitMessage = 'testing commit message'; getSetTimeoutPromise() @@ -124,7 +141,9 @@ describe('RepoCommitSection', () => { }) .then(Vue.nextTick) .then(() => { - expect(vm.$store.state.commit.commitMessage).not.toBe('testing commit message'); + expect(vm.$store.state.commit.commitMessage).not.toBe( + 'testing commit message', + ); }) .then(done) .catch(done.fail); @@ -136,7 +155,7 @@ describe('RepoCommitSection', () => { spyOn(vm, 'commitChanges'); }); - it('calls commitChanges', (done) => { + it('calls commitChanges', done => { vm.$store.state.commit.commitMessage = 'testing commit message'; getSetTimeoutPromise() diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index cda88623497..ae657e8c881 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -1,15 +1,15 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import repoEditor from 'ee/ide/components/repo_editor.vue'; -import monacoLoader from 'ee/ide/monaco_loader'; -import Editor from 'ee/ide/lib/editor'; +import store from '~/ide/stores'; +import repoEditor from '~/ide/components/repo_editor.vue'; +import monacoLoader from '~/ide/monaco_loader'; +import Editor from '~/ide/lib/editor'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; import { file, resetStore } from '../helpers'; describe('RepoEditor', () => { let vm; - beforeEach((done) => { + beforeEach(done => { const f = file(); const RepoEditor = Vue.extend(repoEditor); @@ -39,7 +39,7 @@ describe('RepoEditor', () => { Editor.editorInstance.modelManager.dispose(); }); - it('renders an ide container', (done) => { + it('renders an ide container', done => { Vue.nextTick(() => { expect(vm.shouldHideEditor).toBeFalsy(); @@ -48,7 +48,7 @@ describe('RepoEditor', () => { }); describe('when open file is binary and not raw', () => { - beforeEach((done) => { + beforeEach(done => { vm.file.binary = true; vm.$nextTick(done); @@ -64,7 +64,7 @@ describe('RepoEditor', () => { }); describe('createEditorInstance', () => { - it('calls createInstance when viewer is editor', (done) => { + it('calls createInstance when viewer is editor', done => { spyOn(vm.editor, 'createInstance'); vm.createEditorInstance(); @@ -76,7 +76,7 @@ describe('RepoEditor', () => { }); }); - it('calls createDiffInstance when viewer is diff', (done) => { + it('calls createDiffInstance when viewer is diff', done => { vm.$store.state.viewer = 'diff'; spyOn(vm.editor, 'createDiffInstance'); @@ -124,7 +124,7 @@ describe('RepoEditor', () => { expect(vm.model.events.size).toBe(1); }); - it('updates state when model content changed', (done) => { + it('updates state when model content changed', done => { vm.model.setValue('testing 123'); setTimeout(() => { diff --git a/spec/javascripts/ide/components/repo_file_buttons_spec.js b/spec/javascripts/ide/components/repo_file_buttons_spec.js index 13b452b1936..c86bdb132b4 100644 --- a/spec/javascripts/ide/components/repo_file_buttons_spec.js +++ b/spec/javascripts/ide/components/repo_file_buttons_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import repoFileButtons from 'ee/ide/components/repo_file_buttons.vue'; +import repoFileButtons from '~/ide/components/repo_file_buttons.vue'; import createVueComponent from '../../helpers/vue_mount_component_helper'; import { file } from '../helpers'; @@ -23,7 +23,7 @@ describe('RepoFileButtons', () => { vm.$destroy(); }); - it('renders Raw, Blame, History, Permalink and Preview toggle', (done) => { + it('renders Raw, Blame, History, Permalink and Preview toggle', done => { vm = createComponent(); vm.$nextTick(() => { @@ -37,7 +37,9 @@ describe('RepoFileButtons', () => { expect(blame.textContent.trim()).toEqual('Blame'); expect(history.href).toMatch(`/${activeFile.commitsPath}`); expect(history.textContent.trim()).toEqual('History'); - expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink'); + expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual( + 'Permalink', + ); done(); }); diff --git a/spec/javascripts/ide/components/repo_file_spec.js b/spec/javascripts/ide/components/repo_file_spec.js index 3bd871544ea..ff391cb4351 100644 --- a/spec/javascripts/ide/components/repo_file_spec.js +++ b/spec/javascripts/ide/components/repo_file_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import repoFile from 'ee/ide/components/repo_file.vue'; -import router from 'ee/ide/ide_router'; +import store from '~/ide/stores'; +import repoFile from '~/ide/components/repo_file.vue'; +import router from '~/ide/ide_router'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; import { file } from '../helpers'; diff --git a/spec/javascripts/ide/components/repo_loading_file_spec.js b/spec/javascripts/ide/components/repo_loading_file_spec.js index dd267654289..8f9644216bc 100644 --- a/spec/javascripts/ide/components/repo_loading_file_spec.js +++ b/spec/javascripts/ide/components/repo_loading_file_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import repoLoadingFile from 'ee/ide/components/repo_loading_file.vue'; +import store from '~/ide/stores'; +import repoLoadingFile from '~/ide/components/repo_loading_file.vue'; import { resetStore } from '../helpers'; describe('RepoLoadingFile', () => { @@ -22,7 +22,7 @@ describe('RepoLoadingFile', () => { } function assertColumns(columns) { - columns.forEach((column) => { + columns.forEach(column => { const container = column.querySelector('.animation-container'); const lines = [...container.querySelectorAll(':scope > div')]; @@ -46,7 +46,7 @@ describe('RepoLoadingFile', () => { assertColumns(columns); }); - it('renders 1 column of animated LoC if isMini', (done) => { + it('renders 1 column of animated LoC if isMini', done => { vm = createComponent(); vm.$store.state.leftPanelCollapsed = true; vm.$store.state.openFiles.push('test'); diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js index c3246cd1f1f..ddb5204e3a7 100644 --- a/spec/javascripts/ide/components/repo_tab_spec.js +++ b/spec/javascripts/ide/components/repo_tab_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import repoTab from 'ee/ide/components/repo_tab.vue'; -import router from 'ee/ide/ide_router'; +import store from '~/ide/stores'; +import repoTab from '~/ide/components/repo_tab.vue'; +import router from '~/ide/ide_router'; import { file, resetStore } from '../helpers'; describe('RepoTab', () => { @@ -62,7 +62,7 @@ describe('RepoTab', () => { expect(vm.closeFile).toHaveBeenCalledWith(vm.tab.path); }); - it('changes icon on hover', (done) => { + it('changes icon on hover', done => { const tab = file(); tab.changed = true; vm = createComponent({ @@ -112,13 +112,15 @@ describe('RepoTab', () => { }); it('renders a tooltip', () => { - expect(vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle).toContain('Locked by testuser'); + expect( + vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle, + ).toContain('Locked by testuser'); }); }); describe('methods', () => { describe('closeTab', () => { - it('closes tab if file has changed', (done) => { + it('closes tab if file has changed', done => { const tab = file(); tab.changed = true; tab.opened = true; @@ -140,7 +142,7 @@ describe('RepoTab', () => { }); }); - it('closes tab when clicking close btn', (done) => { + it('closes tab when clicking close btn', done => { const tab = file('lose'); tab.opened = true; vm = createComponent({ diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js index 40834f230a8..ceb0416aff8 100644 --- a/spec/javascripts/ide/components/repo_tabs_spec.js +++ b/spec/javascripts/ide/components/repo_tabs_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import repoTabs from 'ee/ide/components/repo_tabs.vue'; +import repoTabs from '~/ide/components/repo_tabs.vue'; import createComponent from '../../helpers/vue_mount_component_helper'; import { file } from '../helpers'; diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js index 67f9eaff44a..98db6defc7a 100644 --- a/spec/javascripts/ide/helpers.js +++ b/spec/javascripts/ide/helpers.js @@ -1,8 +1,8 @@ -import { decorateData } from 'ee/ide/stores/utils'; -import state from 'ee/ide/stores/state'; -import commitState from 'ee/ide/stores/modules/commit/state'; +import { decorateData } from '~/ide/stores/utils'; +import state from '~/ide/stores/state'; +import commitState from '~/ide/stores/modules/commit/state'; -export const resetStore = (store) => { +export const resetStore = store => { const newState = { ...state(), commit: commitState(), @@ -10,12 +10,13 @@ export const resetStore = (store) => { store.replaceState(newState); }; -export const file = (name = 'name', id = name, type = '') => decorateData({ - id, - type, - icon: 'icon', - url: 'url', - name, - path: name, - lastCommit: {}, -}); +export const file = (name = 'name', id = name, type = '') => + decorateData({ + id, + type, + icon: 'icon', + url: 'url', + name, + path: name, + lastCommit: {}, + }); diff --git a/spec/javascripts/ide/lib/common/disposable_spec.js b/spec/javascripts/ide/lib/common/disposable_spec.js index 677986aff91..af12ca15369 100644 --- a/spec/javascripts/ide/lib/common/disposable_spec.js +++ b/spec/javascripts/ide/lib/common/disposable_spec.js @@ -1,4 +1,4 @@ -import Disposable from 'ee/ide/lib/common/disposable'; +import Disposable from '~/ide/lib/common/disposable'; describe('Multi-file editor library disposable class', () => { let instance; diff --git a/spec/javascripts/ide/lib/common/model_manager_spec.js b/spec/javascripts/ide/lib/common/model_manager_spec.js index 7a1fab0f74d..4381f6fcfd0 100644 --- a/spec/javascripts/ide/lib/common/model_manager_spec.js +++ b/spec/javascripts/ide/lib/common/model_manager_spec.js @@ -1,13 +1,13 @@ /* global monaco */ -import eventHub from 'ee/ide/eventhub'; -import monacoLoader from 'ee/ide/monaco_loader'; -import ModelManager from 'ee/ide/lib/common/model_manager'; +import eventHub from '~/ide/eventhub'; +import monacoLoader from '~/ide/monaco_loader'; +import ModelManager from '~/ide/lib/common/model_manager'; import { file } from '../../helpers'; describe('Multi-file editor library model manager', () => { let instance; - beforeEach((done) => { + beforeEach(done => { monacoLoader(['vs/editor/editor.main'], () => { instance = new ModelManager(monaco); @@ -55,7 +55,10 @@ describe('Multi-file editor library model manager', () => { instance.addModel(f); - expect(eventHub.$on).toHaveBeenCalledWith(`editor.update.model.dispose.${f.path}`, jasmine.anything()); + expect(eventHub.$on).toHaveBeenCalledWith( + `editor.update.model.dispose.${f.path}`, + jasmine.anything(), + ); }); }); @@ -99,7 +102,10 @@ describe('Multi-file editor library model manager', () => { instance.removeCachedModel(f); - expect(eventHub.$off).toHaveBeenCalledWith(`editor.update.model.dispose.${f.path}`, jasmine.anything()); + expect(eventHub.$off).toHaveBeenCalledWith( + `editor.update.model.dispose.${f.path}`, + jasmine.anything(), + ); }); }); diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js index dd9e4946883..adc6a93c06b 100644 --- a/spec/javascripts/ide/lib/common/model_spec.js +++ b/spec/javascripts/ide/lib/common/model_spec.js @@ -1,13 +1,13 @@ /* global monaco */ -import eventHub from 'ee/ide/eventhub'; -import monacoLoader from 'ee/ide/monaco_loader'; -import Model from 'ee/ide/lib/common/model'; +import eventHub from '~/ide/eventhub'; +import monacoLoader from '~/ide/monaco_loader'; +import Model from '~/ide/lib/common/model'; import { file } from '../../helpers'; describe('Multi-file editor library model', () => { let model; - beforeEach((done) => { + beforeEach(done => { spyOn(eventHub, '$on').and.callThrough(); monacoLoader(['vs/editor/editor.main'], () => { @@ -27,7 +27,10 @@ describe('Multi-file editor library model', () => { }); it('adds eventHub listener', () => { - expect(eventHub.$on).toHaveBeenCalledWith(`editor.update.model.dispose.${model.file.path}`, jasmine.anything()); + expect(eventHub.$on).toHaveBeenCalledWith( + `editor.update.model.dispose.${model.file.path}`, + jasmine.anything(), + ); }); describe('path', () => { @@ -64,7 +67,7 @@ describe('Multi-file editor library model', () => { expect(model.events.keys().next().value).toBe('path'); }); - it('calls callback on change', (done) => { + it('calls callback on change', done => { const spy = jasmine.createSpy(); model.onChange(spy); @@ -101,7 +104,10 @@ describe('Multi-file editor library model', () => { model.dispose(); - expect(eventHub.$off).toHaveBeenCalledWith(`editor.update.model.dispose.${model.file.path}`, jasmine.anything()); + expect(eventHub.$off).toHaveBeenCalledWith( + `editor.update.model.dispose.${model.file.path}`, + jasmine.anything(), + ); }); }); }); diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js index 63e4282d4df..092170d086a 100644 --- a/spec/javascripts/ide/lib/decorations/controller_spec.js +++ b/spec/javascripts/ide/lib/decorations/controller_spec.js @@ -1,8 +1,8 @@ /* global monaco */ -import monacoLoader from 'ee/ide/monaco_loader'; -import editor from 'ee/ide/lib/editor'; -import DecorationsController from 'ee/ide/lib/decorations/controller'; -import Model from 'ee/ide/lib/common/model'; +import monacoLoader from '~/ide/monaco_loader'; +import editor from '~/ide/lib/editor'; +import DecorationsController from '~/ide/lib/decorations/controller'; +import Model from '~/ide/lib/common/model'; import { file } from '../../helpers'; describe('Multi-file editor library decorations controller', () => { @@ -10,7 +10,7 @@ describe('Multi-file editor library decorations controller', () => { let controller; let model; - beforeEach((done) => { + beforeEach(done => { monacoLoader(['vs/editor/editor.main'], () => { editorInstance = editor.create(monaco); editorInstance.createInstance(document.createElement('div')); @@ -36,7 +36,9 @@ describe('Multi-file editor library decorations controller', () => { }); it('returns decorations by model URL', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + controller.addDecorations(model, 'key', [ + { decoration: 'decorationValue' }, + ]); const decorations = controller.getAllDecorationsForModel(model); @@ -46,20 +48,28 @@ describe('Multi-file editor library decorations controller', () => { describe('addDecorations', () => { it('caches decorations in a new map', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + controller.addDecorations(model, 'key', [ + { decoration: 'decorationValue' }, + ]); expect(controller.decorations.size).toBe(1); }); it('does not create new cache model', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]); + controller.addDecorations(model, 'key', [ + { decoration: 'decorationValue' }, + ]); + controller.addDecorations(model, 'key', [ + { decoration: 'decorationValue2' }, + ]); expect(controller.decorations.size).toBe(1); }); it('caches decorations by model URL', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + controller.addDecorations(model, 'key', [ + { decoration: 'decorationValue' }, + ]); expect(controller.decorations.size).toBe(1); expect(controller.decorations.keys().next().value).toBe('path'); @@ -68,7 +78,9 @@ describe('Multi-file editor library decorations controller', () => { it('calls decorate method', () => { spyOn(controller, 'decorate'); - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + controller.addDecorations(model, 'key', [ + { decoration: 'decorationValue' }, + ]); expect(controller.decorate).toHaveBeenCalled(); }); @@ -80,7 +92,10 @@ describe('Multi-file editor library decorations controller', () => { controller.decorate(model); - expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []); + expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith( + [], + [], + ); }); it('caches decorations', () => { @@ -102,7 +117,9 @@ describe('Multi-file editor library decorations controller', () => { describe('dispose', () => { it('clears cached decorations', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + controller.addDecorations(model, 'key', [ + { decoration: 'decorationValue' }, + ]); controller.dispose(); @@ -110,7 +127,9 @@ describe('Multi-file editor library decorations controller', () => { }); it('clears cached editorDecorations', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + controller.addDecorations(model, 'key', [ + { decoration: 'decorationValue' }, + ]); controller.dispose(); diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js index 90216f8b07e..c8f3e9f4830 100644 --- a/spec/javascripts/ide/lib/diff/controller_spec.js +++ b/spec/javascripts/ide/lib/diff/controller_spec.js @@ -1,10 +1,13 @@ /* global monaco */ -import monacoLoader from 'ee/ide/monaco_loader'; -import editor from 'ee/ide/lib/editor'; -import ModelManager from 'ee/ide/lib/common/model_manager'; -import DecorationsController from 'ee/ide/lib/decorations/controller'; -import DirtyDiffController, { getDiffChangeType, getDecorator } from 'ee/ide/lib/diff/controller'; -import { computeDiff } from 'ee/ide/lib/diff/diff'; +import monacoLoader from '~/ide/monaco_loader'; +import editor from '~/ide/lib/editor'; +import ModelManager from '~/ide/lib/common/model_manager'; +import DecorationsController from '~/ide/lib/decorations/controller'; +import DirtyDiffController, { + getDiffChangeType, + getDecorator, +} from '~/ide/lib/diff/controller'; +import { computeDiff } from '~/ide/lib/diff/diff'; import { file } from '../../helpers'; describe('Multi-file editor library dirty diff controller', () => { @@ -14,7 +17,7 @@ describe('Multi-file editor library dirty diff controller', () => { let decorationsController; let model; - beforeEach((done) => { + beforeEach(done => { monacoLoader(['vs/editor/editor.main'], () => { editorInstance = editor.create(monaco); editorInstance.createInstance(document.createElement('div')); @@ -38,7 +41,7 @@ describe('Multi-file editor library dirty diff controller', () => { }); describe('getDiffChangeType', () => { - ['added', 'removed', 'modified'].forEach((type) => { + ['added', 'removed', 'modified'].forEach(type => { it(`returns ${type}`, () => { const change = { [type]: true, @@ -50,15 +53,15 @@ describe('Multi-file editor library dirty diff controller', () => { }); describe('getDecorator', () => { - ['added', 'removed', 'modified'].forEach((type) => { + ['added', 'removed', 'modified'].forEach(type => { it(`returns with linesDecorationsClassName for ${type}`, () => { const change = { [type]: true, }; - expect( - getDecorator(change).options.linesDecorationsClassName, - ).toBe(`dirty-diff dirty-diff-${type}`); + expect(getDecorator(change).options.linesDecorationsClassName).toBe( + `dirty-diff dirty-diff-${type}`, + ); }); it('returns with line numbers', () => { @@ -118,7 +121,9 @@ describe('Multi-file editor library dirty diff controller', () => { controller.reDecorate(model); - expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model); + expect(controller.decorationsController.decorate).toHaveBeenCalledWith( + model, + ); }); }); @@ -128,23 +133,33 @@ describe('Multi-file editor library dirty diff controller', () => { controller.decorate({ data: { changes: [], path: 'path' } }); - expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith(model, 'dirtyDiff', jasmine.anything()); + expect( + controller.decorationsController.addDecorations, + ).toHaveBeenCalledWith(model, 'dirtyDiff', jasmine.anything()); }); it('adds decorations into editor', () => { - const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations'); - - controller.decorate({ data: { changes: computeDiff('123', '1234'), path: 'path' } }); - - expect(spy).toHaveBeenCalledWith([], [{ - range: new monaco.Range( - 1, 1, 1, 1, - ), - options: { - isWholeLine: true, - linesDecorationsClassName: 'dirty-diff dirty-diff-modified', - }, - }]); + const spy = spyOn( + controller.decorationsController.editor.instance, + 'deltaDecorations', + ); + + controller.decorate({ + data: { changes: computeDiff('123', '1234'), path: 'path' }, + }); + + expect(spy).toHaveBeenCalledWith( + [], + [ + { + range: new monaco.Range(1, 1, 1, 1), + options: { + isWholeLine: true, + linesDecorationsClassName: 'dirty-diff dirty-diff-modified', + }, + }, + ], + ); }); }); @@ -166,11 +181,16 @@ describe('Multi-file editor library dirty diff controller', () => { }); it('removes worker event listener', () => { - spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough(); + spyOn( + controller.dirtyDiffWorker, + 'removeEventListener', + ).and.callThrough(); controller.dispose(); - expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith('message', jasmine.anything()); + expect( + controller.dirtyDiffWorker.removeEventListener, + ).toHaveBeenCalledWith('message', jasmine.anything()); }); }); }); diff --git a/spec/javascripts/ide/lib/diff/diff_spec.js b/spec/javascripts/ide/lib/diff/diff_spec.js index 3bdd0a77e40..57f3ac3d365 100644 --- a/spec/javascripts/ide/lib/diff/diff_spec.js +++ b/spec/javascripts/ide/lib/diff/diff_spec.js @@ -1,4 +1,4 @@ -import { computeDiff } from 'ee/ide/lib/diff/diff'; +import { computeDiff } from '~/ide/lib/diff/diff'; describe('Multi-file editor library diff calculator', () => { describe('computeDiff', () => { diff --git a/spec/javascripts/ide/lib/editor_options_spec.js b/spec/javascripts/ide/lib/editor_options_spec.js index b974a6befd3..d149a883166 100644 --- a/spec/javascripts/ide/lib/editor_options_spec.js +++ b/spec/javascripts/ide/lib/editor_options_spec.js @@ -1,4 +1,4 @@ -import editorOptions from 'ee/ide/lib/editor_options'; +import editorOptions from '~/ide/lib/editor_options'; describe('Multi-file editor library editor options', () => { it('returns an array', () => { diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js index 76869bbc7ce..d6df35c90e8 100644 --- a/spec/javascripts/ide/lib/editor_spec.js +++ b/spec/javascripts/ide/lib/editor_spec.js @@ -1,6 +1,6 @@ /* global monaco */ -import monacoLoader from 'ee/ide/monaco_loader'; -import editor from 'ee/ide/lib/editor'; +import monacoLoader from '~/ide/monaco_loader'; +import editor from '~/ide/lib/editor'; import { file } from '../helpers'; describe('Multi-file editor library', () => { diff --git a/spec/javascripts/ide/monaco_loader_spec.js b/spec/javascripts/ide/monaco_loader_spec.js index 43bc256e718..7ab315aa8c8 100644 --- a/spec/javascripts/ide/monaco_loader_spec.js +++ b/spec/javascripts/ide/monaco_loader_spec.js @@ -1,13 +1,15 @@ import monacoContext from 'monaco-editor/dev/vs/loader'; -import monacoLoader from 'ee/ide/monaco_loader'; +import monacoLoader from '~/ide/monaco_loader'; describe('MonacoLoader', () => { it('calls require.config and exports require', () => { - expect(monacoContext.require.getConfig()).toEqual(jasmine.objectContaining({ - paths: { - vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase - }, - })); + expect(monacoContext.require.getConfig()).toEqual( + jasmine.objectContaining({ + paths: { + vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase + }, + }), + ); expect(monacoLoader).toBe(monacoContext.require); }); }); diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index 55563e29d7f..5b7c8365641 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -1,8 +1,8 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import service from 'ee/ide/services'; -import router from 'ee/ide/ide_router'; -import eventHub from 'ee/ide/eventhub'; +import store from '~/ide/stores'; +import service from '~/ide/services'; +import router from '~/ide/ide_router'; +import eventHub from '~/ide/eventhub'; import { file, resetStore } from '../../helpers'; describe('Multi-file store file actions', () => { diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js index dba4bc10f9d..381f038067b 100644 --- a/spec/javascripts/ide/stores/actions/tree_spec.js +++ b/spec/javascripts/ide/stores/actions/tree_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; -import store from 'ee/ide/stores'; -import service from 'ee/ide/services'; -import router from 'ee/ide/ide_router'; +import store from '~/ide/stores'; +import service from '~/ide/services'; +import router from '~/ide/ide_router'; import { file, resetStore } from '../../helpers'; describe('Multi-file store tree actions', () => { @@ -35,26 +35,32 @@ describe('Multi-file store tree actions', () => { describe('getFiles', () => { beforeEach(() => { - spyOn(service, 'getFiles').and.returnValue(Promise.resolve({ - json: () => Promise.resolve([ - 'file.txt', - 'folder/fileinfolder.js', - 'folder/subfolder/fileinsubfolder.js', - ]), - })); + spyOn(service, 'getFiles').and.returnValue( + Promise.resolve({ + json: () => + Promise.resolve([ + 'file.txt', + 'folder/fileinfolder.js', + 'folder/subfolder/fileinsubfolder.js', + ]), + }), + ); }); - it('calls service getFiles', (done) => { - store.dispatch('getFiles', basicCallParameters) - .then(() => { - expect(service.getFiles).toHaveBeenCalledWith('', 'master'); + it('calls service getFiles', done => { + store + .dispatch('getFiles', basicCallParameters) + .then(() => { + expect(service.getFiles).toHaveBeenCalledWith('', 'master'); - done(); - }).catch(done.fail); + done(); + }) + .catch(done.fail); }); - it('adds data into tree', (done) => { - store.dispatch('getFiles', basicCallParameters) + it('adds data into tree', done => { + store + .dispatch('getFiles', basicCallParameters) .then(() => { projectTree = store.state.trees['abcproject/master']; expect(projectTree.tree.length).toBe(2); @@ -62,10 +68,13 @@ describe('Multi-file store tree actions', () => { expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js'); expect(projectTree.tree[1].type).toBe('blob'); expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob'); - expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js'); + expect(projectTree.tree[0].tree[0].tree[0].name).toBe( + 'fileinsubfolder.js', + ); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); }); @@ -77,30 +86,38 @@ describe('Multi-file store tree actions', () => { store.state.entries[tree.path] = tree; }); - it('toggles the tree open', (done) => { - store.dispatch('toggleTreeOpen', tree.path).then(() => { - expect(tree.opened).toBeTruthy(); + it('toggles the tree open', done => { + store + .dispatch('toggleTreeOpen', tree.path) + .then(() => { + expect(tree.opened).toBeTruthy(); - done(); - }).catch(done.fail); + done(); + }) + .catch(done.fail); }); }); describe('getLastCommitData', () => { beforeEach(() => { - spyOn(service, 'getTreeLastCommit').and.returnValue(Promise.resolve({ - headers: { - 'more-logs-url': null, - }, - json: () => Promise.resolve([{ - type: 'tree', - file_name: 'testing', - commit: { - message: 'commit message', - authored_date: '123', + spyOn(service, 'getTreeLastCommit').and.returnValue( + Promise.resolve({ + headers: { + 'more-logs-url': null, }, - }]), - })); + json: () => + Promise.resolve([ + { + type: 'tree', + file_name: 'testing', + commit: { + message: 'commit message', + authored_date: '123', + }, + }, + ]), + }), + ); store.state.trees['abcproject/mybranch'] = { tree: [], @@ -111,35 +128,45 @@ describe('Multi-file store tree actions', () => { projectTree.lastCommitPath = 'lastcommitpath'; }); - it('calls service with lastCommitPath', (done) => { - store.dispatch('getLastCommitData', projectTree) + it('calls service with lastCommitPath', done => { + store + .dispatch('getLastCommitData', projectTree) .then(() => { - expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath'); + expect(service.getTreeLastCommit).toHaveBeenCalledWith( + 'lastcommitpath', + ); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('updates trees last commit data', (done) => { - store.dispatch('getLastCommitData', projectTree) - .then(Vue.nextTick) + it('updates trees last commit data', done => { + store + .dispatch('getLastCommitData', projectTree) + .then(Vue.nextTick) .then(() => { expect(projectTree.tree[0].lastCommit.message).toBe('commit message'); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('does not update entry if not found', (done) => { + it('does not update entry if not found', done => { projectTree.tree[0].name = 'a'; - store.dispatch('getLastCommitData', projectTree) + store + .dispatch('getLastCommitData', projectTree) .then(Vue.nextTick) .then(() => { - expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message'); + expect(projectTree.tree[0].lastCommit.message).not.toBe( + 'commit message', + ); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index 0da1226c7aa..cec572f4507 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -1,6 +1,6 @@ import * as urlUtils from '~/lib/utils/url_utility'; -import store from 'ee/ide/stores'; -import router from 'ee/ide/ide_router'; +import store from '~/ide/stores'; +import router from '~/ide/ide_router'; import { resetStore, file } from '../helpers'; describe('Multi-file store actions', () => { diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js index 2fb69339915..a613f3a21cc 100644 --- a/spec/javascripts/ide/stores/getters_spec.js +++ b/spec/javascripts/ide/stores/getters_spec.js @@ -1,5 +1,5 @@ -import * as getters from 'ee/ide/stores/getters'; -import state from 'ee/ide/stores/state'; +import * as getters from '~/ide/stores/getters'; +import state from '~/ide/stores/state'; import { file } from '../helpers'; describe('Multi-file store getters', () => { diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 0aef29f77e3..90ded940227 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -1,9 +1,9 @@ -import store from 'ee/ide/stores'; -import service from 'ee/ide/services'; -import router from 'ee/ide/ide_router'; +import store from '~/ide/stores'; +import service from '~/ide/services'; +import router from '~/ide/ide_router'; import * as urlUtils from '~/lib/utils/url_utility'; -import eventHub from 'ee/ide/eventhub'; -import * as consts from 'ee/ide/stores/modules/commit/constants'; +import eventHub from '~/ide/eventhub'; +import * as consts from '~/ide/stores/modules/commit/constants'; import { resetStore, file } from 'spec/ide/helpers'; describe('IDE commit module actions', () => { @@ -16,8 +16,9 @@ describe('IDE commit module actions', () => { }); describe('updateCommitMessage', () => { - it('updates store with new commit message', (done) => { - store.dispatch('commit/updateCommitMessage', 'testing') + it('updates store with new commit message', done => { + store + .dispatch('commit/updateCommitMessage', 'testing') .then(() => { expect(store.state.commit.commitMessage).toBe('testing'); }) @@ -27,10 +28,11 @@ describe('IDE commit module actions', () => { }); describe('discardDraft', () => { - it('resets commit message to blank', (done) => { + it('resets commit message to blank', done => { store.state.commit.commitMessage = 'testing'; - store.dispatch('commit/discardDraft') + store + .dispatch('commit/discardDraft') .then(() => { expect(store.state.commit.commitMessage).not.toBe('testing'); }) @@ -40,8 +42,9 @@ describe('IDE commit module actions', () => { }); describe('updateCommitAction', () => { - it('updates store with new commit action', (done) => { - store.dispatch('commit/updateCommitAction', '1') + it('updates store with new commit action', done => { + store + .dispatch('commit/updateCommitAction', '1') .then(() => { expect(store.state.commit.commitAction).toBe('1'); }) @@ -51,8 +54,9 @@ describe('IDE commit module actions', () => { }); describe('updateBranchName', () => { - it('updates store with new branch name', (done) => { - store.dispatch('commit/updateBranchName', 'branch-name') + it('updates store with new branch name', done => { + store + .dispatch('commit/updateBranchName', 'branch-name') .then(() => { expect(store.state.commit.newBranchName).toBe('branch-name'); }) @@ -73,8 +77,9 @@ describe('IDE commit module actions', () => { }); }); - it('updates commit message with short_id', (done) => { - store.dispatch('commit/setLastCommitMessage', { short_id: '123' }) + it('updates commit message with short_id', done => { + store + .dispatch('commit/setLastCommitMessage', { short_id: '123' }) .then(() => { expect(store.state.lastCommitMsg).toContain( 'Your changes have been committed. Commit 123', @@ -84,16 +89,19 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('updates commit message with stats', (done) => { - store.dispatch('commit/setLastCommitMessage', { - short_id: '123', - stats: { - additions: '1', - deletions: '2', - }, - }) + it('updates commit message with stats', done => { + store + .dispatch('commit/setLastCommitMessage', { + short_id: '123', + stats: { + additions: '1', + deletions: '2', + }, + }) .then(() => { - expect(store.state.lastCommitMsg).toBe('Your changes have been committed. Commit 123 with 1 additions, 2 deletions.'); + expect(store.state.lastCommitMsg).toBe( + 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.', + ); }) .then(done) .catch(done.fail); @@ -113,31 +121,40 @@ describe('IDE commit module actions', () => { }; }); - it('calls service', (done) => { - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { id: '123' }, - }, - })); + it('calls service', done => { + spyOn(service, 'getBranchData').and.returnValue( + Promise.resolve({ + data: { + commit: { id: '123' }, + }, + }), + ); - store.dispatch('commit/checkCommitStatus') + store + .dispatch('commit/checkCommitStatus') .then(() => { - expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master'); + expect(service.getBranchData).toHaveBeenCalledWith( + 'abcproject', + 'master', + ); done(); }) .catch(done.fail); }); - it('returns true if current ref does not equal returned ID', (done) => { - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { id: '123' }, - }, - })); + it('returns true if current ref does not equal returned ID', done => { + spyOn(service, 'getBranchData').and.returnValue( + Promise.resolve({ + data: { + commit: { id: '123' }, + }, + }), + ); - store.dispatch('commit/checkCommitStatus') - .then((val) => { + store + .dispatch('commit/checkCommitStatus') + .then(val => { expect(val).toBeTruthy(); done(); @@ -145,15 +162,18 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('returns false if current ref equals returned ID', (done) => { - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { id: '1' }, - }, - })); + it('returns false if current ref equals returned ID', done => { + spyOn(service, 'getBranchData').and.returnValue( + Promise.resolve({ + data: { + commit: { id: '1' }, + }, + }), + ); - store.dispatch('commit/checkCommitStatus') - .then((val) => { + store + .dispatch('commit/checkCommitStatus') + .then(val => { expect(val).toBeFalsy(); done(); @@ -198,16 +218,17 @@ describe('IDE commit module actions', () => { }); store.state.openFiles = store.state.changedFiles; - store.state.changedFiles.forEach((changedFile) => { + store.state.changedFiles.forEach(changedFile => { store.state.entries[changedFile.path] = changedFile; }); }); - it('updates stores working reference', (done) => { - store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }) + it('updates stores working reference', done => { + store + .dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) .then(() => { expect( store.state.projects.abcproject.branches.master.workingReference, @@ -217,13 +238,14 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('resets all files changed status', (done) => { - store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }) + it('resets all files changed status', done => { + store + .dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) .then(() => { - store.state.openFiles.forEach((entry) => { + store.state.openFiles.forEach(entry => { expect(entry.changed).toBeFalsy(); }); }) @@ -231,11 +253,12 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('removes all changed files', (done) => { - store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }) + it('removes all changed files', done => { + store + .dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) .then(() => { expect(store.state.changedFiles.length).toBe(0); }) @@ -243,11 +266,12 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('sets files commit data', (done) => { - store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }) + it('sets files commit data', done => { + store + .dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) .then(() => { expect(f.lastCommit.message).toBe(data.message); }) @@ -255,11 +279,12 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('updates raw content for changed file', (done) => { - store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }) + it('updates raw content for changed file', done => { + store + .dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) .then(() => { expect(f.raw).toBe(f.content); }) @@ -267,25 +292,30 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('emits changed event for file', (done) => { - store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }) + it('emits changed event for file', done => { + store + .dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) .then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.content.${f.path}`, f.content); + expect(eventHub.$emit).toHaveBeenCalledWith( + `editor.update.model.content.${f.path}`, + f.content, + ); }) .then(done) .catch(done.fail); }); - it('pushes route to new branch if commitAction is new branch', (done) => { + it('pushes route to new branch if commitAction is new branch', done => { store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; - store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }) + store + .dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) .then(() => { expect(router.push).toHaveBeenCalledWith( `/project/abcproject/blob/master/${f.path}`, @@ -295,15 +325,18 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('resets stores commit actions', (done) => { + it('resets stores commit actions', done => { store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; - store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }) + store + .dispatch('commit/updateFilesAfterCommit', { + data, + branch, + }) .then(() => { - expect(store.state.commit.commitAction).not.toBe(consts.COMMIT_TO_NEW_BRANCH); + expect(store.state.commit.commitAction).not.toBe( + consts.COMMIT_TO_NEW_BRANCH, + ); }) .then(done) .catch(done.fail); @@ -330,7 +363,7 @@ describe('IDE commit module actions', () => { store.state.changedFiles[0].active = true; store.state.openFiles = store.state.changedFiles; - store.state.openFiles.forEach((f) => { + store.state.openFiles.forEach(f => { store.state.entries[f.path] = f; }); @@ -344,106 +377,128 @@ describe('IDE commit module actions', () => { describe('success', () => { beforeEach(() => { - spyOn(service, 'commit').and.returnValue(Promise.resolve({ - data: { - id: '123456', - short_id: '123', - message: 'test message', - committed_date: 'date', - stats: { - additions: '1', - deletions: '2', + spyOn(service, 'commit').and.returnValue( + Promise.resolve({ + data: { + id: '123456', + short_id: '123', + message: 'test message', + committed_date: 'date', + stats: { + additions: '1', + deletions: '2', + }, }, - }, - })); + }), + ); }); - it('calls service', (done) => { - store.dispatch('commit/commitChanges') + it('calls service', done => { + store + .dispatch('commit/commitChanges') .then(() => { expect(service.commit).toHaveBeenCalledWith('abcproject', { branch: jasmine.anything(), commit_message: 'testing 123', - actions: [{ - action: 'update', - file_path: jasmine.anything(), - content: jasmine.anything(), - encoding: jasmine.anything(), - }], + actions: [ + { + action: 'update', + file_path: jasmine.anything(), + content: jasmine.anything(), + encoding: jasmine.anything(), + }, + ], start_branch: 'master', }); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('pushes router to new route', (done) => { - store.dispatch('commit/commitChanges') + it('pushes router to new route', done => { + store + .dispatch('commit/commitChanges') .then(() => { expect(router.push).toHaveBeenCalledWith( - `/project/${store.state.currentProjectId}/blob/${store.getters['commit/newBranchName']}/changed`, + `/project/${store.state.currentProjectId}/blob/${ + store.getters['commit/newBranchName'] + }/changed`, ); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('sets last Commit Msg', (done) => { - store.dispatch('commit/commitChanges') + it('sets last Commit Msg', done => { + store + .dispatch('commit/commitChanges') .then(() => { expect(store.state.lastCommitMsg).toBe( 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.', ); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('adds commit data to changed files', (done) => { - store.dispatch('commit/commitChanges') + it('adds commit data to changed files', done => { + store + .dispatch('commit/commitChanges') .then(() => { - expect(store.state.openFiles[0].lastCommit.message).toBe('test message'); + expect(store.state.openFiles[0].lastCommit.message).toBe( + 'test message', + ); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('redirects to new merge request page', (done) => { + it('redirects to new merge request page', done => { spyOn(eventHub, '$on'); store.state.commit.commitAction = '3'; - store.dispatch('commit/commitChanges') + store + .dispatch('commit/commitChanges') .then(() => { expect(urlUtils.visitUrl).toHaveBeenCalledWith( - `webUrl/merge_requests/new?merge_request[source_branch]=${store.getters['commit/newBranchName']}&merge_request[target_branch]=master`, + `webUrl/merge_requests/new?merge_request[source_branch]=${ + store.getters['commit/newBranchName'] + }&merge_request[target_branch]=master`, ); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); }); describe('failed', () => { beforeEach(() => { - spyOn(service, 'commit').and.returnValue(Promise.resolve({ - data: { - message: 'failed message', - }, - })); + spyOn(service, 'commit').and.returnValue( + Promise.resolve({ + data: { + message: 'failed message', + }, + }), + ); }); - it('shows failed message', (done) => { - store.dispatch('commit/commitChanges') + it('shows failed message', done => { + store + .dispatch('commit/commitChanges') .then(() => { const alert = document.querySelector('.flash-container'); - expect(alert.textContent.trim()).toBe( - 'failed message', - ); + expect(alert.textContent.trim()).toBe('failed message'); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js index b1467bcf3c7..e396284ec2c 100644 --- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js @@ -1,6 +1,6 @@ -import commitState from 'ee/ide/stores/modules/commit/state'; -import * as consts from 'ee/ide/stores/modules/commit/constants'; -import * as getters from 'ee/ide/stores/modules/commit/getters'; +import commitState from '~/ide/stores/modules/commit/state'; +import * as consts from '~/ide/stores/modules/commit/constants'; +import * as getters from '~/ide/stores/modules/commit/getters'; describe('IDE commit module getters', () => { let state; @@ -38,26 +38,34 @@ describe('IDE commit module getters', () => { }; it('returns false when discardDraftButtonDisabled is false & changedFiles is not empty', () => { - expect(getters.commitButtonDisabled(state, localGetters, rootState)).toBeFalsy(); + expect( + getters.commitButtonDisabled(state, localGetters, rootState), + ).toBeFalsy(); }); it('returns true when discardDraftButtonDisabled is false & changedFiles is empty', () => { rootState.changedFiles.length = 0; - expect(getters.commitButtonDisabled(state, localGetters, rootState)).toBeTruthy(); + expect( + getters.commitButtonDisabled(state, localGetters, rootState), + ).toBeTruthy(); }); it('returns true when discardDraftButtonDisabled is true', () => { localGetters.discardDraftButtonDisabled = true; - expect(getters.commitButtonDisabled(state, localGetters, rootState)).toBeTruthy(); + expect( + getters.commitButtonDisabled(state, localGetters, rootState), + ).toBeTruthy(); }); it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => { localGetters.discardDraftButtonDisabled = false; rootState.changedFiles.length = 0; - expect(getters.commitButtonDisabled(state, localGetters, rootState)).toBeTruthy(); + expect( + getters.commitButtonDisabled(state, localGetters, rootState), + ).toBeTruthy(); }); }); @@ -65,7 +73,9 @@ describe('IDE commit module getters', () => { it('includes username, currentBranchId, patch & random number', () => { gon.current_username = 'username'; - const branch = getters.newBranchName(state, null, { currentBranchId: 'testing' }); + const branch = getters.newBranchName(state, null, { + currentBranchId: 'testing', + }); expect(branch).toMatch(/username-testing-patch-\d{5}$/); }); @@ -89,7 +99,7 @@ describe('IDE commit module getters', () => { expect(getters.branchName(state, null, rootState)).toBe('master'); }); - ['COMMIT_TO_NEW_BRANCH', 'COMMIT_TO_NEW_BRANCH_MR'].forEach((type) => { + ['COMMIT_TO_NEW_BRANCH', 'COMMIT_TO_NEW_BRANCH_MR'].forEach(type => { describe(type, () => { beforeEach(() => { Object.assign(state, { @@ -98,7 +108,9 @@ describe('IDE commit module getters', () => { }); it('uses newBranchName when not empty', () => { - expect(getters.branchName(state, localGetters, rootState)).toBe('state-newBranchName'); + expect(getters.branchName(state, localGetters, rootState)).toBe( + 'state-newBranchName', + ); }); it('uses getters newBranchName when state newBranchName is empty', () => { @@ -106,7 +118,9 @@ describe('IDE commit module getters', () => { newBranchName: '', }); - expect(getters.branchName(state, localGetters, rootState)).toBe('newBranchName'); + expect(getters.branchName(state, localGetters, rootState)).toBe( + 'newBranchName', + ); }); }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/mutations_spec.js b/spec/javascripts/ide/stores/modules/commit/mutations_spec.js index fa43e3d9d02..5de7a281d34 100644 --- a/spec/javascripts/ide/stores/modules/commit/mutations_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/mutations_spec.js @@ -1,5 +1,5 @@ -import commitState from 'ee/ide/stores/modules/commit/state'; -import mutations from 'ee/ide/stores/modules/commit/mutations'; +import commitState from '~/ide/stores/modules/commit/state'; +import mutations from '~/ide/stores/modules/commit/mutations'; describe('IDE commit module mutations', () => { let state; diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/javascripts/ide/stores/mutations/branch_spec.js index 1601769144a..a7167537ef2 100644 --- a/spec/javascripts/ide/stores/mutations/branch_spec.js +++ b/spec/javascripts/ide/stores/mutations/branch_spec.js @@ -1,5 +1,5 @@ -import mutations from 'ee/ide/stores/mutations/branch'; -import state from 'ee/ide/stores/state'; +import mutations from '~/ide/stores/mutations/branch'; +import state from '~/ide/stores/state'; describe('Multi-file store branch mutations', () => { let localState; diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js index 944639c3336..131380248e8 100644 --- a/spec/javascripts/ide/stores/mutations/file_spec.js +++ b/spec/javascripts/ide/stores/mutations/file_spec.js @@ -1,5 +1,5 @@ -import mutations from 'ee/ide/stores/mutations/file'; -import state from 'ee/ide/stores/state'; +import mutations from '~/ide/stores/mutations/file'; +import state from '~/ide/stores/state'; import { file } from '../../helpers'; describe('Multi-file store file mutations', () => { diff --git a/spec/javascripts/ide/stores/mutations/tree_spec.js b/spec/javascripts/ide/stores/mutations/tree_spec.js index e321eff8749..e6c085eaff6 100644 --- a/spec/javascripts/ide/stores/mutations/tree_spec.js +++ b/spec/javascripts/ide/stores/mutations/tree_spec.js @@ -1,5 +1,5 @@ -import mutations from 'ee/ide/stores/mutations/tree'; -import state from 'ee/ide/stores/state'; +import mutations from '~/ide/stores/mutations/tree'; +import state from '~/ide/stores/state'; import { file } from '../../helpers'; describe('Multi-file store tree mutations', () => { @@ -26,15 +26,17 @@ describe('Multi-file store tree mutations', () => { }); describe('SET_DIRECTORY_DATA', () => { - const data = [{ - name: 'tree', - }, - { - name: 'submodule', - }, - { - name: 'blob', - }]; + const data = [ + { + name: 'tree', + }, + { + name: 'submodule', + }, + { + name: 'blob', + }, + ]; it('adds directory data', () => { localState.trees['project/master'] = { diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index e0d214010d5..38162a470ad 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -1,5 +1,5 @@ -import mutations from 'ee/ide/stores/mutations'; -import state from 'ee/ide/stores/state'; +import mutations from '~/ide/stores/mutations'; +import state from '~/ide/stores/state'; import { file } from '../helpers'; describe('Multi-file store mutations', () => { diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js index a473d3a4294..f38ac6dd82f 100644 --- a/spec/javascripts/ide/stores/utils_spec.js +++ b/spec/javascripts/ide/stores/utils_spec.js @@ -1,4 +1,4 @@ -import * as utils from 'ee/ide/stores/utils'; +import * as utils from '~/ide/stores/utils'; describe('Multi-file store utils', () => { describe('setPageTitle', () => { @@ -13,11 +13,14 @@ describe('Multi-file store utils', () => { let localState; beforeEach(() => { - localState = [{ - path: '1', - }, { - path: '2', - }]; + localState = [ + { + path: '1', + }, + { + path: '2', + }, + ]; }); it('finds in the index of an entry by path', () => { @@ -34,13 +37,16 @@ describe('Multi-file store utils', () => { beforeEach(() => { localState = { - tree: [{ - type: 'tree', - name: 'test', - }, { - type: 'blob', - name: 'file', - }], + tree: [ + { + type: 'tree', + name: 'test', + }, + { + type: 'blob', + name: 'file', + }, + ], }; }); -- cgit v1.2.1 From 82dbb903cb697a628ccdf75860b34c8d87baa7e3 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 20 Mar 2018 14:39:51 +0000 Subject: fixed SCSS lint --- app/assets/stylesheets/framework/images.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 93cb83b3a4c..df1cafc9f8e 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -42,24 +42,31 @@ svg { &.s8 { @include svg-size(8px); } + &.s12 { @include svg-size(12px); } + &.s16 { @include svg-size(16px); } + &.s18 { @include svg-size(18px); } + &.s24 { @include svg-size(24px); } + &.s32 { @include svg-size(32px); } + &.s48 { @include svg-size(48px); } + &.s72 { @include svg-size(72px); } -- cgit v1.2.1 From e8b85811ab91aea8b2eed325cfdaac16b347b302 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 20 Mar 2018 15:00:58 +0000 Subject: remove un-used IDE helper module --- app/helpers/ide_helper.rb | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 app/helpers/ide_helper.rb diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb deleted file mode 100644 index f090ae71269..00000000000 --- a/app/helpers/ide_helper.rb +++ /dev/null @@ -1,14 +0,0 @@ -module IdeHelper - def ide_edit_button(project = @project, ref = @ref, path = @path, options = {}) - return unless blob = readable_blob(options, path, project, ref) - - common_classes = "btn js-edit-ide #{options[:extra_class]}" - - edit_button_tag(blob, - common_classes, - _('Web IDE'), - ide_edit_path(project, ref, path, options), - project, - ref) - end -end -- cgit v1.2.1 From 2467852ccefaf8af961f54cd601f6f322dfad6c4 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 20 Mar 2018 16:00:10 +0000 Subject: remove JS for cookie toggle --- app/assets/javascripts/profile/profile.js | 41 ++++++++++++++----------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index 3c1bef23446..0af34657d72 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -1,7 +1,6 @@ /* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */ import $ from 'jquery'; -import Cookies from 'js-cookie'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import flash from '../flash'; @@ -10,7 +9,6 @@ export default class Profile { constructor({ form } = {}) { this.onSubmitForm = this.onSubmitForm.bind(this); this.form = form || $('.edit-user'); - this.newRepoActivated = Cookies.get('new_repo'); this.setRepoRadio(); this.bindEvents(); this.initAvatarGlCrop(); @@ -23,21 +21,28 @@ export default class Profile { modalCrop: '.modal-profile-crop', pickImageEl: '.js-choose-user-avatar-button', uploadImageBtn: '.js-upload-user-avatar', - modalCropImg: '.modal-profile-crop-image' + modalCropImg: '.modal-profile-crop-image', }; - this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); + this.avatarGlCrop = $('.js-user-avatar-input') + .glCrop(cropOpts) + .data('glcrop'); } bindEvents() { - $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); - $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie); + $('.js-preferences-form').on( + 'change.preference', + 'input[type=radio]', + this.submitForm, + ); $('#user_notification_email').on('change', this.submitForm); $('#user_notified_of_own_activity').on('change', this.submitForm); this.form.on('submit', this.onSubmitForm); } submitForm() { - return $(this).parents('form').submit(); + return $(this) + .parents('form') + .submit(); } onSubmitForm(e) { @@ -59,21 +64,13 @@ export default class Profile { url: this.form.attr('action'), data: formData, }) - .then(({ data }) => flash(data.message, 'notice')) - .then(() => { - window.scrollTo(0, 0); - // Enable submit button after requests ends - self.form.find(':input[disabled]').enable(); - }) - .catch(error => flash(error.message)); - } - - setNewRepoCookie() { - if (this.value === 'off') { - Cookies.remove('new_repo'); - } else { - Cookies.set('new_repo', true, { expires_in: 365 }); - } + .then(({ data }) => flash(data.message, 'notice')) + .then(() => { + window.scrollTo(0, 0); + // Enable submit button after requests ends + self.form.find(':input[disabled]').enable(); + }) + .catch(error => flash(error.message)); } setRepoRadio() { -- cgit v1.2.1 From edf2060c6d91db079e30ed56178be2eabc352132 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 20 Mar 2018 17:00:56 +0000 Subject: fixed ide_edit_button not existing --- app/helpers/blob_helper.rb | 11 +++++++++++ app/views/projects/blob/_header.html.haml | 1 + 2 files changed, 12 insertions(+) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 5ff09b23a78..2b440e4d584 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -33,6 +33,17 @@ module BlobHelper ref) end + def ide_edit_button(project = @project, ref = @ref, path = @path, options = {}) + return unless blob = readable_blob(options, path, project, ref) + + edit_button_tag(blob, + 'btn btn-default', + _('Web IDE'), + ide_edit_path(project, ref, path, options), + project, + ref) + end + def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) return unless current_user diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml index f93bb02acb9..1b150ec3e5c 100644 --- a/app/views/projects/blob/_header.html.haml +++ b/app/views/projects/blob/_header.html.haml @@ -12,6 +12,7 @@ .btn-group{ role: "group" }< = edit_blob_button + = ide_edit_button - if current_user = replace_blob_link = delete_blob_link -- cgit v1.2.1