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 ""
msgid "Delete Key"
msgstr ""
msgid "Delete Selected"
msgstr ""
msgid "Delete Value Stream"
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