diff --git a/app/assets/javascripts/vue_shared/components/empty_result.stories.js b/app/assets/javascripts/vue_shared/components/empty_result.stories.js new file mode 100644 index 0000000000000000000000000000000000000000..0571ed47b561522b11aebd26b430c495b760eed5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/empty_result.stories.js @@ -0,0 +1,23 @@ +import EmptyResult, { TYPES } from './empty_result.vue'; + +export default { + component: EmptyResult, + title: 'vue_shared/empty_result', + argTypes: { + type: { + control: 'select', + options: Object.values(TYPES), + }, + }, +}; + +const Template = (args, { argTypes }) => ({ + components: { EmptyResult }, + props: Object.keys(argTypes), + template: ``, +}); + +export const Default = Template.bind({}); +Default.args = { + type: TYPES.search, +}; diff --git a/app/assets/javascripts/vue_shared/components/empty_result.vue b/app/assets/javascripts/vue_shared/components/empty_result.vue new file mode 100644 index 0000000000000000000000000000000000000000..ab13b32caeecf37d543c9a67179c9a842b473f1f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/empty_result.vue @@ -0,0 +1,51 @@ + + + diff --git a/app/components/layouts/empty_result_component.haml b/app/components/layouts/empty_result_component.haml new file mode 100644 index 0000000000000000000000000000000000000000..3b86a31761419904dd200f88690606fbb77222ef --- /dev/null +++ b/app/components/layouts/empty_result_component.haml @@ -0,0 +1,10 @@ +- title = _('No results found') +- description = filter? ? _('To widen your search, change or remove filters above.') : _('Edit your search and try again.') +- svg_path = 'illustrations/empty-state/empty-search-md.svg' + += render Pajamas::EmptyStateComponent.new(svg_path: svg_path, + title: title, + empty_state_options: { **@html_options }) do |c| + + - c.with_description do + = description diff --git a/app/components/layouts/empty_result_component.rb b/app/components/layouts/empty_result_component.rb new file mode 100644 index 0000000000000000000000000000000000000000..08aa5379beea8aa6ba737f8ade452902aa34adf4 --- /dev/null +++ b/app/components/layouts/empty_result_component.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Layouts + class EmptyResultComponent < Pajamas::Component + TYPE_OPTIONS = [:search, :filter].freeze + + # @param [Symbol] type + # @param [Hash] html_options + def initialize( + type: :search, + **html_options + ) + @type = filter_attribute(type.to_sym, TYPE_OPTIONS, default: :search) + @html_options = html_options + end + + def filter? + @type == :filter + end + + def html_options + format_options(options: @html_options) + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 11d5e8e02f6655c112d64d3265fcaeb6912b7e7e..d865e884c08ec4fd0f0a91929c3eeafac679435d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -20466,6 +20466,9 @@ msgstr "" msgid "Edit your search and try again" msgstr "" +msgid "Edit your search and try again." +msgstr "" + msgid "Edit your search filter and try again." msgstr "" diff --git a/spec/components/layouts/empty_result_component_spec.rb b/spec/components/layouts/empty_result_component_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4bd7f92de1984117138dc97bf01a35301318fd75 --- /dev/null +++ b/spec/components/layouts/empty_result_component_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Layouts::EmptyResultComponent, type: :component, feature_category: :shared do + let(:type) { :search } + let(:html_options) { { data: { testid: 'empty-result-test-id' } } } + + before do + render_inline described_class.new(type: type, **html_options) + end + + it 'renders search empty result' do + expect(page).to have_css('.gl-empty-state', text: 'No results found') + expect(page).to have_css('.gl-empty-state', text: 'Edit your search and try again.') + end + + it 'renders custom attributes' do + expect(page).to have_css('[data-testid="empty-result-test-id"]') + end + + context 'when type is filter' do + let(:type) { :filter } + + it 'renders empty result' do + expect(page).to have_css('.gl-empty-state', text: 'No results found') + expect(page).to have_css('.gl-empty-state', text: 'To widen your search, change or remove filters above.') + end + end +end diff --git a/spec/components/previews/layouts/empty_result_component_preview.rb b/spec/components/previews/layouts/empty_result_component_preview.rb new file mode 100644 index 0000000000000000000000000000000000000000..52b53cd682c86db6f1432db758b68f77b6fd873e --- /dev/null +++ b/spec/components/previews/layouts/empty_result_component_preview.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Layouts + class EmptyResultComponentPreview < ViewComponent::Preview + # @param type select {{ Layouts::EmptyResultComponent::TYPE_OPTIONS }} + + def default(type: :search) + render(::Layouts::EmptyResultComponent.new( + type: type + )) + end + end +end diff --git a/spec/frontend/vue_shared/components/empty_result_spec.js b/spec/frontend/vue_shared/components/empty_result_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..a9e18d7ed4db8cc48d136fc942258c0108aaedc6 --- /dev/null +++ b/spec/frontend/vue_shared/components/empty_result_spec.js @@ -0,0 +1,36 @@ +import emptyStateSvgPath from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg'; +import { GlEmptyState } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import EmptyResult from '~/vue_shared/components/empty_result.vue'; + +describe('Empty result', () => { + let wrapper; + + const createComponent = (props) => { + wrapper = shallowMount(EmptyResult, { + propsData: props, + }); + }; + + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + + it('renders empty search state', () => { + createComponent({ type: 'search' }); + + expect(findEmptyState().props()).toMatchObject({ + svgPath: emptyStateSvgPath, + title: 'No results found', + description: 'Edit your search and try again.', + }); + }); + + it('renders empty filter state', () => { + createComponent({ type: 'filter' }); + + expect(findEmptyState().props()).toMatchObject({ + svgPath: emptyStateSvgPath, + title: 'No results found', + description: 'To widen your search, change or remove filters above.', + }); + }); +});