Commit 2c6f6233 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '216931-convert-the-image-tag-ui-from-a-table-to-a-list-view-component' into 'master'

Reusable list and delete button components

See merge request gitlab-org/gitlab!34854
parents 0682863d 323b5001
<script>
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
export default {
name: 'DeleteButton',
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
title: {
type: String,
required: true,
},
tooltipTitle: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
tooltipDisabled: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
tooltipConfiguration() {
return {
disabled: this.tooltipDisabled,
title: this.tooltipTitle,
};
},
},
};
</script>
<template>
<div v-gl-tooltip="tooltipConfiguration">
<gl-button
v-gl-tooltip
:disabled="disabled"
:title="title"
:aria-label="title"
category="secondary"
variant="danger"
icon="remove"
@click="$emit('delete')"
/>
</div>
</template>
<script>
export default {
name: 'ListItem',
props: {
index: {
type: Number,
default: 0,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
optionalClasses() {
return {
'gl-border-t-solid gl-border-t-1': this.index === 0,
'disabled-content': this.disabled,
};
},
},
};
</script>
<template>
<div
:class="[
'gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-py-4',
optionalClasses,
]"
>
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-align-items-center">
<slot name="left-primary"></slot>
</div>
<div class="gl-font-sm gl-text-gray-500">
<slot name="left-secondary"></slot>
</div>
</div>
<div>
<slot name="right"></slot>
</div>
</div>
</template>
......@@ -37,7 +37,7 @@ export default {
v-for="(listItem, index) in images"
:key="index"
:item="listItem"
:show-top-border="index === 0"
:index="index"
@delete="$emit('delete', $event)"
/>
......
<script>
import { GlTooltipDirective, GlButton, GlIcon, GlSprintf } from '@gitlab/ui';
import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '../list_item.vue';
import DeleteButton from '../delete_button.vue';
import {
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
......@@ -14,9 +16,10 @@ export default {
name: 'ImageListrow',
components: {
ClipboardButton,
GlButton,
DeleteButton,
GlSprintf,
GlIcon,
ListItem,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -26,9 +29,9 @@ export default {
type: Object,
required: true,
},
showTopBorder: {
type: Boolean,
default: false,
index: {
type: Number,
default: 0,
required: false,
},
},
......@@ -62,75 +65,56 @@ export default {
</script>
<template>
<div
<list-item
v-gl-tooltip="{
placement: 'left',
disabled: !item.deleting,
title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
}"
:index="index"
:disabled="item.deleting"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-py-4 "
:class="{
'gl-border-t-solid gl-border-t-1': showTopBorder,
'disabled-content': item.deleting,
}"
>
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-align-items-center">
<router-link
class="gl-text-black-normal gl-font-weight-bold"
data-testid="detailsLink"
:to="{ name: 'details', params: { id: encodedItem } }"
>
{{ item.path }}
</router-link>
<clipboard-button
v-if="item.location"
:disabled="item.deleting"
:text="item.location"
:title="item.location"
css-class="btn-default btn-transparent btn-clipboard gl-text-gray-500"
/>
<gl-icon
v-if="item.failedDelete"
v-gl-tooltip
:title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
name="warning"
class="text-warning"
/>
</div>
<div class="gl-font-sm gl-text-gray-500">
<span class="gl-display-flex gl-align-items-center" data-testid="tagsCount">
<gl-icon name="tag" class="gl-mr-2" />
<gl-sprintf :message="tagsCountText">
<template #count>
{{ item.tags_count }}
</template>
</gl-sprintf>
</span>
</div>
</div>
<div
v-gl-tooltip="{
disabled: item.destroy_path,
title: $options.i18n.LIST_DELETE_BUTTON_DISABLED,
}"
class="d-none d-sm-block"
data-testid="deleteButtonWrapper"
<template #left-primary>
<router-link
class="gl-text-black-normal gl-font-weight-bold"
data-testid="detailsLink"
:to="{ name: 'details', params: { id: encodedItem } }"
>
<gl-button
v-gl-tooltip
data-testid="deleteImageButton"
:disabled="disabledDelete"
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
category="secondary"
variant="danger"
icon="remove"
@click="$emit('delete', item)"
/>
</div>
</div>
</div>
{{ item.path }}
</router-link>
<clipboard-button
v-if="item.location"
:disabled="item.deleting"
:text="item.location"
:title="item.location"
css-class="btn-default btn-transparent btn-clipboard gl-text-gray-500"
/>
<gl-icon
v-if="item.failedDelete"
v-gl-tooltip="{ title: $options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE }"
name="warning"
class="text-warning"
/>
</template>
<template #left-secondary>
<span class="gl-display-flex gl-align-items-center" data-testid="tagsCount">
<gl-icon name="tag" class="gl-mr-2" />
<gl-sprintf :message="tagsCountText">
<template #count>
{{ item.tags_count }}
</template>
</gl-sprintf>
</span>
</template>
<template #right>
<delete-button
class="gl-display-none d-sm-block"
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:disabled="disabledDelete"
:tooltip-disabled="Boolean(item.destroy_path)"
:tooltip-title="$options.i18n.LIST_DELETE_BUTTON_DISABLED"
@delete="$emit('delete', item)"
/>
</template>
</list-item>
</template>
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import component from '~/registry/explorer/components/delete_button.vue';
describe('delete_button', () => {
let wrapper;
const defaultProps = {
title: 'Foo title',
tooltipTitle: 'Bar tooltipTitle',
};
const findButton = () => wrapper.find(GlButton);
const mountComponent = props => {
wrapper = shallowMount(component, {
propsData: {
...defaultProps,
...props,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('tooltip', () => {
it('the title is controlled by tooltipTitle prop', () => {
mountComponent();
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(defaultProps.tooltipTitle);
});
it('is disabled when tooltipTitle is disabled', () => {
mountComponent({ tooltipDisabled: true });
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip.value.disabled).toBe(true);
});
describe('button', () => {
it('exists', () => {
mountComponent();
expect(findButton().exists()).toBe(true);
});
it('has the correct props/attributes bound', () => {
mountComponent({ disabled: true });
expect(findButton().attributes()).toMatchObject({
'aria-label': 'Foo title',
category: 'secondary',
icon: 'remove',
title: 'Foo title',
variant: 'danger',
disabled: 'true',
});
});
it('emits a delete event', () => {
mountComponent();
expect(wrapper.emitted('delete')).toEqual(undefined);
findButton().vm.$emit('click');
expect(wrapper.emitted('delete')).toEqual([[]]);
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import component from '~/registry/explorer/components/list_item.vue';
describe('list item', () => {
let wrapper;
const findLeftPrimarySlot = () => wrapper.find('[data-testid="left-primary"]');
const findLeftSecondarySlot = () => wrapper.find('[data-testid="left-secondary"]');
const findRightSlot = () => wrapper.find('[data-testid="right"]');
const mountComponent = propsData => {
wrapper = shallowMount(component, {
propsData,
slots: {
'left-primary': '<div data-testid="left-primary" />',
'left-secondary': '<div data-testid="left-secondary" />',
right: '<div data-testid="right" />',
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('has a left primary slot', () => {
mountComponent();
expect(findLeftPrimarySlot().exists()).toBe(true);
});
it('has a left secondary slot', () => {
mountComponent();
expect(findLeftSecondarySlot().exists()).toBe(true);
});
it('has a right slot', () => {
mountComponent();
expect(findRightSlot().exists()).toBe(true);
});
describe('disabled prop', () => {
it('when true applies disabled-content class', () => {
mountComponent({ disabled: true });
expect(wrapper.classes('disabled-content')).toBe(true);
});
it('when false does not apply disabled-content class', () => {
mountComponent({ disabled: false });
expect(wrapper.classes('disabled-content')).toBe(false);
});
});
describe('index prop', () => {
it('when index is 0 displays a top border', () => {
mountComponent({ index: 0 });
expect(wrapper.classes()).toEqual(
expect.arrayContaining(['gl-border-t-solid', 'gl-border-t-1']),
);
});
it('when index is not 0 hides top border', () => {
mountComponent({ index: 1 });
expect(wrapper.classes('gl-border-t-solid')).toBe(false);
expect(wrapper.classes('gl-border-t-1')).toBe(false);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlSprintf } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
import ListItem from '~/registry/explorer/components/list_item.vue';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
import {
ROW_SCHEDULED_FOR_DELETION,
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
} from '~/registry/explorer/constants';
import { RouterLink } from '../../stubs';
import { imagesListResponse } from '../../mock_data';
......@@ -13,10 +16,10 @@ import { imagesListResponse } from '../../mock_data';
describe('Image List Row', () => {
let wrapper;
const item = imagesListResponse.data[0];
const findDeleteBtn = () => wrapper.find('[data-testid="deleteImageButton"]');
const findDetailsLink = () => wrapper.find('[data-testid="detailsLink"]');
const findTagsCount = () => wrapper.find('[data-testid="tagsCount"]');
const findDeleteButtonWrapper = () => wrapper.find('[data-testid="deleteButtonWrapper"]');
const findDeleteBtn = () => wrapper.find(DeleteButton);
const findClipboardButton = () => wrapper.find(ClipboardButton);
const mountComponent = props => {
......@@ -24,6 +27,7 @@ describe('Image List Row', () => {
stubs: {
RouterLink,
GlSprintf,
ListItem,
},
propsData: {
item,
......@@ -72,29 +76,24 @@ describe('Image List Row', () => {
});
});
describe('delete button wrapper', () => {
it('has a tooltip', () => {
mountComponent();
const tooltip = getBinding(findDeleteButtonWrapper().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(LIST_DELETE_BUTTON_DISABLED);
});
it('tooltip is enabled when destroy_path is falsy', () => {
mountComponent({ item: { ...item, destroy_path: null } });
const tooltip = getBinding(findDeleteButtonWrapper().element, 'gl-tooltip');
expect(tooltip.value.disabled).toBeFalsy();
});
});
describe('delete button', () => {
it('exists', () => {
mountComponent();
expect(findDeleteBtn().exists()).toBe(true);
});
it('has the correct props', () => {
mountComponent();
expect(findDeleteBtn().attributes()).toMatchObject({
title: REMOVE_REPOSITORY_LABEL,
tooltipdisabled: `${Boolean(item.destroy_path)}`,
tooltiptitle: LIST_DELETE_BUTTON_DISABLED,
});
});
it('emits a delete event', () => {
mountComponent();
findDeleteBtn().vm.$emit('click');
findDeleteBtn().vm.$emit('delete');
expect(wrapper.emitted('delete')).toEqual([[item]]);
});
......
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