Skip to content

Commit

Permalink
Fixes: Mobile - Fixed problems with customer and organization field i…
Browse files Browse the repository at this point in the history
…n the ticket create.
  • Loading branch information
dominikklein committed Nov 28, 2022
1 parent 456c482 commit c4de870
Show file tree
Hide file tree
Showing 25 changed files with 171 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const defaultUser = (): ConfidentTake<UserQuery, 'user'> => {
],
totalCount: 1,
},
hasSecondaryOrganizations: true,
objectAttributeValues: [
{
attribute: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const SearchDocument = gql`
}
customer {
id
internalId
fullname
}
updatedAt
Expand All @@ -39,6 +40,7 @@ export const SearchDocument = gql`
image
organization {
id
internalId
name
}
updatedAt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ query search(
}
customer {
id
internalId
fullname
}
updatedAt
Expand All @@ -35,6 +36,7 @@ query search(
image
organization {
id
internalId
name
}
updatedAt
Expand Down
4 changes: 2 additions & 2 deletions app/frontend/apps/mobile/pages/ticket/views/TicketCreate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions app/frontend/shared/components/Form/Form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@ const executeFormHandler = (
formNode.value,
currentValues,
props.changeFields,
updateSchemaDataField,
schemaData,
changedField,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand All @@ -37,7 +39,7 @@ const emit = defineEmits<{

const { sortedOptions, selectOption } = useSelectOptions(
toRef(props, 'options'),
toRef(props, 'context'),
contextReactive,
)

let areLocalOptionsReplaced = false
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -266,7 +272,7 @@ useTraverseOptions(autocompleteList)
role="listbox"
>
<div
v-for="(option, index) in filter
v-for="(option, index) in filter || context.defaultFilter
? sortedAutocompleteOptions
: sortedOptions"
:key="String(option.value)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import FieldAutoCompleteInput from './FieldAutoCompleteInput.vue'
export const autoCompleteProps = [
'action',
'actionIcon',
'additionalQueryParams',
'allowUnknownValues',
'clearable',
'debounceInterval',
'defaultFilter',
'filterInputPlaceholder',
'filterInputValidation',
'limit',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type AutoCompleteProps = FormFieldContext<{
clearable?: boolean
debounceInterval: number
disabled?: boolean
defaultFilter?: string
filterInputPlaceholder?: string
filterInputValidation?: string
limit?: number
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Object.assign(props.context, {
value: SelectValue,
context: Props['context'],
) => {
if (!context.belongsToObjectField) return null
if (!context.belongsToObjectField || !initialEntityObject) return null

const belongsToObject = initialEntityObject[context.belongsToObjectField]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ const props = defineProps<Props>()

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
Expand All @@ -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,
})
</script>
Expand Down
3 changes: 3 additions & 0 deletions app/frontend/shared/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ export type FormHandlerFunction = (
formNode: FormKitNode | undefined,
values: FormValues,
changeFields: Record<string, Partial<FormSchemaField>>,
updateSchemaDataField: (
field: FormSchemaField | SetRequired<Partial<FormSchemaField>, 'name'>,
) => void,
schemaData: ReactiveFormSchemData,
changedField?: ChangedField,
) => void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const useObjectAttributeFormData = (
const internalObjectAttributeValues: Record<string, FormFieldValue> = {}
const additionalObjectAttributeValues: ObjectAttributeValueInput[] = []

const fullRelationID = (relation: string, value: number) => {
const fullRelationID = (relation: string, value: number | string) => {
return convertToGraphQLId(toClassName(relation), value)
}

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Organization>) => {
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,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -59,25 +72,36 @@ export const useTicketFormOganizationHandling = (): FormHandler => {
}

const setOrganizationField = (
customerId: Scalars['ID'],
organization?: Maybe<Partial<Organization>>,
) => {
if (!organization) return

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,
Expand Down
41 changes: 41 additions & 0 deletions app/frontend/shared/graphql/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
Loading

0 comments on commit c4de870

Please sign in to comment.