Skip to content
代码片段 群组 项目
未验证 提交 ef019cfe 编辑于 作者: Coung Ngo's avatar Coung Ngo 提交者: GitLab
浏览文件

Merge branch 'cngo-migrate-weight-widget' into 'master'

Migrate work_item_weight.vue to use WorkItemSidebarWidget

See merge request https://gitlab.com/gitlab-org/gitlab/-/merge_requests/184194



Merged-by: default avatarCoung Ngo <cngo@gitlab.com>
Approved-by: default avatarCindy Halim <chalim@gitlab.com>
Approved-by: default avatarFlorie Guibert <fguibert@gitlab.com>
Reviewed-by: default avatarCoung Ngo <cngo@gitlab.com>
No related branches found
No related tags found
无相关合并请求
......@@ -609,7 +609,6 @@ export default {
'ee/app/assets/javascripts/work_items/components/work_item_links/work_item_rolled_up_health_status.vue',
'ee/app/assets/javascripts/work_items/components/work_item_progress.vue',
'ee/app/assets/javascripts/work_items/components/work_item_rolledup_dates.vue',
'ee/app/assets/javascripts/work_items/components/work_item_weight.vue',
'ee/app/assets/javascripts/workspaces/common/components/workspaces_list/workspaces_table.vue',
'ee/app/assets/javascripts/workspaces/dropdown_group/components/workspace_dropdown_item.vue',
'ee/app/assets/javascripts/workspaces/user/pages/list.vue',
......
<script>
import { GlButton, GlOutsideDirective as Outside } from '@gitlab/ui';
import { GlButton, GlLoadingIcon, GlOutsideDirective as Outside } from '@gitlab/ui';
import { Mousetrap } from '~/lib/mousetrap';
import { keysFor, SIDEBAR_CLOSE_WIDGET } from '~/behaviors/shortcuts/keybindings';
export default {
components: {
GlButton,
GlLoadingIcon,
},
directives: {
Outside,
......@@ -56,6 +57,7 @@ export default {
<h3 class="gl-heading-5 gl-mb-0">
<slot name="title"></slot>
</h3>
<gl-loading-icon v-if="isUpdating" />
<gl-button
v-if="canUpdate && !isEditing"
key="edit-button"
......
<script>
import { GlButton, GlForm, GlFormInput, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { GlButton, GlFormInput, GlTooltipDirective } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_UPDATING,
sprintfWorkItem,
TRACKING_CATEGORY_SHOW,
} from '~/work_items/constants';
import updateNewWorkItemMutation from '~/work_items/graphql/update_new_work_item.mutation.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import { newWorkItemId } from '~/work_items/utils';
import WorkItemSidebarWidget from '~/work_items/components/shared/work_item_sidebar_widget.vue';
export default {
inputId: 'weight-widget-input',
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
WorkItemSidebarWidget,
GlButton,
GlForm,
GlFormInput,
GlLoadingIcon,
},
mixins: [Tracking.mixin()],
inject: ['hasIssueWeightsFeature'],
......@@ -42,10 +41,6 @@ export default {
type: String,
required: true,
},
workItemIid: {
type: String,
required: true,
},
workItemType: {
type: String,
required: true,
......@@ -53,9 +48,7 @@ export default {
},
data() {
return {
isEditing: false,
clickingClearButton: false,
workItem: {},
dirtyWeight: this.widget.weight,
isUpdating: false,
};
},
......@@ -69,6 +62,7 @@ export default {
showRemoveWeight() {
return this.hasWeight && !this.isUpdating;
},
// eslint-disable-next-line vue/no-unused-properties
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,
......@@ -88,27 +82,19 @@ export default {
},
},
methods: {
blurInput() {
this.$refs.input.$el.blur();
},
handleFocus() {
this.isEditing = true;
clearWeight(stopEditing) {
this.dirtyWeight = '';
stopEditing();
this.updateWeight();
},
updateWeightFromInput(event) {
if (event.target.value === '') {
this.updateWeight(null);
updateWeight() {
if (!this.canUpdate) {
return;
}
const weight = Number(event.target.value);
this.updateWeight(weight);
},
updateWeight(weight) {
if (this.clickingClearButton) return;
if (!this.canUpdate) return;
const newWeight = this.dirtyWeight === '' ? null : Number(this.dirtyWeight);
if (this.weight === weight) {
this.isEditing = false;
if (this.weight === newWeight) {
return;
}
......@@ -123,13 +109,12 @@ export default {
input: {
workItemType: this.workItemType,
fullPath: this.fullPath,
weight,
weight: newWeight,
},
},
});
this.isUpdating = false;
this.isEditing = false;
return;
}
......@@ -140,7 +125,7 @@ export default {
input: {
id: this.workItemId,
weightWidget: {
weight,
weight: newWeight,
},
},
},
......@@ -157,7 +142,6 @@ export default {
})
.finally(() => {
this.isUpdating = false;
this.isEditing = false;
});
},
},
......@@ -165,74 +149,49 @@ export default {
</script>
<template>
<div v-if="displayWeightWidget" data-testid="work-item-weight">
<div class="gl-flex gl-items-center gl-justify-between">
<!-- hide header when editing, since we then have a form label. Keep it reachable for screenreader nav -->
<h3 :class="{ 'gl-sr-only': isEditing }" class="gl-heading-5 !gl-mb-0">
{{ __('Weight') }}
</h3>
<gl-button
v-if="canUpdate && !isEditing"
data-testid="edit-weight"
category="tertiary"
size="small"
@click="isEditing = true"
>{{ __('Edit') }}</gl-button
>
</div>
<gl-form v-if="isEditing" @submit.prevent="blurInput">
<div class="gl-flex gl-items-center">
<label :for="$options.inputId" class="gl-mb-0">{{ __('Weight') }}</label>
<gl-loading-icon v-if="isUpdating" size="sm" inline class="gl-ml-3" />
<gl-button
data-testid="apply-weight"
category="tertiary"
size="small"
class="gl-ml-auto"
:disabled="isUpdating"
@click="isEditing = false"
>{{ __('Apply') }}</gl-button
>
</div>
<!-- wrapper for the form input so the borders fit inside the sidebar -->
<work-item-sidebar-widget
v-if="displayWeightWidget"
:can-update="canUpdate"
:is-updating="isUpdating"
data-testid="work-item-weight"
@stopEditing="updateWeight"
>
<template #title>
{{ __('Weight') }}
</template>
<template #content>
<template v-if="hasWeight">
{{ weight }}
</template>
<span v-else class="gl-text-subtle">
{{ __('None') }}
</span>
</template>
<template #editing-content="{ stopEditing }">
<div class="gl-relative gl-px-2">
<gl-form-input
:id="$options.inputId"
ref="input"
v-model="dirtyWeight"
autofocus
min="0"
class="hide-unfocused-input-decoration gl-block"
type="number"
:disabled="isUpdating"
:placeholder="__('Enter a number')"
:value="weight"
autofocus
@blur="updateWeightFromInput"
@focus="handleFocus"
@keydown.exact.esc.stop="blurInput"
type="number"
:aria-label="__('Enter a number')"
@keydown.enter="stopEditing"
@keydown.exact.esc.stop="stopEditing"
/>
<gl-button
v-if="showRemoveWeight"
v-gl-tooltip
data-testid="remove-weight"
variant="default"
class="gl-absolute gl-right-7 gl-top-2"
category="tertiary"
size="small"
name="clear"
icon="clear"
class="gl-clear-icon-button gl-absolute gl-right-7 gl-top-2"
size="small"
:title="__('Remove weight')"
:aria-label="__('Remove weight')"
@mousedown="clickingClearButton = true"
@mouseup="clickingClearButton = false"
@click="updateWeight(null)"
data-testid="remove-weight"
@click="clearWeight(stopEditing)"
/>
</div>
</gl-form>
<template v-else-if="hasWeight">
<div>{{ weight }}</div>
</template>
<template v-else>
<div class="gl-text-subtle">{{ __('None') }}</div>
</template>
</div>
</work-item-sidebar-widget>
</template>
import { GlForm, GlFormInput, GlLoadingIcon } from '@gitlab/ui';
import { GlFormInput } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import WorkItemWeight from 'ee/work_items/components/work_item_weight.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import { updateWorkItemMutationResponse } from 'jest/work_items/mock_data';
import WorkItemSidebarWidget from '~/work_items/components/shared/work_item_sidebar_widget.vue';
import { ENTER_KEY, ESC_KEY } from '~/lib/utils/keys';
describe('WorkItemWeight component', () => {
Vue.use(VueApollo);
......@@ -16,145 +18,60 @@ describe('WorkItemWeight component', () => {
let wrapper;
const workItemId = 'gid://gitlab/WorkItem/1';
const defaultWorkItemType = 'Task';
const findHeader = () => wrapper.find('h3');
const findEditButton = () => wrapper.find('[data-testid="edit-weight"]');
const findApplyButton = () => wrapper.find('[data-testid="apply-weight"]');
const findLabel = () => wrapper.find('label');
const findForm = () => wrapper.findComponent(GlForm);
const findEditButton = () => wrapper.findByTestId('edit-button');
const findApplyButton = () => wrapper.findByTestId('apply-button');
const findInput = () => wrapper.findComponent(GlFormInput);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findClearButton = () => wrapper.find('[data-testid="remove-weight"]');
const findClearButton = () => wrapper.findByTestId('remove-weight');
const createComponent = ({
canUpdate = true,
fullPath = 'gitlab-org/gitlab',
hasIssueWeightsFeature = true,
isEditing = false,
weight = null,
editable = true,
workItemIid = '1',
workItemType = defaultWorkItemType,
mutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse),
} = {}) => {
wrapper = mountExtended(WorkItemWeight, {
wrapper = shallowMountExtended(WorkItemWeight, {
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
canUpdate,
fullPath,
fullPath: 'gitlab-org/gitlab',
widget: {
weight,
widgetDefinition: { editable },
},
workItemId,
workItemIid,
workItemType,
workItemType: 'Task',
},
provide: {
hasIssueWeightsFeature,
},
stubs: {
WorkItemSidebarWidget,
},
});
if (isEditing) {
findEditButton().trigger('click');
findEditButton().vm.$emit('click');
}
};
describe('rendering widget', () => {
it('renders nothing if license not available', async () => {
createComponent({ hasIssueWeightsFeature: false });
await nextTick();
expect(findHeader().exists()).toBe(false);
expect(findForm().exists()).toBe(false);
});
// 'editable' property means if it's available for that work item type
it('renders nothing if not editable', async () => {
createComponent({ editable: false });
await nextTick();
expect(findHeader().exists()).toBe(false);
expect(findForm().exists()).toBe(false);
});
});
describe('label', () => {
it('shows header when not editing', () => {
createComponent();
expect(findHeader().exists()).toBe(true);
expect(findHeader().classes('gl-sr-only')).toBe(false);
expect(findLabel().exists()).toBe(false);
});
it('shows label and hides header while editing', async () => {
createComponent({ isEditing: true });
await nextTick();
expect(findLabel().exists()).toBe(true);
expect(findHeader().classes('gl-sr-only')).toBe(true);
});
it('shows loading spinner while updating', async () => {
createComponent({
isEditing: true,
weight: 0,
canUpdate: true,
});
await nextTick();
findInput().setValue('1');
findInput().trigger('blur');
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
});
});
describe('edit button', () => {
it('is not shown if user cannot edit', () => {
createComponent({ canUpdate: false });
expect(findEditButton().exists()).toBe(false);
});
it('is shown if user can edit', () => {
createComponent({ canUpdate: true });
expect(findEditButton().exists()).toBe(true);
});
it('triggers edit mode on click', async () => {
createComponent();
findEditButton().trigger('click');
await nextTick();
expect(findLabel().exists()).toBe(true);
expect(findForm().exists()).toBe(true);
});
it('is replaced by Apply button while editing', async () => {
createComponent();
findEditButton().trigger('click');
await nextTick();
expect(findEditButton().exists()).toBe(false);
expect(findApplyButton().exists()).toBe(true);
});
});
......@@ -173,36 +90,22 @@ describe('WorkItemWeight component', () => {
});
});
describe('form', () => {
it('is not shown while not editing', async () => {
await createComponent();
expect(findForm().exists()).toBe(false);
});
it('is shown while editing', async () => {
await createComponent({ isEditing: true });
expect(findForm().exists()).toBe(true);
});
});
describe('weight input', () => {
it('is not shown while not editing', async () => {
await createComponent();
createComponent();
await nextTick();
expect(findInput().exists()).toBe(false);
});
it('has weight-y attributes', async () => {
await createComponent({ isEditing: true });
it('renders when editing', async () => {
createComponent({ isEditing: true });
await nextTick();
expect(findInput().attributes()).toEqual(
expect.objectContaining({
min: '0',
type: 'number',
}),
);
expect(findInput().attributes()).toMatchObject({
min: '0',
type: 'number',
});
});
it('clear button triggers mutation', async () => {
......@@ -213,10 +116,9 @@ describe('WorkItemWeight component', () => {
mutationHandler: mutationSpy,
canUpdate: true,
});
await nextTick();
findClearButton().trigger('click');
findClearButton().vm.$emit('click');
expect(mutationSpy).toHaveBeenCalledWith({
input: {
......@@ -236,11 +138,10 @@ describe('WorkItemWeight component', () => {
mutationHandler: mutationSpy,
canUpdate: true,
});
await nextTick();
findInput().setValue('1');
findInput().trigger('blur');
findInput().vm.$emit('input', '1');
findInput().vm.$emit('keydown', new KeyboardEvent('keydown', { key: ENTER_KEY }));
expect(mutationSpy).toHaveBeenCalledWith({
input: {
......@@ -252,36 +153,19 @@ describe('WorkItemWeight component', () => {
});
});
it('is disabled while updating, and removed after', async () => {
it('does not call a mutation to update the weight when the input value is the same', async () => {
const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
createComponent({
isEditing: true,
weight: 0,
mutationHandler: mutationSpy,
canUpdate: true,
});
await nextTick();
findInput().setValue('1');
findInput().trigger('blur');
await nextTick();
expect(findInput().attributes('disabled')).toBe('disabled');
await waitForPromises();
expect(findInput().exists()).toBe(false);
});
it('does not call a mutation to update the weight when the input value is the same', async () => {
const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
createComponent({ isEditing: true, mutationHandler: mutationSpy, canUpdate: true });
await nextTick();
findInput().trigger('blur');
findInput().vm.$emit('keydown', new KeyboardEvent('keydown', { key: ESC_KEY }));
expect(mutationSpy).not.toHaveBeenCalledWith();
expect(mutationSpy).not.toHaveBeenCalled();
});
it('emits an error when there is a GraphQL error', async () => {
......@@ -298,12 +182,10 @@ describe('WorkItemWeight component', () => {
mutationHandler: jest.fn().mockResolvedValue(response),
canUpdate: true,
});
await nextTick();
findInput().setValue('1');
findInput().trigger('blur');
findInput().vm.$emit('input', '1');
findApplyButton().vm.$emit('click');
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([
......@@ -317,12 +199,10 @@ describe('WorkItemWeight component', () => {
mutationHandler: jest.fn().mockRejectedValue(new Error()),
canUpdate: true,
});
await nextTick();
findInput().setValue('1');
findInput().trigger('blur');
findInput().vm.$emit('input', '1');
findApplyButton().vm.$emit('click');
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([
......@@ -333,11 +213,10 @@ describe('WorkItemWeight component', () => {
it('tracks updating the weight', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
createComponent({ isEditing: true, canUpdate: true });
await nextTick();
findInput().setValue('1');
findInput().trigger('blur');
findInput().vm.$emit('input', '1');
findApplyButton().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_weight', {
category: TRACKING_CATEGORY_SHOW,
......
import { GlLoadingIcon } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { Mousetrap } from '~/lib/mousetrap';
......@@ -111,11 +112,17 @@ describe('WorkItemSidebarWidget component', () => {
});
describe('when updating', () => {
it('renders Edit button as disabled', () => {
beforeEach(() => {
createComponent({ canUpdate: true, isUpdating: true });
});
it('renders Edit button as disabled', () => {
expect(findEditButton().props('disabled')).toBe(true);
});
it('shows loading icon', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
});
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册