From e736741835c3ad223db227e1ae05dc9203d811b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20Macek?= Date: Wed, 18 Dec 2024 23:14:02 +0100 Subject: [PATCH 01/20] create organization --- src/components/form/FormFieldSelectTable.vue | 31 +++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/components/form/FormFieldSelectTable.vue b/src/components/form/FormFieldSelectTable.vue index b53e45a1b..508c5ac7a 100644 --- a/src/components/form/FormFieldSelectTable.vue +++ b/src/components/form/FormFieldSelectTable.vue @@ -57,6 +57,7 @@ import { useOrganizations } from '../../composables/useOrganizations'; import { useSelectTable } from '../../composables/useSelectTable'; import { useValidation } from '../../composables/useValidation'; import { useApiPostTeam } from '../../composables/useApiPostTeam'; +import { useApiPostOrganization } from '../../composables/useApiPostOrganization'; // enums import { OrganizationType, OrganizationLevel } from '../types/Organization'; @@ -187,6 +188,7 @@ export default defineComponent({ }; const { createTeam } = useApiPostTeam(logger); + const { createOrganization } = useApiPostOrganization(logger); const registerChallengeStore = useRegisterChallengeStore(); /** @@ -196,8 +198,35 @@ export default defineComponent({ * @returns {Promise} */ const submitDialogForm = async (): Promise => { + // create organization if (props.organizationLevel === OrganizationLevel.organization) { - // TODO: Create a new company + if (!props.organizationType) { + logger?.info('No organization type provided.'); + return; + } + + logger?.info('Create organization.'); + const data = await createOrganization( + organizationNew.value.name, + organizationNew.value.vatId, + props.organizationType, + ); + + if (data?.id) { + logger?.debug( + `New organization was created with ID <${data.id}> and name <${data.name}>.`, + ); + // emit `create:option` event + emit('create:option', data); + // close dialog + isDialogOpen.value = false; + logger?.info('Close add organization modal dialog.'); + // store data in v-model (emits to parent component) + inputValue.value = data.id; + logger?.debug( + `New organization model ID set to <${inputValue.value}>.`, + ); + } } else if (props.organizationLevel === OrganizationLevel.team) { logger?.info('Create team.'); const subsidiaryId = registerChallengeStore.getSubsidiaryId; From da0854418a55b5ea2dbae33b3f0dec9a82fcea63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20Macek?= Date: Wed, 18 Dec 2024 23:40:39 +0100 Subject: [PATCH 02/20] create organization + subsidiary --- src/components/form/FormFieldSelectTable.vue | 79 +++++++++++++++---- .../form/FormSelectOrganization.vue | 16 +++- 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/components/form/FormFieldSelectTable.vue b/src/components/form/FormFieldSelectTable.vue index 508c5ac7a..556dd4a7e 100644 --- a/src/components/form/FormFieldSelectTable.vue +++ b/src/components/form/FormFieldSelectTable.vue @@ -58,6 +58,7 @@ import { useSelectTable } from '../../composables/useSelectTable'; import { useValidation } from '../../composables/useValidation'; import { useApiPostTeam } from '../../composables/useApiPostTeam'; import { useApiPostOrganization } from '../../composables/useApiPostOrganization'; +import { useApiPostSubsidiary } from '../../composables/useApiPostSubsidiary'; // enums import { OrganizationType, OrganizationLevel } from '../types/Organization'; @@ -160,14 +161,6 @@ export default defineComponent({ // controls dialog visibility const isDialogOpen = ref(false); - // close dialog - const onClose = (): void => { - if (formRef.value) { - formRef.value.reset(); - } - isDialogOpen.value = false; - }; - /** * Validates the form. * If form is valid it submits the data. @@ -188,8 +181,20 @@ export default defineComponent({ }; const { createTeam } = useApiPostTeam(logger); - const { createOrganization } = useApiPostOrganization(logger); + const { isLoading: isLoadingCreateOrganization, createOrganization } = + useApiPostOrganization(logger); + const { isLoading: isLoadingCreateSubsidiary, createSubsidiary } = + useApiPostSubsidiary(logger); const registerChallengeStore = useRegisterChallengeStore(); + const subsidiaryId = computed({ + get: (): number | null => registerChallengeStore.getSubsidiaryId, + set: (value: number | null) => + registerChallengeStore.setSubsidiaryId(value), + }); + const isLoading = computed( + () => + isLoadingCreateOrganization.value || isLoadingCreateSubsidiary.value, + ); /** * Submit dialog form based on organization level @@ -216,16 +221,47 @@ export default defineComponent({ logger?.debug( `New organization was created with ID <${data.id}> and name <${data.name}>.`, ); - // emit `create:option` event - emit('create:option', data); - // close dialog - isDialogOpen.value = false; - logger?.info('Close add organization modal dialog.'); - // store data in v-model (emits to parent component) + + // create subsidiary + logger?.info('Create subsidiary.'); + const subsidiaryData = await createSubsidiary( + data.id, + organizationNew.value.address, + ); + if (subsidiaryData) { + logger?.debug( + `New subsidiary was created with data <${JSON.stringify(subsidiaryData, null, 2)}>.`, + ); + if (subsidiaryData.id) { + // set subsidiary ID in store + subsidiaryId.value = subsidiaryData.id; + logger?.debug( + `Subsidiary ID model set to <${subsidiaryId.value}>.`, + ); + } else { + logger?.error('New subsidiary ID not found.'); + } + } + + /** + * Set modelValue + * This will save the organization ID into the store. + * That will in turn trigger the loading of subsidiary options + * so we do not need to manually handle the new subsidiary option. + */ inputValue.value = data.id; logger?.debug( `New organization model ID set to <${inputValue.value}>.`, ); + /** + * Emit `create:option` event + * This will add new organization to the options list. + */ + emit('create:option', data); + // close dialog + onClose(); + } else { + logger?.error('New organization ID not found.'); } } else if (props.organizationLevel === OrganizationLevel.team) { logger?.info('Create team.'); @@ -242,8 +278,7 @@ export default defineComponent({ // emit `create:option` event emit('create:option', data); // close dialog - isDialogOpen.value = false; - logger?.info('Close add team modal dialog.'); + onClose(); // store data in v-model (emits to parent component) inputValue.value = data.id; logger?.debug(`New team model ID set to <${inputValue.value}>.`); @@ -251,6 +286,15 @@ export default defineComponent({ } }; + // close dialog + const onClose = (): void => { + if (formRef.value) { + formRef.value.reset(); + } + isDialogOpen.value = false; + logger?.info('Close add option modal dialog.'); + }; + const { getOrganizationLabels } = useOrganizations(); const { getSelectTableLabels } = useSelectTable(); @@ -307,6 +351,7 @@ export default defineComponent({ teamNew, titleDialog, isFilled, + isLoading, onClose, onSubmit, OrganizationType, diff --git a/src/components/form/FormSelectOrganization.vue b/src/components/form/FormSelectOrganization.vue index d5c25cfd5..663432f15 100644 --- a/src/components/form/FormSelectOrganization.vue +++ b/src/components/form/FormSelectOrganization.vue @@ -33,7 +33,8 @@ import { OrganizationLevel, OrganizationType } from '../types/Organization'; // types import type { FormSelectOption } from '../types/Form'; import type { Logger } from '../types/Logger'; - +import type { OrganizationOption } from '../types/Organization'; +import type { PostOrganizationResponse } from '../types/apiOrganization'; // stores import { useRegisterChallengeStore } from 'src/stores/registerChallenge'; @@ -47,7 +48,7 @@ export default defineComponent({ const logger = inject('vuejs3-logger') as Logger | null; const opts = ref([]); const formFieldSelectTableRef = ref(null); - const { options, isLoading, loadOrganizations } = + const { options, organizations, isLoading, loadOrganizations } = useApiGetOrganizations(logger); const registerChallengeStore = useRegisterChallengeStore(); @@ -79,7 +80,7 @@ export default defineComponent({ loadOrganizations(newValue).then(() => { logger?.info('All organizations data was loaded from the API.'); // Lazy loading - opts.value = options; + opts.value = options.value; }); } }, @@ -92,6 +93,11 @@ export default defineComponent({ formFieldSelectTableRef.value.selectOrganizationRef.validate(); }; + const onCreateOption = (data: PostOrganizationResponse): void => { + const newOrganization: OrganizationOption = data; + organizations.value.push(newOrganization); + }; + return { formFieldSelectTableRef, isLoading, @@ -101,6 +107,7 @@ export default defineComponent({ OrganizationLevel, organizationType, onCloseAddSubsidiaryDialog, + onCreateOption, }; }, }); @@ -111,11 +118,12 @@ export default defineComponent({ Date: Thu, 19 Dec 2024 13:22:20 +0100 Subject: [PATCH 03/20] remove outdated comment --- src/components/form/FormAddCompany.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/form/FormAddCompany.vue b/src/components/form/FormAddCompany.vue index 8006876a6..66e21879b 100644 --- a/src/components/form/FormAddCompany.vue +++ b/src/components/form/FormAddCompany.vue @@ -154,7 +154,7 @@ export default defineComponent({ {{ $t('form.company.textSubsidiaryAddress') }}

