+
+
+
+
+ {{ value }}
+
+
+
+
diff --git a/app/assets/javascripts/tags/index.js b/app/assets/javascripts/tags/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..68510f3fe3a7100678bca8faeae081ac0be803b7
--- /dev/null
+++ b/app/assets/javascripts/tags/index.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import SortDropdown from './components/sort_dropdown.vue';
+
+const mountDropdownApp = (el) => {
+ const { sortOptions, filterTagsPath } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'SortTagsDropdownApp',
+ components: {
+ SortDropdown,
+ },
+ provide: {
+ sortOptions: JSON.parse(sortOptions),
+ filterTagsPath,
+ },
+ render: (createElement) => createElement(SortDropdown),
+ });
+};
+
+export default () => {
+ const el = document.getElementById('js-tags-sort-dropdown');
+ return el ? mountDropdownApp(el) : null;
+};
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 94b0473e1f38769e4ddf8aaee119be0e26dd5662..3bf9988ca22bc1de2835ca91c9746b6bdff03c65 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -9,6 +9,9 @@ class Projects::TagsController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :authorize_download_code!
before_action :authorize_admin_tag!, only: [:new, :create, :destroy]
+ before_action do
+ push_frontend_feature_flag(:gldropdown_tags, default_enabled: :yaml)
+ end
feature_category :source_code_management, [:index, :show, :new, :destroy]
feature_category :release_evidence, [:create]
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index b5b394723ab09ce9159399d5b3bee9167b1a9918..229f13d0ff3a39bd30d608cfba4e0b915cc729e1 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -9,20 +9,23 @@
= s_('TagsPage|Tags give the ability to mark specific points in history as being important')
.nav-controls
- = form_tag(filter_tags_path, method: :get) do
- = search_field_tag :search, params[:search], { placeholder: s_('TagsPage|Filter by tag name'), id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
+ - unless Gitlab::Ci::Features.gldropdown_tags_enabled?
+ = form_tag(filter_tags_path, method: :get) do
+ = search_field_tag :search, params[:search], { placeholder: s_('TagsPage|Filter by tag name'), id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
- .dropdown
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown'} }
- %span.light
- = tags_sort_options_hash[@sort]
- = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
- %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
- %li.dropdown-header
- = s_('TagsPage|Sort by')
- - tags_sort_options_hash.each do |value, title|
- %li
- = link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value)
+ .dropdown
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown'} }
+ %span.light
+ = tags_sort_options_hash[@sort]
+ = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
+ %li.dropdown-header
+ = s_('TagsPage|Sort by')
+ - tags_sort_options_hash.each do |value, title|
+ %li
+ = link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value)
+ - else
+ #js-tags-sort-dropdown{ data: { filter_tags_path: filter_tags_path, sort_options: tags_sort_options_hash.to_json } }
- if can?(current_user, :admin_tag, @project)
= link_to new_project_tag_path(@project), class: 'btn gl-button btn-confirm', data: { qa_selector: "new_tag_button" } do
= s_('TagsPage|New tag')
diff --git a/config/feature_flags/development/gldropdown_tags.yml b/config/feature_flags/development/gldropdown_tags.yml
new file mode 100644
index 0000000000000000000000000000000000000000..704f276ac372da8228c2b19fa6fc036f1310166b
--- /dev/null
+++ b/config/feature_flags/development/gldropdown_tags.yml
@@ -0,0 +1,8 @@
+---
+name: gldropdown_tags
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58589
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327055
+milestone: '13.11'
+type: development
+group: group::continuous integration
+default_enabled: false
diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb
index 320f3a959fafe962cfb0ec1db3b6c7760c45cfb5..a86109677085a859757d29a143c275db76e91e07 100644
--- a/lib/gitlab/ci/features.rb
+++ b/lib/gitlab/ci/features.rb
@@ -63,6 +63,10 @@ def self.display_codequality_backend_comparison?(project)
def self.multiple_cache_per_job?
::Feature.enabled?(:multiple_cache_per_job, default_enabled: :yaml)
end
+
+ def self.gldropdown_tags_enabled?
+ ::Feature.enabled?(:gldropdown_tags, default_enabled: :yaml)
+ end
end
end
end
diff --git a/spec/frontend/tags/components/sort_dropdown_spec.js b/spec/frontend/tags/components/sort_dropdown_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b0fd98ec68e18e7a16e299005e3f49e9fa3d4a72
--- /dev/null
+++ b/spec/frontend/tags/components/sort_dropdown_spec.js
@@ -0,0 +1,81 @@
+import { GlDropdownItem, GlSearchBoxByClick } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import * as urlUtils from '~/lib/utils/url_utility';
+import SortDropdown from '~/tags/components/sort_dropdown.vue';
+
+describe('Tags sort dropdown', () => {
+ let wrapper;
+
+ const createWrapper = (props = {}) => {
+ return extendedWrapper(
+ mount(SortDropdown, {
+ provide: {
+ filterTagsPath: '/root/ci-cd-project-demo/-/tags',
+ sortOptions: {
+ name_asc: 'Name',
+ updated_asc: 'Oldest updated',
+ updated_desc: 'Last updated',
+ },
+ ...props,
+ },
+ }),
+ );
+ };
+
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByClick);
+ const findTagsDropdown = () => wrapper.findByTestId('tags-dropdown');
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('default state', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
+ it('should have a search box with a placeholder', () => {
+ const searchBox = findSearchBox();
+
+ expect(searchBox.exists()).toBe(true);
+ expect(searchBox.find('input').attributes('placeholder')).toBe('Filter by tag name');
+ });
+
+ it('should have a sort order dropdown', () => {
+ const branchesDropdown = findTagsDropdown();
+
+ expect(branchesDropdown.exists()).toBe(true);
+ });
+ });
+
+ describe('when submitting a search term', () => {
+ beforeEach(() => {
+ urlUtils.visitUrl = jest.fn();
+
+ wrapper = createWrapper();
+ });
+
+ it('should call visitUrl', () => {
+ const searchBox = findSearchBox();
+
+ searchBox.vm.$emit('submit');
+
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith(
+ '/root/ci-cd-project-demo/-/tags?sort=updated_desc',
+ );
+ });
+
+ it('should send a sort parameter', () => {
+ const sortDropdownItems = findTagsDropdown().findAllComponents(GlDropdownItem).at(0);
+
+ sortDropdownItems.vm.$emit('click');
+
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith(
+ '/root/ci-cd-project-demo/-/tags?sort=name_asc',
+ );
+ });
+ });
+});
diff --git a/spec/views/projects/tags/index.html.haml_spec.rb b/spec/views/projects/tags/index.html.haml_spec.rb
index dc008875062f6cb856ff6edc592798327990607f..18b42f98e0b47e331d96b54590c370296d446e68 100644
--- a/spec/views/projects/tags/index.html.haml_spec.rb
+++ b/spec/views/projects/tags/index.html.haml_spec.rb
@@ -21,6 +21,7 @@
end
it 'defaults sort dropdown toggle to last updated' do
+ stub_feature_flags(gldropdown_tags: false)
render
expect(rendered).to have_button('Last updated')
end