Commit c388c0d3 authored by James Fargher's avatar James Fargher

Merge branch '2256-add-feat-edit-crm-contact' into 'master'

Add edit CRM contact UI

See merge request gitlab-org/gitlab!76610
parents 921fd6ab 3b4adf96
......@@ -4,7 +4,8 @@ import { produce } from 'immer';
import { __, s__ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_GROUP } from '~/graphql_shared/constants';
import createContact from './queries/create_contact.mutation.graphql';
import createContactMutation from './queries/create_contact.mutation.graphql';
import updateContactMutation from './queries/update_contact.mutation.graphql';
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
export default {
......@@ -21,6 +22,11 @@ export default {
type: Boolean,
required: true,
},
contact: {
type: Object,
required: false,
default: () => {},
},
},
data() {
return {
......@@ -35,66 +41,111 @@ export default {
},
computed: {
invalid() {
return this.firstName === '' || this.lastName === '' || this.email === '';
const { firstName, lastName, email } = this;
return firstName.trim() === '' || lastName.trim() === '' || email.trim() === '';
},
editMode() {
return Boolean(this.contact);
},
title() {
return this.editMode ? this.$options.i18n.editTitle : this.$options.i18n.newTitle;
},
buttonLabel() {
return this.editMode
? this.$options.i18n.editButtonLabel
: this.$options.i18n.createButtonLabel;
},
mutation() {
return this.editMode ? updateContactMutation : createContactMutation;
},
variables() {
const { contact, firstName, lastName, phone, email, description, editMode, groupId } = this;
const variables = {
input: {
firstName,
lastName,
phone,
email,
description,
},
};
if (editMode) {
variables.input.id = contact.id;
} else {
variables.input.groupId = convertToGraphQLId(TYPE_GROUP, groupId);
}
return variables;
},
},
mounted() {
if (this.editMode) {
const { contact } = this;
this.firstName = contact.firstName || '';
this.lastName = contact.lastName || '';
this.phone = contact.phone || '';
this.email = contact.email || '';
this.description = contact.description || '';
}
},
methods: {
save() {
const { mutation, variables, updateCache, close } = this;
this.submitting = true;
return this.$apollo
.mutate({
mutation: createContact,
variables: {
input: {
groupId: convertToGraphQLId(TYPE_GROUP, this.groupId),
firstName: this.firstName,
lastName: this.lastName,
phone: this.phone,
email: this.email,
description: this.description,
},
},
update: this.updateCache,
mutation,
variables,
update: updateCache,
})
.then(({ data }) => {
if (data.customerRelationsContactCreate.errors.length === 0) this.close(true);
if (
data.customerRelationsContactCreate?.errors.length === 0 ||
data.customerRelationsContactUpdate?.errors.length === 0
) {
close(true);
}
this.submitting = false;
})
.catch(() => {
this.errorMessages = [__('Something went wrong. Please try again.')];
this.errorMessages = [this.$options.i18n.somethingWentWrong];
this.submitting = false;
});
},
close(success) {
this.$emit('close', success);
},
updateCache(store, { data: { customerRelationsContactCreate } }) {
if (customerRelationsContactCreate.errors.length > 0) {
this.errorMessages = customerRelationsContactCreate.errors;
updateCache(store, { data }) {
const mutationData =
data.customerRelationsContactCreate || data.customerRelationsContactUpdate;
if (mutationData?.errors.length > 0) {
this.errorMessages = mutationData.errors;
return;
}
const variables = {
groupFullPath: this.groupFullPath,
};
const sourceData = store.readQuery({
const queryArgs = {
query: getGroupContactsQuery,
variables,
});
variables: { groupFullPath: this.groupFullPath },
};
const data = produce(sourceData, (draftState) => {
const sourceData = store.readQuery(queryArgs);
queryArgs.data = produce(sourceData, (draftState) => {
draftState.group.contacts.nodes = [
...sourceData.group.contacts.nodes,
customerRelationsContactCreate.contact,
...sourceData.group.contacts.nodes.filter(({ id }) => id !== this.contact?.id),
mutationData.contact,
];
});
store.writeQuery({
query: getGroupContactsQuery,
variables,
data,
});
store.writeQuery(queryArgs);
},
getDrawerHeaderHeight() {
const wrapperEl = document.querySelector('.content-wrapper');
......@@ -107,14 +158,17 @@ export default {
},
},
i18n: {
buttonLabel: s__('Crm|Create new contact'),
createButtonLabel: s__('Crm|Create new contact'),
editButtonLabel: __('Save changes'),
cancel: __('Cancel'),
firstName: s__('Crm|First name'),
lastName: s__('Crm|Last name'),
email: s__('Crm|Email'),
phone: s__('Crm|Phone number (optional)'),
description: s__('Crm|Description (optional)'),
title: s__('Crm|New Contact'),
newTitle: s__('Crm|New contact'),
editTitle: s__('Crm|Edit contact'),
somethingWentWrong: __('Something went wrong. Please try again.'),
},
};
</script>
......@@ -127,7 +181,7 @@ export default {
@close="close(false)"
>
<template #title>
<h4>{{ $options.i18n.title }}</h4>
<h3>{{ title }}</h3>
</template>
<gl-alert v-if="errorMessages.length" variant="danger" @dismiss="errorMessages = []">
<ul class="gl-mb-0! gl-ml-5">
......@@ -160,9 +214,9 @@ export default {
variant="confirm"
:disabled="invalid"
:loading="submitting"
data-testid="create-new-contact-button"
data-testid="save-contact-button"
type="submit"
>{{ $options.i18n.buttonLabel }}</gl-button
>{{ buttonLabel }}</gl-button
>
</span>
</form>
......
......@@ -2,9 +2,11 @@
import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_CRM_CONTACT } from '~/graphql_shared/constants';
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '../constants';
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
import NewContactForm from './new_contact_form.vue';
import ContactForm from './contact_form.vue';
export default {
components: {
......@@ -12,7 +14,7 @@ export default {
GlButton,
GlLoadingIcon,
GlTable,
NewContactForm,
ContactForm,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -47,11 +49,19 @@ export default {
return this.$apollo.queries.contacts.loading;
},
showNewForm() {
return this.$route.path.startsWith('/new');
return this.$route.name === NEW_ROUTE_NAME;
},
canCreateNew() {
showEditForm() {
return !this.isLoading && this.$route.name === EDIT_ROUTE_NAME;
},
canAdmin() {
return parseBoolean(this.canAdminCrmContact);
},
editingContact() {
return this.contacts.find(
(contact) => contact.id === convertToGraphQLId(TYPE_CRM_CONTACT, this.$route.params.id),
);
},
},
methods: {
extractContacts(data) {
......@@ -61,16 +71,28 @@ export default {
displayNewForm() {
if (this.showNewForm) return;
this.$router.push({ path: '/new' });
this.$router.push({ name: NEW_ROUTE_NAME });
},
hideNewForm(success) {
if (success) this.$toast.show(s__('Crm|Contact has been added'));
this.$router.replace({ path: '/' });
this.$router.replace({ name: INDEX_ROUTE_NAME });
},
hideEditForm(success) {
if (success) this.$toast.show(s__('Crm|Contact has been updated'));
this.editingContactId = 0;
this.$router.replace({ name: INDEX_ROUTE_NAME });
},
getIssuesPath(path, value) {
return `${path}?scope=all&state=opened&crm_contact_id=${value}`;
},
edit(value) {
if (this.showEditForm) return;
this.editingContactId = value;
this.$router.push({ name: EDIT_ROUTE_NAME, params: { id: value } });
},
},
fields: [
{ key: 'firstName', sortable: true },
......@@ -87,7 +109,7 @@ export default {
},
{
key: 'id',
label: __('Issues'),
label: '',
formatter: (id) => {
return getIdFromGraphQLId(id);
},
......@@ -96,6 +118,7 @@ export default {
i18n: {
emptyText: s__('Crm|No contacts found'),
issuesButtonLabel: __('View issues'),
editButtonLabel: __('Edit'),
title: s__('Crm|Customer Relations Contacts'),
newContact: s__('Crm|New contact'),
errorText: __('Something went wrong. Please try again.'),
......@@ -116,7 +139,7 @@ export default {
</h2>
<div class="gl-display-none gl-md-display-flex gl-align-items-center gl-justify-content-end">
<gl-button
v-if="canCreateNew"
v-if="canAdmin"
variant="confirm"
data-testid="new-contact-button"
@click="displayNewForm"
......@@ -125,7 +148,13 @@ export default {
</gl-button>
</div>
</div>
<new-contact-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
<contact-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
<contact-form
v-if="showEditForm"
:contact="editingContact"
:drawer-open="showEditForm"
@close="hideEditForm"
/>
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
<gl-table
v-else
......@@ -138,11 +167,20 @@ export default {
<template #cell(id)="data">
<gl-button
v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
class="gl-mr-3"
data-testid="issues-link"
icon="issues"
:aria-label="$options.i18n.issuesButtonLabel"
:href="getIssuesPath(groupIssuesPath, data.value)"
/>
<gl-button
v-if="canAdmin"
v-gl-tooltip.hover.bottom="$options.i18n.editButtonLabel"
data-testid="edit-contact-button"
icon="pencil"
:aria-label="$options.i18n.editButtonLabel"
@click="edit(data.value)"
/>
</template>
</gl-table>
</div>
......
#import "./crm_contact_fields.fragment.graphql"
mutation updateContact($input: CustomerRelationsContactUpdateInput!) {
customerRelationsContactUpdate(input: $input) {
contact {
...ContactFragment
}
errors
}
}
export const INDEX_ROUTE_NAME = 'index';
export const NEW_ROUTE_NAME = 'new';
export const EDIT_ROUTE_NAME = 'edit';
......@@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import CrmContactsRoot from './components/contacts_root.vue';
import routes from './routes';
Vue.use(VueApollo);
Vue.use(VueRouter);
......@@ -25,14 +26,7 @@ export default () => {
const router = new VueRouter({
base: basePath,
mode: 'history',
routes: [
{
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Contacts List',
path: '/',
component: CrmContactsRoot,
},
],
routes,
});
return new Vue({
......
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from './constants';
import CrmContactsRoot from './components/contacts_root.vue';
export default [
{
name: INDEX_ROUTE_NAME,
path: '/',
component: CrmContactsRoot,
},
{
name: NEW_ROUTE_NAME,
path: '/new',
component: CrmContactsRoot,
},
{
name: EDIT_ROUTE_NAME,
path: '/:id/edit',
component: CrmContactsRoot,
},
];
export const MINIMUM_SEARCH_LENGTH = 3;
export const TYPE_CI_RUNNER = 'Ci::Runner';
export const TYPE_CRM_CONTACT = 'CustomerRelations::Contact';
export const TYPE_DISCUSSION = 'Discussion';
export const TYPE_EPIC = 'Epic';
export const TYPE_GROUP = 'Group';
export const TYPE_ISSUE = 'Issue';
......@@ -8,11 +10,10 @@ export const TYPE_ITERATION = 'Iteration';
export const TYPE_ITERATIONS_CADENCE = 'Iterations::Cadence';
export const TYPE_MERGE_REQUEST = 'MergeRequest';
export const TYPE_MILESTONE = 'Milestone';
export const TYPE_NOTE = 'Note';
export const TYPE_PACKAGES_PACKAGE = 'Packages::Package';
export const TYPE_PROJECT = 'Project';
export const TYPE_SCANNER_PROFILE = 'DastScannerProfile';
export const TYPE_SITE_PROFILE = 'DastSiteProfile';
export const TYPE_USER = 'User';
export const TYPE_VULNERABILITY = 'Vulnerability';
export const TYPE_NOTE = 'Note';
export const TYPE_DISCUSSION = 'Discussion';
export const TYPE_PACKAGES_PACKAGE = 'Packages::Package';
......@@ -9,6 +9,10 @@ class Groups::Crm::ContactsController < Groups::ApplicationController
render action: "index"
end
def edit
render action: "index"
end
private
def authorize_read_crm_contact!
......
......@@ -127,7 +127,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
end
namespace :crm do
resources :contacts, only: [:index, :new]
resources :contacts, only: [:index, :new, :edit]
resources :organizations, only: [:index]
end
end
......
......@@ -10282,6 +10282,9 @@ msgstr ""
msgid "Crm|Contact has been added"
msgstr ""
msgid "Crm|Contact has been updated"
msgstr ""
msgid "Crm|Create new contact"
msgstr ""
......@@ -10291,6 +10294,9 @@ msgstr ""
msgid "Crm|Description (optional)"
msgstr ""
msgid "Crm|Edit contact"
msgstr ""
msgid "Crm|Email"
msgstr ""
......@@ -10300,9 +10306,6 @@ msgstr ""
msgid "Crm|Last name"
msgstr ""
msgid "Crm|New Contact"
msgstr ""
msgid "Crm|New contact"
msgstr ""
......
......@@ -4,41 +4,49 @@ import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import NewContactForm from '~/crm/components/new_contact_form.vue';
import ContactForm from '~/crm/components/contact_form.vue';
import createContactMutation from '~/crm/components/queries/create_contact.mutation.graphql';
import updateContactMutation from '~/crm/components/queries/update_contact.mutation.graphql';
import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
import {
createContactMutationErrorResponse,
createContactMutationResponse,
getGroupContactsQueryResponse,
updateContactMutationErrorResponse,
updateContactMutationResponse,
} from './mock_data';
describe('Customer relations contacts root app', () => {
describe('Customer relations contact form component', () => {
Vue.use(VueApollo);
let wrapper;
let fakeApollo;
let mutation;
let queryHandler;
const findCreateNewContactButton = () => wrapper.findByTestId('create-new-contact-button');
const findSaveContactButton = () => wrapper.findByTestId('save-contact-button');
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findForm = () => wrapper.find('form');
const findError = () => wrapper.findComponent(GlAlert);
const mountComponent = ({ mountFunction = shallowMountExtended } = {}) => {
fakeApollo = createMockApollo([[createContactMutation, queryHandler]]);
const mountComponent = ({ mountFunction = shallowMountExtended, editForm = false } = {}) => {
fakeApollo = createMockApollo([[mutation, queryHandler]]);
fakeApollo.clients.defaultClient.cache.writeQuery({
query: getGroupContactsQuery,
variables: { groupFullPath: 'flightjs' },
data: getGroupContactsQueryResponse.data,
});
wrapper = mountFunction(NewContactForm, {
const propsData = { drawerOpen: true };
if (editForm)
propsData.contact = { firstName: 'First', lastName: 'Last', email: 'email@example.com' };
wrapper = mountFunction(ContactForm, {
provide: { groupId: 26, groupFullPath: 'flightjs' },
apolloProvider: fakeApollo,
propsData: { drawerOpen: true },
propsData,
});
};
beforeEach(() => {
mutation = createContactMutation;
queryHandler = jest.fn().mockResolvedValue(createContactMutationResponse);
});
......@@ -47,14 +55,14 @@ describe('Customer relations contacts root app', () => {
fakeApollo = null;
});
describe('Create new contact button', () => {
it('should be disabled by default', () => {
describe('Save contact button', () => {
it('should be disabled when required fields are empty', () => {
mountComponent();
expect(findCreateNewContactButton().attributes('disabled')).toBeTruthy();
expect(findSaveContactButton().props('disabled')).toBe(true);
});
it('should not be disabled when first, last and email have values', async () => {
it('should not be disabled when required fields have values', async () => {
mountComponent();
wrapper.find('#contact-first-name').vm.$emit('input', 'A');
......@@ -62,7 +70,7 @@ describe('Customer relations contacts root app', () => {
wrapper.find('#contact-email').vm.$emit('input', 'C');
await waitForPromises();
expect(findCreateNewContactButton().attributes('disabled')).toBeFalsy();
expect(findSaveContactButton().props('disabled')).toBe(false);
});
});
......@@ -74,7 +82,7 @@ describe('Customer relations contacts root app', () => {
expect(wrapper.emitted().close).toBeTruthy();
});
describe('when query is successful', () => {
describe('when create mutation is successful', () => {
it("should emit 'close'", async () => {
mountComponent();
......@@ -85,7 +93,7 @@ describe('Customer relations contacts root app', () => {
});
});
describe('when query fails', () => {
describe('when create mutation fails', () => {
it('should show error on reject', async () => {
queryHandler = jest.fn().mockRejectedValue('ERROR');
mountComponent();
......@@ -107,4 +115,43 @@ describe('Customer relations contacts root app', () => {
expect(findError().text()).toBe('Phone is invalid.');
});
});
describe('when update mutation is successful', () => {
it("should emit 'close'", async () => {
mutation = updateContactMutation;
queryHandler = jest.fn().mockResolvedValue(updateContactMutationResponse);
mountComponent({ editForm: true });
findForm().trigger('submit');
await waitForPromises();
expect(wrapper.emitted().close).toBeTruthy();
});
});
describe('when update mutation fails', () => {
beforeEach(() => {
mutation = updateContactMutation;
});
it('should show error on reject', async () => {
queryHandler = jest.fn().mockRejectedValue('ERROR');
mountComponent({ editForm: true });
findForm().trigger('submit');
await waitForPromises();
expect(findError().exists()).toBe(true);
});
it('should show error on error response', async () => {
queryHandler = jest.fn().mockResolvedValue(updateContactMutationErrorResponse);
mountComponent({ editForm: true });
findForm().trigger('submit');
await waitForPromises();
expect(findError().exists()).toBe(true);
expect(findError().text()).toBe('Email is invalid.');
});
});
});
......@@ -6,8 +6,10 @@ import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_help
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ContactsRoot from '~/crm/components/contacts_root.vue';
import NewContactForm from '~/crm/components/new_contact_form.vue';
import ContactForm from '~/crm/components/contact_form.vue';
import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
import { NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '~/crm/constants';
import routes from '~/crm/routes';
import { getGroupContactsQueryResponse } from './mock_data';
describe('Customer relations contacts root app', () => {
......@@ -21,7 +23,8 @@ describe('Customer relations contacts root app', () => {
const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
const findIssuesLinks = () => wrapper.findAllByTestId('issues-link');
const findNewContactButton = () => wrapper.findByTestId('new-contact-button');
const findNewContactForm = () => wrapper.findComponent(NewContactForm);
const findEditContactButton = () => wrapper.findByTestId('edit-contact-button');
const findContactForm = () => wrapper.findComponent(ContactForm);
const findError = () => wrapper.findComponent(GlAlert);
const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse);
......@@ -49,7 +52,7 @@ describe('Customer relations contacts root app', () => {
router = new VueRouter({
base: basePath,
mode: 'history',
routes: [],
routes,
});
});
......@@ -79,12 +82,12 @@ describe('Customer relations contacts root app', () => {
});
});
describe('new contact form', () => {
describe('contact form', () => {
it('should not exist by default', async () => {
mountComponent();
await waitForPromises();
expect(findNewContactForm().exists()).toBe(false);
expect(findContactForm().exists()).toBe(false);
});
it('should exist when user clicks new contact button', async () => {
......@@ -93,25 +96,54 @@ describe('Customer relations contacts root app', () => {
findNewContactButton().vm.$emit('click');
await waitForPromises();
expect(findNewContactForm().exists()).toBe(true);
expect(findContactForm().exists()).toBe(true);
});
it('should exist when user navigates directly to /new', async () => {
router.replace({ path: '/new' });
it('should exist when user navigates directly to `new` route', async () => {
router.replace({ name: NEW_ROUTE_NAME });
mountComponent();
await waitForPromises();
expect(findNewContactForm().exists()).toBe(true);
expect(findContactForm().exists()).toBe(true);
});
it('should not exist when form emits close', async () => {
router.replace({ path: '/new' });
it('should exist when user clicks edit contact button', async () => {
mountComponent({ mountFunction: mountExtended });
await waitForPromises();
findEditContactButton().vm.$emit('click');
await waitForPromises();
expect(findContactForm().exists()).toBe(true);
});
it('should exist when user navigates directly to `edit` route', async () => {
router.replace({ name: EDIT_ROUTE_NAME, params: { id: 16 } });
mountComponent();
await waitForPromises();
expect(findContactForm().exists()).toBe(true);
});
it('should not exist when new form emits close', async () => {
router.replace({ name: NEW_ROUTE_NAME });
mountComponent();
findContactForm().vm.$emit('close');
await waitForPromises();
expect(findContactForm().exists()).toBe(false);
});
it('should not exist when edit form emits close', async () => {
router.replace({ name: EDIT_ROUTE_NAME, params: { id: 16 } });
mountComponent();
await waitForPromises();
findNewContactForm().vm.$emit('close');
findContactForm().vm.$emit('close');
await waitForPromises();
expect(findNewContactForm().exists()).toBe(false);
expect(findContactForm().exists()).toBe(false);
});
});
......
......@@ -106,3 +106,31 @@ export const createContactMutationErrorResponse = {
},
},
};
export const updateContactMutationResponse = {
data: {
customerRelationsContactUpdate: {
__typeName: 'CustomerRelationsContactCreatePayload',
contact: {
__typename: 'CustomerRelationsContact',
id: 'gid://gitlab/CustomerRelations::Contact/1',
firstName: 'First',
lastName: 'Last',
email: 'email@example.com',
phone: null,
description: null,
organization: null,
},
errors: [],
},
},
};
export const updateContactMutationErrorResponse = {
data: {
customerRelationsContactUpdate: {
contact: null,
errors: ['Email is invalid.'],
},
},
};
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::Crm::ContactsController do
let_it_be(:user) { create(:user) }
shared_examples 'response with 404 status' do
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
shared_examples 'ok response with index template' do
it 'renders the index template' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
end
shared_examples 'ok response with index template if authorized' do
context 'private group' do
let(:group) { create(:group, :private) }
context 'with authorized user' do
before do
group.add_reporter(user)
sign_in(user)
end
context 'when feature flag is enabled' do
it_behaves_like 'ok response with index template'
end
context 'when feature flag is not enabled' do
before do
stub_feature_flags(customer_relations: false)
end
it_behaves_like 'response with 404 status'
end
end
context 'with unauthorized user' do
before do
sign_in(user)
end
it_behaves_like 'response with 404 status'
end
context 'with anonymous user' do
it 'blah' do
subject
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(new_user_session_path)
end
end
end
context 'public group' do
let(:group) { create(:group, :public) }
context 'with anonymous user' do
it_behaves_like 'ok response with index template'
end
end
end
describe 'GET #index' do
subject do
get group_crm_contacts_path(group)
response
end
it_behaves_like 'ok response with index template if authorized'
end
describe 'GET #new' do
subject do
get new_group_crm_contact_path(group)
response
end
it_behaves_like 'ok response with index template if authorized'
end
describe 'GET #edit' do
subject do
get edit_group_crm_contact_path(group, id: 1)
response
end
it_behaves_like 'ok response with index template if authorized'
end
end
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