From 45478853727d40b60b78c6359fb11fc7606d4a9c Mon Sep 17 00:00:00 2001 From: Jennifer Q <66472237+latin-panda@users.noreply.github.com> Date: Wed, 10 Jan 2024 11:17:00 +0700 Subject: [PATCH] feat(#8074): add filter by parent to db-object widget (#8759) This feature is applied when using the appearance:descendant-of-current-contact The scope remains for forms that are opened in the contact tab; it takes the contact ID from the URL. It uses the CouchDB view contact_by_parent in combination with the contact type. --- .../search/src/generate-search-requests.js | 97 +++++++++++-------- .../search/test/generate-search-requests.js | 57 +++++++++++ .../enketo/db-object-widget.wdio-spec.js | 82 ++++++++++++++++ .../default/enketo/forms/db-object-widget.xml | 32 ++++++ .../default/enketo/generic-form.wdio.page.js | 23 +++++ .../src/js/enketo/widgets/db-object-widget.js | 3 +- .../ts/modules/contacts/contacts.component.ts | 6 +- .../ts/modules/reports/reports.component.ts | 4 +- .../contact-view-model-generator.service.ts | 4 +- webapp/src/ts/services/search.service.ts | 9 +- .../src/ts/services/select2-search.service.ts | 34 +++++-- .../karma/ts/services/search.service.spec.ts | 8 +- .../services/select2-search.service.spec.ts | 87 ++++++++++++++++- 13 files changed, 383 insertions(+), 63 deletions(-) create mode 100644 tests/e2e/default/enketo/db-object-widget.wdio-spec.js create mode 100644 tests/e2e/default/enketo/forms/db-object-widget.xml diff --git a/shared-libs/search/src/generate-search-requests.js b/shared-libs/search/src/generate-search-requests.js index 894c70e002b..e95da4f46eb 100644 --- a/shared-libs/search/src/generate-search-requests.js +++ b/shared-libs/search/src/generate-search-requests.js @@ -121,6 +121,20 @@ const subjectRequest = function(filters) { return getRequestWithMappedKeys('medic-client/reports_by_subject', subjectIds); }; +const getContactsByParentRequest = function(filters) { + if (!filters.parent) { + return; + } + + const types = filters?.types?.selected; + return { + view: 'medic-client/contacts_by_parent', + params: { + keys: types ? types.map(type => ([ filters.parent, type ])) : [ filters.parent ], + }, + }; +}; + const contactTypeRequest = function(filters, sortByLastVisitedDate) { if (!filters.types) { return; @@ -175,6 +189,34 @@ const sortByLastVisitedDate = function() { }; }; +const makeCombinedParams = function(freetextRequest, typeKey) { + const type = typeKey[0]; + const params = {}; + if (freetextRequest.key) { + params.key = [ type, freetextRequest.params.key[0] ]; + } else { + params.startkey = [ type, freetextRequest.params.startkey[0] ]; + params.endkey = [ type, freetextRequest.params.endkey[0] ]; + } + return params; +}; + +const getContactsByTypeAndFreetextRequest = function(typeRequests, freetextRequest) { + const result = { + view: 'medic-client/contacts_by_type_freetext', + union: typeRequests.params.keys.length > 1 + }; + + if (result.union) { + result.paramSets = + typeRequests.params.keys.map(_.partial(makeCombinedParams, freetextRequest, _)); + return result; + } + + result.params = makeCombinedParams(freetextRequest, typeRequests.params.keys[0]); + return result; +}; + const requestBuilders = { reports: function(filters) { let requests = [ @@ -196,57 +238,34 @@ const requestBuilders = { contacts: function(filters, extensions) { const shouldSortByLastVisitedDate = module.exports.shouldSortByLastVisitedDate(extensions); - const typeRequest = contactTypeRequest(filters, shouldSortByLastVisitedDate); - const hasTypeRequest = typeRequest && typeRequest.params.keys.length; - const freetextRequests = freetextRequest(filters, 'medic-client/contacts_by_freetext'); - const hasFreetextRequests = freetextRequests && freetextRequests.length; - - if (hasTypeRequest && hasFreetextRequests) { - - const makeCombinedParams = function(freetextRequest, typeKey) { - const type = typeKey[0]; - const params = {}; - if (freetextRequest.key) { - params.key = [ type, freetextRequest.params.key[0] ]; - } else { - params.startkey = [ type, freetextRequest.params.startkey[0] ]; - params.endkey = [ type, freetextRequest.params.endkey[0] ]; - } - return params; - }; - - const makeCombinedRequest = function(typeRequests, freetextRequest) { - const result = { - view: 'medic-client/contacts_by_type_freetext', - union: typeRequests.params.keys.length > 1 - }; - - if (result.union) { - result.paramSets = - typeRequests.params.keys.map(_.partial(makeCombinedParams, freetextRequest, _)); - return result; - } - - result.params = makeCombinedParams(freetextRequest, typeRequests.params.keys[0]); - return result; - }; + const contactsByParentRequest = getContactsByParentRequest(filters); + const typeRequest = contactTypeRequest(filters, shouldSortByLastVisitedDate); + const hasTypeRequest = typeRequest?.params.keys.length; - return freetextRequests.map(_.partial(makeCombinedRequest, typeRequest, _)); + if (contactsByParentRequest && hasTypeRequest && !freetextRequests?.length) { + // The request's keys already have the type included. + return [ contactsByParentRequest ]; } - let requests = [ freetextRequests, typeRequest ]; - requests = _.compact(_.flatten(requests)); + if (hasTypeRequest && freetextRequests?.length) { + const combinedRequests = freetextRequests.map(_.partial(getContactsByTypeAndFreetextRequest, typeRequest, _)); + if (contactsByParentRequest) { + combinedRequests.unshift(contactsByParentRequest); + } + return combinedRequests; + } + const requests = _.compact(_.flatten([ freetextRequests, typeRequest, contactsByParentRequest ])); if (!requests.length) { requests.push(defaultContactRequest()); } if (shouldSortByLastVisitedDate) { - // Always push this last, search:getIntersection uses the last request - // result and we'll need it later for sorting + // Always push this last, search:getIntersection uses the last request's result, we'll need it later for sorting. requests.push(sortByLastVisitedDate()); } + return requests; } }; diff --git a/shared-libs/search/test/generate-search-requests.js b/shared-libs/search/test/generate-search-requests.js index 792c96744f7..38a61fea8e1 100644 --- a/shared-libs/search/test/generate-search-requests.js +++ b/shared-libs/search/test/generate-search-requests.js @@ -168,6 +168,63 @@ describe('GenerateSearchRequests service', function() { }); }); + it('creates request to filter contacts by parent when contact ID and types are provided', function() { + const filters = { + types: { + selected: [ 'person' ], + }, + parent: 'S-123', + }; + + const result = service('contacts', filters); + + chai.expect(result.length).to.equal(1); + chai.expect(result[0]).to.deep.equal({ + view: 'medic-client/contacts_by_parent', + params: { + keys: [ [ 'S-123', 'person' ] ], + }, + }); + }); + + it('creates request to filter contacts by parent and freetext', function() { + const filters = { + types: { selected: [ 'person' ] }, + search: 'someth', + parent: 'S-123', + }; + + const result = service('contacts', filters); + + chai.expect(result.length).to.equal(2); + chai.expect(result[0]).to.deep.equal({ + view: 'medic-client/contacts_by_parent', + params: { + keys: [ [ 'S-123', 'person' ] ], + }, + }); + chai.expect(result[1]).to.deep.equal({ + view: 'medic-client/contacts_by_type_freetext', + union: false, + params: { + endkey: [ 'person', 'someth\ufff0' ], + startkey: [ 'person', 'someth' ], + }, + }); + }); + + it('creates request to filter contacts by parent when types are not provided', function() { + const filters = { parent: 'S-123' }; + + const result = service('contacts', filters); + + chai.expect(result.length).to.equal(1); + chai.expect(result[0]).to.deep.equal({ + view: 'medic-client/contacts_by_parent', + params: { keys: [ 'S-123' ] }, + }); + }); + it('creates unfiltered contacts request for types filter when all options are selected', function() { const filters = { types: { diff --git a/tests/e2e/default/enketo/db-object-widget.wdio-spec.js b/tests/e2e/default/enketo/db-object-widget.wdio-spec.js new file mode 100644 index 00000000000..36f82321a4f --- /dev/null +++ b/tests/e2e/default/enketo/db-object-widget.wdio-spec.js @@ -0,0 +1,82 @@ +const fs = require('fs'); + +const utils = require('@utils'); +const userFactory = require('@factories/cht/users/users'); +const placeFactory = require('@factories/cht/contacts/place'); +const personFactory = require('@factories/cht/contacts/person'); +const commonPage = require('@page-objects/default/common/common.wdio.page'); +const loginPage = require('@page-objects/default/login/login.wdio.page'); +const genericForm = require('@page-objects/default/enketo/generic-form.wdio.page'); +const reportsPage = require('@page-objects/default/reports/reports.wdio.page'); + +describe('DB Object Widget', () => { + const formId = 'db-object-widget'; + const form = fs.readFileSync(`${__dirname}/forms/${formId}.xml`, 'utf8'); + const formDocument = { + _id: `form:${formId}`, + internalId: formId, + title: `Form ${formId}`, + type: 'form', + context: { person: true, place: true }, + _attachments: { + xml: { + content_type: 'application/octet-stream', + data: Buffer.from(form).toString('base64') + } + } + }; + + const places = placeFactory.generateHierarchy(); + const districtHospital = places.get('district_hospital'); + const area1 = places.get('health_center'); + const area2 = placeFactory.place().build({ + _id: 'area2', + name: 'area 2', + type: 'health_center', + parent: { _id: districtHospital._id } + }); + + const offlineUser = userFactory.build({ place: districtHospital._id, roles: [ 'chw' ] }); + const personArea1 = personFactory.build({ parent: { _id: area1._id, parent: area1.parent } }); + const personArea2 = personFactory.build({ name: 'Patricio', parent: { _id: area2._id, parent: area2.parent } }); + + before(async () => { + await utils.saveDocs([ ...places.values(), area2, personArea1, personArea2, formDocument ]); + await utils.createUsers([ offlineUser ]); + await loginPage.login(offlineUser); + }); + + it('should display only the contacts from the parent contact', async () => { + await commonPage.goToPeople(area1._id); + await commonPage.openFastActionReport(formId); + + const sameParent = await genericForm.getDBObjectWidgetValues('/db_object_form/people/person_test_same_parent'); + await sameParent[0].click(); + expect(sameParent.length).to.equal(1); + expect(sameParent[0].name).to.equal(personArea1.name); + + const allContacts = await genericForm.getDBObjectWidgetValues('/db_object_form/people/person_test_all'); + await allContacts[2].click(); + expect(allContacts.length).to.equal(3); + expect(allContacts[0].name).to.equal(personArea1.name); + expect(allContacts[1].name).to.equal(offlineUser.contact.name); + expect(allContacts[2].name).to.equal(personArea2.name); + + await genericForm.submitForm(); + await commonPage.waitForPageLoaded(); + await commonPage.goToReports(); + + const firstReport = await reportsPage.getListReportInfo(await reportsPage.firstReport()); + expect(firstReport.heading).to.equal(offlineUser.contact.name); + expect(firstReport.form).to.equal('Form db-object-widget'); + + await reportsPage.openReport(firstReport.dataId); + expect(await reportsPage.getReportDetailFieldValueByLabel( + 'report.db-object-widget.people.person_test_same_parent' + )).to.equal(personArea1._id); + expect(await reportsPage.getReportDetailFieldValueByLabel( + 'report.db-object-widget.people.person_test_all' + )).to.equal(personArea2._id); + }); + +}); diff --git a/tests/e2e/default/enketo/forms/db-object-widget.xml b/tests/e2e/default/enketo/forms/db-object-widget.xml new file mode 100644 index 00000000000..6f945b1cc65 --- /dev/null +++ b/tests/e2e/default/enketo/forms/db-object-widget.xml @@ -0,0 +1,32 @@ + + + + DB Object Form + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/page-objects/default/enketo/generic-form.wdio.page.js b/tests/page-objects/default/enketo/generic-form.wdio.page.js index 1b61e776d37..9905c242f44 100644 --- a/tests/page-objects/default/enketo/generic-form.wdio.page.js +++ b/tests/page-objects/default/enketo/generic-form.wdio.page.js @@ -107,6 +107,28 @@ const selectYesNoOption = async (selector, value = 'yes') => { return value === 'yes'; }; +const getDBObjectWidgetValues = async (field) => { + const widget = $(`[data-contains-ref-target="${field}"] .selection`); + await (await widget).waitForClickable(); + await (await widget).click(); + + const dropdown = $('.select2-dropdown--below'); + await (await dropdown).waitForDisplayed(); + const firstElement = $('.select2-results__options > li'); + await (await firstElement).waitForClickable(); + + const list = await $$('.select2-results__options > li'); + const contacts = []; + for (const item of list) { + contacts.push({ + name: await (item.$('.name').getText()), + click: () => item.click(), + }); + } + + return contacts; +}; + module.exports = { getFormTitle, getErrorMessage, @@ -125,4 +147,5 @@ module.exports = { currentFormView, formTitle, selectYesNoOption, + getDBObjectWidgetValues, }; diff --git a/webapp/src/js/enketo/widgets/db-object-widget.js b/webapp/src/js/enketo/widgets/db-object-widget.js index ad566b1e4a9..81ca00ffc18 100644 --- a/webapp/src/js/enketo/widgets/db-object-widget.js +++ b/webapp/src/js/enketo/widgets/db-object-widget.js @@ -55,7 +55,8 @@ const construct = ( element ) => { } const contactTypes = getContactTypes($question, $textInput); const allowNew = $question.hasClass('or-appearance-allow-new'); - Select2Search.init($selectInput, contactTypes, { allowNew }).then(function() { + const filterByParent = $question.hasClass('or-appearance-descendant-of-current-contact'); + Select2Search.init($selectInput, contactTypes, { allowNew, filterByParent }).then(function() { // select2 doesn't understand readonly $selectInput.prop('disabled', $textInput.prop('readonly')); }); diff --git a/webapp/src/ts/modules/contacts/contacts.component.ts b/webapp/src/ts/modules/contacts/contacts.component.ts index 85dc4218bbc..66e5a347170 100644 --- a/webapp/src/ts/modules/contacts/contacts.component.ts +++ b/webapp/src/ts/modules/contacts/contacts.component.ts @@ -15,7 +15,7 @@ import { AuthService } from '@mm-services/auth.service'; import { SettingsService } from '@mm-services/settings.service'; import { UHCSettingsService } from '@mm-services/uhc-settings.service'; import { Selectors } from '@mm-selectors/index'; -import { SearchService } from '@mm-services/search.service'; +import { Filter, SearchService } from '@mm-services/search.service'; import { ContactTypesService } from '@mm-services/contact-types.service'; import { RelativeDateService } from '@mm-services/relative-date.service'; import { ScrollLoaderProvider } from '@mm-providers/scroll-loader.provider'; @@ -44,8 +44,8 @@ export class ContactsComponent implements OnInit, AfterViewInit, OnDestroy { error; appending: boolean; hasContacts = true; - filters:any = {}; - defaultFilters:any = {}; + filters: Filter = {}; + defaultFilters: Filter = {}; moreItems; usersHomePlace; contactTypes; diff --git a/webapp/src/ts/modules/reports/reports.component.ts b/webapp/src/ts/modules/reports/reports.component.ts index 68e804f9daf..4f51b3d63db 100644 --- a/webapp/src/ts/modules/reports/reports.component.ts +++ b/webapp/src/ts/modules/reports/reports.component.ts @@ -9,7 +9,7 @@ import { GlobalActions } from '@mm-actions/global'; import { ReportsActions } from '@mm-actions/reports'; import { ServicesActions } from '@mm-actions/services'; import { ChangesService } from '@mm-services/changes.service'; -import { SearchService } from '@mm-services/search.service'; +import { Filter, SearchService } from '@mm-services/search.service'; import { Selectors } from '@mm-selectors/index'; import { AddReadStatusService } from '@mm-services/add-read-status.service'; import { ExportService } from '@mm-services/export.service'; @@ -52,7 +52,7 @@ export class ReportsComponent implements OnInit, AfterViewInit, OnDestroy { loading = true; appending = false; moreItems: boolean; - filters:any = {}; + filters: Filter = {}; hasReports: boolean; selectMode = false; selectModeAvailable = false; diff --git a/webapp/src/ts/services/contact-view-model-generator.service.ts b/webapp/src/ts/services/contact-view-model-generator.service.ts index f68eea09d4e..6021486569a 100644 --- a/webapp/src/ts/services/contact-view-model-generator.service.ts +++ b/webapp/src/ts/services/contact-view-model-generator.service.ts @@ -307,9 +307,9 @@ export class ContactViewModelGeneratorService { contactDocs.forEach((doc) => { subjectIds.push(registrationUtils.getSubjectIds(doc)); }); - const searchOptions = { subjectIds: _flattenDeep(subjectIds) }; + const filter = { subjectIds: _flattenDeep(subjectIds) }; return this.searchService - .search('reports', searchOptions, { include_docs: true }) + .search('reports', filter, { include_docs: true }) .then((reports) => { reports.forEach((report) => { report.valid = !report.errors || !report.errors.length; diff --git a/webapp/src/ts/services/search.service.ts b/webapp/src/ts/services/search.service.ts index 93a86afa20b..900f3eed14c 100644 --- a/webapp/src/ts/services/search.service.ts +++ b/webapp/src/ts/services/search.service.ts @@ -122,7 +122,7 @@ export class SearchService { }); } - search(type, filters, options:any = {}, extensions:any = {}, docIds: any[] | undefined = undefined) { + search(type, filters: Filter, options:any = {}, extensions:any = {}, docIds: any[] | undefined = undefined) { return this.ngZone.runOutsideAngular(() => this._search(type, filters, options, extensions, docIds)); } @@ -201,3 +201,10 @@ export class SearchService { }); } } + +export interface Filter { + types?: { selected: string }; + search?: string; + parent?: string; + subjectIds?: string[]; +} diff --git a/webapp/src/ts/services/select2-search.service.ts b/webapp/src/ts/services/select2-search.service.ts index 80277f2d0a6..221d43f0fab 100644 --- a/webapp/src/ts/services/select2-search.service.ts +++ b/webapp/src/ts/services/select2-search.service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; import { sortBy as _sortBy } from 'lodash-es'; import * as phoneNumber from '@medic/phone-number'; import { FormatProvider } from '@mm-providers/format.provider'; import { LineageModelGeneratorService } from '@mm-services/lineage-model-generator.service'; -import { SearchService } from '@mm-services/search.service'; +import { Filter, SearchService } from '@mm-services/search.service'; import { SessionService } from '@mm-services/session.service'; import { SettingsService } from '@mm-services/settings.service'; import { ContactMutedService } from '@mm-services/contact-muted.service'; @@ -16,6 +17,7 @@ import { TranslateService } from '@mm-services/translate.service'; export class Select2SearchService { constructor( + private route: ActivatedRoute, private formatProvider: FormatProvider, private translateService: TranslateService, private lineageModelGeneratorService: LineageModelGeneratorService, @@ -51,19 +53,27 @@ export class Select2SearchService { .map(doc => ({ id: doc._id, doc: doc })); } + private calculateSkip(page: number, pageSize: number): number { + return ((page || 1) - 1) * pageSize; + } + private query(params, successCb, failureCb, options, types) { const currentQuery = params.data.q; - const skip = ((params.data.page || 1) - 1) * options.pageSize; - const filters = { - types: { selected: types }, - search: params.data.q - }; + const searchOptions = { limit: options.pageSize, - skip, + skip: this.calculateSkip(params.data.page, options.pageSize), hydrateContactNames: true, }; + const filters: Filter = { + types: { selected: types }, + search: params.data.q, + }; + if (options.filterByParent) { + filters.parent = this.getContactId(); + } + this.searchService .search('contacts', filters, searchOptions) .then((documents) => { @@ -195,6 +205,15 @@ export class Select2SearchService { } } + private getContactId() { + let activeRoute = this.route.firstChild; + while (activeRoute?.firstChild) { + activeRoute = activeRoute.firstChild; + } + const params = activeRoute?.snapshot?.params; + return params?.parent_id || params?.id; + } + async init(selectEl, _types, _options:any = {}) { const settings = await this.settingsService.get(); const types = Array.isArray(_types) ? _types : [ _types ]; @@ -204,6 +223,7 @@ export class Select2SearchService { initialValue: _options.initialValue || selectEl.val(), sendMessageExtras: _options.sendMessageExtras || this.defaultSendMessageExtras, allowNew: _options.allowNew || false, + filterByParent: _options.filterByParent || false, pageSize: _options.pageSize || 20, tags: _options.tags || false, templateResult: _options.templateResult || this.defaultTemplateResult.bind(this) diff --git a/webapp/tests/karma/ts/services/search.service.spec.ts b/webapp/tests/karma/ts/services/search.service.spec.ts index 6690a8939cd..884ed26e865 100644 --- a/webapp/tests/karma/ts/services/search.service.spec.ts +++ b/webapp/tests/karma/ts/services/search.service.spec.ts @@ -87,7 +87,7 @@ describe('Search service', () => { .onSecondCall().resolves([ { id: 'b' } ]); let firstReturned = false; - const filters = { foo: 'bar' }; + const filters = { search: 'bar' }; service .search('reports', filters) .then((actual) => { @@ -96,7 +96,7 @@ describe('Search service', () => { }) .catch(err => assert.fail(err)); - filters.foo = 'test'; + filters.search = 'test'; service .search('reports', filters) .then((actual) => { @@ -112,7 +112,7 @@ describe('Search service', () => { .onSecondCall().resolves([ { id: 'b' } ]); let firstReturned = false; service - .search('reports', { freetext: 'first' }) + .search('reports', { search: 'first' }) .then((actual) => { expect(actual).to.deep.equal([ { id: 'a' } ]); firstReturned = true; @@ -120,7 +120,7 @@ describe('Search service', () => { .catch(err => assert.fail(err)); service - .search('reports', { freetext: 'second' }) + .search('reports', { search: 'second' }) .then((actual) => { expect(actual).to.deep.equal([ { id: 'b' } ]); expect(firstReturned).to.equal(true); diff --git a/webapp/tests/karma/ts/services/select2-search.service.spec.ts b/webapp/tests/karma/ts/services/select2-search.service.spec.ts index ee487c273ef..6930ff5eb67 100644 --- a/webapp/tests/karma/ts/services/select2-search.service.spec.ts +++ b/webapp/tests/karma/ts/services/select2-search.service.spec.ts @@ -1,6 +1,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; -import { TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { TestBed, fakeAsync, flush } from '@angular/core/testing'; import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { ContactMutedService } from '@mm-services/contact-muted.service'; @@ -17,7 +18,8 @@ describe('Select2SearchService', () => { let lineageModelGeneratorService; let sessionService; let settingsService; - + let searchService; + let activatedRoute; let selectEl; let val; let select2Val; @@ -35,7 +37,8 @@ describe('Select2SearchService', () => { contactMutedService = { getMuted: sinon.stub().returns(false) }; - + searchService = { search: sinon.stub().resolves() }; + activatedRoute = { firstChild: {} }; val = ''; select2Val = [{}]; selectEl = { @@ -59,9 +62,10 @@ describe('Select2SearchService', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: TranslateFakeLoader } }), ], providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, { provide: ContactMutedService, useValue: contactMutedService }, { provide: LineageModelGeneratorService, useValue: lineageModelGeneratorService }, - { provide: SearchService, useValue: { } }, + { provide: SearchService, useValue: searchService }, { provide: SessionService, useValue: sessionService }, { provide: SettingsService, useValue: settingsService }, ] @@ -124,5 +128,80 @@ describe('Select2SearchService', () => { expect(selectEl.trigger.callCount).to.equal(1); expect(selectEl.trigger.args[0]).to.deep.equal([ 'change' ]); // the change is notified to the component }); + + it('should set the filter by parent contact when app form is opened from contacts tab', fakeAsync(async () => { + activatedRoute.firstChild = { firstChild: { firstChild: { snapshot: { params: { id: 'A-123' } } } } }; + + await service.init(selectEl, [ 'person' ], { initialValue: '', filterByParent: true }); + + const selectConfig = selectEl.select2.args[0][0]; + selectConfig.ajax.transport({ data: { q: 'Eric' } }, () => {}, () => {}); + flush(); + + expect(searchService.search.calledOnce).to.be.true; + expect(searchService.search.args[0][0]).to.equal('contacts'); + expect(searchService.search.args[0][1]).to.deep.equal({ + types: { selected: [ 'person' ] }, + search: 'Eric', + parent: 'A-123' + }); + expect(searchService.search.args[0][2]).to.deep.equal({ limit: 20, skip: 0, hydrateContactNames: true }); + })); + + it('should set the filter by parent contact when contact form is opened from contacts tab', fakeAsync(async () => { + activatedRoute.firstChild = { firstChild: { firstChild: { snapshot: { params: { parent_id: 'A-456' } } } } }; + + await service.init(selectEl, [ 'person' ], { initialValue: '', filterByParent: true }); + + const selectConfig = selectEl.select2.args[0][0]; + selectConfig.ajax.transport({ data: { q: 'Eric' } }, () => {}, () => {}); + flush(); + + expect(searchService.search.calledOnce).to.be.true; + expect(searchService.search.args[0][0]).to.equal('contacts'); + expect(searchService.search.args[0][1]).to.deep.equal({ + types: { selected: [ 'person' ] }, + search: 'Eric', + parent: 'A-456' + }); + expect(searchService.search.args[0][2]).to.deep.equal({ limit: 20, skip: 0, hydrateContactNames: true }); + })); + + it('should not set the filter by parent contact when no contact ID', fakeAsync(async () => { + activatedRoute.firstChild = { firstChild: { snapshot: { params: undefined } } }; + + await service.init(selectEl, [ 'person' ], { initialValue: '', filterByParent: true }); + + const selectConfig = selectEl.select2.args[0][0]; + selectConfig.ajax.transport({ data: { q: 'Eric' } }, () => {}, () => {}); + flush(); + + expect(searchService.search.calledOnce).to.be.true; + expect(searchService.search.args[0][0]).to.equal('contacts'); + expect(searchService.search.args[0][1]).to.deep.equal({ + types: { selected: [ 'person' ] }, + search: 'Eric', + parent: undefined + }); + expect(searchService.search.args[0][2]).to.deep.equal({ limit: 20, skip: 0, hydrateContactNames: true }); + })); + + it('should not set the filter by parent contact when filterByParent turn off', fakeAsync(async () => { + activatedRoute.firstChild = { firstChild: { firstChild: { snapshot: { params: { id: 'A-123' } } } } }; + + await service.init(selectEl, [ 'person' ], { initialValue: '', filterByParent: false }); + + const selectConfig = selectEl.select2.args[0][0]; + selectConfig.ajax.transport({ data: { q: 'Eric' } }, () => {}, () => {}); + flush(); + + expect(searchService.search.calledOnce).to.be.true; + expect(searchService.search.args[0][0]).to.equal('contacts'); + expect(searchService.search.args[0][1]).to.deep.equal({ + types: { selected: [ 'person' ] }, + search: 'Eric', + }); + expect(searchService.search.args[0][2]).to.deep.equal({ limit: 20, skip: 0, hydrateContactNames: true }); + })); }); });