From fd2d4cae87723edda28031c9bc39299f7001d43d Mon Sep 17 00:00:00 2001 From: nraj0408 Date: Tue, 9 Dec 2025 16:32:30 +0530 Subject: [PATCH] Ui for tags sorting added --- .../tags/components/sort_dropdown.vue | 93 +++++++----- app/assets/javascripts/tags/constants.js | 6 + app/assets/javascripts/tags/index.js | 3 +- .../tags/components/sort_dropdown_spec.js | 139 +++++++++++------- 4 files changed, 147 insertions(+), 94 deletions(-) diff --git a/app/assets/javascripts/tags/components/sort_dropdown.vue b/app/assets/javascripts/tags/components/sort_dropdown.vue index 90a4788074d138..32bc901456b5db 100644 --- a/app/assets/javascripts/tags/components/sort_dropdown.vue +++ b/app/assets/javascripts/tags/components/sort_dropdown.vue @@ -1,57 +1,69 @@ diff --git a/app/assets/javascripts/tags/constants.js b/app/assets/javascripts/tags/constants.js index a8096a08a97b58..246831aa2b130b 100644 --- a/app/assets/javascripts/tags/constants.js +++ b/app/assets/javascripts/tags/constants.js @@ -35,3 +35,9 @@ export const I18N_DELETE_TAG_MODAL = { deleteButtonText: DELETE_BUTTON_TEXT, deleteButtonTextProtectedTag: DELETE_BUTTON_TEXT_PROTECTED_TAG, }; + +export const SORT_DIRECTION_ASC = 'asc'; +export const SORT_DIRECTION_DESC = 'desc'; +export const SORT_OPTION_NAME = 'name'; +export const SORT_OPTION_UPDATED = 'updated'; +export const SORT_OPTION_VERSION = 'version'; diff --git a/app/assets/javascripts/tags/index.js b/app/assets/javascripts/tags/index.js index 078b6d0a847220..e072194c1c3fe9 100644 --- a/app/assets/javascripts/tags/index.js +++ b/app/assets/javascripts/tags/index.js @@ -3,7 +3,7 @@ import initSourceCodeDropdowns from '~/vue_shared/components/download_dropdown/i import SortDropdown from './components/sort_dropdown.vue'; const mountDropdownApp = (el) => { - const { sortOptions, filterTagsPath } = el.dataset; + const { filterTagsPath } = el.dataset; return new Vue({ el, @@ -12,7 +12,6 @@ const mountDropdownApp = (el) => { SortDropdown, }, provide: { - sortOptions: JSON.parse(sortOptions), filterTagsPath, }, render: (createElement) => createElement(SortDropdown), diff --git a/spec/frontend/tags/components/sort_dropdown_spec.js b/spec/frontend/tags/components/sort_dropdown_spec.js index a0ba263e83280d..4ceead1123ff74 100644 --- a/spec/frontend/tags/components/sort_dropdown_spec.js +++ b/spec/frontend/tags/components/sort_dropdown_spec.js @@ -1,89 +1,124 @@ -import { GlListboxItem, 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 { GlSearchBoxByClick, GlSorting } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { SORT_OPTION_NAME, SORT_OPTION_UPDATED, SORT_OPTION_VERSION } from '~/tags/constants'; + +import { visitUrl } from '~/lib/utils/url_utility'; import SortDropdown from '~/tags/components/sort_dropdown.vue'; import setWindowLocation from 'helpers/set_window_location_helper'; -describe('Tags sort dropdown', () => { +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + visitUrl: jest.fn(), +})); + +describe('SortDropdown', () => { 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 defaultPath = '/root/ci-cd-project-demo/-/tags'; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(SortDropdown, { + provide: { filterTagsPath: defaultPath }, + ...props, + }); }; const findSearchBox = () => wrapper.findComponent(GlSearchBoxByClick); - const findTagsDropdown = () => wrapper.findByTestId('tags-dropdown'); + const findSorting = () => wrapper.findComponent(GlSorting); + + beforeEach(() => { + jest.clearAllMocks(); + }); - describe('default state', () => { + describe('default rendering', () => { beforeEach(() => { - wrapper = createWrapper(); + setWindowLocation(defaultPath); + createComponent(); }); - it('should have a search box with a placeholder', () => { - const searchBox = findSearchBox(); + it('renders a search box with correct placeholder', () => { + expect(findSearchBox().props('placeholder')).toBe('Filter by tag name'); + }); - expect(searchBox.exists()).toBe(true); - expect(searchBox.find('input').attributes('placeholder')).toBe('Filter by tag name'); + it('renders a sorting component with SORT_OPTIONS', () => { + const SORT_OPTIONS = [ + { value: SORT_OPTION_NAME, text: 'Name' }, + { value: SORT_OPTION_UPDATED, text: 'Updated date' }, + { value: SORT_OPTION_VERSION, text: 'Version' }, + ]; + expect(findSorting().props('sortOptions')).toEqual(SORT_OPTIONS); }); - it('should have a sort order dropdown', () => { - const tagsDropdown = findTagsDropdown(); + it('has default sortBy=updated and order=desc', () => { + expect(findSorting().props('sortBy')).toBe('updated'); + expect(findSorting().props('isAscending')).toBe(false); + }); + }); + + describe('when URL contains query parameters', () => { + beforeEach(() => { + setWindowLocation(`${defaultPath}?search=release&sort=updated_desc`); + createComponent(); + }); - expect(tagsDropdown.exists()).toBe(true); + it('initializes state from URL params', () => { + expect(findSearchBox().props('value')).toBe('release'); + expect(findSorting().props('sortBy')).toBe('updated'); + expect(findSorting().props('isAscending')).toBe(false); }); }); - describe('when url contains a search param', () => { - const branchName = 'branch-1'; + describe('on search submit', () => { + beforeEach(() => { + setWindowLocation(defaultPath); + createComponent(); + }); + + it('navigates with search, sort, and order params', async () => { + await findSearchBox().vm.$emit('input', 'frontend'); + await findSearchBox().vm.$emit('submit'); + expect(visitUrl).toHaveBeenCalledWith(`${defaultPath}?sort=updated_desc&search=frontend`); + }); + }); + + describe('on sort changes', () => { beforeEach(() => { - setWindowLocation(`/root/ci-cd-project-demo/-/branches?search=${branchName}`); - wrapper = createWrapper(); + setWindowLocation(defaultPath); + createComponent(); }); - it('should set the default the input value to search param', () => { - expect(findSearchBox().props('value')).toBe(branchName); + it('calls visitUrl when sortBy changes', async () => { + await findSorting().vm.$emit('sortByChange', 'version'); + + expect(visitUrl).toHaveBeenCalledWith(`${defaultPath}?sort=version_desc`); }); }); - describe('when submitting a search term', () => { + describe('when sortDirection changes', () => { beforeEach(() => { - urlUtils.visitUrl = jest.fn(); - wrapper = createWrapper(); + setWindowLocation(defaultPath); + createComponent(); }); - it('should call visitUrl', () => { - const searchTerm = 'branch-1'; - const searchBox = findSearchBox(); - searchBox.vm.$emit('input', searchTerm); - searchBox.vm.$emit('submit'); + it('calls visitUrl when sort direction changes', async () => { + await findSorting().vm.$emit('sortDirectionChange', true); // ascending - expect(urlUtils.visitUrl).toHaveBeenCalledWith( - '/root/ci-cd-project-demo/-/tags?search=branch-1&sort=updated_desc', - ); + expect(visitUrl).toHaveBeenCalledWith(`${defaultPath}?sort=updated_asc`); }); + }); - it('should send a sort parameter', () => { - const sortDropdownItem = findTagsDropdown().findAllComponents(GlListboxItem).at(0); + describe('when search term is empty', () => { + beforeEach(() => { + setWindowLocation(defaultPath); + createComponent(); + }); - sortDropdownItem.trigger('click'); + it('omits search parameter in URL', async () => { + await findSearchBox().vm.$emit('input', ''); + wrapper.vm.visitUrlFromOption(); - expect(urlUtils.visitUrl).toHaveBeenCalledWith( - '/root/ci-cd-project-demo/-/tags?sort=name_asc', - ); + expect(visitUrl).toHaveBeenCalledWith(`${defaultPath}?sort=updated_desc`); }); }); }); -- GitLab