Commit bc8ead52 authored by Florie Guibert's avatar Florie Guibert Committed by Natalia Tepluhina

Sidebar weight widget [RUN AS-IF-FOSS]

parent 7bd32bfe
......@@ -25,8 +25,8 @@ export default {
SidebarSubscriptionsWidget,
SidebarDropdownWidget,
MountingPortal,
BoardSidebarWeightInput: () =>
import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'),
SidebarWeightWidget: () =>
import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
IterationSidebarDropdownWidget: () =>
import('ee_component/sidebar/components/iteration_sidebar_dropdown_widget.vue'),
},
......@@ -65,7 +65,12 @@ export default {
},
},
methods: {
...mapActions(['toggleBoardItem', 'setAssignees', 'setActiveItemConfidential']),
...mapActions([
'toggleBoardItem',
'setAssignees',
'setActiveItemConfidential',
'setActiveItemWeight',
]),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
......@@ -144,7 +149,13 @@ export default {
data-testid="sidebar-due-date"
/>
<board-sidebar-labels-select class="labels" />
<board-sidebar-weight-input v-if="weightFeatureAvailable" class="weight" />
<sidebar-weight-widget
v-if="weightFeatureAvailable"
:iid="activeBoardItem.iid"
:full-path="fullPath"
:issuable-type="issuableType"
@weightUpdated="setActiveItemWeight($event)"
/>
<sidebar-confidentiality-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
......
......@@ -704,4 +704,7 @@ export default {
unsetError: ({ commit }) => {
commit(types.SET_ERROR, undefined);
},
// EE action needs CE empty equivalent
setActiveItemWeight: () => {},
};
......@@ -75,7 +75,7 @@
.js-sidebar-labels{ data: sidebar_labels_data(issuable_sidebar, @project) }
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar, can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid]
- if issuable_sidebar[:supports_severity]
#js-severity
......
......@@ -36,7 +36,7 @@ export default {
},
},
methods: {
...mapActions(['setActiveIssueWeight', 'setError']),
...mapActions(['setActiveItemWeight', 'setError']),
handleFormSubmit() {
this.$refs.sidebarItem.collapse({ emitEvent: false });
this.setWeight();
......@@ -51,7 +51,7 @@ export default {
this.loading = true;
try {
await this.setActiveIssueWeight({ weight, projectPath: this.projectPathForActiveIssue });
await this.setActiveItemWeight({ weight, projectPath: this.projectPathForActiveIssue });
this.weight = weight;
} catch (e) {
this.weight = this.activeBoardItem.weight;
......
......@@ -35,7 +35,6 @@ import epicMoveListMutation from '../graphql/epic_move_list.mutation.graphql';
import epicsSwimlanesQuery from '../graphql/epics_swimlanes.query.graphql';
import groupBoardIterationsQuery from '../graphql/group_board_iterations.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import issueSetWeightMutation from '../graphql/issue_set_weight.mutation.graphql';
import listUpdateLimitMetricsMutation from '../graphql/list_update_limit_metrics.mutation.graphql';
import listsEpicsQuery from '../graphql/lists_epics.query.graphql';
import projectBoardIterationsQuery from '../graphql/project_board_iterations.query.graphql';
......@@ -326,26 +325,11 @@ export default {
commit(types.RESET_EPICS);
},
setActiveIssueWeight: async ({ commit, getters }, input) => {
const { data } = await gqlClient.mutate({
mutation: issueSetWeightMutation,
variables: {
input: {
iid: String(getters.activeBoardItem.iid),
weight: input.weight,
projectPath: input.projectPath,
},
},
});
if (!data.issueSetWeight || data.issueSetWeight?.errors?.length > 0) {
throw new Error(data.issueSetWeight?.errors);
}
setActiveItemWeight: async ({ commit, getters }, weight) => {
commit(typesCE.UPDATE_BOARD_ITEM_BY_ID, {
itemId: getters.activeBoardItem.id,
prop: 'weight',
value: data.issueSetWeight.issue.weight,
value: weight,
});
},
......
<script>
import {
GlButton,
GlForm,
GlFormInput,
GlLoadingIcon,
GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { weightQueries, MAX_DISPLAY_WEIGHT } from '../../constants';
export default {
tracking: {
event: 'click_edit_button',
label: 'right_sidebar',
property: 'weight',
},
components: {
GlButton,
GlForm,
GlFormInput,
GlIcon,
GlLoadingIcon,
SidebarEditableItem,
},
directives: {
autofocusonshow,
GlTooltip: GlTooltipDirective,
},
inject: ['canUpdate'],
props: {
iid: {
type: String,
required: true,
},
fullPath: {
type: String,
required: true,
},
issuableType: {
required: true,
type: String,
},
},
data() {
return {
weight: null,
loading: false,
};
},
apollo: {
weight: {
query() {
return weightQueries[this.issuableType].query;
},
variables() {
return {
fullPath: this.fullPath,
iid: String(this.iid),
};
},
update(data) {
return data.workspace?.issuable?.weight || null;
},
result({ data }) {
this.$emit('weightUpdated', data.workspace?.issuable?.weight || null);
},
error() {
createFlash({
message: sprintf(__('Something went wrong while setting %{issuableType} weight.'), {
issuableType: this.issuableType,
}),
});
},
},
},
computed: {
isLoading() {
return this.$apollo.queries?.weight?.loading || this.loading;
},
hasWeight() {
return this.weight !== null;
},
weightLabel() {
return this.hasWeight ? this.weight : this.$options.i18n.noWeightLabel;
},
tooltipTitle() {
let tooltipTitle = this.$options.i18n.weight;
if (this.hasWeight) {
tooltipTitle += ` ${this.weight}`;
}
return tooltipTitle;
},
collapsedWeightLabel() {
return this.hasWeight
? this.weight.toString().substr(0, 5)
: this.$options.i18n.noWeightLabel;
},
},
methods: {
setWeight(remove) {
const weight = remove ? null : this.weight;
this.loading = true;
this.$apollo
.mutate({
mutation: weightQueries[this.issuableType].mutation,
variables: {
input: {
projectPath: this.fullPath,
iid: this.iid,
weight,
},
},
})
.then(
({
data: {
issuableSetWeight: { errors },
},
}) => {
if (errors.length) {
createFlash({
message: errors[0],
});
}
},
)
.catch(() => {
createFlash({
message: sprintf(__('Something went wrong while setting %{issuableType} weight.'), {
issuableType: this.issuableType,
}),
});
})
.finally(() => {
this.loading = false;
});
},
expandSidebar() {
this.$refs.editable.expand();
this.$emit('expandSidebar');
},
handleFormSubmit() {
this.$refs.editable.collapse({ emitEvent: false });
this.setWeight();
},
},
i18n: {
weight: __('Weight'),
noWeightLabel: __('None'),
removeWeight: __('remove weight'),
inputPlaceholder: __('Enter a number'),
},
maxDisplayWeight: MAX_DISPLAY_WEIGHT,
};
</script>
<template>
<sidebar-editable-item
ref="editable"
:title="$options.i18n.weight"
:tracking="$options.tracking"
:loading="isLoading"
class="block weight"
data-testid="sidebar-weight"
@close="setWeight()"
>
<template #collapsed>
<div class="gl-display-flex gl-align-items-center hide-collapsed">
<span
:class="hasWeight ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'"
data-testid="sidebar-weight-value"
data-qa-selector="weight_label_value"
>
{{ weightLabel }}
</span>
<div v-if="hasWeight && canUpdate" class="gl-display-flex">
<span class="gl-mx-2">-</span>
<gl-button
variant="link"
class="gl-text-gray-500!"
:disabled="loading"
@click="setWeight(true)"
>
{{ $options.i18n.removeWeight }}
</gl-button>
</div>
</div>
<div
v-gl-tooltip.left.viewport
:title="tooltipTitle"
class="sidebar-collapsed-icon js-weight-collapsed-block"
@click="expandSidebar"
>
<gl-icon :size="16" name="weight" />
<gl-loading-icon v-if="isLoading" class="js-weight-collapsed-loading-icon" />
<span v-else class="js-weight-collapsed-weight-label">
{{ collapsedWeightLabel }}
<template v-if="weight > $options.maxDisplayWeight">&hellip;</template>
</span>
</div>
</template>
<template #default>
<gl-form @submit.prevent="handleFormSubmit()">
<gl-form-input
v-model.number="weight"
v-autofocusonshow
type="number"
min="0"
:placeholder="$options.i18n.inputPlaceholder"
/>
</gl-form>
</template>
</sidebar-editable-item>
</template>
......@@ -8,10 +8,12 @@ import {
import epicAncestorsQuery from './queries/epic_ancestors.query.graphql';
import groupEpicsQuery from './queries/group_epics.query.graphql';
import groupIterationsQuery from './queries/group_iterations.query.graphql';
import issueWeightQuery from './queries/issue_weight.query.graphql';
import projectIssueEpicMutation from './queries/project_issue_epic.mutation.graphql';
import projectIssueEpicQuery from './queries/project_issue_epic.query.graphql';
import projectIssueIterationMutation from './queries/project_issue_iteration.mutation.graphql';
import projectIssueIterationQuery from './queries/project_issue_iteration.query.graphql';
import updateIssueWeightMutation from './queries/update_issue_weight.mutation.graphql';
export const healthStatus = {
ON_TRACK: 'onTrack',
......@@ -128,3 +130,10 @@ export const ancestorsQueries = {
query: epicAncestorsQuery,
},
};
export const weightQueries = {
[IssuableType.Issue]: {
query: issueWeightQuery,
mutation: updateIssueWeightMutation,
},
};
......@@ -9,7 +9,7 @@ import CveIdRequest from './components/cve_id_request/cve_id_request_sidebar.vue
import IterationSidebarDropdownWidget from './components/iteration_sidebar_dropdown_widget.vue';
import SidebarDropdownWidget from './components/sidebar_dropdown_widget.vue';
import SidebarStatus from './components/status/sidebar_status.vue';
import SidebarWeight from './components/weight/sidebar_weight.vue';
import SidebarWeightWidget from './components/weight/sidebar_weight_widget.vue';
import { IssuableAttributeType } from './constants';
Vue.use(VueApollo);
......@@ -19,12 +19,26 @@ const mountWeightComponent = () => {
if (!el) return false;
const { canEdit, projectPath, issueIid } = el.dataset;
return new Vue({
el,
apolloProvider,
components: {
SidebarWeight,
SidebarWeightWidget,
},
provide: {
canUpdate: parseBoolean(canEdit),
isClassicSidebar: true,
},
render: (createElement) => createElement('sidebar-weight'),
render: (createElement) =>
createElement('sidebar-weight-widget', {
props: {
fullPath: projectPath,
iid: issueIid,
issuableType: IssuableType.Issue,
},
}),
});
};
......
query issueWeight($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
__typename
issuable: issue(iid: $iid) {
__typename
id
weight
}
}
}
mutation issueSetWeight($input: IssueSetWeightInput!) {
issuableSetWeight: issueSetWeight(input: $input) {
issuable: issue {
id
weight
}
errors
}
}
- if issuable_sidebar[:supports_weight]
- if issuable_sidebar[:features_available][:issue_weights]
.js-sidebar-weight-entry-point
.js-sidebar-weight-entry-point{ data: { can_edit: can_edit, project_path: project_path, issue_iid: issue_iid } }
- else
= render 'shared/promotions/promote_issue_weights'
......@@ -222,7 +222,7 @@ RSpec.describe 'Issue Boards', :js do
context 'weight' do
let(:weight_widget) { find('[data-testid="sidebar-weight"]') }
let(:weight_value) { find('[data-testid="sidebar-weight"] .value') }
let(:weight_value) { find('[data-testid="sidebar-weight-value"]') }
it 'displays weight async' do
click_card(card1)
......
......@@ -41,10 +41,10 @@ RSpec.describe 'Issue Sidebar' do
it 'updates weight in sidebar to 1' do
page.within '.weight' do
click_link 'Edit'
click_button 'Edit'
find('input').send_keys 1, :enter
page.within '.value' do
page.within '[data-testid="sidebar-weight-value"]' do
expect(page).to have_content '1'
end
end
......@@ -52,16 +52,16 @@ RSpec.describe 'Issue Sidebar' do
it 'updates weight in sidebar to no weight' do
page.within '.weight' do
click_link 'Edit'
click_button 'Edit'
find('input').send_keys 1, :enter
page.within '.value' do
page.within '[data-testid="sidebar-weight-value"]' do
expect(page).to have_content '1'
end
click_link 'remove weight'
click_button 'remove weight'
page.within '.value' do
page.within '[data-testid="sidebar-weight-value"]' do
expect(page).to have_content 'None'
end
end
......
......@@ -19,11 +19,11 @@ RSpec.describe 'Issue weight', :js do
page.within('.weight') do
expect(page).to have_content "None"
click_link 'Edit'
click_button 'Edit'
find('.block.weight input').send_keys 1, :enter
page.within('.value') do
page.within('[data-testid="sidebar-weight-value"]') do
expect(page).to have_content "1"
end
end
......@@ -37,11 +37,11 @@ RSpec.describe 'Issue weight', :js do
page.within('.weight') do
expect(page).to have_content "2"
click_link 'Edit'
click_button 'Edit'
find('.block.weight input').send_keys 3, :enter
page.within('.value') do
page.within('[data-testid="sidebar-weight-value"]') do
expect(page).to have_content "3"
end
end
......@@ -55,9 +55,9 @@ RSpec.describe 'Issue weight', :js do
page.within('.weight') do
expect(page).to have_content "5"
click_link 'remove weight'
click_button 'remove weight'
page.within('.value') do
page.within('[data-testid="sidebar-weight-value"]') do
expect(page).to have_content "None"
end
end
......
......@@ -64,8 +64,10 @@ exports[`ee/BoardContentSidebar matches the snapshot 1`] = `
class="labels"
/>
<boardsidebarweightinput-stub
class="weight"
<sidebarweightwidget-stub
full-path="gitlab-org/gitlab-test"
iid="27"
issuable-type="issue"
/>
<sidebarconfidentialitywidget-stub
......
......@@ -69,7 +69,7 @@ describe('ee/BoardContentSidebar', () => {
SidebarConfidentialityWidget: true,
SidebarDateWidget: true,
SidebarSubscriptionsWidget: true,
BoardSidebarWeightInput: true,
SidebarWeightWidget: true,
SidebarDropdownWidget: true,
MountingPortal: true,
},
......
......@@ -52,7 +52,7 @@ describe('ee/boards/components/sidebar/board_sidebar_weight_input.vue', () => {
describe('when weight is submitted', () => {
beforeEach(async () => {
createWrapper();
jest.spyOn(wrapper.vm, 'setActiveIssueWeight');
jest.spyOn(wrapper.vm, 'setActiveItemWeight');
findWeightInput().vm.$emit('input', TEST_WEIGHT);
findWeightForm().vm.$emit('submit', { preventDefault: () => {} });
store.state.boardItems[TEST_ISSUE.id].weight = TEST_WEIGHT;
......@@ -66,7 +66,7 @@ describe('ee/boards/components/sidebar/board_sidebar_weight_input.vue', () => {
});
it('commits change to the server', () => {
expect(wrapper.vm.setActiveIssueWeight).toHaveBeenCalledWith({
expect(wrapper.vm.setActiveItemWeight).toHaveBeenCalledWith({
weight: TEST_WEIGHT,
projectPath: 'h/b',
});
......@@ -76,7 +76,7 @@ describe('ee/boards/components/sidebar/board_sidebar_weight_input.vue', () => {
describe('when weight is set to 0', () => {
beforeEach(async () => {
createWrapper({ weight: TEST_WEIGHT });
jest.spyOn(wrapper.vm, 'setActiveIssueWeight');
jest.spyOn(wrapper.vm, 'setActiveItemWeight');
findWeightInput().vm.$emit('input', 0);
findWeightForm().vm.$emit('submit', { preventDefault: () => {} });
store.state.boardItems[TEST_ISSUE.id].weight = 0;
......@@ -84,7 +84,7 @@ describe('ee/boards/components/sidebar/board_sidebar_weight_input.vue', () => {
});
it('collapses sidebar and renders "None"', () => {
expect(wrapper.vm.setActiveIssueWeight).toHaveBeenCalled();
expect(wrapper.vm.setActiveItemWeight).toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toBe('None');
});
......@@ -93,14 +93,14 @@ describe('ee/boards/components/sidebar/board_sidebar_weight_input.vue', () => {
describe('when weight is resetted', () => {
beforeEach(async () => {
createWrapper({ weight: TEST_WEIGHT });
jest.spyOn(wrapper.vm, 'setActiveIssueWeight');
jest.spyOn(wrapper.vm, 'setActiveItemWeight');
findResetButton().vm.$emit('click');
store.state.boardItems[TEST_ISSUE.id].weight = 0;
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders "None"', () => {
expect(wrapper.vm.setActiveIssueWeight).toHaveBeenCalled();
expect(wrapper.vm.setActiveItemWeight).toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toBe('None');
});
......@@ -109,7 +109,7 @@ describe('ee/boards/components/sidebar/board_sidebar_weight_input.vue', () => {
describe('when the mutation fails', () => {
beforeEach(async () => {
createWrapper({ weight: TEST_WEIGHT });
jest.spyOn(wrapper.vm, 'setActiveIssueWeight').mockImplementation(() => {
jest.spyOn(wrapper.vm, 'setActiveItemWeight').mockImplementation(() => {
throw new Error(['failed mutation']);
});
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
......
......@@ -608,27 +608,13 @@ describe('resetEpics', () => {
});
});
describe('setActiveIssueWeight', () => {
describe('setActiveItemWeight', () => {
const state = { boardItems: { [mockIssue.id]: mockIssue } };
const getters = { activeBoardItem: mockIssue };
const testWeight = mockIssue.weight + 1;
const input = {
weight: testWeight,
projectPath: 'h/b',
};
it('should commit weight after setting the issue', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueSetWeight: {
issue: {
weight: testWeight,
},
errors: [],
},
},
});
const input = testWeight;
it('should commit weight', (done) => {
const payload = {
itemId: getters.activeBoardItem.id,
prop: 'weight',
......@@ -636,7 +622,7 @@ describe('setActiveIssueWeight', () => {
};
testAction(
actions.setActiveIssueWeight,
actions.setActiveItemWeight,
input,
{ ...state, ...getters },
[
......@@ -655,7 +641,7 @@ describe('setActiveIssueWeight', () => {
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { issueSetWeight: { errors: ['failed mutation'] } } });
await expect(actions.setActiveIssueWeight({ getters }, input)).rejects.toThrow(Error);
await expect(actions.setActiveItemWeight({ getters }, input)).rejects.toThrow(Error);
});
});
......
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import SidebarWeightWidget from 'ee_component/sidebar/components/weight/sidebar_weight_widget.vue';
import issueWeightQuery from 'ee_component/sidebar/queries/issue_weight.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { issueNoWeightResponse, issueWeightResponse } from '../../mock_data';
jest.mock('~/flash');
Vue.use(VueApollo);
describe('Sidebar Weight Widget', () => {
let wrapper;
let fakeApollo;
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findWeightValue = () => wrapper.findByTestId('sidebar-weight-value');
const createComponent = ({
weightQueryHandler = jest.fn().mockResolvedValue(issueNoWeightResponse()),
} = {}) => {
fakeApollo = createMockApollo([[issueWeightQuery, weightQueryHandler]]);
wrapper = extendedWrapper(
shallowMount(SidebarWeightWidget, {
apolloProvider: fakeApollo,
provide: {
canUpdate: true,
},
propsData: {
fullPath: 'group/project',
iid: '1',
issuableType: 'issue',
},
stubs: {
SidebarEditableItem,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
});
it('passes a `loading` prop as true to editable item when query is loading', () => {
createComponent();
expect(findEditableItem().props('loading')).toBe(true);
});
describe('when issue has no weight', () => {
beforeEach(() => {
createComponent();
return waitForPromises();
});
it('passes a `loading` prop as false to editable item', () => {
expect(findEditableItem().props('loading')).toBe(false);
});
it('toggle is unchecked', () => {
expect(findWeightValue().text()).toBe('None');
});
it('emits `weightUpdated` event with a `null` payload', () => {
expect(wrapper.emitted('weightUpdated')).toEqual([[null]]);
});
});
describe('when issue has weight', () => {
beforeEach(() => {
createComponent({
weightQueryHandler: jest.fn().mockResolvedValue(issueWeightResponse(true)),
});
return waitForPromises();
});
it('passes a `loading` prop as false to editable item', () => {
expect(findEditableItem().props('loading')).toBe(false);
});
it('toggle is checked', () => {
expect(findWeightValue().text()).toBe('1');
});
it('emits `weightUpdated` event with a `true` payload', () => {
expect(wrapper.emitted('weightUpdated')).toEqual([[1]]);
});
});
it('displays a flash message when query is rejected', async () => {
createComponent({
weightQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
});
});
......@@ -166,3 +166,21 @@ export const epicAncestorsResponse = () => ({
},
},
});
export const issueNoWeightResponse = () => ({
data: {
workspace: {
issuable: { id: mockIssueId, weight: null, __typename: 'Issue' },
__typename: 'Project',
},
},
});
export const issueWeightResponse = () => ({
data: {
workspace: {
issuable: { id: mockIssueId, weight: 1, __typename: 'Issue' },
__typename: 'Project',
},
},
});
......@@ -30480,6 +30480,9 @@ msgstr ""
msgid "Something went wrong while setting %{issuableType} to-do item."
msgstr ""
msgid "Something went wrong while setting %{issuableType} weight."
msgstr ""
msgid "Something went wrong while stopping this environment. Please try again."
msgstr ""
......
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