diff --git a/app/seeders/demo_data/query_builder.rb b/app/seeders/demo_data/query_builder.rb index 4abe866528..17ba39c7f0 100644 --- a/app/seeders/demo_data/query_builder.rb +++ b/app/seeders/demo_data/query_builder.rb @@ -74,10 +74,15 @@ module DemoData end def create_view(query) + type = config.fetch(:module, 'work_packages_table') View.create!( - type: config.fetch(:module, 'work_packages_table'), + type: type, query: query ) + + # Save information that a view has been seeded. + # This information can be used for example in the onboarding tour + Setting["demo_view_of_type_#{type}_seeded"] = 'true' end def set_project!(attr) diff --git a/app/views/layouts/_common_head.html.erb b/app/views/layouts/_common_head.html.erb index 1cb58073b3..1940d6b5da 100644 --- a/app/views/layouts/_common_head.html.erb +++ b/app/views/layouts/_common_head.html.erb @@ -21,6 +21,7 @@ <% if Setting.demo_projects_available %><% end %> <% if Setting.boards_demo_data_available %><% end %> +<% if Setting.demo_view_of_type_team_planner_seeded %><% end %> <%= csrf_meta_tags %> <%= initializer_meta_tag %> @@ -53,4 +54,4 @@ <% end %> <% end %> <% end %> -<%= crowdin_in_context_translation %> \ No newline at end of file +<%= crowdin_in_context_translation %> diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index a2e9bc670f..854cd63a0b 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -556,6 +556,12 @@ en: create_button: 'The + Create button will add a new work package to your project.' timeline_button: 'You can activate the Gantt chart view to create a timeline for your project.' timeline: 'Here you can edit your project plan, create new work packages, such as tasks, milestones, phases, and more, as well as add dependencies. All team members can see and update the latest plan at any time.' + team_planner: + overview: 'The team planner lets you visually assign tasks to team members and get an overview of who is working on what.' + calendar: 'The weekly or biweekly planning board displays all work packages assigned to your team members.' + add_assignee: 'To get started, add assignees to the team planner.' + add_existing: 'Search for existing work packages and drag them to the team planner to instantly assign them to a team member and define start and end dates.' + card: 'Drag work packages horizontally to move them backwards or forwards in time, drag the edges to change start and end dates and even drag them vertically to a different row to assign them to another member.' notifications: title: "Notifications" @@ -1267,7 +1273,7 @@ en: load_error_message: 'There was an error loading the form' validation_error_message: 'Please fix the errors present in the form' advanced_settings: 'Advanced settings' - + spot: filter_chip: remove: 'Remove' diff --git a/config/settings.yml b/config/settings.yml index 8af73f083d..021a5e6f87 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -350,6 +350,10 @@ demo_projects_available: default: false boards_demo_data_available: default: false +demo_view_of_type_work_packages_table_seeded: + default: false +demo_view_of_type_team_planner_seeded: + default: false security_badge_displayed: default: true installation_uuid: diff --git a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts index 725239bd6b..5e7b4e12ef 100644 --- a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts +++ b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts @@ -14,6 +14,7 @@ import { scrumBacklogsTourSteps, scrumTaskBoardTourSteps, } from 'core-app/core/setup/globals/onboarding/tours/backlogs_tour'; +import { teamPlannerTourSteps } from 'core-app/core/setup/globals/onboarding/tours/team_planners_tour'; require('core-vendor/enjoyhint'); @@ -68,23 +69,36 @@ function startTour(steps:OnboardingStep[]) { window.onboardingTourInstance.run(); } +function moduleVisible(name:string):boolean { + return document.getElementsByClassName(`${name}-view-menu-item`).length > 0; +} + function mainTour() { initializeTour('mainTourFinished'); const boardsDemoDataAvailable = jQuery('meta[name=boards_demo_data_available]').attr('content') === 'true'; + const teamPlannerDemoDataAvailable = jQuery('meta[name=demo_view_of_type_team_planner_seeded]').attr('content') === 'true'; const eeTokenAvailable = !jQuery('body').hasClass('ee-banners-visible'); waitForElement('.work-package--results-tbody', '#content', () => { - let steps:OnboardingStep[]; + let steps:OnboardingStep[] = wpOnboardingTourSteps(); + + // Check for EE edition + if (eeTokenAvailable) { + // ... and available seed data of boards. + // Then add boards to the tour, otherwise skip it. + if (boardsDemoDataAvailable && moduleVisible('board')) { + steps = steps.concat(boardTourSteps()); + } - // Check for EE edition, and available seed data of boards. - // Then add boards to the tour, otherwise skip it. - if (eeTokenAvailable && boardsDemoDataAvailable) { - steps = wpOnboardingTourSteps().concat(boardTourSteps()).concat(menuTourSteps()); - } else { - steps = wpOnboardingTourSteps().concat(menuTourSteps()); + // ... same for team planners + if (teamPlannerDemoDataAvailable && moduleVisible('team-planner')) { + steps = steps.concat(teamPlannerTourSteps()); + } } + steps = steps.concat(menuTourSteps()); + startTour(steps); }); } diff --git a/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts new file mode 100644 index 0000000000..05254a3f64 --- /dev/null +++ b/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts @@ -0,0 +1,52 @@ +import { waitForElement } from 'core-app/core/setup/globals/onboarding/helpers'; +import { OnboardingStep } from 'core-app/core/setup/globals/onboarding/onboarding_tour'; + +export function teamPlannerTourSteps():OnboardingStep[] { + return [ + { + 'next .team-planner-view-menu-item': I18n.t('js.onboarding.steps.team_planner.overview'), + showSkip: false, + nextButton: { text: I18n.t('js.onboarding.buttons.next') }, + onNext() { + jQuery('.team-planner-view-menu-item ~ .toggler')[0].click(); + waitForElement('.op-sidemenu--items', '#main-menu', () => { + jQuery(".op-sidemenu--item-action:contains('Team planner')")[0].click(); + }); + }, + }, + { + 'next [data-qa-selector="op-team-planner--calendar-pane"]': I18n.t('js.onboarding.steps.team_planner.calendar'), + showSkip: false, + nextButton: { text: I18n.t('js.onboarding.buttons.next') }, + containerClass: '-dark -hidden-arrow', + timeout: () => new Promise((resolve) => { + waitForElement('.op-wp-single-card', '#content', () => { + resolve(undefined); + }); + }), + }, + { + 'next [data-qa-selector="tp-assignee-add-button"]': I18n.t('js.onboarding.steps.team_planner.add_assignee'), + showSkip: false, + nextButton: { text: I18n.t('js.onboarding.buttons.next') }, + }, + { + 'next [data-qa-selector="op-team-planner--add-existing-toggle"]': I18n.t('js.onboarding.steps.team_planner.add_existing'), + showSkip: false, + nextButton: { text: I18n.t('js.onboarding.buttons.next') }, + }, + { + 'next [data-qa-selector="op-wp-single-card"]': I18n.t('js.onboarding.steps.team_planner.card'), + showSkip: false, + nextButton: { text: I18n.t('js.onboarding.buttons.next') }, + onNext() { + const backArrows = Array.from(document.getElementsByClassName('main-menu--arrow-left-to-project')); + const teamPlannerBackArrow = backArrows.find((backArrow) => (backArrow.nextElementSibling as HTMLElement).innerText === 'Team planners') as HTMLElement; + + if (teamPlannerBackArrow) { + teamPlannerBackArrow.click(); + } + }, + }, + ]; +} diff --git a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.html b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.html index 8c62172a5d..dbcbccd675 100644 --- a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.html +++ b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.html @@ -26,7 +26,10 @@ -
+
diff --git a/modules/team_planner/spec/features/onboarding/team_planner_onboarding_tour_spec.rb b/modules/team_planner/spec/features/onboarding/team_planner_onboarding_tour_spec.rb new file mode 100644 index 0000000000..3766241a82 --- /dev/null +++ b/modules/team_planner/spec/features/onboarding/team_planner_onboarding_tour_spec.rb @@ -0,0 +1,113 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require 'spec_helper' +require_relative './../../support/onboarding/onboarding_steps' + +describe 'team planner onboarding tour', js: true do + let(:next_button) { find('.enjoyhint_next_btn') } + + let(:demo_project) do + create :project, + name: 'Demo project', + identifier: 'demo-project', + public: true, + enabled_module_names: %w[work_package_tracking wiki team_planner_view] + end + let(:scrum_project) do + create :project, + name: 'Scrum project', + identifier: 'your-scrum-project', + public: true, + enabled_module_names: %w[work_package_tracking wiki] + end + + let(:user) do + create :admin, + member_in_project: demo_project, + member_with_permissions: %w[view_work_packages edit_work_packages add_work_packages + view_team_planner manage_team_planner save_queries manage_public_queries + work_package_assigned] + end + + let!(:wp1) do + create(:work_package, + project: demo_project, + assigned_to: user, + start_date: Time.zone.today, + due_date: Time.zone.today) + end + let!(:wp2) { create(:work_package, project: scrum_project) } + + let(:query) { create :query, user: user, project: demo_project, public: true, name: 'Team planner' } + let(:team_plan) do + create :view_team_planner, + query: query, + assignees: [user], + projects: [demo_project, scrum_project] + end + + before do + team_plan + with_enterprise_token :team_planner_view + login_as user + + allow(Setting).to receive(:demo_projects_available).and_return(true) + allow(Setting).to receive(:demo_view_of_type_team_planner_seeded).and_return(true) + end + + after do + # Clear session to avoid that the onboarding tour starts + page.execute_script("window.sessionStorage.clear();") + end + + context 'as a new user' do + it 'I see the team planner onboarding tour in the demo project' do + # Set the tour parameter so that we can start on the wp page + visit "/projects/#{demo_project.identifier}/work_packages?start_onboarding_tour=true" + + step_through_onboarding_wp_tour demo_project, wp1 + + step_through_onboarding_team_planner_tour + + step_through_onboarding_main_menu_tour has_full_capabilities: true + end + + it "I do not see the team planner onboarding tour in the scrum project" do + # Set sessionStorage value so that the tour knows that it is in the scum tour + page.execute_script("window.sessionStorage.setItem('openProject-onboardingTour', 'startMainTourFromBacklogs');") + + # Set the tour parameter so that we can start on the wp page + visit "/projects/#{scrum_project.identifier}/work_packages?start_onboarding_tour=true" + + step_through_onboarding_wp_tour scrum_project, wp2 + + step_through_onboarding_main_menu_tour has_full_capabilities: true + end + end +end diff --git a/modules/team_planner/spec/support/onboarding/onboarding_steps.rb b/modules/team_planner/spec/support/onboarding/onboarding_steps.rb new file mode 100644 index 0000000000..9bd46d2222 --- /dev/null +++ b/modules/team_planner/spec/support/onboarding/onboarding_steps.rb @@ -0,0 +1,54 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module OnboardingSteps + def step_through_onboarding_team_planner_tour + next_button.click + expect(page).to have_text sanitize_string(I18n.t('js.onboarding.steps.team_planner.overview')), normalize_ws: true + + next_button.click + expect(page) + .to have_text sanitize_string(I18n.t('js.onboarding.steps.team_planner.calendar')), normalize_ws: true, wait: 5 + + next_button.click + expect(page) + .to have_text sanitize_string(I18n.t('js.onboarding.steps.team_planner.add_assignee')), normalize_ws: true + + next_button.click + expect(page) + .to have_text sanitize_string(I18n.t('js.onboarding.steps.team_planner.add_existing')), normalize_ws: true + + next_button.click + expect(page) + .to have_text sanitize_string(I18n.t('js.onboarding.steps.team_planner.card')), normalize_ws: true + end +end + +RSpec.configure do |config| + config.include OnboardingSteps +end diff --git a/spec/factories/view_factory.rb b/spec/factories/view_factory.rb index 27d8051d77..426af819fe 100644 --- a/spec/factories/view_factory.rb +++ b/spec/factories/view_factory.rb @@ -34,10 +34,6 @@ FactoryBot.define do factory :view_work_packages_table, parent: :view - factory :view_team_planner, parent: :view do - type { 'team_planner' } - end - factory :view_work_packages_calendar, parent: :view do type { 'work_packages_calendar' } end diff --git a/spec/factories/view_team_planner_factory.rb b/spec/factories/view_team_planner_factory.rb new file mode 100644 index 0000000000..01a23a647f --- /dev/null +++ b/spec/factories/view_team_planner_factory.rb @@ -0,0 +1,53 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +FactoryBot.define do + factory :view_team_planner, parent: :view do + type { 'team_planner' } + transient do + assignees { [] } + projects { [] } + end + + callback(:after_create) do |view, evaluator| + query = view.query + + if evaluator.assignees.any? + query.add_filter('assigned_to_id', '=', evaluator.assignees.map(&:id).uniq) + end + + if evaluator.projects.any? + query.add_filter('project_id', '=', ([query.project.id] + evaluator.projects.map(&:id)).uniq) + end + + User.system.run_given do + query.save! + end + end + end +end