Skip to content
Snippets Groups Projects
Commit 5fbadd1b authored by Kushal Pandya's avatar Kushal Pandya
Browse files

Merge branch '23466-convert-groups-overview-tabs-to-vue' into 'master'

Convert group overview tabs to Vue

See merge request !95850
parents bd4e24a2 327a1fb6
No related branches found
No related tags found
2 merge requests!97251Synchronize ruby3 branch manually due to conflicts,!95850Convert group overview tabs to Vue
Pipeline #633214167 passed with warnings
Pipeline: GitLab

#633232730

    Pipeline: GitLab

    #633232515

      Showing
      with 335 additions and 44 deletions
      ......@@ -17,11 +17,6 @@ export default {
      GlLoadingIcon,
      EmptyState,
      },
      inject: {
      renderEmptyState: {
      default: false,
      },
      },
      props: {
      action: {
      type: String,
      ......@@ -45,6 +40,11 @@ export default {
      type: Boolean,
      required: true,
      },
      renderEmptyState: {
      type: Boolean,
      required: false,
      default: false,
      },
      },
      data() {
      return {
      ......@@ -224,6 +224,9 @@ export default {
      },
      showLegacyEmptyState() {
      const { containerEl } = this;
      if (!containerEl) return;
      const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS);
      const emptyStateEl = containerEl.querySelector('.empty-state');
      ......
      <script>
      import { GlTabs, GlTab } from '@gitlab/ui';
      import { __ } from '~/locale';
      import GroupsStore from '../store/groups_store';
      import GroupsService from '../service/groups_service';
      import {
      ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
      ACTIVE_TAB_SHARED,
      ACTIVE_TAB_ARCHIVED,
      } from '../constants';
      import GroupsApp from './app.vue';
      export default {
      components: { GlTabs, GlTab, GroupsApp },
      inject: ['endpoints'],
      data() {
      return {
      tabs: [
      {
      title: this.$options.i18n.subgroupsAndProjects,
      key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
      renderEmptyState: true,
      lazy: false,
      service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
      store: new GroupsStore({ showSchemaMarkup: true }),
      },
      {
      title: this.$options.i18n.sharedProjects,
      key: ACTIVE_TAB_SHARED,
      renderEmptyState: false,
      lazy: true,
      service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]),
      store: new GroupsStore(),
      },
      {
      title: this.$options.i18n.archivedProjects,
      key: ACTIVE_TAB_ARCHIVED,
      renderEmptyState: false,
      lazy: true,
      service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]),
      store: new GroupsStore(),
      },
      ],
      activeTabIndex: 0,
      };
      },
      methods: {
      handleTabInput(tabIndex) {
      this.activeTabIndex = tabIndex;
      const tab = this.tabs[tabIndex];
      tab.lazy = false;
      },
      },
      i18n: {
      subgroupsAndProjects: __('Subgroups and projects'),
      sharedProjects: __('Shared projects'),
      archivedProjects: __('Archived projects'),
      },
      };
      </script>
      <template>
      <gl-tabs content-class="gl-pt-0" :value="activeTabIndex" @input="handleTabInput">
      <gl-tab
      v-for="{ key, title, renderEmptyState, lazy, service, store } in tabs"
      :key="key"
      :title="title"
      :lazy="lazy"
      >
      <groups-app
      :action="key"
      :service="service"
      :store="store"
      :hide-projects="false"
      :render-empty-state="renderEmptyState"
      />
      </gl-tab>
      </gl-tabs>
      </template>
      ......@@ -52,7 +52,6 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
      newSubgroupIllustration,
      newProjectIllustration,
      emptySubgroupIllustration,
      renderEmptyState,
      canCreateSubgroups,
      canCreateProjects,
      currentGroupVisibility,
      ......@@ -65,7 +64,6 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
      newSubgroupIllustration,
      newProjectIllustration,
      emptySubgroupIllustration,
      renderEmptyState: parseBoolean(renderEmptyState),
      canCreateSubgroups: parseBoolean(canCreateSubgroups),
      canCreateProjects: parseBoolean(canCreateProjects),
      currentGroupVisibility,
      ......@@ -75,6 +73,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
      const { dataset } = dataEl || this.$options.el;
      const hideProjects = parseBoolean(dataset.hideProjects);
      const showSchemaMarkup = parseBoolean(dataset.showSchemaMarkup);
      const renderEmptyState = parseBoolean(dataset.renderEmptyState);
      const service = new GroupsService(endpoint || dataset.endpoint);
      const store = new GroupsStore({ hideProjects, showSchemaMarkup });
      ......@@ -83,6 +82,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
      store,
      service,
      hideProjects,
      renderEmptyState,
      loading: true,
      containerId,
      };
      ......@@ -119,6 +119,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
      store: this.store,
      service: this.service,
      hideProjects: this.hideProjects,
      renderEmptyState: this.renderEmptyState,
      containerId: this.containerId,
      },
      });
      ......
      import Vue from 'vue';
      import { GlToast } from '@gitlab/ui';
      import { parseBoolean } from '~/lib/utils/common_utils';
      import GroupFolder from './components/group_folder.vue';
      import GroupItem from './components/group_item.vue';
      import {
      ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
      ACTIVE_TAB_SHARED,
      ACTIVE_TAB_ARCHIVED,
      } from './constants';
      import OverviewTabs from './components/overview_tabs.vue';
      export const initGroupOverviewTabs = () => {
      const el = document.getElementById('js-group-overview-tabs');
      if (!el) return false;
      Vue.component('GroupFolder', GroupFolder);
      Vue.component('GroupItem', GroupItem);
      Vue.use(GlToast);
      const {
      newSubgroupPath,
      newProjectPath,
      newSubgroupIllustration,
      newProjectIllustration,
      emptySubgroupIllustration,
      canCreateSubgroups,
      canCreateProjects,
      currentGroupVisibility,
      subgroupsAndProjectsEndpoint,
      sharedProjectsEndpoint,
      archivedProjectsEndpoint,
      } = el.dataset;
      return new Vue({
      el,
      provide: {
      newSubgroupPath,
      newProjectPath,
      newSubgroupIllustration,
      newProjectIllustration,
      emptySubgroupIllustration,
      canCreateSubgroups: parseBoolean(canCreateSubgroups),
      canCreateProjects: parseBoolean(canCreateProjects),
      currentGroupVisibility,
      endpoints: {
      [ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]: subgroupsAndProjectsEndpoint,
      [ACTIVE_TAB_SHARED]: sharedProjectsEndpoint,
      [ACTIVE_TAB_ARCHIVED]: archivedProjectsEndpoint,
      },
      },
      render(createElement) {
      return createElement(OverviewTabs);
      },
      });
      };
      import { initGroupOverviewTabs } from '~/groups/init_overview_tabs';
      import initGroupDetails from '../shared/group_details';
      initGroupDetails('details');
      initGroupOverviewTabs();
      import leaveByUrl from '~/namespaces/leave_by_url';
      import { initGroupOverviewTabs } from '~/groups/init_overview_tabs';
      import initGroupDetails from '../shared/group_details';
      leaveByUrl('group');
      initGroupDetails();
      initGroupOverviewTabs();
      ......@@ -172,6 +172,15 @@ def subgroups_and_projects_list_app_data(group)
      }
      end
      def group_overview_tabs_app_data(group)
      {
      subgroups_and_projects_endpoint: group_children_path(group, format: :json),
      shared_projects_endpoint: group_shared_projects_path(group, format: :json),
      archived_projects_endpoint: group_children_path(group, format: :json, archived: 'only'),
      current_group_visibility: group.visibility
      }.merge(subgroups_and_projects_list_app_data(group))
      end
      def enabled_git_access_protocol_options_for_group
      case ::Gitlab::CurrentSettings.enabled_git_access_protocol
      when nil, ""
      ......
      ......@@ -33,33 +33,36 @@
      = render_if_exists 'groups/group_activity_analytics', group: @group
      .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
      .top-area.group-nav-container.justify-content-between
      .scrolling-tabs-container.inner-page-scroll-tabs
      .fade-left= sprite_icon('chevron-lg-left', size: 12)
      .fade-right= sprite_icon('chevron-lg-right', size: 12)
      -# `item_active` is set to `false` as the active state is set by `app/assets/javascripts/pages/groups/shared/group_details.js`
      -# TODO: Replace this approach in https://gitlab.com/gitlab-org/gitlab/-/issues/23466
      = gl_tabs_nav({ class: 'nav-links scrolling-tabs gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do
      = gl_tab_link_to group_path, item_active: false, tab_class: 'js-subgroups_and_projects-tab', data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab' } do
      = _("Subgroups and projects")
      = gl_tab_link_to group_shared_path, item_active: false, tab_class: 'js-shared-tab', data: { target: 'div#shared', action: 'shared', toggle: 'tab' } do
      = _("Shared projects")
      = gl_tab_link_to group_archived_path, item_active: false, tab_class: 'js-archived-tab', data: { target: 'div#archived', action: 'archived', toggle: 'tab' } do
      = _("Archived projects")
      - if Feature.enabled?(:group_overview_tabs_vue, @group)
      #js-group-overview-tabs{ data: group_overview_tabs_app_data(@group) }
      - else
      .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
      .top-area.group-nav-container.justify-content-between
      .scrolling-tabs-container.inner-page-scroll-tabs
      .fade-left= sprite_icon('chevron-lg-left', size: 12)
      .fade-right= sprite_icon('chevron-lg-right', size: 12)
      -# `item_active` is set to `false` as the active state is set by `app/assets/javascripts/pages/groups/shared/group_details.js`
      -# TODO: Replace this approach in https://gitlab.com/gitlab-org/gitlab/-/issues/23466
      = gl_tabs_nav({ class: 'nav-links scrolling-tabs gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do
      = gl_tab_link_to group_path, item_active: false, tab_class: 'js-subgroups_and_projects-tab', data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab' } do
      = _("Subgroups and projects")
      = gl_tab_link_to group_shared_path, item_active: false, tab_class: 'js-shared-tab', data: { target: 'div#shared', action: 'shared', toggle: 'tab' } do
      = _("Shared projects")
      = gl_tab_link_to group_archived_path, item_active: false, tab_class: 'js-archived-tab', data: { target: 'div#archived', action: 'archived', toggle: 'tab' } do
      = _("Archived projects")
      .nav-controls.d-block.d-md-flex
      .group-search
      = render "shared/groups/search_form"
      .nav-controls.d-block.d-md-flex
      .group-search
      = render "shared/groups/search_form"
      = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash
      = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash
      .tab-content
      #subgroups_and_projects.tab-pane
      = render "subgroups_and_projects", group: @group
      .tab-content
      #subgroups_and_projects.tab-pane
      = render "subgroups_and_projects", group: @group
      #shared.tab-pane
      = render "shared_projects", group: @group
      #shared.tab-pane
      = render "shared_projects", group: @group
      #archived.tab-pane
      = render "archived_projects", group: @group
      #archived.tab-pane
      = render "archived_projects", group: @group
      ---
      name: group_overview_tabs_vue
      introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95850
      rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370872
      milestone: '15.4'
      type: development
      group: group::workspace
      default_enabled: false
      import '~/pages/groups/show';
      import initGroupAnalytics from 'ee/analytics/group_analytics/group_analytics_bundle';
      import { shouldQrtlyReconciliationMount } from 'ee/billings/qrtly_reconciliation';
      import leaveByUrl from '~/namespaces/leave_by_url';
      import initGroupDetails from '~/pages/groups/shared/group_details';
      import initVueAlerts from '~/vue_alerts';
      leaveByUrl('group');
      initGroupDetails();
      initGroupAnalytics();
      initVueAlerts();
      shouldQrtlyReconciliationMount();
      ......@@ -331,6 +331,7 @@
      end
      it 'does not include structured markup in shared projects tab', :aggregate_failures, :js do
      stub_feature_flags(group_overview_tabs_vue: false)
      other_project = create(:project, :public)
      other_project.project_group_links.create!(group: group)
      ......@@ -342,6 +343,7 @@
      end
      it 'does not include structured markup in archived projects tab', :aggregate_failures, :js do
      stub_feature_flags(group_overview_tabs_vue: false)
      project.update!(archived: true)
      visit group_archived_path(group)
      ......
      ......@@ -24,6 +24,7 @@
      end
      it "is set on the group_canonical_path" do
      stub_feature_flags(group_overview_tabs_vue: false)
      visit(group_canonical_path(group))
      within '[data-testid=group_sort_by_dropdown]' do
      ......@@ -32,6 +33,7 @@
      end
      it "is set on the details_group_path" do
      stub_feature_flags(group_overview_tabs_vue: false)
      visit(details_group_path(group))
      within '[data-testid=group_sort_by_dropdown]' do
      ......@@ -64,6 +66,7 @@
      context 'from group homepage', :js do
      before do
      stub_feature_flags(group_overview_tabs_vue: false)
      sign_in(user)
      visit(group_canonical_path(group))
      within '[data-testid=group_sort_by_dropdown]' do
      ......@@ -77,6 +80,7 @@
      context 'from group details', :js do
      before do
      stub_feature_flags(group_overview_tabs_vue: false)
      sign_in(user)
      visit(details_group_path(group))
      within '[data-testid=group_sort_by_dropdown]' do
      ......
      ......@@ -40,7 +40,7 @@ describe('AppComponent', () => {
      const store = new GroupsStore({ hideProjects: false });
      const service = new GroupsService(mockEndpoint);
      const createShallowComponent = ({ propsData = {}, provide = {} } = {}) => {
      const createShallowComponent = ({ propsData = {} } = {}) => {
      store.state.pageInfo = mockPageInfo;
      wrapper = shallowMount(appComponent, {
      propsData: {
      ......@@ -53,10 +53,6 @@ describe('AppComponent', () => {
      mocks: {
      $toast,
      },
      provide: {
      renderEmptyState: false,
      ...provide,
      },
      });
      vm = wrapper.vm;
      };
      ......@@ -402,8 +398,7 @@ describe('AppComponent', () => {
      ({ action, groups, fromSearch, renderEmptyState, expected }) => {
      it(expected ? 'renders empty state' : 'does not render empty state', async () => {
      createShallowComponent({
      propsData: { action },
      provide: { renderEmptyState },
      propsData: { action, renderEmptyState },
      });
      vm.updateGroups(groups, fromSearch);
      ......@@ -420,7 +415,6 @@ describe('AppComponent', () => {
      it('renders legacy empty state', async () => {
      createShallowComponent({
      propsData: { action: 'subgroups_and_projects' },
      provide: { renderEmptyState: false },
      });
      vm.updateGroups([], false);
      ......
      import { GlTab } from '@gitlab/ui';
      import { nextTick } from 'vue';
      import AxiosMockAdapter from 'axios-mock-adapter';
      import { mountExtended } from 'helpers/vue_test_utils_helper';
      import OverviewTabs from '~/groups/components/overview_tabs.vue';
      import GroupsApp from '~/groups/components/app.vue';
      import GroupsStore from '~/groups/store/groups_store';
      import GroupsService from '~/groups/service/groups_service';
      import {
      ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
      ACTIVE_TAB_SHARED,
      ACTIVE_TAB_ARCHIVED,
      } from '~/groups/constants';
      import axios from '~/lib/utils/axios_utils';
      describe('OverviewTabs', () => {
      let wrapper;
      const endpoints = {
      subgroups_and_projects: '/groups/foobar/-/children.json',
      shared: '/groups/foobar/-/shared_projects.json',
      archived: '/groups/foobar/-/children.json?archived=only',
      };
      const createComponent = async () => {
      wrapper = mountExtended(OverviewTabs, {
      provide: {
      endpoints,
      },
      });
      await nextTick();
      };
      const findTabPanels = () => wrapper.findAllComponents(GlTab);
      const findTab = (name) => wrapper.findByRole('tab', { name });
      afterEach(() => {
      wrapper.destroy();
      });
      beforeEach(async () => {
      // eslint-disable-next-line no-new
      new AxiosMockAdapter(axios);
      await createComponent();
      });
      it('renders `Subgroups and projects` tab with `GroupsApp` component', async () => {
      const tabPanel = findTabPanels().at(0);
      expect(tabPanel.vm.$attrs).toMatchObject({
      title: OverviewTabs.i18n.subgroupsAndProjects,
      lazy: false,
      });
      expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
      action: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
      store: new GroupsStore({ showSchemaMarkup: true }),
      service: new GroupsService(endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
      hideProjects: false,
      renderEmptyState: true,
      });
      });
      it('renders `Shared projects` tab and renders `GroupsApp` component after clicking tab', async () => {
      const tabPanel = findTabPanels().at(1);
      expect(tabPanel.vm.$attrs).toMatchObject({
      title: OverviewTabs.i18n.sharedProjects,
      lazy: true,
      });
      await findTab(OverviewTabs.i18n.sharedProjects).trigger('click');
      expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
      action: ACTIVE_TAB_SHARED,
      store: new GroupsStore(),
      service: new GroupsService(endpoints[ACTIVE_TAB_SHARED]),
      hideProjects: false,
      renderEmptyState: false,
      });
      expect(tabPanel.vm.$attrs.lazy).toBe(false);
      });
      it('renders `Archived projects` tab and renders `GroupsApp` component after clicking tab', async () => {
      const tabPanel = findTabPanels().at(2);
      expect(tabPanel.vm.$attrs).toMatchObject({
      title: OverviewTabs.i18n.archivedProjects,
      lazy: true,
      });
      await findTab(OverviewTabs.i18n.archivedProjects).trigger('click');
      expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
      action: ACTIVE_TAB_ARCHIVED,
      store: new GroupsStore(),
      service: new GroupsService(endpoints[ACTIVE_TAB_ARCHIVED]),
      hideProjects: false,
      renderEmptyState: false,
      });
      expect(tabPanel.vm.$attrs.lazy).toBe(false);
      });
      });
      ......@@ -520,6 +520,29 @@
      end
      end
      describe '#group_overview_tabs_app_data' do
      let_it_be(:group) { create(:group) }
      let_it_be(:user) { create(:user) }
      before do
      allow(helper).to receive(:current_user).and_return(user)
      allow(helper).to receive(:can?).with(user, :create_subgroup, group) { true }
      allow(helper).to receive(:can?).with(user, :create_projects, group) { true }
      end
      it 'returns expected hash' do
      expect(helper.group_overview_tabs_app_data(group)).to match(
      {
      subgroups_and_projects_endpoint: including("/groups/#{group.path}/-/children.json"),
      shared_projects_endpoint: including("/groups/#{group.path}/-/shared_projects.json"),
      archived_projects_endpoint: including("/groups/#{group.path}/-/children.json?archived=only"),
      current_group_visibility: group.visibility
      }.merge(helper.group_overview_tabs_app_data(group))
      )
      end
      end
      describe "#enabled_git_access_protocol_options_for_group" do
      subject { helper.enabled_git_access_protocol_options_for_group }
      ......
      0% Loading or .
      You are about to add 0 people to the discussion. Proceed with caution.
      Finish editing this message first!
      Please register or to comment