diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 08:17:02 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 08:17:02 +0000 |
commit | b39512ed755239198a9c294b6a45e65c05900235 (patch) | |
tree | d234a3efade1de67c46b9e5a38ce813627726aa7 /app/assets/javascripts/crm | |
parent | d31474cf3b17ece37939d20082b07f6657cc79a9 (diff) | |
download | gitlab-ce-b39512ed755239198a9c294b6a45e65c05900235.tar.gz |
Add latest changes from gitlab-org/gitlab@15-3-stable-eev15.3.0-rc42
Diffstat (limited to 'app/assets/javascripts/crm')
11 files changed, 267 insertions, 78 deletions
diff --git a/app/assets/javascripts/crm/components/form.vue b/app/assets/javascripts/crm/components/form.vue index 72def54aedf..ea6a6892bbd 100644 --- a/app/assets/javascripts/crm/components/form.vue +++ b/app/assets/javascripts/crm/components/form.vue @@ -1,5 +1,13 @@ <script> -import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui'; +import { + GlAlert, + GlButton, + GlDrawer, + GlFormCheckbox, + GlFormGroup, + GlFormInput, + GlFormSelect, +} from '@gitlab/ui'; import { get as getPropValueByPath, isEmpty } from 'lodash'; import { produce } from 'immer'; import { MountingPortal } from 'portal-vue'; @@ -26,6 +34,7 @@ export default { GlAlert, GlButton, GlDrawer, + GlFormCheckbox, GlFormGroup, GlFormInput, GlFormSelect, @@ -113,7 +122,9 @@ export default { const { fields, model } = this; return fields.some((field) => { - return field.required && isEmpty(model[field.name]); + return ( + field.required && isEmpty(model[field.name]) && typeof model[field.name] !== 'boolean' + ); }); }, variables() { @@ -216,6 +227,8 @@ export default { }); }, getFieldLabel(field) { + if (field.bool) return null; + const optionalSuffix = field.required ? '' : ` ${MSG_OPTIONAL}`; return field.label + optionalSuffix; }, @@ -273,6 +286,9 @@ export default { v-model="model[field.name]" :options="field.values" /> + <gl-form-checkbox v-else-if="field.bool" :id="field.name" v-model="model[field.name]" + ><span class="gl-font-weight-bold">{{ field.label }}</span></gl-form-checkbox + > <gl-form-input v-else :id="field.name" v-bind="field.input" v-model="model[field.name]" /> </gl-form-group> <span class="gl-float-right"> diff --git a/app/assets/javascripts/crm/constants.js b/app/assets/javascripts/crm/constants.js index 3b085837aea..815289e075e 100644 --- a/app/assets/javascripts/crm/constants.js +++ b/app/assets/javascripts/crm/constants.js @@ -1,3 +1,7 @@ export const INDEX_ROUTE_NAME = 'index'; export const NEW_ROUTE_NAME = 'new'; export const EDIT_ROUTE_NAME = 'edit'; +export const trackViewsOptions = { + category: 'Customer Relations' /* eslint-disable-line @gitlab/require-i18n-strings */, + action: 'view_contacts_list', +}; diff --git a/app/assets/javascripts/crm/contacts/bundle.js b/app/assets/javascripts/crm/contacts/bundle.js index f49ec64210f..fe62b7cfbe3 100644 --- a/app/assets/javascripts/crm/contacts/bundle.js +++ b/app/assets/javascripts/crm/contacts/bundle.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import VueRouter from 'vue-router'; import createDefaultClient from '~/lib/graphql'; +import { parseBoolean } from '~/lib/utils/common_utils'; import CrmContactsRoot from './components/contacts_root.vue'; import routes from './routes'; @@ -21,7 +22,14 @@ export default () => { return false; } - const { basePath, groupFullPath, groupIssuesPath, canAdminCrmContact, groupId } = el.dataset; + const { + basePath, + groupFullPath, + groupIssuesPath, + canAdminCrmContact, + groupId, + textQuery, + } = el.dataset; const router = new VueRouter({ base: basePath, @@ -33,7 +41,13 @@ export default () => { el, router, apolloProvider, - provide: { groupFullPath, groupIssuesPath, canAdminCrmContact, groupId }, + provide: { + groupFullPath, + groupIssuesPath, + canAdminCrmContact: parseBoolean(canAdminCrmContact), + groupId, + textQuery, + }, render(createElement) { return createElement(CrmContactsRoot); }, diff --git a/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue b/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue index f114ffedfe6..b29089519e2 100644 --- a/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue +++ b/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue @@ -57,7 +57,7 @@ export default { getQuery() { return { query: getGroupContactsQuery, - variables: { groupFullPath: this.groupFullPath }, + variables: { groupFullPath: this.groupFullPath, ids: [this.contactGraphQLId] }, }; }, title() { @@ -74,7 +74,7 @@ export default { return { groupId: this.groupGraphQLId }; }, fields() { - return [ + const fields = [ { name: 'firstName', label: __('First name'), required: true }, { name: 'lastName', label: __('Last name'), required: true }, { name: 'email', label: __('Email'), required: true }, @@ -86,6 +86,11 @@ export default { }, { name: 'description', label: __('Description') }, ]; + + if (this.isEditMode) + fields.push({ name: 'active', label: s__('Crm|Active'), required: true, bool: true }); + + return fields; }, organizationSelectValues() { const values = this.organizations.map((o) => { diff --git a/app/assets/javascripts/crm/contacts/components/contacts_root.vue b/app/assets/javascripts/crm/contacts/components/contacts_root.vue index 9d6f34c73b7..562363ff88e 100644 --- a/app/assets/javascripts/crm/contacts/components/contacts_root.vue +++ b/app/assets/javascripts/crm/contacts/components/contacts_root.vue @@ -1,36 +1,54 @@ <script> -import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui'; -import { parseBoolean } from '~/lib/utils/common_utils'; +import { GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui'; import { s__, __ } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { EDIT_ROUTE_NAME, NEW_ROUTE_NAME } from '../../constants'; -import getGroupContactsQuery from './graphql/get_group_contacts.query.graphql'; +import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue'; +import { + bodyTrClass, + initialPaginationState, +} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants'; +import { convertToSnakeCase } from '~/lib/utils/text_utility'; +import { EDIT_ROUTE_NAME, NEW_ROUTE_NAME, trackViewsOptions } from '../../constants'; +import getGroupContacts from './graphql/get_group_contacts.query.graphql'; +import getGroupContactsCountByState from './graphql/get_group_contacts_count_by_state.graphql'; export default { components: { - GlAlert, GlButton, GlLoadingIcon, GlTable, + PaginatedTableWithSearchAndTabs, }, directives: { GlTooltip: GlTooltipDirective, }, - inject: ['canAdminCrmContact', 'groupFullPath', 'groupIssuesPath'], + inject: ['canAdminCrmContact', 'groupFullPath', 'groupIssuesPath', 'textQuery'], data() { return { - contacts: [], + contacts: { list: [] }, + contactsCount: {}, error: false, + filteredByStatus: '', + pagination: initialPaginationState, + statusFilter: 'all', + searchTerm: this.textQuery, + sort: 'LAST_NAME_ASC', + sortDesc: false, }; }, apollo: { contacts: { - query() { - return getGroupContactsQuery; - }, + query: getGroupContacts, variables() { return { groupFullPath: this.groupFullPath, + searchTerm: this.searchTerm, + state: this.statusFilter, + sort: this.sort, + firstPageSize: this.pagination.firstPageSize, + lastPageSize: this.pagination.lastPageSize, + prevPageCursor: this.pagination.prevPageCursor, + nextPageCursor: this.pagination.nextPageCursor, }; }, update(data) { @@ -40,19 +58,52 @@ export default { this.error = true; }, }, + contactsCount: { + query: getGroupContactsCountByState, + variables() { + return { + groupFullPath: this.groupFullPath, + searchTerm: this.searchTerm, + }; + }, + update(data) { + return data?.group?.contactStateCounts; + }, + error() { + this.error = true; + }, + }, }, computed: { isLoading() { return this.$apollo.queries.contacts.loading; }, - canAdmin() { - return parseBoolean(this.canAdminCrmContact); + tbodyTrClass() { + return { + [bodyTrClass]: !this.loading && !this.isEmpty, + }; }, }, methods: { + errorAlertDismissed() { + this.error = true; + }, extractContacts(data) { const contacts = data?.group?.contacts?.nodes || []; - return contacts.slice().sort((a, b) => a.firstName.localeCompare(b.firstName)); + const pageInfo = data?.group?.contacts?.pageInfo || {}; + return { + list: contacts, + pageInfo, + }; + }, + fetchSortedData({ sortBy, sortDesc }) { + const sortingColumn = convertToSnakeCase(sortBy).toUpperCase(); + const sortingDirection = sortDesc ? 'DESC' : 'ASC'; + this.pagination = initialPaginationState; + this.sort = `${sortingColumn}_${sortingDirection}`; + }, + filtersChanged({ searchTerm }) { + this.searchTerm = searchTerm; }, getIssuesPath(path, value) { return `${path}?crm_contact_id=${value}`; @@ -60,6 +111,13 @@ export default { getEditRoute(id) { return { name: this.$options.EDIT_ROUTE_NAME, params: { id } }; }, + pageChanged(pagination) { + this.pagination = pagination; + }, + statusChanged({ filters, status }) { + this.statusFilter = filters; + this.filteredByStatus = status; + }, }, fields: [ { key: 'firstName', sortable: true }, @@ -92,57 +150,109 @@ export default { }, EDIT_ROUTE_NAME, NEW_ROUTE_NAME, + statusTabs: [ + { + title: __('Active'), + status: 'ACTIVE', + filters: 'active', + }, + { + title: __('Inactive'), + status: 'INACTIVE', + filters: 'inactive', + }, + { + title: __('All'), + status: 'ALL', + filters: 'all', + }, + ], + trackViewsOptions, + emptyArray: [], }; </script> <template> <div> - <gl-alert v-if="error" variant="danger" class="gl-mt-6" @dismiss="error = false"> - {{ $options.i18n.errorText }} - </gl-alert> - <div - class="gl-display-flex gl-align-items-baseline gl-flex-direction-row gl-justify-content-space-between gl-mt-6" + <paginated-table-with-search-and-tabs + :show-items="true" + :show-error-msg="false" + :i18n="$options.i18n" + :items="contacts.list" + :page-info="contacts.pageInfo" + :items-count="contactsCount" + :status-tabs="$options.statusTabs" + :track-views-options="$options.trackViewsOptions" + :filter-search-tokens="$options.emptyArray" + filter-search-key="contacts" + @page-changed="pageChanged" + @tabs-changed="statusChanged" + @filters-changed="filtersChanged" + @error-alert-dismissed="errorAlertDismissed" > - <h2 class="gl-font-size-h2 gl-my-0"> - {{ $options.i18n.title }} - </h2> - <div v-if="canAdmin"> - <router-link :to="{ name: $options.NEW_ROUTE_NAME }"> - <gl-button variant="confirm" data-testid="new-contact-button"> + <template #header-actions> + <router-link v-if="canAdminCrmContact" :to="{ name: $options.NEW_ROUTE_NAME }"> + <gl-button class="gl-my-3 gl-mr-5" variant="confirm" data-testid="new-contact-button"> {{ $options.i18n.newContact }} </gl-button> </router-link> - </div> - </div> - <router-view /> - <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" /> - <gl-table - v-else - class="gl-mt-5" - :items="contacts" - :fields="$options.fields" - :empty-text="$options.i18n.emptyText" - show-empty - > - <template #cell(id)="{ value: id }"> - <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, id)" - /> - <router-link :to="getEditRoute(id)"> - <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" - /> - </router-link> </template> - </gl-table> + + <template #title> + {{ $options.i18n.title }} + </template> + + <template #table> + <gl-table + :items="contacts.list" + :fields="$options.fields" + :busy="isLoading" + stacked="md" + :tbody-tr-class="tbodyTrClass" + sort-direction="asc" + :sort-desc.sync="sortDesc" + sort-by="createdAt" + show-empty + no-local-sorting + sort-icon-left + fixed + @sort-changed="fetchSortedData" + > + <template #cell(id)="{ value: id }"> + <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, id)" + /> + <router-link :to="getEditRoute(id)"> + <gl-button + v-if="canAdminCrmContact" + v-gl-tooltip.hover.bottom="$options.i18n.editButtonLabel" + data-testid="edit-contact-button" + icon="pencil" + :aria-label="$options.i18n.editButtonLabel" + /> + </router-link> + </template> + + <template #table-busy> + <gl-loading-icon size="lg" color="dark" class="mt-3" /> + </template> + + <template #empty> + <span v-if="error"> + {{ $options.i18n.errorText }} + </span> + <span v-else> + {{ $options.i18n.emptyText }} + </span> + </template> + </gl-table> + </template> + </paginated-table-with-search-and-tabs> + <router-view /> </div> </template> diff --git a/app/assets/javascripts/crm/contacts/components/graphql/crm_contact_fields.fragment.graphql b/app/assets/javascripts/crm/contacts/components/graphql/crm_contact_fields.fragment.graphql index cef4083b446..545ddbf5f72 100644 --- a/app/assets/javascripts/crm/contacts/components/graphql/crm_contact_fields.fragment.graphql +++ b/app/assets/javascripts/crm/contacts/components/graphql/crm_contact_fields.fragment.graphql @@ -1,13 +1,12 @@ fragment ContactFragment on CustomerRelationsContact { - __typename id firstName lastName email phone description + active organization { - __typename id name } diff --git a/app/assets/javascripts/crm/contacts/components/graphql/get_group_contacts.query.graphql b/app/assets/javascripts/crm/contacts/components/graphql/get_group_contacts.query.graphql index 2a8150e42e3..f04d02122fc 100644 --- a/app/assets/javascripts/crm/contacts/components/graphql/get_group_contacts.query.graphql +++ b/app/assets/javascripts/crm/contacts/components/graphql/get_group_contacts.query.graphql @@ -1,13 +1,37 @@ #import "./crm_contact_fields.fragment.graphql" -query contacts($groupFullPath: ID!) { +query contacts( + $groupFullPath: ID! + $state: CustomerRelationsContactState + $searchTerm: String + $sort: ContactSort + $firstPageSize: Int + $lastPageSize: Int + $prevPageCursor: String = "" + $nextPageCursor: String = "" + $ids: [CustomerRelationsContactID!] +) { group(fullPath: $groupFullPath) { - __typename id - contacts { + contacts( + state: $state + search: $searchTerm + sort: $sort + first: $firstPageSize + last: $lastPageSize + after: $nextPageCursor + before: $prevPageCursor + ids: $ids + ) { nodes { ...ContactFragment } + pageInfo { + hasNextPage + endCursor + hasPreviousPage + startCursor + } } } } diff --git a/app/assets/javascripts/crm/contacts/components/graphql/get_group_contacts_count_by_state.graphql b/app/assets/javascripts/crm/contacts/components/graphql/get_group_contacts_count_by_state.graphql new file mode 100644 index 00000000000..6b591240096 --- /dev/null +++ b/app/assets/javascripts/crm/contacts/components/graphql/get_group_contacts_count_by_state.graphql @@ -0,0 +1,11 @@ +query contactsCountByState($groupFullPath: ID!, $searchTerm: String) { + group(fullPath: $groupFullPath) { + __typename + id + contactStateCounts(search: $searchTerm) { + all + active + inactive + } + } +} diff --git a/app/assets/javascripts/crm/organizations/components/graphql/crm_organization_fields.fragment.graphql b/app/assets/javascripts/crm/organizations/components/graphql/crm_organization_fields.fragment.graphql index 4adc5742d3a..d723bf32ef5 100644 --- a/app/assets/javascripts/crm/organizations/components/graphql/crm_organization_fields.fragment.graphql +++ b/app/assets/javascripts/crm/organizations/components/graphql/crm_organization_fields.fragment.graphql @@ -1,7 +1,7 @@ fragment OrganizationFragment on CustomerRelationsOrganization { - __typename id name defaultRate description + active } diff --git a/app/assets/javascripts/crm/organizations/components/graphql/get_group_organizations.query.graphql b/app/assets/javascripts/crm/organizations/components/graphql/get_group_organizations.query.graphql index e8d8109431e..97b75091cac 100644 --- a/app/assets/javascripts/crm/organizations/components/graphql/get_group_organizations.query.graphql +++ b/app/assets/javascripts/crm/organizations/components/graphql/get_group_organizations.query.graphql @@ -2,7 +2,6 @@ query organizations($groupFullPath: ID!) { group(fullPath: $groupFullPath) { - __typename id organizations { nodes { diff --git a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue index 38468e1f4e4..5fd0294b0ea 100644 --- a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue +++ b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue @@ -52,16 +52,23 @@ export default { additionalCreateParams() { return { groupId: this.groupGraphQLId }; }, - }, - fields: [ - { name: 'name', label: __('Name'), required: true }, - { - name: 'defaultRate', - label: s__('Crm|Default rate'), - input: { type: 'number', step: '0.01' }, + fields() { + const fields = [ + { name: 'name', label: __('Name'), required: true }, + { + name: 'defaultRate', + label: s__('Crm|Default rate'), + input: { type: 'number', step: '0.01' }, + }, + { name: 'description', label: __('Description') }, + ]; + + if (this.isEditMode) + fields.push({ name: 'active', label: s__('Crm|Active'), required: true, bool: true }); + + return fields; }, - { name: 'description', label: __('Description') }, - ], + }, }; </script> @@ -73,7 +80,7 @@ export default { :mutation="mutation" :additional-create-params="additionalCreateParams" :existing-id="organizationGraphQLId" - :fields="$options.fields" + :fields="fields" :title="title" :success-message="successMessage" /> |