Commit 7e4efaf2 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '336934-improved-ux-for-bulk-deleting-container-image-tags-2' into 'master'

Add a registry list component

See merge request gitlab-org/gitlab!75237
parents cf5889bd 7355d8b5
<script>
import { GlButton, GlFormCheckbox, GlKeysetPagination } from '@gitlab/ui';
import { filter } from 'lodash';
import { __ } from '~/locale';
export default {
name: 'RegistryList',
components: {
GlButton,
GlFormCheckbox,
GlKeysetPagination,
},
props: {
title: {
type: String,
required: true,
},
isLoading: {
type: Boolean,
default: false,
required: false,
},
hiddenDelete: {
type: Boolean,
default: false,
required: false,
},
pagination: {
type: Object,
required: false,
default: () => ({}),
},
items: {
type: Array,
required: false,
default: () => [],
},
idProperty: {
type: String,
required: false,
default: 'id',
},
},
data() {
return {
selectedReferences: {},
};
},
computed: {
showPagination() {
return this.pagination.hasPreviousPage || this.pagination.hasNextPage;
},
disableDeleteButton() {
return this.isLoading || filter(this.selectedReferences).length === 0;
},
selectedItems() {
return this.items.filter(this.isSelected);
},
selectAll: {
get() {
return this.items.every(this.isSelected);
},
set(value) {
this.items.forEach((item) => {
const id = item[this.idProperty];
this.$set(this.selectedReferences, id, value);
});
},
},
},
methods: {
selectItem(item) {
const id = item[this.idProperty];
this.$set(this.selectedReferences, id, !this.selectedReferences[id]);
},
isSelected(item) {
const id = item[this.idProperty];
return this.selectedReferences[id];
},
},
i18n: {
deleteSelected: __('Delete Selected'),
},
};
</script>
<template>
<div>
<div class="gl-display-flex gl-justify-content-space-between gl-mb-3">
<gl-form-checkbox v-if="!hiddenDelete" v-model="selectAll" class="gl-ml-2">
<span class="gl-font-weight-bold">{{ title }}</span>
</gl-form-checkbox>
<gl-button
v-if="!hiddenDelete"
:disabled="disableDeleteButton"
category="secondary"
variant="danger"
@click="$emit('delete', selectedItems)"
>
{{ $options.i18n.deleteSelected }}
</gl-button>
</div>
<div v-for="(item, index) in items" :key="index">
<slot
:select-item="selectItem"
:is-selected="isSelected"
:item="item"
:first="index === 0"
></slot>
</div>
<div class="gl-display-flex gl-justify-content-center">
<gl-keyset-pagination
v-if="showPagination"
v-bind="pagination"
class="gl-mt-3"
@prev="$emit('prev-page')"
@next="$emit('next-page')"
/>
</div>
</div>
</template>
...@@ -11174,6 +11174,9 @@ msgstr "" ...@@ -11174,6 +11174,9 @@ msgstr ""
msgid "Delete Key" msgid "Delete Key"
msgstr "" msgstr ""
msgid "Delete Selected"
msgstr ""
msgid "Delete Value Stream" msgid "Delete Value Stream"
msgstr "" msgstr ""
......
import { GlButton, GlFormCheckbox, GlKeysetPagination } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import component from '~/packages_and_registries/shared/components/registry_list.vue';
describe('Registry List', () => {
let wrapper;
const items = [{ id: 'a' }, { id: 'b' }];
const defaultPropsData = {
title: 'test_title',
items,
};
const rowScopedSlot = `
<div data-testid="scoped-slot">
<button @click="props.selectItem(props.item)">Select</button>
<span>{{props.first}}</span>
<p>{{props.isSelected(props.item)}}</p>
</div>`;
const mountComponent = ({ propsData = defaultPropsData } = {}) => {
wrapper = shallowMountExtended(component, {
propsData,
scopedSlots: {
default: rowScopedSlot,
},
});
};
const findSelectAll = () => wrapper.findComponent(GlFormCheckbox);
const findDeleteSelected = () => wrapper.findComponent(GlButton);
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findScopedSlots = () => wrapper.findAllByTestId('scoped-slot');
const findScopedSlotSelectButton = (index) => findScopedSlots().at(index).find('button');
const findScopedSlotFirstValue = (index) => findScopedSlots().at(index).find('span');
const findScopedSlotIsSelectedValue = (index) => findScopedSlots().at(index).find('p');
afterEach(() => {
wrapper.destroy();
});
describe('header', () => {
it('renders the title passed in the prop', () => {
mountComponent();
expect(wrapper.text()).toContain(defaultPropsData.title);
});
describe('select all checkbox', () => {
beforeEach(() => {
mountComponent();
});
it('exists', () => {
expect(findSelectAll().exists()).toBe(true);
});
it('select and unselect all', async () => {
// no row is not selected
items.forEach((item, index) => {
expect(findScopedSlotIsSelectedValue(index).text()).toBe('');
});
// simulate selection
findSelectAll().vm.$emit('input', true);
await nextTick();
// all rows selected
items.forEach((item, index) => {
expect(findScopedSlotIsSelectedValue(index).text()).toBe('true');
});
// simulate de-selection
findSelectAll().vm.$emit('input', '');
await nextTick();
// no row is not selected
items.forEach((item, index) => {
expect(findScopedSlotIsSelectedValue(index).text()).toBe('');
});
});
});
describe('delete button', () => {
it('has the correct text', () => {
mountComponent();
expect(findDeleteSelected().text()).toBe(component.i18n.deleteSelected);
});
it('is hidden when hiddenDelete is true', () => {
mountComponent({ propsData: { ...defaultPropsData, hiddenDelete: true } });
expect(findDeleteSelected().exists()).toBe(false);
});
it('is disabled when isLoading is true', () => {
mountComponent({ propsData: { ...defaultPropsData, isLoading: true } });
expect(findDeleteSelected().props('disabled')).toBe(true);
});
it('is disabled when no row is selected', async () => {
mountComponent();
expect(findDeleteSelected().props('disabled')).toBe(true);
await findScopedSlotSelectButton(0).trigger('click');
expect(findDeleteSelected().props('disabled')).toBe(false);
});
it('on click emits the delete event with the selected rows', async () => {
mountComponent();
await findScopedSlotSelectButton(0).trigger('click');
findDeleteSelected().vm.$emit('click');
expect(wrapper.emitted('delete')).toEqual([[[items[0]]]]);
});
});
});
describe('main area', () => {
beforeEach(() => {
mountComponent();
});
it('renders scopedSlots based on the items props', () => {
expect(findScopedSlots()).toHaveLength(items.length);
});
it('populates the scope of the slot correctly', async () => {
expect(findScopedSlots().at(0).exists()).toBe(true);
// it's the first slot
expect(findScopedSlotFirstValue(0).text()).toBe('true');
// item is not selected, falsy is translated to empty string
expect(findScopedSlotIsSelectedValue(0).text()).toBe('');
// find the button with the bound function
await findScopedSlotSelectButton(0).trigger('click');
// the item is selected
expect(findScopedSlotIsSelectedValue(0).text()).toBe('true');
});
});
describe('footer', () => {
let pagination;
beforeEach(() => {
pagination = { hasPreviousPage: false, hasNextPage: true };
});
it('has a pagination', () => {
mountComponent({
propsData: { ...defaultPropsData, pagination },
});
expect(findPagination().props()).toMatchObject(pagination);
});
it.each`
hasPreviousPage | hasNextPage | visible
${true} | ${true} | ${true}
${true} | ${false} | ${true}
${false} | ${true} | ${true}
${false} | ${false} | ${false}
`(
'when hasPreviousPage is $hasPreviousPage and hasNextPage is $hasNextPage is $visible that the pagination is shown',
({ hasPreviousPage, hasNextPage, visible }) => {
pagination = { hasPreviousPage, hasNextPage };
mountComponent({
propsData: { ...defaultPropsData, pagination },
});
expect(findPagination().exists()).toBe(visible);
},
);
it('pagination emits the correct events', () => {
mountComponent({
propsData: { ...defaultPropsData, pagination },
});
findPagination().vm.$emit('prev');
expect(wrapper.emitted('prev-page')).toEqual([[]]);
findPagination().vm.$emit('next');
expect(wrapper.emitted('next-page')).toEqual([[]]);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment