Commit 14954c41 authored by Kushal Pandya's avatar Kushal Pandya

Add support for toast messages on user actions

Adds support for showing toast messages on
user actions like create, edit, archive & reopen.
Also adds missing license check for nav sidebar
requirements link.
parent a516e498
<script>
import * as Sentry from '@sentry/browser';
import { GlPagination } from '@gitlab/ui';
import { __ } from '~/locale';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import { updateHistory, setUrlParams, visitUrl } from '~/lib/utils/url_utility';
......@@ -149,18 +149,41 @@ export default {
requirementsListEmpty() {
return !this.$apollo.queries.requirements.loading && !this.requirements.list.length;
},
totalRequirements() {
/**
* We want to ensure that count `0` is prioritized
* over `this.requirements.count` (GraphQL) or `this.requirementsCount` (HAML prop)
* as both of them are invalid once user does archive/reopen actions.
* this is a technical debt that we want to clean up once mutations support
* `requirementStatesCount` connection.
*/
totalRequirementsForCurrentTab() {
if (this.filterBy === FilterState.opened) {
return this.openedCount === 0
? 0
: this.requirements.count.OPENED || this.requirementsCount.OPENED;
} else if (this.filterBy === FilterState.archived) {
return this.archivedCount === 0
? 0
: this.requirements.count.ARCHIVED || this.requirementsCount.ARCHIVED;
}
return this.requirements.count[this.filterBy] || this.requirementsCount[this.filterBy];
},
showEmptyState() {
return (
(this.requirementsListEmpty && !this.showCreateForm) || !this.totalRequirementsForCurrentTab
);
},
showPaginationControls() {
return this.totalRequirements > DEFAULT_PAGE_SIZE && !this.requirementsListEmpty;
return this.totalRequirementsForCurrentTab > DEFAULT_PAGE_SIZE && !this.requirementsListEmpty;
},
prevPage() {
return Math.max(this.currentPage - 1, 0);
},
nextPage() {
const nextPage = this.currentPage + 1;
return nextPage > Math.ceil(this.totalRequirements / DEFAULT_PAGE_SIZE) ? null : nextPage;
return nextPage > Math.ceil(this.totalRequirementsForCurrentTab / DEFAULT_PAGE_SIZE)
? null
: nextPage;
},
},
watch: {
......@@ -273,7 +296,7 @@ export default {
this.showUpdateFormForRequirement = iid;
},
handleNewRequirementSave(title) {
const reloadPage = this.totalRequirements === 0;
const reloadPage = this.totalRequirementsForCurrentTab === 0;
this.createRequirementRequestActive = true;
return this.$apollo
.mutate({
......@@ -293,6 +316,11 @@ export default {
this.showCreateForm = false;
this.$apollo.queries.requirements.refetch();
this.openedCount += 1;
this.$toast.show(
sprintf(__('Requirement %{reference} has been added'), {
reference: `REQ-${data.createRequirement.requirement.iid}`,
}),
);
}
} else {
throw new Error(`Error creating a requirement`);
......@@ -318,6 +346,11 @@ export default {
.then(({ data }) => {
if (!data.updateRequirement.errors.length) {
this.showUpdateFormForRequirement = 0;
this.$toast.show(
sprintf(__('Requirement %{reference} has been updated'), {
reference: `REQ-${data.updateRequirement.requirement.iid}`,
}),
);
} else {
throw new Error(`Error updating a requirement`);
}
......@@ -337,16 +370,23 @@ export default {
}).then(({ data }) => {
if (!data.updateRequirement.errors.length) {
this.stateChangeRequestActiveFor = 0;
} else {
throw new Error(`Error archiving a requirement`);
}
let toastMessage;
if (params.state === FilterState.opened) {
this.openedCount += 1;
this.archivedCount -= 1;
toastMessage = sprintf(__('Requirement %{reference} has been reopened'), {
reference: `REQ-${data.updateRequirement.requirement.iid}`,
});
} else {
this.openedCount -= 1;
this.archivedCount += 1;
toastMessage = sprintf(__('Requirement %{reference} has been archived'), {
reference: `REQ-${data.updateRequirement.requirement.iid}`,
});
}
this.$toast.show(toastMessage);
} else {
throw new Error(`Error archiving a requirement`);
}
});
},
......@@ -385,7 +425,7 @@ export default {
@cancel="handleNewRequirementCancel"
/>
<requirements-empty-state
v-if="requirementsListEmpty && !showCreateForm"
v-if="showEmptyState"
:filter-by="filterBy"
:empty-state-path="emptyStatePath"
:requirements-count="requirementsCount"
......
......@@ -2,5 +2,8 @@ mutation createRequirement($createRequirementInput: CreateRequirementInput!) {
createRequirement(input: $createRequirementInput) {
clientMutationId
errors
requirement {
iid
}
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlToast } from '@gitlab/ui';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
......@@ -8,6 +9,7 @@ import RequirementsRoot from './components/requirements_root.vue';
import { FilterState } from './constants';
Vue.use(VueApollo);
Vue.use(GlToast);
export default () => {
const el = document.getElementById('js-requirements-app');
......
- return unless Feature.enabled?(:requirements_management, project)
- return unless can?(current_user, :read_requirement, project)
- requirements_count = Hash.new(0).merge(project.requirements.counts_by_state)
- total_count = requirements_count['opened'] + requirements_count['archived']
......
......@@ -93,6 +93,15 @@ describe 'Project navbar' do
context 'when requirements is available' do
before do
stub_licensed_features(requirements: true)
stub_feature_flags(requirements_management: { enabled: true, thing: project })
insert_after_nav_item(
_('Merge Requests'),
new_nav_item: {
nav_item: _('Requirements'),
nav_sub_items: [_('List')]
}
)
visit project_path(project)
end
......
......@@ -25,6 +25,7 @@ describe 'Requirements list', :js do
before do
stub_licensed_features(requirements: true)
stub_feature_flags(requirements_management: { enabled: true, thing: project })
project.add_maintainer(user)
sign_in(user)
......
......@@ -36,6 +36,10 @@ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
}));
const $toast = {
show: jest.fn(),
};
const createComponent = ({
projectPath = 'gitlab-org/gitlab-shell',
filterBy = FilterState.opened,
......@@ -67,6 +71,7 @@ const createComponent = ({
},
mutate: jest.fn(),
},
$toast,
},
});
......@@ -92,9 +97,22 @@ describe('RequirementsRoot', () => {
});
describe('computed', () => {
describe('totalRequirements', () => {
describe('totalRequirementsForCurrentTab', () => {
it('returns number representing total requirements for current tab', () => {
expect(wrapper.vm.totalRequirements).toBe(mockRequirementsCount.OPENED);
expect(wrapper.vm.totalRequirementsForCurrentTab).toBe(mockRequirementsCount.OPENED);
});
it('returns 0 when `openedCount` is 0 and filterBy represents opened tab', () => {
wrapper.setProps({
filterBy: FilterState.opened,
});
wrapper.setData({
openedCount: 0,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.totalRequirementsForCurrentTab).toBe(0);
});
});
});
......@@ -323,6 +341,9 @@ describe('RequirementsRoot', () => {
data: {
createRequirement: {
errors: [],
requirement: {
iid: '1',
},
},
},
};
......@@ -388,6 +409,14 @@ describe('RequirementsRoot', () => {
});
});
it('calls `$toast.show` with string "Requirement added successfully" when request is successful', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockMutationResult);
return wrapper.vm.handleNewRequirementSave('foo').then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Requirement REQ-1 has been added');
});
});
it('sets `createRequirementRequestActive` prop to `false` and calls `createFlash` when `$apollo.mutate` request fails', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
......@@ -442,6 +471,21 @@ describe('RequirementsRoot', () => {
});
});
it('calls `$toast.show` with string "Requirement updated successfully" when request is successful', () => {
jest.spyOn(wrapper.vm, 'updateRequirement').mockResolvedValue(mockUpdateMutationResult);
return wrapper.vm
.handleUpdateRequirementSave({
iid: '1',
title: 'foo',
})
.then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
'Requirement REQ-1 has been updated',
);
});
});
it('sets `createRequirementRequestActive` prop to `false` when request fails', () => {
jest.spyOn(wrapper.vm, 'updateRequirement').mockRejectedValue(new Error());
......@@ -542,6 +586,19 @@ describe('RequirementsRoot', () => {
});
});
it('calls `$toast.show` with string "Requirement has been reopened" when `params.state` is "OPENED" and request is successful', () => {
return wrapper.vm
.handleRequirementStateChange({
iid: '1',
state: FilterState.opened,
})
.then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
'Requirement REQ-1 has been reopened',
);
});
});
it('decrements `openedCount` by 1 and increments `archivedCount` by 1 when `params.state` is "ARCHIVED"', () => {
wrapper.setData({
openedCount: 1,
......@@ -558,6 +615,19 @@ describe('RequirementsRoot', () => {
expect(wrapper.vm.archivedCount).toBe(2);
});
});
it('calls `$toast.show` with string "Requirement has been archived" when `params.state` is "ARCHIVED" and request is successful', () => {
return wrapper.vm
.handleRequirementStateChange({
iid: '1',
state: FilterState.archived,
})
.then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
'Requirement REQ-1 has been archived',
);
});
});
});
describe('handleUpdateRequirementCancel', () => {
......
......@@ -17178,6 +17178,18 @@ msgstr ""
msgid "Requirement"
msgstr ""
msgid "Requirement %{reference} has been added"
msgstr ""
msgid "Requirement %{reference} has been archived"
msgstr ""
msgid "Requirement %{reference} has been reopened"
msgstr ""
msgid "Requirement %{reference} has been updated"
msgstr ""
msgid "Requirement title cannot have more than %{limit} characters."
msgstr ""
......
# frozen_string_literal: true
RSpec.shared_context 'project navbar structure' do
let(:requirements_nav_item) do
{
nav_item: _('Requirements'),
nav_sub_items: [_('List')]
}
end
let(:analytics_nav_item) do
{
nav_item: _('Analytics'),
......@@ -56,7 +49,6 @@ RSpec.shared_context 'project navbar structure' do
nav_item: _('Merge Requests'),
nav_sub_items: []
},
(requirements_nav_item if Gitlab.ee?),
{
nav_item: _('CI / CD'),
nav_sub_items: [
......
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