Commit 376781b5 authored by Alexander Turinske's avatar Alexander Turinske Committed by Phil Hughes

Remove vuex from project manager component

- create a project manager component that utilizes GraphQL
  and the GraphQL cache instead of Vuex
- move the remaining Vuex store and actions into the component
- update child components to handle new project structure
- update tests
parent fed9026b
......@@ -17,7 +17,10 @@ export default {
project: {
type: Object,
required: true,
validator: p => Number.isFinite(p.id) && isString(p.name) && isString(p.name_with_namespace),
validator: p =>
(Number.isFinite(p.id) || isString(p.id)) &&
isString(p.name) &&
(isString(p.name_with_namespace) || isString(p.nameWithNamespace)),
},
selected: {
type: Boolean,
......@@ -30,8 +33,11 @@ export default {
},
},
computed: {
projectNameWithNamespace() {
return this.project.nameWithNamespace || this.project.name_with_namespace;
},
truncatedNamespace() {
return truncateNamespace(this.project.name_with_namespace);
return truncateNamespace(this.projectNameWithNamespace);
},
highlightedProjectName() {
return highlight(this.project.name, this.matcher);
......@@ -58,7 +64,7 @@ export default {
<div class="d-flex flex-wrap project-namespace-name-container">
<div
v-if="truncatedNamespace"
:title="project.name_with_namespace"
:title="projectNameWithNamespace"
class="text-secondary text-truncate js-project-namespace"
>
{{ truncatedNamespace }}
......
......@@ -41,7 +41,8 @@ export default {
},
totalResults: {
type: Number,
required: true,
required: false,
default: 0,
},
},
data() {
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlLoadingIcon, GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import { __, s__ } from '~/locale';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import InstanceSecurityVulnerabilities from './first_class_instance_security_dashboard_vulnerabilities.vue';
import VulnerabilitySeverity from 'ee/security_dashboard/components/vulnerability_severity.vue';
import VulnerabilityChart from 'ee/security_dashboard/components/first_class_vulnerability_chart.vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import ProjectManager from './project_manager.vue';
import projectsQuery from 'ee/security_dashboard/graphql/get_instance_security_dashboard_projects.query.graphql';
import ProjectManager from './first_class_project_manager/project_manager.vue';
import CsvExportButton from './csv_export_button.vue';
import vulnerabilityHistoryQuery from '../graphql/instance_vulnerability_history.graphql';
......@@ -38,30 +39,38 @@ export default {
type: String,
required: true,
},
projectAddEndpoint: {
type: String,
required: true,
},
projectListEndpoint: {
type: String,
required: true,
},
vulnerabilitiesExportEndpoint: {
type: String,
required: true,
},
},
apollo: {
projects: {
query: projectsQuery,
update(data) {
return data.instanceSecurityDashboard.projects.nodes;
},
error() {
createFlash(__('Something went wrong, unable to get projects'));
},
},
},
data() {
return {
filters: {},
graphqlProjectList: [], // TODO: Rename me to projects once we back the project selector with GraphQL as well
showProjectSelector: false,
vulnerabilityHistoryQuery,
projects: [],
isManipulatingProjects: false,
};
},
computed: {
...mapState('projectSelector', ['projects']),
...mapGetters('projectSelector', ['isUpdatingProjects']),
isLoadingProjects() {
return this.$apollo.queries.projects.loading;
},
isUpdatingProjects() {
return this.isLoadingProjects || this.isManipulatingProjects;
},
hasProjectsData() {
return !this.isUpdatingProjects && this.projects.length > 0;
},
......@@ -81,24 +90,15 @@ export default {
};
},
},
created() {
this.setProjectEndpoints({
add: this.projectAddEndpoint,
list: this.projectListEndpoint,
});
this.fetchProjects();
},
methods: {
...mapActions('projectSelector', ['setProjectEndpoints', 'fetchProjects']),
handleFilterChange(filters) {
this.filters = filters;
},
toggleProjectSelector() {
this.showProjectSelector = !this.showProjectSelector;
},
handleProjectFetch(projects) {
this.graphqlProjectList = projects;
handleProjectManipulation(value) {
this.isManipulatingProjects = value;
},
},
};
......@@ -119,12 +119,7 @@ export default {
</header>
</template>
<template #sticky>
<filters
v-if="shouldShowDashboard"
:projects="graphqlProjectList"
@filterChange="handleFilterChange"
@projectFetch="handleProjectFetch"
/>
<filters v-if="shouldShowDashboard" :projects="projects" @filterChange="handleFilterChange" />
</template>
<instance-security-vulnerabilities
v-if="shouldShowDashboard"
......@@ -132,7 +127,6 @@ export default {
:dashboard-documentation="dashboardDocumentation"
:empty-state-svg-path="emptyStateSvgPath"
:filters="filters"
@projectFetch="handleProjectFetch"
/>
<gl-empty-state
v-else-if="shouldShowEmptyState"
......@@ -156,7 +150,12 @@ export default {
</template>
</gl-empty-state>
<div v-else class="d-flex justify-content-center">
<project-manager v-if="showProjectSelector" />
<project-manager
v-if="showProjectSelector"
:projects="projects"
:is-manipulating-projects="isManipulatingProjects"
@handle-project-manipulation="handleProjectManipulation"
/>
<gl-loading-icon v-else size="lg" class="mt-4" />
</div>
<template #aside>
......
......@@ -57,7 +57,6 @@ export default {
result({ data, loading }) {
this.isFirstResultLoading = loading;
this.pageInfo = data.vulnerabilities.pageInfo;
this.$emit('projectFetch', data.instanceSecurityDashboard.projects.nodes);
},
error() {
this.errorLoadingVulnerabilities = true;
......
......@@ -53,7 +53,7 @@ export default {
>
<project-avatar class="flex-shrink-0" :project="project" :size="32" />
<span>
{{ project.name_with_namespace }}
{{ project.name_with_namespace || project.nameWithNamespace }}
</span>
<gl-deprecated-button
v-gl-tooltip
......
<script>
import createFlash from '~/flash';
import { __, s__, sprintf } from '~/locale';
import { GlButton } from '@gitlab/ui';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
import ProjectList from './project_list.vue';
import getProjects from 'ee/security_dashboard/graphql/get_projects.query.graphql';
import projectsQuery from 'ee/security_dashboard/graphql/get_instance_security_dashboard_projects.query.graphql';
import addProjectToSecurityDashboard from 'ee/security_dashboard/graphql/add_project_to_security_dashboard.mutation.graphql';
import deleteProjectFromSecurityDashboard from 'ee/security_dashboard/graphql/delete_project_from_security_dashboard.mutation.graphql';
import { createInvalidProjectMessage } from 'ee/security_dashboard/utils/first_class_project_manager_utils';
export default {
MINIMUM_QUERY_LENGTH: 3,
PROJECTS_PER_PAGE: 20,
components: {
GlButton,
ProjectList,
ProjectSelector,
},
props: {
isManipulatingProjects: {
type: Boolean,
required: true,
},
projects: {
type: Array,
required: true,
},
},
data() {
return {
searchQuery: '',
projectSearchResults: [],
selectedProjects: [],
messages: {
noResults: false,
searchError: false,
minimumQuery: false,
},
searchCount: 0,
pageInfo: {
endCursor: '',
hasNextPage: true,
},
};
},
computed: {
canAddProjects() {
return !this.isManipulatingProjects && this.selectedProjects.length > 0;
},
isSearchingProjects() {
return this.searchCount > 0;
},
},
methods: {
toggleSelectedProject(project) {
const isProjectSelected = this.selectedProjects.some(({ id }) => id === project.id);
if (isProjectSelected) {
this.selectedProjects = this.selectedProjects.filter(p => p.id !== project.id);
} else {
this.selectedProjects.push(project);
}
},
addProjects() {
this.$emit('handleProjectManipulation', true);
const addProjectsPromises = this.selectedProjects.map(project => {
return this.$apollo
.mutate({
mutation: addProjectToSecurityDashboard,
variables: { id: project.id },
update(store, { data: results }) {
const data = store.readQuery({
query: projectsQuery,
});
data.instanceSecurityDashboard.projects.nodes.push(
results.addProjectToSecurityDashboard.project,
);
store.writeQuery({ query: projectsQuery, data });
},
})
.catch(() => {
return { error: true, project };
});
});
return Promise.all(addProjectsPromises)
.then(response => {
const invalidProjects = response.filter(value => value.error).map(value => value.project);
this.$emit('handleProjectManipulation', false);
if (invalidProjects.length) {
const invalidProjectsMessage = createInvalidProjectMessage(invalidProjects);
createFlash(
sprintf(s__('SecurityReports|Unable to add %{invalidProjectsMessage}'), {
invalidProjectsMessage,
}),
);
}
})
.finally(() => {
this.projectSearchResults = [];
this.selectedProjects = [];
});
},
removeProject(project) {
const { id } = project;
this.$emit('handleProjectManipulation', true);
this.$apollo
.mutate({
mutation: deleteProjectFromSecurityDashboard,
variables: { id },
update(store) {
const data = store.readQuery({
query: projectsQuery,
});
data.instanceSecurityDashboard.projects.nodes = data.instanceSecurityDashboard.projects.nodes.filter(
curr => curr.id !== id,
);
store.writeQuery({ query: projectsQuery, data });
},
})
.then(() => {
this.$emit('handleProjectManipulation', false);
})
.catch(() => createFlash(__('Something went wrong, unable to remove project')));
},
searched(query) {
this.searchQuery = query;
this.pageInfo = { endCursor: '', hasNextPage: true };
this.messages.minimumQuery = false;
this.searchCount += 1;
this.fetchSearchResults(true);
},
fetchSearchResults(isFirstSearch) {
if (!this.pageInfo.hasNextPage) {
return Promise.resolve();
}
if (!this.searchQuery || this.searchQuery.length < this.$options.MINIMUM_QUERY_LENGTH) {
return this.cancelSearch();
}
return this.searchProjects(this.searchQuery, this.pageInfo)
.then(payload => {
const {
data: {
projects: { nodes, pageInfo },
},
} = payload;
if (isFirstSearch) {
this.projectSearchResults = nodes;
this.updateMessagesData(this.projectSearchResults.length === 0, false, false);
this.searchCount = Math.max(0, this.searchCount - 1);
} else {
this.projectSearchResults = this.projectSearchResults.concat(nodes);
}
this.pageInfo = pageInfo;
})
.catch(this.fetchSearchResultsError);
},
cancelSearch() {
this.projectSearchResults = [];
this.pageInfo = {
endCursor: '',
hasNextPage: true,
};
this.updateMessagesData(false, false, true);
this.searchCount = Math.max(0, this.searchCount - 1);
},
searchProjects(searchQuery, pageInfo) {
return this.$apollo.query({
query: getProjects,
variables: {
search: searchQuery,
first: this.$options.PROJECTS_PER_PAGE,
after: pageInfo.endCursor,
},
});
},
fetchSearchResultsError() {
this.projectSearchResults = [];
this.updateMessagesData(false, true, false);
this.searchCount = Math.max(0, this.searchCount - 1);
},
updateMessagesData(noResults, searchError, minimumQuery) {
this.messages = {
noResults,
searchError,
minimumQuery,
};
},
},
};
</script>
<template>
<section class="container">
<div class="row justify-content-center mt-md-4">
<div class="col col-lg-7">
<h3 class="text-3 font-weight-bold border-bottom mb-4 pb-3">
{{ s__('SecurityReports|Add or remove projects from your dashboard') }}
</h3>
<div class="d-flex flex-column flex-md-row">
<project-selector
class="flex-grow mr-md-2"
:project-search-results="projectSearchResults"
:selected-projects="selectedProjects"
:show-no-results-message="messages.noResults"
:show-loading-indicator="isSearchingProjects"
:show-minimum-search-query-message="messages.minimumQuery"
:show-search-error-message="messages.searchError"
@searched="searched"
@projectClicked="toggleSelectedProject"
@bottomReached="fetchSearchResults"
/>
<div class="mb-3">
<gl-button
:disabled="!canAddProjects"
variant="success"
category="primary"
@click="addProjects"
>
{{ s__('SecurityReports|Add projects') }}
</gl-button>
</div>
</div>
</div>
</div>
<div class="row justify-content-center mt-md-3">
<project-list
:projects="projects"
:show-loading-indicator="isManipulatingProjects"
class="col col-lg-7"
@projectRemoved="removeProject"
/>
</div>
</section>
</template>
......@@ -2,7 +2,7 @@
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlDeprecatedButton } from '@gitlab/ui';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
import ProjectList from './project_list.vue';
import ProjectList from './first_class_project_manager/project_list.vue';
export default {
components: {
......
......@@ -7,8 +7,6 @@ import FirstClassInstanceSecurityDashboard from './components/first_class_instan
import UnavailableState from './components/unavailable_state.vue';
import createStore from './store';
import createRouter from './router';
import projectsPlugin from './store/plugins/projects';
import projectSelector from './store/plugins/project_selector';
import apolloProvider from './graphql/provider';
const isRequired = message => {
......@@ -63,10 +61,7 @@ export default (
}
const router = createRouter();
const store = createStore({
dashboardType,
plugins: [projectSelector, projectsPlugin],
});
const store = createStore({ dashboardType });
return new Vue({
el,
......
#import "ee/security_dashboard/graphql/project.fragment.graphql"
query getInstanceSecurityDashboardProjects {
query projectsQuery {
instanceSecurityDashboard {
projects {
nodes {
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "ee/security_dashboard/graphql/project.fragment.graphql"
query getProjects($search: String!) {
projects(search: $search, membership: true) {
query getProjects($search: String!, $after: String = "", $first: Int!) {
projects(search: $search, after: $after, first: $first, membership: true) {
nodes {
...Project
avatarUrl
......
import { s__, sprintf } from '~/locale';
/**
* Creates the notification text to show the user regarding projects that failed to get added
* to the dashboard
*
* @param {Array} invalidProjects all the projects that failed to be added
* @returns {String} the invalid projects formated in a user-friendly way
*/
export const createInvalidProjectMessage = invalidProjects => {
const [firstProject, secondProject, ...rest] = invalidProjects.map(project => project.name);
const translationValues = {
firstProject,
secondProject,
rest: rest.join(', '),
};
if (rest.length > 0) {
return sprintf(
s__('SecurityReports|%{firstProject}, %{secondProject}, and %{rest}'),
translationValues,
);
} else if (secondProject) {
return sprintf(s__('SecurityReports|%{firstProject} and %{secondProject}'), translationValues);
}
return firstProject;
};
export default {};
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlButton } from '@gitlab/ui';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import FirstClassInstanceDashboard from 'ee/security_dashboard/components/first_class_instance_security_dashboard.vue';
......@@ -8,21 +7,17 @@ import VulnerabilitySeverity from 'ee/security_dashboard/components/vulnerabilit
import VulnerabilityChart from 'ee/security_dashboard/components/first_class_vulnerability_chart.vue';
import CsvExportButton from 'ee/security_dashboard/components/csv_export_button.vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import ProjectManager from 'ee/security_dashboard/components/project_manager.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
import ProjectManager from 'ee/security_dashboard/components/first_class_project_manager/project_manager.vue';
describe('First Class Instance Dashboard Component', () => {
let wrapper;
let store;
const defaultMocks = { $apollo: { queries: { projects: { loading: false } } } };
const dashboardDocumentation = 'dashboard-documentation';
const emptyStateSvgPath = 'empty-state-path';
const vulnerableProjectsEndpoint = '/vulnerable/projects';
const vulnerabilitiesExportEndpoint = '/vulnerabilities/exports';
const projectAddEndpoint = 'projectAddEndpoint';
const projectListEndpoint = 'projectListEndpoint';
const findInstanceVulnerabilities = () => wrapper.find(FirstClassInstanceVulnerabilities);
const findVulnerabilitySeverity = () => wrapper.find(VulnerabilitySeverity);
......@@ -32,33 +27,15 @@ describe('First Class Instance Dashboard Component', () => {
const findEmptyState = () => wrapper.find(GlEmptyState);
const findFilters = () => wrapper.find(Filters);
const createWrapper = ({ isUpdatingProjects = false, projects = [], stubs }) => {
store = new Vuex.Store({
modules: {
projectSelector: {
namespaced: true,
actions: {
fetchProjects() {},
setProjectEndpoints() {},
},
getters: {
isUpdatingProjects: jest.fn().mockReturnValue(isUpdatingProjects),
},
state: {
projects,
},
},
},
});
const createWrapper = ({ data = {}, stubs }) => {
return shallowMount(FirstClassInstanceDashboard, {
localVue,
store,
data() {
return { ...data };
},
mocks: { ...defaultMocks },
propsData: {
dashboardDocumentation,
emptyStateSvgPath,
projectAddEndpoint,
projectListEndpoint,
vulnerableProjectsEndpoint,
vulnerabilitiesExportEndpoint,
},
......@@ -77,8 +54,9 @@ describe('First Class Instance Dashboard Component', () => {
describe('when initialized', () => {
beforeEach(() => {
wrapper = createWrapper({
isUpdatingProjects: false,
projects: [{ id: 1 }, { id: 2 }],
data: {
projects: [{ id: 1 }, { id: 2 }],
},
});
});
......@@ -98,14 +76,6 @@ describe('First Class Instance Dashboard Component', () => {
expect(findVulnerabilityChart().props('groupFullPath')).toBeUndefined();
});
it('responds to the projectFetch event', () => {
const projects = [{ id: 1, name: 'GitLab Org' }];
findInstanceVulnerabilities().vm.$listeners.projectFetch(projects);
return wrapper.vm.$nextTick(() => {
expect(findFilters().props('projects')).toEqual(projects);
});
});
it('responds to the filterChange event', () => {
const filters = { severity: 'critical' };
findFilters().vm.$listeners.filterChange(filters);
......@@ -129,10 +99,12 @@ describe('First Class Instance Dashboard Component', () => {
describe('when uninitialized', () => {
beforeEach(() => {
wrapper = createWrapper({
isUpdatingProjects: false,
stubs: {
GlEmptyState,
GlButton,
data: {
isManipulatingProjects: false,
stubs: {
GlEmptyState,
GlButton,
},
},
});
});
......
import { shallowMount } from '@vue/test-utils';
import { GlDeprecatedBadge as GlBadge, GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
import ProjectList from 'ee/security_dashboard/components/project_list.vue';
import ProjectList from 'ee/security_dashboard/components/first_class_project_manager/project_list.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
const getArrayWithLength = n => [...Array(n).keys()];
......@@ -67,7 +67,7 @@ describe('Project List component', () => {
).toBe(true);
});
it('renders a project-item with the project name', () => {
it('renders a project-item with a project name', () => {
const projectNameWithNamespace = 'foo';
factory({
......@@ -77,6 +77,15 @@ describe('Project List component', () => {
expect(getFirstProjectItem().text()).toContain(projectNameWithNamespace);
});
it('renders a project-item with a GraphQL project name', () => {
const projectNameWithNamespace = 'foo';
factory({
projects: generateMockProjects(1, { nameWithNamespace: projectNameWithNamespace }),
});
expect(getFirstProjectItem().text()).toContain(projectNameWithNamespace);
});
it('renders a project-item with a remove button', () => {
factory({ projects: generateMockProjects(1) });
......
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import createFlash from '~/flash';
import ProjectManager from 'ee/security_dashboard/components/first_class_project_manager/project_manager.vue';
import ProjectList from 'ee/security_dashboard/components/first_class_project_manager/project_list.vue';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
import getProjects from 'ee/security_dashboard/graphql/get_projects.query.graphql';
import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/flash');
const mockProject = { id: 1, name: 'Sample Project 1' };
const singleProjectList = [mockProject];
const multipleProjectsList = [
{ id: 2, name: 'Sample Project 2' },
{ id: 3, name: 'Sample Project 3' },
];
const mockPageInfo = { hasNextPage: false, endCursor: '' };
describe('Project Manager component', () => {
let wrapper;
let spyQuery;
let spyMutate;
const defaultMocks = {
$apollo: {
query: jest.fn().mockResolvedValue({
data: { projects: { nodes: singleProjectList, pageInfo: mockPageInfo } },
}),
mutate: jest.fn().mockResolvedValue({}),
},
};
const defaultProps = {
isManipulatingProjects: false,
projects: [],
};
const createWrapper = ({ data = {}, mocks = {}, props = {} }) => {
spyQuery = defaultMocks.$apollo.query;
spyMutate = defaultMocks.$apollo.mutate;
wrapper = shallowMount(ProjectManager, {
data() {
return { ...data };
},
mocks: { ...defaultMocks, ...mocks },
propsData: { ...defaultProps, ...props },
});
};
const findAddProjectsButton = () => wrapper.find(GlButton);
const findProjectList = () => wrapper.find(ProjectList);
const findProjectSelector = () => wrapper.find(ProjectSelector);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('it renders', () => {
beforeEach(() => createWrapper({}));
it('contains a project-selector component', () => {
expect(findProjectSelector().exists()).toBe(true);
});
it('contains a project-list component', () => {
expect(findProjectList().exists()).toBe(true);
});
it('contains the add project button', () => {
expect(findAddProjectsButton().exists()).toBe(true);
});
});
describe('searching projects', () => {
beforeEach(() => createWrapper({}));
it('searches with the query', () => {
findProjectSelector().vm.$emit('searched', 'test');
expect(spyQuery).toHaveBeenCalledTimes(1);
expect(spyQuery).toHaveBeenCalledWith({
query: getProjects,
variables: {
search: 'test',
first: wrapper.vm.$options.PROJECTS_PER_PAGE,
after: '',
},
});
});
it('does not search if the query is below the minimum query limit', () => {
findProjectSelector().vm.$emit('searched', 'te');
expect(spyQuery).not.toHaveBeenCalled();
});
it('passes the search results to the project-selector on a successful search', () => {
findProjectSelector().vm.$emit('searched', 'test');
return waitForPromises().then(() => {
expect(findProjectSelector().props('projectSearchResults')).toBe(singleProjectList);
});
});
it('passes an empty array to the project-selector on a failed search', () => {
const mocks = {
$apollo: {
query: jest.fn().mockRejectedValue(),
},
};
createWrapper({ data: { selectedProjects: singleProjectList }, mocks });
findProjectSelector().vm.$emit('searched', 'test');
return waitForPromises().then(() => {
expect(findProjectSelector().props('projectSearchResults')).toEqual([]);
});
});
});
describe('project selection', () => {
it('adds a project to the list of selected projects', () => {
createWrapper({});
findProjectSelector().vm.$emit('projectClicked', mockProject);
return waitForPromises().then(() => {
expect(findProjectSelector().props('selectedProjects')).toEqual(singleProjectList);
});
});
it('removes a project from the list of selected projects', () => {
createWrapper({ data: { selectedProjects: singleProjectList } });
findProjectSelector().vm.$emit('projectClicked', mockProject);
return waitForPromises().then(() => {
expect(findProjectSelector().props('selectedProjects')).toEqual([]);
});
});
});
describe('adding projects', () => {
it('disables the add project button if no projects are selected', () => {
createWrapper({});
expect(findAddProjectsButton().attributes('disabled')).toBe('true');
});
it('enables the add project button if projects are selected', () => {
createWrapper({ data: { selectedProjects: singleProjectList } });
expect(findAddProjectsButton().attributes('disabled')).toBeFalsy();
});
it('adding a project successfully updates the projects list', () => {
createWrapper({ data: { selectedProjects: singleProjectList } });
findAddProjectsButton().vm.$emit('click');
expect(spyMutate).toHaveBeenCalledTimes(1);
});
it('adding a project unsuccessfully shows a flash', () => {
const mocks = {
$apollo: {
mutate: jest.fn().mockRejectedValue(),
},
};
createWrapper({ data: { selectedProjects: singleProjectList }, mocks });
findAddProjectsButton().vm.$emit('click');
return waitForPromises().then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith('Unable to add Sample Project 1');
});
});
it('adding many projects unsuccessfully shows a flash', () => {
const mocks = {
$apollo: {
mutate: jest.fn().mockRejectedValue(),
},
};
createWrapper({ data: { selectedProjects: multipleProjectsList }, mocks });
findAddProjectsButton().vm.$emit('click');
return waitForPromises().then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(
'Unable to add Sample Project 2 and Sample Project 3',
);
});
});
});
describe('removing projects', () => {
it('removing a project calls the mutatation', () => {
createWrapper({ props: { projects: singleProjectList } });
findProjectList().vm.$emit('projectRemoved', mockProject);
expect(spyMutate).toHaveBeenCalledTimes(1);
});
it('removing a project unsuccessfully shows a flash', () => {
const mocks = {
$apollo: {
mutate: jest.fn().mockRejectedValue(),
},
};
createWrapper({ props: { selectedProjects: multipleProjectsList }, mocks });
findProjectList().vm.$emit('projectRemoved', mockProject);
return waitForPromises().then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
});
});
});
describe('infinite scrolling', () => {
it('if the bottom is reached and there is another page, it appends the next page to the projects array', () => {
createWrapper({ data: { searchQuery: 'test' } });
findProjectSelector().vm.$emit('bottomReached');
expect(spyQuery).toHaveBeenCalledTimes(1);
});
it('if the bottom is reached and there is not another page, it does nothing', () => {
createWrapper({ data: { pageInfo: { hasNextPage: false, endCursor: '' } } });
findProjectSelector().vm.$emit('bottomReached');
expect(spyQuery).not.toHaveBeenCalled();
});
});
});
......@@ -7,7 +7,7 @@ import createDefaultState from 'ee/security_dashboard/store/modules/project_sele
import { GlDeprecatedButton } from '@gitlab/ui';
import ProjectManager from 'ee/security_dashboard/components/project_manager.vue';
import ProjectList from 'ee/security_dashboard/components/project_list.vue';
import ProjectList from 'ee/security_dashboard/components/first_class_project_manager/project_list.vue';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
const localVue = createLocalVue();
......
......@@ -20112,6 +20112,9 @@ msgstr ""
msgid "SecurityReports|There was an error while generating the report."
msgstr ""
msgid "SecurityReports|Unable to add %{invalidProjectsMessage}"
msgstr ""
msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
......
......@@ -74,6 +74,16 @@ describe('ProjectListItem component', () => {
expect(renderedNamespace).toBe('a / ... / e /');
});
it(`renders a simple namespace name of a GraphQL project`, () => {
options.propsData.project.name_with_namespace = undefined;
options.propsData.project.nameWithNamespace = 'test';
wrapper = shallowMount(Component, options);
const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
expect(renderedNamespace).toBe('test /');
});
it(`renders the project name`, () => {
options.propsData.project.name = 'my-test-project';
......
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