From c4de870e91d7862bd5a59487052c1a8417c2be99 Mon Sep 17 00:00:00 2001 From: Dominik Klein Date: Mon, 28 Nov 2022 12:39:37 +0100 Subject: [PATCH] Fixes: Mobile - Fixed problems with customer and organization field in the ticket create. --- .../user/__tests__/mocks/user-mocks.ts | 1 + .../graphql/queries/searchOverview.api.ts | 2 + .../graphql/queries/searchOverview.graphql | 2 + .../pages/ticket/views/TicketCreate.vue | 4 +- app/frontend/shared/components/Form/Form.vue | 1 + .../FieldAutoCompleteInputDialog.vue | 32 ++++++---- .../Form/fields/FieldAutoComplete/index.ts | 2 + .../Form/fields/FieldAutoComplete/types.ts | 1 + .../FieldCustomer/FieldCustomerWrapper.vue | 2 +- .../FieldOrganizationWrapper.vue | 8 +-- app/frontend/shared/components/Form/types.ts | 3 + .../composables/useObjectAttributeFormData.ts | 5 +- .../utils/getAutoCompleteOption.ts | 8 ++- .../composables/useTicketCreateArticleType.ts | 1 - .../useTicketFormOrganizationHandler.ts | 62 +++++++++++++------ .../shared/graphql/__tests__/utils.spec.ts | 41 ++++++++++++ .../graphql/fragments/userAttributes.api.ts | 2 + .../graphql/fragments/userAttributes.graphql | 2 + .../fragments/userDetailAttributes.api.ts | 1 + .../fragments/userDetailAttributes.graphql | 1 + app/frontend/shared/graphql/types.ts | 22 +++---- app/frontend/shared/graphql/utils.ts | 7 +++ .../shared/stores/__tests__/session.spec.ts | 2 + app/graphql/gql/types/user_type.rb | 2 +- spec/graphql/gql/queries/user_spec.rb | 16 +++++ 25 files changed, 171 insertions(+), 59 deletions(-) create mode 100644 app/frontend/shared/graphql/__tests__/utils.spec.ts diff --git a/app/frontend/apps/mobile/entities/user/__tests__/mocks/user-mocks.ts b/app/frontend/apps/mobile/entities/user/__tests__/mocks/user-mocks.ts index 5fa4da03aa6c..f6819460ec2d 100644 --- a/app/frontend/apps/mobile/entities/user/__tests__/mocks/user-mocks.ts +++ b/app/frontend/apps/mobile/entities/user/__tests__/mocks/user-mocks.ts @@ -52,6 +52,7 @@ export const defaultUser = (): ConfidentTake => { ], totalCount: 1, }, + hasSecondaryOrganizations: true, objectAttributeValues: [ { attribute: { diff --git a/app/frontend/apps/mobile/pages/search/graphql/queries/searchOverview.api.ts b/app/frontend/apps/mobile/pages/search/graphql/queries/searchOverview.api.ts index f1efeb0a7e70..67bdc8e4ac77 100644 --- a/app/frontend/apps/mobile/pages/search/graphql/queries/searchOverview.api.ts +++ b/app/frontend/apps/mobile/pages/search/graphql/queries/searchOverview.api.ts @@ -23,6 +23,7 @@ export const SearchDocument = gql` } customer { id + internalId fullname } updatedAt @@ -39,6 +40,7 @@ export const SearchDocument = gql` image organization { id + internalId name } updatedAt diff --git a/app/frontend/apps/mobile/pages/search/graphql/queries/searchOverview.graphql b/app/frontend/apps/mobile/pages/search/graphql/queries/searchOverview.graphql index bbfd3271f685..af883d10b16a 100644 --- a/app/frontend/apps/mobile/pages/search/graphql/queries/searchOverview.graphql +++ b/app/frontend/apps/mobile/pages/search/graphql/queries/searchOverview.graphql @@ -19,6 +19,7 @@ query search( } customer { id + internalId fullname } updatedAt @@ -35,6 +36,7 @@ query search( image organization { id + internalId name } updatedAt diff --git a/app/frontend/apps/mobile/pages/ticket/views/TicketCreate.vue b/app/frontend/apps/mobile/pages/ticket/views/TicketCreate.vue index 7ad0f4198de1..b9640c9533c8 100644 --- a/app/frontend/apps/mobile/pages/ticket/views/TicketCreate.vue +++ b/app/frontend/apps/mobile/pages/ticket/views/TicketCreate.vue @@ -12,7 +12,7 @@ import { useMultiStepForm, useForm } from '@shared/components/Form' import { useApplicationStore } from '@shared/stores/application' import { useTicketCreateArticleType } from '@shared/entities/ticket/composables/useTicketCreateArticleType' import { ButtonVariant } from '@shared/components/Form/fields/FieldButton/types' -import { useTicketFormOganizationHandling } from '@shared/entities/ticket/composables/useTicketFormOrganizationHandler' +import { useTicketFormOganizationHandler } from '@shared/entities/ticket/composables/useTicketFormOrganizationHandler' import { FormData, type FormSchemaNode } from '@shared/components/Form/types' import { i18n } from '@shared/i18n' import { MutationHandler } from '@shared/server/apollo/handler' @@ -317,7 +317,7 @@ const submitButtonDisabled = computed(() => { ref="form" class="text-left" :schema="formSchema" - :handlers="[useTicketFormOganizationHandling()]" + :handlers="[useTicketFormOganizationHandler()]" :multi-step-form-groups="Object.keys(allSteps)" :schema-data="schemaData" :form-updater-id="EnumFormUpdaterId.FormUpdaterUpdaterTicketCreate" diff --git a/app/frontend/shared/components/Form/Form.vue b/app/frontend/shared/components/Form/Form.vue index 009652b8ab35..647834d1fa67 100644 --- a/app/frontend/shared/components/Form/Form.vue +++ b/app/frontend/shared/components/Form/Form.vue @@ -518,6 +518,7 @@ const executeFormHandler = ( formNode.value, currentValues, props.changeFields, + updateSchemaDataField, schemaData, changedField, ) diff --git a/app/frontend/shared/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInputDialog.vue b/app/frontend/shared/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInputDialog.vue index 2e59e4a8ce92..e62f92343bd8 100644 --- a/app/frontend/shared/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInputDialog.vue +++ b/app/frontend/shared/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInputDialog.vue @@ -5,7 +5,7 @@ import type { ConcreteComponent, Ref } from 'vue' import { computed, nextTick, onMounted, ref, toRef, watch } from 'vue' import { useRouter } from 'vue-router' import { cloneDeep } from 'lodash-es' -import { refDebounced } from '@vueuse/core' +import { refDebounced, watchOnce } from '@vueuse/core' import { useLazyQuery } from '@vue/apollo-composable' import gql from 'graphql-tag' import type { NameNode, OperationDefinitionNode, SelectionNode } from 'graphql' @@ -28,7 +28,9 @@ const props = defineProps<{ optionIconComponent: ConcreteComponent }>() -const { isCurrentValue } = useValue(toRef(props, 'context')) +const contextReactive = toRef(props, 'context') + +const { isCurrentValue } = useValue(contextReactive) const emit = defineEmits<{ (e: 'updateOptions', options: AutoCompleteOption[]): void @@ -37,7 +39,7 @@ const emit = defineEmits<{ const { sortedOptions, selectOption } = useSelectOptions( toRef(props, 'options'), - toRef(props, 'context'), + contextReactive, ) let areLocalOptionsReplaced = false @@ -102,20 +104,24 @@ const AutocompleteSearchDocument = gql` const autocompleteQueryHandler = new QueryHandler( useLazyQuery(AutocompleteSearchDocument, () => ({ input: { - query: debouncedFilter.value, + query: debouncedFilter.value || props.context.defaultFilter || '', limit: props.context.limit, - ...props.context.additionalQueryParams, + ...(props.context.additionalQueryParams || {}), }, })), ) -watch( - () => debouncedFilter.value, - (newValue) => { - if (!newValue.length) return - autocompleteQueryHandler.load() - }, -) +if (props.context.defaultFilter) { + autocompleteQueryHandler.load() +} else { + watchOnce( + () => debouncedFilter.value, + (newValue) => { + if (!newValue.length) return + autocompleteQueryHandler.load() + }, + ) +} const autocompleteQueryResultKey = ( (AutocompleteSearchDocument.definitions[0] as OperationDefinitionNode) @@ -266,7 +272,7 @@ useTraverseOptions(autocompleteList) role="listbox" >
{ - if (!context.belongsToObjectField) return null + if (!context.belongsToObjectField || !initialEntityObject) return null const belongsToObject = initialEntityObject[context.belongsToObjectField] diff --git a/app/frontend/shared/components/Form/fields/FieldOrganization/FieldOrganizationWrapper.vue b/app/frontend/shared/components/Form/fields/FieldOrganization/FieldOrganizationWrapper.vue index 4cd54b30c0fa..6b3d6b2483ef 100644 --- a/app/frontend/shared/components/Form/fields/FieldOrganization/FieldOrganizationWrapper.vue +++ b/app/frontend/shared/components/Form/fields/FieldOrganization/FieldOrganizationWrapper.vue @@ -30,13 +30,12 @@ const props = defineProps() Object.assign(props.context, { optionIconComponent: markRaw(FieldOrganizationOptionIcon), - initialOptionBuilder: ( initialEntityObject: ObjectLike, value: SelectValue, context: Props['context'], ) => { - if (!context.belongsToObjectField) return null + if (!context.belongsToObjectField || !initialEntityObject) return null const belongsToObject = initialEntityObject[ context.belongsToObjectField @@ -46,11 +45,6 @@ Object.assign(props.context, { return getAutoCompleteOption(belongsToObject) }, - - // TODO: change the action to the actual new organization route - action: '/tickets', - actionIcon: 'mobile-new-organization', - gqlQuery: AutocompleteSearchOrganizationDocument, }) diff --git a/app/frontend/shared/components/Form/types.ts b/app/frontend/shared/components/Form/types.ts index e6581ff6187f..6dfda50092c9 100644 --- a/app/frontend/shared/components/Form/types.ts +++ b/app/frontend/shared/components/Form/types.ts @@ -193,6 +193,9 @@ export type FormHandlerFunction = ( formNode: FormKitNode | undefined, values: FormValues, changeFields: Record>, + updateSchemaDataField: ( + field: FormSchemaField | SetRequired, 'name'>, + ) => void, schemaData: ReactiveFormSchemData, changedField?: ChangedField, ) => void diff --git a/app/frontend/shared/entities/object-attributes/composables/useObjectAttributeFormData.ts b/app/frontend/shared/entities/object-attributes/composables/useObjectAttributeFormData.ts index 465199541b4f..a2865d1c47be 100644 --- a/app/frontend/shared/entities/object-attributes/composables/useObjectAttributeFormData.ts +++ b/app/frontend/shared/entities/object-attributes/composables/useObjectAttributeFormData.ts @@ -16,7 +16,7 @@ export const useObjectAttributeFormData = ( const internalObjectAttributeValues: Record = {} const additionalObjectAttributeValues: ObjectAttributeValueInput[] = [] - const fullRelationID = (relation: string, value: number) => { + const fullRelationID = (relation: string, value: number | string) => { return convertToGraphQLId(toClassName(relation), value) } @@ -28,7 +28,8 @@ export const useObjectAttributeFormData = ( if (objectAttribute.isInternal) { internalObjectAttributeValues[camelize(objectAttribute.name)] = - objectAttribute.dataOption.relation && typeof value === 'number' + objectAttribute.dataOption.relation && + (typeof value === 'number' || typeof value === 'string') ? fullRelationID(objectAttribute.dataOption.relation, value) : value } else { diff --git a/app/frontend/shared/entities/organization/utils/getAutoCompleteOption.ts b/app/frontend/shared/entities/organization/utils/getAutoCompleteOption.ts index 556cd706ede7..d2adda6d92e3 100644 --- a/app/frontend/shared/entities/organization/utils/getAutoCompleteOption.ts +++ b/app/frontend/shared/entities/organization/utils/getAutoCompleteOption.ts @@ -1,12 +1,16 @@ // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ import type { Organization } from '@shared/graphql/types' +import { ensureGraphqlId } from '@shared/graphql/utils' export const getAutoCompleteOption = (organization: Partial) => { return { label: organization.name, - value: organization.internalId, - // disabled: !object.active, // TODO: we can not use disabled for the active/inactive flag, because it will be no longer possible to select the option + value: + organization.internalId || + (organization.id + ? ensureGraphqlId('Organization', organization.id) + : null), organization, } } diff --git a/app/frontend/shared/entities/ticket/composables/useTicketCreateArticleType.ts b/app/frontend/shared/entities/ticket/composables/useTicketCreateArticleType.ts index 7baee6761348..b4323fee52b8 100644 --- a/app/frontend/shared/entities/ticket/composables/useTicketCreateArticleType.ts +++ b/app/frontend/shared/entities/ticket/composables/useTicketCreateArticleType.ts @@ -2,7 +2,6 @@ import { computed } from 'vue' import { useApplicationStore } from '@shared/stores/application' -import type { RadioOption } from '@shared/components/Form/fields/FieldRadio' import { TicketCreateArticleType } from '../types' export const useTicketCreateArticleType = () => { diff --git a/app/frontend/shared/entities/ticket/composables/useTicketFormOrganizationHandler.ts b/app/frontend/shared/entities/ticket/composables/useTicketFormOrganizationHandler.ts index 7bbc89d0967c..155bf67e447f 100644 --- a/app/frontend/shared/entities/ticket/composables/useTicketFormOrganizationHandler.ts +++ b/app/frontend/shared/entities/ticket/composables/useTicketFormOrganizationHandler.ts @@ -4,31 +4,44 @@ import { getNode } from '@formkit/core' import { FormHandlerExecution } from '@shared/components/Form' import type { FormHandlerFunction, FormHandler } from '@shared/components/Form' import { useSessionStore } from '@shared/stores/session' -import type { Organization } from '@shared/graphql/types' +import type { Organization, Scalars } from '@shared/graphql/types' import type { AutoCompleteCustomerOption } from '@shared/components/Form/fields/FieldCustomer' import type { UserData } from '@shared/types/store' // TODO: remove this import -import type { FormSchemaField } from '@shared/components/Form/types' +import type { + FormSchemaField, + ReactiveFormSchemData, + ChangedField, +} from '@shared/components/Form/types' import { getAutoCompleteOption } from '@shared/entities/organization/utils/getAutoCompleteOption' // TODO: needs to be aligned, when auto completes has a final state. -export const useTicketFormOganizationHandling = (): FormHandler => { +export const useTicketFormOganizationHandler = (): FormHandler => { + const executeHandler = ( + execution: FormHandlerExecution, + schemaData: ReactiveFormSchemData, + changedField?: ChangedField, + ) => { + if (!schemaData.fields.organization_id) return false + if ( + execution === FormHandlerExecution.FieldChange && + (!changedField || changedField.name !== 'customer_id') + ) { + return false + } + + return true + } + const handleOrganizationField: FormHandlerFunction = ( execution, formNode, values, changeFields, + updateSchemaDataField, schemaData, changedField, - // TODO ... - // eslint-disable-next-line sonarjs/cognitive-complexity ) => { - if (!schemaData.fields.organization_id) return - if ( - execution === FormHandlerExecution.FieldChange && - (!changedField || changedField.name !== 'customer_id') - ) { - return - } + if (!executeHandler(execution, schemaData, changedField)) return const session = useSessionStore() @@ -59,6 +72,7 @@ export const useTicketFormOganizationHandling = (): FormHandler => { } const setOrganizationField = ( + customerId: Scalars['ID'], organization?: Maybe>, ) => { if (!organization) return @@ -66,18 +80,28 @@ export const useTicketFormOganizationHandling = (): FormHandler => { organizationField.show = true organizationField.required = true - organizationField.props = { - options: [getAutoCompleteOption(organization)], - } - organizationField.value = organization.id + const currentValueOption = getAutoCompleteOption(organization) + + // Some information can be changed during the next user interactions, so update only the current schema data. + updateSchemaDataField({ + name: 'organization_id', + props: { + defaultFilter: '*', + options: [currentValueOption], + additionalQueryParams: { + customerId, + }, + }, + value: currentValueOption.value, + }) } const customer = setCustomer() - // TODO: extend if with secondary orga... check - if (customer) { - setOrganizationField(customer.organization as Organization) + if (customer?.hasSecondaryOrganizations) { + setOrganizationField(customer.id, customer.organization as Organization) } + // This values should be fixed, until the user change something in the customer_id field. changeFields.organization_id = { ...(changeFields.organization_id || {}), ...organizationField, diff --git a/app/frontend/shared/graphql/__tests__/utils.spec.ts b/app/frontend/shared/graphql/__tests__/utils.spec.ts new file mode 100644 index 000000000000..d4a629d0d039 --- /dev/null +++ b/app/frontend/shared/graphql/__tests__/utils.spec.ts @@ -0,0 +1,41 @@ +// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ + +import {convertToGraphQLId, isGraphQLId, ensureGraphqlId, getIdFromGraphQLId} from '../utils' + +describe('isGraphQLId', () => { + it('check for valid id', async () => { + expect(isGraphQLId('gid://zammad/Organization/1')).toBe(true) + }) + + it('check for invalid id', async () => { + expect(isGraphQLId('invalid')).toBe(false) + }) +}) + +describe('convertToGraphQLId', () => { + it('check convertion', async () => { + expect(convertToGraphQLId('Organization', 1)).toBe('gid://zammad/Organization/1') + }) +}) + +describe('convertToGraphQLId', () => { + it('check convertion', async () => { + expect(convertToGraphQLId('Organization', 1)).toBe('gid://zammad/Organization/1') + }) +}) + +describe('ensureGraphqlId', () => { + it('check that we have always a GraphQL id', async () => { + expect(ensureGraphqlId('Organization', 1)).toBe('gid://zammad/Organization/1') + }) + + it('check that we have always a GraphQL id (also when it has the correct format)', async () => { + expect(ensureGraphqlId('Organization', 'gid://zammad/Organization/1')).toBe('gid://zammad/Organization/1') + }) +}) + +describe('getIdFromGraphQLId', () => { + it('check that ID can parsed from graphqlId ', async () => { + expect(getIdFromGraphQLId('gid://zammad/Organization/1')).toBe(1) + }) +}) diff --git a/app/frontend/shared/graphql/fragments/userAttributes.api.ts b/app/frontend/shared/graphql/fragments/userAttributes.api.ts index 8f89d80ce716..5edf926de4ea 100644 --- a/app/frontend/shared/graphql/fragments/userAttributes.api.ts +++ b/app/frontend/shared/graphql/fragments/userAttributes.api.ts @@ -16,11 +16,13 @@ export const UserAttributesFragmentDoc = gql` } organization { id + internalId name active objectAttributeValues { ...objectAttributeValues } } + hasSecondaryOrganizations } ${ObjectAttributeValuesFragmentDoc}`; \ No newline at end of file diff --git a/app/frontend/shared/graphql/fragments/userAttributes.graphql b/app/frontend/shared/graphql/fragments/userAttributes.graphql index 322b72775f2e..f71e98963750 100644 --- a/app/frontend/shared/graphql/fragments/userAttributes.graphql +++ b/app/frontend/shared/graphql/fragments/userAttributes.graphql @@ -11,10 +11,12 @@ fragment userAttributes on User { } organization { id + internalId name active objectAttributeValues { ...objectAttributeValues } } + hasSecondaryOrganizations } diff --git a/app/frontend/shared/graphql/fragments/userDetailAttributes.api.ts b/app/frontend/shared/graphql/fragments/userDetailAttributes.api.ts index c8e95a53ab3a..f370fc1ce959 100644 --- a/app/frontend/shared/graphql/fragments/userDetailAttributes.api.ts +++ b/app/frontend/shared/graphql/fragments/userDetailAttributes.api.ts @@ -31,6 +31,7 @@ export const UserDetailAttributesFragmentDoc = gql` closed } } + hasSecondaryOrganizations ticketsCount { open closed diff --git a/app/frontend/shared/graphql/fragments/userDetailAttributes.graphql b/app/frontend/shared/graphql/fragments/userDetailAttributes.graphql index 5f6bb528071f..370ae78e522f 100644 --- a/app/frontend/shared/graphql/fragments/userDetailAttributes.graphql +++ b/app/frontend/shared/graphql/fragments/userDetailAttributes.graphql @@ -26,6 +26,7 @@ fragment userDetailAttributes on User { closed } } + hasSecondaryOrganizations ticketsCount { open closed diff --git a/app/frontend/shared/graphql/types.ts b/app/frontend/shared/graphql/types.ts index 2d49d2a9e2df..139f4b516439 100644 --- a/app/frontend/shared/graphql/types.ts +++ b/app/frontend/shared/graphql/types.ts @@ -1867,7 +1867,7 @@ export type UserQueryVariables = Exact<{ }>; -export type UserQuery = { __typename?: 'Queries', user: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, email?: string | null, web?: string | null, vip?: boolean | null, phone?: string | null, mobile?: string | null, fax?: string | null, note?: string | null, active?: boolean | null, secondaryOrganizations?: { __typename?: 'OrganizationConnection', totalCount: number, edges: Array<{ __typename?: 'OrganizationEdge', node: { __typename?: 'Organization', id: string, internalId: number, active?: boolean | null, name?: string | null } }> } | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null } | null, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null } }; +export type UserQuery = { __typename?: 'Queries', user: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, email?: string | null, web?: string | null, vip?: boolean | null, phone?: string | null, mobile?: string | null, fax?: string | null, note?: string | null, active?: boolean | null, hasSecondaryOrganizations?: boolean | null, secondaryOrganizations?: { __typename?: 'OrganizationConnection', totalCount: number, edges: Array<{ __typename?: 'OrganizationEdge', node: { __typename?: 'Organization', id: string, internalId: number, active?: boolean | null, name?: string | null } }> } | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null } | null, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null } }; export type AccountAvatarAddMutationVariables = Exact<{ images: AvatarInput; @@ -1902,7 +1902,7 @@ export type SearchQueryVariables = Exact<{ }>; -export type SearchQuery = { __typename?: 'Queries', search: Array<{ __typename?: 'Organization', id: string, internalId: number, active?: boolean | null, name?: string | null, updatedAt: string, members?: { __typename?: 'UserConnection', totalCount: number, edges: Array<{ __typename?: 'UserEdge', node: { __typename?: 'User', id: string, fullname?: string | null } }> } | null, updatedBy?: { __typename?: 'User', id: string, fullname?: string | null }, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null } | { __typename?: 'Ticket', id: string, internalId: number, title: string, number: string, updatedAt: string, state: { __typename?: 'TicketState', name: string }, priority?: { __typename?: 'TicketPriority', name: string, defaultCreate: boolean, uiColor?: string | null }, customer: { __typename?: 'User', id: string, fullname?: string | null }, updatedBy?: { __typename?: 'User', id: string, fullname?: string | null } } | { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, image?: string | null, updatedAt: string, organization?: { __typename?: 'Organization', id: string, name?: string | null } | null, updatedBy?: { __typename?: 'User', id: string, fullname?: string | null }, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null }> }; +export type SearchQuery = { __typename?: 'Queries', search: Array<{ __typename?: 'Organization', id: string, internalId: number, active?: boolean | null, name?: string | null, updatedAt: string, members?: { __typename?: 'UserConnection', totalCount: number, edges: Array<{ __typename?: 'UserEdge', node: { __typename?: 'User', id: string, fullname?: string | null } }> } | null, updatedBy?: { __typename?: 'User', id: string, fullname?: string | null }, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null } | { __typename?: 'Ticket', id: string, internalId: number, title: string, number: string, updatedAt: string, state: { __typename?: 'TicketState', name: string }, priority?: { __typename?: 'TicketPriority', name: string, defaultCreate: boolean, uiColor?: string | null }, customer: { __typename?: 'User', id: string, internalId: number, fullname?: string | null }, updatedBy?: { __typename?: 'User', id: string, fullname?: string | null } } | { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, image?: string | null, updatedAt: string, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null } | null, updatedBy?: { __typename?: 'User', id: string, fullname?: string | null }, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null }> }; export type TicketArticleAttributesFragment = { __typename?: 'TicketArticle', id: string, internalId: number, subject?: string | null, messageId?: string | null, messageIdMd5?: string | null, inReplyTo?: string | null, contentType: string, references?: string | null, preferences?: any | null, body: string, internal: boolean, createdAt: string, from?: { __typename?: 'AddressesField', raw: string, parsed?: Array<{ __typename?: 'EmailAddress', name?: string | null, emailAddress?: string | null }> | null } | null, to?: { __typename?: 'AddressesField', raw: string, parsed?: Array<{ __typename?: 'EmailAddress', name?: string | null, emailAddress?: string | null }> | null } | null, cc?: { __typename?: 'AddressesField', raw: string, parsed?: Array<{ __typename?: 'EmailAddress', name?: string | null, emailAddress?: string | null }> | null } | null, replyTo?: { __typename?: 'AddressesField', raw: string, parsed?: Array<{ __typename?: 'EmailAddress', name?: string | null, emailAddress?: string | null }> | null } | null, attachments: Array<{ __typename?: 'StoredFile', internalId: number, name: string, size?: number | null, type?: string | null, preferences?: any | null }>, createdBy?: { __typename?: 'User', id: string, fullname?: string | null, firstname?: string | null, lastname?: string | null }, type?: { __typename?: 'TicketArticleType', name?: string | null } | null, sender?: { __typename?: 'TicketArticleSender', name?: string | null } | null }; @@ -1963,7 +1963,7 @@ export type UserAddMutationVariables = Exact<{ }>; -export type UserAddMutation = { __typename?: 'Mutations', userAdd?: { __typename?: 'UserAddPayload', user?: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null } | null, errors?: Array<{ __typename?: 'UserError', message: string, field?: string | null }> | null } | null }; +export type UserAddMutation = { __typename?: 'Mutations', userAdd?: { __typename?: 'UserAddPayload', user?: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, hasSecondaryOrganizations?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null } | null, errors?: Array<{ __typename?: 'UserError', message: string, field?: string | null }> | null } | null }; export type UserUpdateMutationVariables = Exact<{ id: Scalars['ID']; @@ -1971,14 +1971,14 @@ export type UserUpdateMutationVariables = Exact<{ }>; -export type UserUpdateMutation = { __typename?: 'Mutations', userUpdate?: { __typename?: 'UserUpdatePayload', user?: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null } | null, errors?: Array<{ __typename?: 'UserError', message: string, field?: string | null }> | null } | null }; +export type UserUpdateMutation = { __typename?: 'Mutations', userUpdate?: { __typename?: 'UserUpdatePayload', user?: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, hasSecondaryOrganizations?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null } | null, errors?: Array<{ __typename?: 'UserError', message: string, field?: string | null }> | null } | null }; export type AutocompleteSearchUserQueryVariables = Exact<{ input: AutocompleteSearchInput; }>; -export type AutocompleteSearchUserQuery = { __typename?: 'Queries', autocompleteSearchUser: Array<{ __typename?: 'AutocompleteUserEntry', value: string, label: string, labelPlaceholder?: Array | null, heading?: string | null, headingPlaceholder?: Array | null, disabled?: boolean | null, icon?: string | null, user: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null } }> }; +export type AutocompleteSearchUserQuery = { __typename?: 'Queries', autocompleteSearchUser: Array<{ __typename?: 'AutocompleteUserEntry', value: string, label: string, labelPlaceholder?: Array | null, heading?: string | null, headingPlaceholder?: Array | null, disabled?: boolean | null, icon?: string | null, user: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, hasSecondaryOrganizations?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null } }> }; export type KnowledgeBaseAnswerSuggestionContentTransformMutationVariables = Exact<{ translationId: Scalars['ID']; @@ -2108,7 +2108,7 @@ export type TicketOverviewsQueryVariables = Exact<{ export type TicketOverviewsQuery = { __typename?: 'Queries', ticketOverviews: { __typename?: 'OverviewConnection', edges: Array<{ __typename?: 'OverviewEdge', cursor: string, node: { __typename?: 'Overview', id: string, name: string, link: string, prio: number, orderBy: string, orderDirection: EnumOrderDirection, active: boolean, ticketCount?: number, viewColumns: Array<{ __typename?: 'KeyValue', key: string, value?: string | null }>, orderColumns: Array<{ __typename?: 'KeyValue', key: string, value?: string | null }> } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean } } }; -export type CurrentUserAttributesFragment = { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, permissions?: { __typename?: 'UserPermission', names: Array } | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null }; +export type CurrentUserAttributesFragment = { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, hasSecondaryOrganizations?: boolean | null, permissions?: { __typename?: 'UserPermission', names: Array } | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null }; export type ErrorsFragment = { __typename?: 'UserError', message: string, field?: string | null }; @@ -2116,9 +2116,9 @@ export type ObjectAttributeValuesFragment = { __typename?: 'ObjectAttributeValue export type PublicLinkAttributesFragment = { __typename?: 'PublicLink', id: string, link: string, title: string, description?: string | null, newTab: boolean }; -export type UserAttributesFragment = { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null }; +export type UserAttributesFragment = { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, hasSecondaryOrganizations?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null }; -export type UserDetailAttributesFragment = { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, email?: string | null, web?: string | null, vip?: boolean | null, phone?: string | null, mobile?: string | null, fax?: string | null, note?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null } | null, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null }; +export type UserDetailAttributesFragment = { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, email?: string | null, web?: string | null, vip?: boolean | null, phone?: string | null, mobile?: string | null, fax?: string | null, note?: string | null, active?: boolean | null, hasSecondaryOrganizations?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null } | null, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null }; export type LoginMutationVariables = Exact<{ input: LoginInput; @@ -2150,7 +2150,7 @@ export type ApplicationConfigQuery = { __typename?: 'Queries', applicationConfig export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type CurrentUserQuery = { __typename?: 'Queries', currentUser: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, permissions?: { __typename?: 'UserPermission', names: Array } | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null } }; +export type CurrentUserQuery = { __typename?: 'Queries', currentUser: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, hasSecondaryOrganizations?: boolean | null, permissions?: { __typename?: 'UserPermission', names: Array } | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null } }; export type LocalesQueryVariables = Exact<{ onlyActive?: InputMaybe; @@ -2187,7 +2187,7 @@ export type CurrentUserUpdatesSubscriptionVariables = Exact<{ }>; -export type CurrentUserUpdatesSubscription = { __typename?: 'Subscriptions', userUpdates: { __typename?: 'UserUpdatesPayload', user?: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, permissions?: { __typename?: 'UserPermission', names: Array } | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null } | null } }; +export type CurrentUserUpdatesSubscription = { __typename?: 'Subscriptions', userUpdates: { __typename?: 'UserUpdatesPayload', user?: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, preferences?: any | null, hasSecondaryOrganizations?: boolean | null, permissions?: { __typename?: 'UserPermission', names: Array } | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null } | null } | null } }; export type PushMessagesSubscriptionVariables = Exact<{ [key: string]: never; }>; @@ -2199,4 +2199,4 @@ export type UserUpdatesSubscriptionVariables = Exact<{ }>; -export type UserUpdatesSubscription = { __typename?: 'Subscriptions', userUpdates: { __typename?: 'UserUpdatesPayload', user?: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, email?: string | null, web?: string | null, vip?: boolean | null, phone?: string | null, mobile?: string | null, fax?: string | null, note?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null } | null, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null } | null } }; +export type UserUpdatesSubscription = { __typename?: 'Subscriptions', userUpdates: { __typename?: 'UserUpdatesPayload', user?: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, email?: string | null, web?: string | null, vip?: boolean | null, phone?: string | null, mobile?: string | null, fax?: string | null, note?: string | null, active?: boolean | null, hasSecondaryOrganizations?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string, dataType: string, dataOption?: any | null } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null } | null, ticketsCount?: { __typename?: 'TicketCount', open: number, closed: number } | null } | null } }; diff --git a/app/frontend/shared/graphql/utils.ts b/app/frontend/shared/graphql/utils.ts index ac9d4fd189cf..53275f09c0b0 100644 --- a/app/frontend/shared/graphql/utils.ts +++ b/app/frontend/shared/graphql/utils.ts @@ -8,6 +8,13 @@ export const convertToGraphQLId = (type: string, id: number | string) => { return `gid://zammad/${type}/${id}`; } +const parseGraphqlId = (graphqlId: string) => parseInt(`${graphqlId}`.replace(/gid:\/\/zammad\/.*\//g, ''), 10); + +export const getIdFromGraphQLId = (graphqlId = '') => { + const parsedGraphqlId = parseGraphqlId(graphqlId); + return Number.isInteger(parsedGraphqlId) ? parsedGraphqlId : null; +} + export const ensureGraphqlId = (type: string, id: number | string): string => { if (isGraphQLId(id)) { return id; diff --git a/app/frontend/shared/stores/__tests__/session.spec.ts b/app/frontend/shared/stores/__tests__/session.spec.ts index fb5873a91eb6..a58cff5cc70f 100644 --- a/app/frontend/shared/stores/__tests__/session.spec.ts +++ b/app/frontend/shared/stores/__tests__/session.spec.ts @@ -24,6 +24,7 @@ const userData = { organization: { __typename: 'Organization', id: '234241', + internalId: 1, name: 'Zammad Foundation', objectAttributeValues: [], active: true, @@ -32,6 +33,7 @@ const userData = { __typename: 'Permission', names: ['admin'], }, + hasSecondaryOrganizations: false, } describe('Session Store', () => { diff --git a/app/graphql/gql/types/user_type.rb b/app/graphql/gql/types/user_type.rb index 31b6b0ddb580..4885e893563f 100644 --- a/app/graphql/gql/types/user_type.rb +++ b/app/graphql/gql/types/user_type.rb @@ -22,7 +22,7 @@ def self.nested_access_pundit_method belongs_to :organization, Gql::Types::OrganizationType field :secondary_organizations, Gql::Types::OrganizationType.connection_type - field :has_secondary_organizations, Boolean, method: 'secondary_organizations?' + field :has_secondary_organizations, Boolean, resolver_method: :secondary_organizations? field :firstname, String field :lastname, String diff --git a/spec/graphql/gql/queries/user_spec.rb b/spec/graphql/gql/queries/user_spec.rb index 3d35d043483a..b793c7be9918 100644 --- a/spec/graphql/gql/queries/user_spec.rb +++ b/spec/graphql/gql/queries/user_spec.rb @@ -14,6 +14,7 @@ user(user: { userId: $userId, userInternalId: $userInternalId }) { id firstname + hasSecondaryOrganizations } } QUERY @@ -42,6 +43,21 @@ it 'has data' do expect(gql.result.data).to include('firstname' => user.firstname) end + + it 'has no secondary organizations' do + expect(gql.result.data).to include('hasSecondaryOrganizations' => false) + end + end + + context 'with secondardy organizations' do + let(:organization) { create(:organization) } + let(:secondary_organizations) { create_list(:organization, 2) } + let(:user) { create(:user, organization_id: organization.id, organization_ids: secondary_organizations.map(&:id)) } + let(:authenticated) { user } + + it 'has secondary organizations' do + expect(gql.result.data).to include('hasSecondaryOrganizations' => true) + end end context 'with customer' do