- + Date: Mon, 23 Dec 2024 16:00:56 +0100 Subject: [PATCH 04/20] add test for creating organization in SelectTable --- .../__tests__/FormFieldSelectTable.cy.js | 109 +++++++++++++++ src/components/form/FormFieldSelectTable.vue | 127 ++++++++++-------- .../form/FormSelectOrganization.vue | 3 + .../fixtures/apiPostSubsidiaryResponse.json | 8 +- test/cypress/support/commands.js | 30 +++++ 5 files changed, 216 insertions(+), 61 deletions(-) diff --git a/src/components/__tests__/FormFieldSelectTable.cy.js b/src/components/__tests__/FormFieldSelectTable.cy.js index e4a1ec2ab..7a1f340e6 100644 --- a/src/components/__tests__/FormFieldSelectTable.cy.js +++ b/src/components/__tests__/FormFieldSelectTable.cy.js @@ -1,18 +1,24 @@ +import { ref } from 'vue'; import FormFieldSelectTable from 'components/form/FormFieldSelectTable.vue'; import { rideToWorkByBikeConfig } from 'src/boot/global_vars'; import { i18n } from '../../boot/i18n'; import { useApiGetOrganizations } from 'src/composables/useApiGetOrganizations'; import { createPinia, setActivePinia } from 'pinia'; +import { vModelAdapter } from 'app/test/cypress/utils'; import { OrganizationLevel, OrganizationType, } from 'src/components/types/Organization'; +import { interceptOrganizationsApi } from '../../../test/cypress/support/commonTests'; import { useRegisterChallengeStore } from 'src/stores/registerChallenge'; +// variables const { contactEmail } = rideToWorkByBikeConfig; +const model = ref(null); describe('', () => { let options; + let organizationId; let subsidiaryId; before(() => { @@ -32,6 +38,11 @@ describe('', () => { }, ); }); + cy.fixture('formFieldCompanyCreate').then( + (formFieldCompanyCreateResponse) => { + organizationId = formFieldCompanyCreateResponse.id; + }, + ); // set common subsidiaryId from fixture cy.fixture('formOrganizationOptions').then((formOrganizationOptions) => { subsidiaryId = formOrganizationOptions[0].subsidiaries[0].id; @@ -95,8 +106,19 @@ describe('', () => { context('organization company', () => { beforeEach(() => { cy.interceptCitiesGetApi(rideToWorkByBikeConfig, i18n); + interceptOrganizationsApi( + rideToWorkByBikeConfig, + i18n, + OrganizationType.company, + ); + cy.interceptSubsidiaryPostApi( + rideToWorkByBikeConfig, + i18n, + organizationId, + ); cy.mount(FormFieldSelectTable, { props: { + ...vModelAdapter(model), options: options, organizationLevel: OrganizationLevel.organization, organizationType: OrganizationType.company, @@ -219,6 +241,93 @@ describe('', () => { cy.dataCy('dialog-button-submit').click(); cy.dataCy('dialog-add-option').should('not.exist'); }); + + it.only('allows to add a new organization', () => { + cy.fixture('apiPostSubsidiaryRequest').then( + (apiPostSubsidiaryRequest) => { + cy.fixture('formFieldCompanyCreateRequest').then( + (formFieldCompanyCreateRequest) => { + cy.dataCy('button-add-option').click(); + // dialog + cy.dataCy('dialog-add-option').should('be.visible'); + cy.dataCy('dialog-add-option') + .find('h3') + .should('be.visible') + .and('contain', i18n.global.t('form.company.titleAddCompany')); + // fill form + cy.dataCy('form-add-company-name') + .find('input') + .type(formFieldCompanyCreateRequest.name); + cy.dataCy('form-add-company-vat-id') + .find('input') + .type(formFieldCompanyCreateRequest.vatId); + cy.dataCy('form-add-subsidiary-street') + .find('input') + .type(apiPostSubsidiaryRequest.address.street); + cy.dataCy('form-add-subsidiary-house-number') + .find('input') + .type(apiPostSubsidiaryRequest.address.street_number); + cy.dataCy('form-add-subsidiary-city') + .find('input') + .type(apiPostSubsidiaryRequest.address.city); + cy.dataCy('form-add-subsidiary-zip') + .find('input') + .type(apiPostSubsidiaryRequest.address.psc); + cy.dataCy('form-add-subsidiary-city-challenge').click(); + cy.get('.q-menu') + .should('be.visible') + .find('.q-item') + .first() + .click(); + // submit form + cy.dataCy('dialog-button-submit').click(); + // wait for API call + cy.waitForOrganizationCreateApi(); + // verify that dialog is closed + cy.dataCy('dialog-add-option').should('not.exist'); + cy.fixture('formFieldCompanyCreate').then( + (formFieldCompanyCreateResponse) => { + // test emitted events + cy.wrap(Cypress.vueWrapper.emitted('create:option')).should( + 'have.length', + 1, + ); + cy.wrap( + Cypress.vueWrapper.emitted('create:option')[0][0], + ).should('deep.equal', formFieldCompanyCreateResponse); + // test that model value was updated + cy.wrap(model) + .its('value') + .should('eq', formFieldCompanyCreateResponse.id); + }, + ); + // open dialog again + cy.dataCy('button-add-option').click(); + cy.dataCy('dialog-add-option').should('be.visible'); + // verify that dialog form was reset + cy.dataCy('form-add-company-name') + .find('input') + .should('have.value', ''); + cy.dataCy('form-add-company-vat-id') + .find('input') + .should('have.value', ''); + cy.dataCy('form-add-subsidiary-street') + .find('input') + .should('have.value', ''); + cy.dataCy('form-add-subsidiary-house-number') + .find('input') + .should('have.value', ''); + cy.dataCy('form-add-subsidiary-city') + .find('input') + .should('have.value', ''); + cy.dataCy('form-add-subsidiary-zip') + .find('input') + .should('have.value', ''); + }, + ); + }, + ); + }); }); context('organization company selected', () => { diff --git a/src/components/form/FormFieldSelectTable.vue b/src/components/form/FormFieldSelectTable.vue index 556dd4a7e..f25c60a52 100644 --- a/src/components/form/FormFieldSelectTable.vue +++ b/src/components/form/FormFieldSelectTable.vue @@ -75,6 +75,22 @@ import { FormTeamFields, } from '../types/Form'; +// utils +import { deepObjectWithSimplePropsCopy } from '../../utils'; + +const emptyFormCompanyFields: FormCompanyFields = { + name: '', + vatId: '', + address: { + street: '', + houseNumber: '', + city: '', + zip: '', + cityChallenge: null, + department: '', + }, +}; + export default defineComponent({ name: 'FormFieldSelectTable', components: { @@ -112,21 +128,12 @@ export default defineComponent({ // user input for filtering const query = ref(''); const formRef = ref(null); - const organizationNew = ref({ - name: '', - vatId: '', - address: { - street: '', - houseNumber: '', - city: '', - zip: '', - cityChallenge: null, - department: '', - }, - }); - const teamNew = ref({ - name: '', - }); + const organizationNew = ref( + deepObjectWithSimplePropsCopy( + emptyFormCompanyFields, + ) as FormCompanyFields, + ); + const teamNew = ref({ name: '' }); const selectOrganizationRef = ref(null); /** @@ -203,66 +210,66 @@ export default defineComponent({ * @returns {Promise} */ const submitDialogForm = async (): Promise => { - // create organization if (props.organizationLevel === OrganizationLevel.organization) { + // create organization if (!props.organizationType) { logger?.info('No organization type provided.'); return; } - logger?.info('Create organization.'); - const data = await createOrganization( + const organizationData = await createOrganization( organizationNew.value.name, organizationNew.value.vatId, props.organizationType, ); - if (data?.id) { - logger?.debug( - `New organization was created with ID <${data.id}> and name <${data.name}>.`, - ); - - // create subsidiary - logger?.info('Create subsidiary.'); - const subsidiaryData = await createSubsidiary( - data.id, - organizationNew.value.address, - ); - if (subsidiaryData) { - logger?.debug( - `New subsidiary was created with data <${JSON.stringify(subsidiaryData, null, 2)}>.`, - ); - if (subsidiaryData.id) { - // set subsidiary ID in store - subsidiaryId.value = subsidiaryData.id; - logger?.debug( - `Subsidiary ID model set to <${subsidiaryId.value}>.`, - ); - } else { - logger?.error('New subsidiary ID not found.'); - } - } + if (!organizationData?.id) { + logger?.error('New organization ID not found.'); + return; + } + logger?.debug( + `New organization was created with ID <${organizationData.id}> and name <${organizationData.name}>.`, + ); + // create subsidiary + logger?.info('Create subsidiary.'); + const subsidiaryData = await createSubsidiary( + organizationData.id, + organizationNew.value.address, + ); + if (subsidiaryData?.id) { /** - * Set modelValue - * This will save the organization ID into the store. - * That will in turn trigger the loading of subsidiary options - * so we do not need to manually handle the new subsidiary option. + * We are not using early return because if creating subsidiary fails + * we still want to run subsequent actions (setting modelValue). */ - inputValue.value = data.id; logger?.debug( - `New organization model ID set to <${inputValue.value}>.`, + `New subsidiary was created with data <${JSON.stringify(subsidiaryData, null, 2)}>.`, ); - /** - * Emit `create:option` event - * This will add new organization to the options list. - */ - emit('create:option', data); - // close dialog - onClose(); + // set subsidiary ID in store + subsidiaryId.value = subsidiaryData.id; + logger?.debug(`Subsidiary ID model set to <${subsidiaryId.value}>.`); } else { - logger?.error('New organization ID not found.'); + logger?.error('New subsidiary data not found.'); } + + /** + * Set modelValue (organization ID) + * This will save the organization ID into the store. + * We are waiting until createSubsidiary action is done because saving + * modelValue triggers reloading subsidiary options list and so we + * do not need to manually append the new subsidiary option. + */ + inputValue.value = organizationData.id; + logger?.debug( + `New organization model ID set to <${inputValue.value}>.`, + ); + /** + * Emit `create:option` event + * This will add new organization to the options list. + */ + emit('create:option', organizationData); + // close dialog + onClose(); } else if (props.organizationLevel === OrganizationLevel.team) { logger?.info('Create team.'); const subsidiaryId = registerChallengeStore.getSubsidiaryId; @@ -291,6 +298,12 @@ export default defineComponent({ if (formRef.value) { formRef.value.reset(); } + // reset organizationNew and teamNew + organizationNew.value = deepObjectWithSimplePropsCopy( + emptyFormCompanyFields, + ) as FormCompanyFields; + teamNew.value = { name: '' }; + // close dialog isDialogOpen.value = false; logger?.info('Close add option modal dialog.'); }; diff --git a/src/components/form/FormSelectOrganization.vue b/src/components/form/FormSelectOrganization.vue index 663432f15..bae5f0023 100644 --- a/src/components/form/FormSelectOrganization.vue +++ b/src/components/form/FormSelectOrganization.vue @@ -95,6 +95,9 @@ export default defineComponent({ const onCreateOption = (data: PostOrganizationResponse): void => { const newOrganization: OrganizationOption = data; + logger?.debug( + `Add new organization to organizations array <${JSON.stringify(newOrganization, null, 2)}>.`, + ); organizations.value.push(newOrganization); }; diff --git a/test/cypress/fixtures/apiPostSubsidiaryResponse.json b/test/cypress/fixtures/apiPostSubsidiaryResponse.json index b04af5756..8f67a4309 100644 --- a/test/cypress/fixtures/apiPostSubsidiaryResponse.json +++ b/test/cypress/fixtures/apiPostSubsidiaryResponse.json @@ -4,9 +4,9 @@ "active": true, "address": { "street": "Subsidiary Street", - "street_number": "4", - "recipient": "Recipient 4", - "psc": 50004, - "city": "City 4" + "street_number": "1", + "recipient": "Subsidiary Recipient", + "city": "Subsidiary City", + "psc": 12345 } } diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 443ee4be1..79db117a2 100644 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -1445,3 +1445,33 @@ Cypress.Commands.add('applyInvalidVoucher', (config, i18n) => { .should('have.value', invalid); }); }); + +/** + * Wait for intercept organization creation API call and compare request/response object + * Wait for `@createOrganization` intercept + */ +Cypress.Commands.add('waitForOrganizationCreateApi', () => { + cy.fixture('formFieldCompanyCreateRequest').then( + (formFieldCompanyCreateRequest) => { + cy.fixture('formFieldCompanyCreate').then( + (formFieldCompanyCreateResponse) => { + cy.wait('@createOrganization').then(({ request, response }) => { + expect(request.headers.authorization).to.include(bearerTokeAuth); + expect(request.body).to.deep.equal({ + name: formFieldCompanyCreateRequest.name, + vatId: formFieldCompanyCreateRequest.vatId, + organization_type: + formFieldCompanyCreateRequest.organization_type, + }); + if (response) { + expect(response.statusCode).to.equal(httpSuccessfullStatus); + expect(response.body).to.deep.equal( + formFieldCompanyCreateResponse, + ); + } + }); + }, + ); + }, + ); +}); From 60629f8b540f73e21f33596dc43967e60654c6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20Macek?= Date: Mon, 23 Dec 2024 16:02:33 +0100 Subject: [PATCH 05/20] remove only --- src/components/__tests__/FormFieldSelectTable.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/__tests__/FormFieldSelectTable.cy.js b/src/components/__tests__/FormFieldSelectTable.cy.js index 7a1f340e6..046dd1ef9 100644 --- a/src/components/__tests__/FormFieldSelectTable.cy.js +++ b/src/components/__tests__/FormFieldSelectTable.cy.js @@ -242,7 +242,7 @@ describe('', () => { cy.dataCy('dialog-add-option').should('not.exist'); }); - it.only('allows to add a new organization', () => { + it('allows to add a new organization', () => { cy.fixture('apiPostSubsidiaryRequest').then( (apiPostSubsidiaryRequest) => { cy.fixture('formFieldCompanyCreateRequest').then( From f3aebdb2460ac95d575baad0e0aefa2c316ea285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20Macek?= Date: Mon, 23 Dec 2024 18:37:11 +0100 Subject: [PATCH 06/20] organization and subsidiary creation --- .../__tests__/FormFieldSelectTable.cy.js | 36 +--- .../__tests__/FormSelectOrganization.cy.js | 174 +++++++++++++++++- .../form/FormFieldCompanyAddress.vue | 5 + src/components/form/FormFieldSelectTable.vue | 56 +++--- .../form/FormSelectOrganization.vue | 36 +++- src/composables/useApiGetSubsidiaries.ts | 10 +- .../apiGetSubsidiariesResponseNew.json | 18 ++ test/cypress/fixtures/formFieldCompany.json | 2 +- .../fixtures/formFieldCompanyNext.json | 2 +- test/cypress/support/commands.js | 89 ++++++++- 10 files changed, 363 insertions(+), 65 deletions(-) create mode 100644 test/cypress/fixtures/apiGetSubsidiariesResponseNew.json diff --git a/src/components/__tests__/FormFieldSelectTable.cy.js b/src/components/__tests__/FormFieldSelectTable.cy.js index 046dd1ef9..ad0534ee0 100644 --- a/src/components/__tests__/FormFieldSelectTable.cy.js +++ b/src/components/__tests__/FormFieldSelectTable.cy.js @@ -248,41 +248,21 @@ describe('', () => { cy.fixture('formFieldCompanyCreateRequest').then( (formFieldCompanyCreateRequest) => { cy.dataCy('button-add-option').click(); - // dialog + // verify that dialog is visible cy.dataCy('dialog-add-option').should('be.visible'); cy.dataCy('dialog-add-option') .find('h3') .should('be.visible') .and('contain', i18n.global.t('form.company.titleAddCompany')); - // fill form - cy.dataCy('form-add-company-name') - .find('input') - .type(formFieldCompanyCreateRequest.name); - cy.dataCy('form-add-company-vat-id') - .find('input') - .type(formFieldCompanyCreateRequest.vatId); - cy.dataCy('form-add-subsidiary-street') - .find('input') - .type(apiPostSubsidiaryRequest.address.street); - cy.dataCy('form-add-subsidiary-house-number') - .find('input') - .type(apiPostSubsidiaryRequest.address.street_number); - cy.dataCy('form-add-subsidiary-city') - .find('input') - .type(apiPostSubsidiaryRequest.address.city); - cy.dataCy('form-add-subsidiary-zip') - .find('input') - .type(apiPostSubsidiaryRequest.address.psc); - cy.dataCy('form-add-subsidiary-city-challenge').click(); - cy.get('.q-menu') - .should('be.visible') - .find('.q-item') - .first() - .click(); - // submit form + // fill in the form + cy.fillOrganizationSubsidiaryForm( + formFieldCompanyCreateRequest, + apiPostSubsidiaryRequest, + ); + // submit tghe form cy.dataCy('dialog-button-submit').click(); // wait for API call - cy.waitForOrganizationCreateApi(); + cy.waitForOrganizationPostApi(); // verify that dialog is closed cy.dataCy('dialog-add-option').should('not.exist'); cy.fixture('formFieldCompanyCreate').then( diff --git a/src/components/__tests__/FormSelectOrganization.cy.js b/src/components/__tests__/FormSelectOrganization.cy.js index 4be4adb57..3d8dd27de 100644 --- a/src/components/__tests__/FormSelectOrganization.cy.js +++ b/src/components/__tests__/FormSelectOrganization.cy.js @@ -1,8 +1,14 @@ +import { computed } from 'vue'; import { createPinia, setActivePinia } from 'pinia'; import FormSelectOrganization from 'components/form/FormSelectOrganization.vue'; import { i18n } from '../../boot/i18n'; import { OrganizationType } from 'src/components/types/Organization'; import { useRegisterChallengeStore } from 'src/stores/registerChallenge'; +import { + interceptOrganizationsApi, + waitForOrganizationsApi, +} from '../../../test/cypress/support/commonTests'; +import { rideToWorkByBikeConfig } from 'src/boot/global_vars'; // selectors const selectorFormFieldCompanyAddress = 'form-company-address'; @@ -10,13 +16,33 @@ const selectorFormFieldSelectTable = 'form-select-table-company'; const selectorFormSelectOrganization = 'form-select-organization'; describe('', () => { + let organizationId; + it('has translation for all strings', () => { cy.testLanguageStringsInContext([], 'index.component', i18n); }); + before(() => { + setActivePinia(createPinia()); + cy.fixture('formFieldCompanyCreate').then( + (formFieldCompanyCreateResponse) => { + organizationId = formFieldCompanyCreateResponse.id; + }, + ); + }); + context('desktop', () => { beforeEach(() => { - setActivePinia(createPinia()); + cy.interceptSubsidiariesGetApi( + rideToWorkByBikeConfig, + i18n, + organizationId, + ); + interceptOrganizationsApi( + rideToWorkByBikeConfig, + i18n, + OrganizationType.company, + ); cy.mount(FormSelectOrganization, { props: {}, }); @@ -28,7 +54,16 @@ describe('', () => { context('mobile', () => { beforeEach(() => { - setActivePinia(createPinia()); + cy.interceptSubsidiariesGetApi( + rideToWorkByBikeConfig, + i18n, + organizationId, + ); + interceptOrganizationsApi( + rideToWorkByBikeConfig, + i18n, + OrganizationType.company, + ); cy.mount(FormSelectOrganization, { props: {}, }); @@ -37,6 +72,109 @@ describe('', () => { coreTests(); }); + + context('create new organization and subsidiary', () => { + beforeEach(() => { + cy.interceptCitiesGetApi(rideToWorkByBikeConfig, i18n); + cy.interceptSubsidiariesNewGetApi( + rideToWorkByBikeConfig, + i18n, + organizationId, + ); + interceptOrganizationsApi( + rideToWorkByBikeConfig, + i18n, + OrganizationType.company, + ); + cy.interceptSubsidiaryPostApi( + rideToWorkByBikeConfig, + i18n, + organizationId, + ); + cy.mount(FormSelectOrganization, { + props: {}, + }); + cy.viewport('iphone-6'); + }); + + it('allows to create a new organization and subsidiary', () => { + cy.fixture('formFieldCompany').then((formFieldCompanyResponse) => { + cy.fixture('apiPostSubsidiaryRequest').then( + (apiPostSubsidiaryRequest) => { + cy.fixture('apiPostSubsidiaryResponse').then( + (apiPostSubsidiaryResponse) => { + cy.fixture('formFieldCompanyCreateRequest').then( + (formFieldCompanyCreateRequest) => { + cy.fixture('formFieldCompanyCreate').then( + (formFieldCompanyCreateResponse) => { + cy.wrap(useRegisterChallengeStore()).then((store) => { + store.setOrganizationType(OrganizationType.company); + const organizationId = computed( + () => store.getOrganizationId, + ); + const subsidiaryId = computed( + () => store.getSubsidiaryId, + ); + // open dialog form + cy.dataCy('button-add-option').click(); + // verify that dialog is visible + cy.dataCy('dialog-add-option').should('be.visible'); + cy.dataCy('dialog-add-option') + .find('h3') + .should('be.visible') + .and( + 'contain', + i18n.global.t('form.company.titleAddCompany'), + ); + // fill in the form + cy.fillOrganizationSubsidiaryForm( + formFieldCompanyCreateRequest, + apiPostSubsidiaryRequest, + ); + // submit the form + cy.dataCy('dialog-button-submit').click(); + // check that POST organization API was called + cy.waitForOrganizationPostApi(); + // check that POST subsidiary API was called + cy.waitForSubsidiaryPostApi(); + // check that a new organization was added to the list + cy.dataCy('form-select-table-options') + .find('.q-radio__label') + .should( + 'have.length', + formFieldCompanyResponse.count + 1, + ); + cy.dataCy('form-select-table-options') + .find('.q-radio__label') + .contains(formFieldCompanyCreateRequest.name) + .should('exist'); + // check that organizationId was saved in store + cy.wrap(organizationId) + .its('value') + .should('equal', formFieldCompanyCreateResponse.id); + // check that subsidiaryId was saved in store + cy.wrap(subsidiaryId) + .its('value') + .should('equal', apiPostSubsidiaryResponse.id); + // check that subsidiaries were refetched + cy.waitForSubsidiariesNewApi(); + // check that the subsidary (address) input has correct value + cy.dataCy('form-company-address').should( + 'contain', + apiPostSubsidiaryRequest.address.street, + ); + }); + }, + ); + }, + ); + }, + ); + }, + ); + }); + }); + }); }); function coreTests() { @@ -78,4 +216,36 @@ function coreTests() { .should('have.attr', 'data-organization-type') .and('equal', OrganizationType.family); }); + + it('allows to select an organization', () => { + cy.wrap(useRegisterChallengeStore()).then((store) => { + store.setOrganizationType(OrganizationType.company); + const organizationId = computed(() => store.getOrganizationId); + // check that options are available + cy.fixture('formFieldCompany').then((formFieldCompanyResponse) => { + cy.fixture('formFieldCompanyNext').then( + (formFieldCompanyNextResponse) => { + // wait for API call to finish + waitForOrganizationsApi( + formFieldCompanyResponse, + formFieldCompanyNextResponse, + ); + // check that options exist + cy.dataCy('form-select-table-options') + .find('.q-radio__label') + .should('have.length', formFieldCompanyResponse.count); + // select first option + cy.dataCy('form-select-table-options') + .find('.q-radio__label') + .first() + .click(); + // check that option was saved in store + cy.wrap(organizationId) + .its('value') + .should('equal', formFieldCompanyResponse.results[0].id); + }, + ); + }); + }); + }); } diff --git a/src/components/form/FormFieldCompanyAddress.vue b/src/components/form/FormFieldCompanyAddress.vue index ee2c4629e..b9e9d931c 100644 --- a/src/components/form/FormFieldCompanyAddress.vue +++ b/src/components/form/FormFieldCompanyAddress.vue @@ -89,6 +89,7 @@ export default defineComponent({ const { subsidiaries, isLoading: isLoadingSubsidiaries, + appendSubsidiaryResults, loadSubsidiaries, } = useApiGetSubsidiaries(logger); const { isLoading: isLoadingCreateSubsidiary, createSubsidiary } = @@ -126,6 +127,9 @@ export default defineComponent({ `Register challenge store organization ID updated to <${newValue}>.`, ); subsidiaryId.value = null; + logger?.debug( + `Subsidiary ID reset to <${subsidiaryId.value}> as organization ID changed.`, + ); if (newValue) { logger?.info('Loading subsidiaries.'); loadSubsidiaries(newValue); @@ -260,6 +264,7 @@ export default defineComponent({ isLoadingCreateSubsidiary, onClose, onSubmit, + appendSubsidiaryResults, }; }, }); diff --git a/src/components/form/FormFieldSelectTable.vue b/src/components/form/FormFieldSelectTable.vue index f25c60a52..cd5bc42eb 100644 --- a/src/components/form/FormFieldSelectTable.vue +++ b/src/components/form/FormFieldSelectTable.vue @@ -121,7 +121,7 @@ export default defineComponent({ default: null, }, }, - emits: ['update:modelValue', 'create:option'], + emits: ['update:modelValue', 'create:option', 'create:subsidiary'], setup(props, { emit }) { const logger = inject('vuejs3-logger') as Logger | null; @@ -192,20 +192,21 @@ export default defineComponent({ useApiPostOrganization(logger); const { isLoading: isLoadingCreateSubsidiary, createSubsidiary } = useApiPostSubsidiary(logger); + const isLoading = computed( + () => + isLoadingCreateOrganization.value || isLoadingCreateSubsidiary.value, + ); + const registerChallengeStore = useRegisterChallengeStore(); const subsidiaryId = computed({ get: (): number | null => registerChallengeStore.getSubsidiaryId, set: (value: number | null) => registerChallengeStore.setSubsidiaryId(value), }); - const isLoading = computed( - () => - isLoadingCreateOrganization.value || isLoadingCreateSubsidiary.value, - ); /** * Submit dialog form based on organization level - * If `company`, create a new company + * If `company`, create a new company (and subsidiary) * If `team`, create a new team * @returns {Promise} */ @@ -230,6 +231,22 @@ export default defineComponent({ logger?.debug( `New organization was created with ID <${organizationData.id}> and name <${organizationData.name}>.`, ); + /** + * Set modelValue (organization ID) + * ! Careful, this automatically resets subsidiary ID to null + * Subsidiary creation is done after this and result + * is manually appended to subsidiay options array. + */ + logger?.debug( + `Updating organization model ID from <${inputValue.value}> to <${organizationData.id}>`, + ); + inputValue.value = organizationData.id; + /** + * Emit `create:option` event + * This appends new organization to the options list. + */ + emit('create:option', organizationData); + // create subsidiary logger?.info('Create subsidiary.'); const subsidiaryData = await createSubsidiary( @@ -238,36 +255,21 @@ export default defineComponent({ ); if (subsidiaryData?.id) { - /** - * We are not using early return because if creating subsidiary fails - * we still want to run subsequent actions (setting modelValue). - */ logger?.debug( `New subsidiary was created with data <${JSON.stringify(subsidiaryData, null, 2)}>.`, ); // set subsidiary ID in store - subsidiaryId.value = subsidiaryData.id; + logger?.debug( + `Updating subsidiary ID from <${subsidiaryId.value}> to <${subsidiaryData.id}>`, + ); + registerChallengeStore.setSubsidiaryId(subsidiaryData.id); logger?.debug(`Subsidiary ID model set to <${subsidiaryId.value}>.`); } else { logger?.error('New subsidiary data not found.'); } + // emit event to append data to subsidiary options + emit('create:subsidiary', subsidiaryData); - /** - * Set modelValue (organization ID) - * This will save the organization ID into the store. - * We are waiting until createSubsidiary action is done because saving - * modelValue triggers reloading subsidiary options list and so we - * do not need to manually append the new subsidiary option. - */ - inputValue.value = organizationData.id; - logger?.debug( - `New organization model ID set to <${inputValue.value}>.`, - ); - /** - * Emit `create:option` event - * This will add new organization to the options list. - */ - emit('create:option', organizationData); // close dialog onClose(); } else if (props.organizationLevel === OrganizationLevel.team) { diff --git a/src/components/form/FormSelectOrganization.vue b/src/components/form/FormSelectOrganization.vue index bae5f0023..ed24eedc4 100644 --- a/src/components/form/FormSelectOrganization.vue +++ b/src/components/form/FormSelectOrganization.vue @@ -18,7 +18,7 @@ */ // libraries -import { defineComponent, computed, inject, watch, ref } from 'vue'; +import { defineComponent, computed, inject, nextTick, watch, ref } from 'vue'; // components import FormFieldSelectTable from '../form/FormFieldSelectTable.vue'; @@ -33,8 +33,12 @@ import { OrganizationLevel, OrganizationType } from '../types/Organization'; // types import type { FormSelectOption } from '../types/Form'; import type { Logger } from '../types/Logger'; -import type { OrganizationOption } from '../types/Organization'; +import type { + OrganizationOption, + OrganizationSubsidiary, +} from '../types/Organization'; import type { PostOrganizationResponse } from '../types/apiOrganization'; + // stores import { useRegisterChallengeStore } from 'src/stores/registerChallenge'; @@ -46,6 +50,11 @@ export default defineComponent({ }, setup() { const logger = inject('vuejs3-logger') as Logger | null; + // template ref for form-field-company-address + const formFieldCompanyAddressRef = ref< + typeof FormFieldCompanyAddress | null + >(null); + const opts = ref([]); const formFieldSelectTableRef = ref(null); const { options, organizations, isLoading, loadOrganizations } = @@ -93,12 +102,30 @@ export default defineComponent({ formFieldSelectTableRef.value.selectOrganizationRef.validate(); }; - const onCreateOption = (data: PostOrganizationResponse): void => { + const onCreateOption = async ( + data: PostOrganizationResponse, + ): Promise => { const newOrganization: OrganizationOption = data; logger?.debug( `Add new organization to organizations array <${JSON.stringify(newOrganization, null, 2)}>.`, ); organizations.value.push(newOrganization); + logger?.debug( + `Organizations array updated to <${JSON.stringify(organizations.value, null, 2)}>.`, + ); + // lazy load new options + await nextTick(); + opts.value = options.value; + }; + + const onCreateSubsidiary = (data: OrganizationSubsidiary): void => { + logger?.debug( + `Append new subsidiary <${JSON.stringify(data, null, 2)}> to subsidiary options.`, + ); + // append new subsidiary to subsidiary options + if (formFieldCompanyAddressRef.value) { + formFieldCompanyAddressRef.value.appendSubsidiaryResults([data]); + } }; return { @@ -111,6 +138,7 @@ export default defineComponent({ organizationType, onCloseAddSubsidiaryDialog, onCreateOption, + onCreateSubsidiary, }; }, }); @@ -127,9 +155,11 @@ export default defineComponent({ :data-organization-type="organizationType" ref="formFieldSelectTableRef" @create:option="onCreateOption" + @create:subsidiary="onCreateSubsidiary" data-cy="form-select-table-company" /> ; isLoading: Ref; loadSubsidiaries: (organizationId: number) => Promise; + appendSubsidiaryResults: (results: OrganizationSubsidiary[]) => void; }; /** @@ -78,7 +79,7 @@ export const useApiGetSubsidiaries = ( }); if (data?.results?.length) { - subsidiaries.value.push(...data.results); + appendSubsidiaryResults(data.results); } // if data has multiple pages, fetch all pages @@ -112,7 +113,7 @@ export const useApiGetSubsidiaries = ( // store results if (data?.results?.length) { - subsidiaries.value.push(...data.results); + appendSubsidiaryResults(data.results); } // if data has multiple pages, fetch all pages @@ -121,9 +122,14 @@ export const useApiGetSubsidiaries = ( } }; + const appendSubsidiaryResults = (results: OrganizationSubsidiary[]) => { + subsidiaries.value.push(...results); + }; + return { subsidiaries, isLoading, loadSubsidiaries, + appendSubsidiaryResults, }; }; diff --git a/test/cypress/fixtures/apiGetSubsidiariesResponseNew.json b/test/cypress/fixtures/apiGetSubsidiariesResponseNew.json new file mode 100644 index 000000000..a5ba31ff9 --- /dev/null +++ b/test/cypress/fixtures/apiGetSubsidiariesResponseNew.json @@ -0,0 +1,18 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 5351, + "address": { + "street": "Subsidiary Street", + "street_number": "1", + "recipient": "Subsidiary Recipient", + "city": "Subsidiary City", + "psc": 12345 + }, + "teams": [] + } + ] +} diff --git a/test/cypress/fixtures/formFieldCompany.json b/test/cypress/fixtures/formFieldCompany.json index 22a69584b..f76fbc3d2 100644 --- a/test/cypress/fixtures/formFieldCompany.json +++ b/test/cypress/fixtures/formFieldCompany.json @@ -1,5 +1,5 @@ { - "count": 10, + "count": 20, "next": "https://test.dopracenakole.cz/rest/organizations/company/?limit=10&offset=10", "previous": null, "results": [ diff --git a/test/cypress/fixtures/formFieldCompanyNext.json b/test/cypress/fixtures/formFieldCompanyNext.json index ebb2e7e33..90d447af1 100644 --- a/test/cypress/fixtures/formFieldCompanyNext.json +++ b/test/cypress/fixtures/formFieldCompanyNext.json @@ -1,5 +1,5 @@ { - "count": 10, + "count": 20, "next": null, "previous": null, "results": [ diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 79db117a2..a8de7d987 100644 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -1450,7 +1450,7 @@ Cypress.Commands.add('applyInvalidVoucher', (config, i18n) => { * Wait for intercept organization creation API call and compare request/response object * Wait for `@createOrganization` intercept */ -Cypress.Commands.add('waitForOrganizationCreateApi', () => { +Cypress.Commands.add('waitForOrganizationPostApi', () => { cy.fixture('formFieldCompanyCreateRequest').then( (formFieldCompanyCreateRequest) => { cy.fixture('formFieldCompanyCreate').then( @@ -1475,3 +1475,90 @@ Cypress.Commands.add('waitForOrganizationCreateApi', () => { }, ); }); + +/** + * Fill organization and subsidiary form with data + * @param {Object} formFieldCompanyCreateRequest - Organization data + * @param {Object} apiPostSubsidiaryRequest - Subsidiary data + */ +Cypress.Commands.add( + 'fillOrganizationSubsidiaryForm', + (formFieldCompanyCreateRequest, apiPostSubsidiaryRequest) => { + // fill organization data + cy.dataCy('form-add-company-name') + .find('input') + .type(formFieldCompanyCreateRequest.name); + cy.dataCy('form-add-company-vat-id') + .find('input') + .type(formFieldCompanyCreateRequest.vatId); + // fill subsidiary address data + cy.dataCy('form-add-subsidiary-street') + .find('input') + .type(apiPostSubsidiaryRequest.address.street); + cy.dataCy('form-add-subsidiary-house-number') + .find('input') + .type(apiPostSubsidiaryRequest.address.street_number); + cy.dataCy('form-add-subsidiary-city') + .find('input') + .type(apiPostSubsidiaryRequest.address.city); + cy.dataCy('form-add-subsidiary-zip') + .find('input') + .type(apiPostSubsidiaryRequest.address.psc); + cy.dataCy('form-add-subsidiary-department').type( + apiPostSubsidiaryRequest.address.recipient, + ); + // select city challenge + cy.dataCy('form-add-subsidiary-city-challenge').click(); + cy.get('.q-menu').should('be.visible').find('.q-item').first().click(); + }, +); + +/** + * Intercept subsidiaries GET API call for newly created subsidiary + * Provides `@getSubsidiariesNew` alias + * @param {Object} config - App global config + * @param {Object|String} i18n - i18n instance or locale lang string e.g. en + * @param {Number} organizationId - Organization ID + */ +Cypress.Commands.add( + 'interceptSubsidiariesNewGetApi', + (config, i18n, organizationId) => { + const { apiBase, apiDefaultLang, urlApiOrganizations, urlApiSubsidiaries } = + config; + const apiBaseUrl = getApiBaseUrlWithLang( + null, + apiBase, + apiDefaultLang, + i18n, + ); + const urlApiSubsidiariesLocalized = `${apiBaseUrl}${urlApiOrganizations}${organizationId}/${urlApiSubsidiaries}`; + + cy.fixture('apiGetSubsidiariesResponseNew').then( + (subsidiariesResponseNew) => { + // intercept subsidiaries API call + cy.intercept('GET', urlApiSubsidiariesLocalized, { + statusCode: httpSuccessfullStatus, + body: subsidiariesResponseNew, + }).as('getSubsidiariesNew'); + }, + ); + }, +); + +/** + * Wait for intercept subsidiaries API call and compare request/response object + * Wait for `@getSubsidiariesNew` intercept + */ +Cypress.Commands.add('waitForSubsidiariesNewApi', () => { + cy.fixture('apiGetSubsidiariesResponseNew').then( + (subsidiariesResponseNew) => { + cy.wait('@getSubsidiariesNew').then(({ request, response }) => { + expect(request.headers.authorization).to.include(bearerTokeAuth); + if (response) { + expect(response.statusCode).to.equal(httpSuccessfullStatus); + expect(response.body).to.deep.equal(subsidiariesResponseNew); + } + }); + }, + ); +}); From b0ada87b0b3cec59266831d9b6059f6b2d189f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20Macek?= Date: Mon, 23 Dec 2024 22:50:24 +0100 Subject: [PATCH 07/20] update loading subsidiaries via store and post-creation update --- .../__tests__/FormSelectOrganization.cy.js | 7 --- .../form/FormFieldCompanyAddress.vue | 41 +++------------ src/components/form/FormFieldSelectTable.vue | 43 +++++++++++----- .../form/FormSelectOrganization.vue | 34 ++++++------- src/stores/registerChallenge.ts | 23 ++++++++- test/cypress/support/commands.js | 50 ------------------- 6 files changed, 73 insertions(+), 125 deletions(-) diff --git a/src/components/__tests__/FormSelectOrganization.cy.js b/src/components/__tests__/FormSelectOrganization.cy.js index 3d8dd27de..826f4ce9c 100644 --- a/src/components/__tests__/FormSelectOrganization.cy.js +++ b/src/components/__tests__/FormSelectOrganization.cy.js @@ -76,11 +76,6 @@ describe('', () => { context('create new organization and subsidiary', () => { beforeEach(() => { cy.interceptCitiesGetApi(rideToWorkByBikeConfig, i18n); - cy.interceptSubsidiariesNewGetApi( - rideToWorkByBikeConfig, - i18n, - organizationId, - ); interceptOrganizationsApi( rideToWorkByBikeConfig, i18n, @@ -156,8 +151,6 @@ describe('', () => { cy.wrap(subsidiaryId) .its('value') .should('equal', apiPostSubsidiaryResponse.id); - // check that subsidiaries were refetched - cy.waitForSubsidiariesNewApi(); // check that the subsidary (address) input has correct value cy.dataCy('form-company-address').should( 'contain', diff --git a/src/components/form/FormFieldCompanyAddress.vue b/src/components/form/FormFieldCompanyAddress.vue index b9e9d931c..62099e110 100644 --- a/src/components/form/FormFieldCompanyAddress.vue +++ b/src/components/form/FormFieldCompanyAddress.vue @@ -33,7 +33,6 @@ import DialogDefault from 'src/components/global/DialogDefault.vue'; import FormAddSubsidiary from 'src/components/form/FormAddSubsidiary.vue'; // composables -import { useApiGetSubsidiaries } from 'src/composables/useApiGetSubsidiaries'; import { useValidation } from 'src/composables/useValidation'; import { useApiPostSubsidiary } from '../../composables/useApiPostSubsidiary'; @@ -86,19 +85,16 @@ export default defineComponent({ ) as FormCompanyAddressFields, ); - const { - subsidiaries, - isLoading: isLoadingSubsidiaries, - appendSubsidiaryResults, - loadSubsidiaries, - } = useApiGetSubsidiaries(logger); + const store = useRegisterChallengeStore(); + + const isLoadingSubsidiaries = computed(() => store.isLoadingSubsidiaries); + const subsidiaries = computed(() => store.subsidiaries); + const { isLoading: isLoadingCreateSubsidiary, createSubsidiary } = useApiPostSubsidiary(logger); const isLoading = computed( () => isLoadingSubsidiaries.value || isLoadingCreateSubsidiary.value, ); - - const store = useRegisterChallengeStore(); /** * If organization ID is set, load subsidiaries. * This ensures, that options are loaded on page refresh. @@ -106,7 +102,7 @@ export default defineComponent({ onMounted(async () => { if (store.getOrganizationId) { logger?.info('Loading subsidiaries.'); - await loadSubsidiaries(store.getOrganizationId); + await store.loadSubsidiariesToStore(logger); } else { logger?.debug( `Organization was not selected <${store.getOrganizationId}>,` + @@ -114,31 +110,9 @@ export default defineComponent({ ); } }); - /** - * Watch for organization ID changes. - * This clears the subsidiary options and state after organization change. - * Then loads subsidiaries for the new organization. - * Should not be triggered on mounted not to clear saved subsidiary ID. - */ - watch( - () => store.getOrganizationId, - (newValue) => { - logger?.debug( - `Register challenge store organization ID updated to <${newValue}>.`, - ); - subsidiaryId.value = null; - logger?.debug( - `Subsidiary ID reset to <${subsidiaryId.value}> as organization ID changed.`, - ); - if (newValue) { - logger?.info('Loading subsidiaries.'); - loadSubsidiaries(newValue); - } - }, - ); const options = computed(() => - subsidiaries.value?.map((subsidiary) => ({ + store.getSubsidiaries?.map((subsidiary) => ({ label: getAddressString(subsidiary.address), value: subsidiary.id, })), @@ -264,7 +238,6 @@ export default defineComponent({ isLoadingCreateSubsidiary, onClose, onSubmit, - appendSubsidiaryResults, }; }, }); diff --git a/src/components/form/FormFieldSelectTable.vue b/src/components/form/FormFieldSelectTable.vue index cd5bc42eb..56e8b2422 100644 --- a/src/components/form/FormFieldSelectTable.vue +++ b/src/components/form/FormFieldSelectTable.vue @@ -74,6 +74,7 @@ import { FormSelectTableOption, FormTeamFields, } from '../types/Form'; +import type { OrganizationSubsidiary } from '../types/Organization'; // utils import { deepObjectWithSimplePropsCopy } from '../../utils'; @@ -121,7 +122,7 @@ export default defineComponent({ default: null, }, }, - emits: ['update:modelValue', 'create:option', 'create:subsidiary'], + emits: ['update:modelValue', 'create:option'], setup(props, { emit }) { const logger = inject('vuejs3-logger') as Logger | null; @@ -204,6 +205,14 @@ export default defineComponent({ registerChallengeStore.setSubsidiaryId(value), }); + const onChangeOption = (value: number | null): void => { + logger?.debug(`Organization option changed <${value}>`); + logger?.debug('Resetting subsidiary ID to null'); + registerChallengeStore.setSubsidiaryId(null); + logger?.debug('Reloading subsidiaries'); + registerChallengeStore.loadSubsidiariesToStore(logger); + }; + /** * Submit dialog form based on organization level * If `company`, create a new company (and subsidiary) @@ -231,20 +240,14 @@ export default defineComponent({ logger?.debug( `New organization was created with ID <${organizationData.id}> and name <${organizationData.name}>.`, ); - /** - * Set modelValue (organization ID) - * ! Careful, this automatically resets subsidiary ID to null - * Subsidiary creation is done after this and result - * is manually appended to subsidiay options array. - */ + logger?.debug( `Updating organization model ID from <${inputValue.value}> to <${organizationData.id}>`, ); + // set organization ID in store inputValue.value = organizationData.id; - /** - * Emit `create:option` event - * This appends new organization to the options list. - */ + + // emit event to append data to organization options emit('create:option', organizationData); // create subsidiary @@ -264,11 +267,23 @@ export default defineComponent({ ); registerChallengeStore.setSubsidiaryId(subsidiaryData.id); logger?.debug(`Subsidiary ID model set to <${subsidiaryId.value}>.`); + // create a new subsidiary array in store + const newSubsidiary: OrganizationSubsidiary = { + id: subsidiaryData.id, + address: { + street: subsidiaryData.street, + houseNumber: subsidiaryData.houseNumber, + city: subsidiaryData.city, + zip: subsidiaryData.zip, + cityChallenge: subsidiaryData.cityChallenge, + department: subsidiaryData.department, + }, + teams: [], + }; + registerChallengeStore.setSubsidiaries([newSubsidiary]); } else { logger?.error('New subsidiary data not found.'); } - // emit event to append data to subsidiary options - emit('create:subsidiary', subsidiaryData); // close dialog onClose(); @@ -369,6 +384,7 @@ export default defineComponent({ isLoading, onClose, onSubmit, + onChangeOption, OrganizationType, OrganizationLevel, selectOrganizationRef, @@ -440,6 +456,7 @@ export default defineComponent({ :label="item.label" color="primary" data-cy="form-select-table-option" + @update:model-value="onChangeOption" />