Skip to content

Commit

Permalink
Maintenance: Add support for excepting users from autocomplete search…
Browse files Browse the repository at this point in the history
… results.
  • Loading branch information
dvuckovic committed Apr 19, 2024
1 parent 6ed5c8a commit 629f0e1
Show file tree
Hide file tree
Showing 18 changed files with 277 additions and 25 deletions.
Expand Up @@ -16,6 +16,7 @@ interface Props {
context: FormFieldContext<
AutoCompleteProps & {
options?: AutoCompleteAgentOption[]
exceptUserInternalId?: number
}
>
}
Expand Down Expand Up @@ -47,6 +48,9 @@ Object.assign(props.context, {
return buildEntityOption(belongsToObject)
},
gqlQuery: AutocompleteSearchAgentDocument,
additionalQueryParams: {
exceptInternalId: props.context.exceptUserInternalId,
},
})
</script>

Expand Down
Expand Up @@ -278,4 +278,33 @@ describe('Form - Field - Agent - Query', () => {

expect(wrapper.getByIconName('check2')).toBeInTheDocument()
})

it('supports filtering out specific user', async () => {
const wrapper = renderComponent(FormKit, {
...wrapperParameters,
props: {
...testProps,
debounceInterval: 0,
exceptUserInternalId: 999,
},
})

await wrapper.events.click(await wrapper.findByLabelText('Select…'))

const filterElement = wrapper.getByRole('searchbox')

mockAutocompleteSearchAgentQuery({
autocompleteSearchAgent: [...testOptions.slice(0, 1)],
})

await wrapper.events.type(filterElement, '*')

const calls = await waitForAutocompleteSearchAgentQueryCalls()

expect(calls.at(-1)?.variables).toEqual({
input: expect.objectContaining({
exceptInternalId: 999,
}),
})
})
})
Expand Up @@ -6,9 +6,13 @@ import formUpdaterTrigger from '#shared/form/features/formUpdaterTrigger.ts'
import FieldAgentWrapper from './FieldAgentWrapper.vue'
import { autoCompleteProps } from '../FieldAutoComplete/index.ts'

const fieldDefinition = createInput(FieldAgentWrapper, autoCompleteProps, {
features: [addLink, formUpdaterTrigger()],
})
const fieldDefinition = createInput(
FieldAgentWrapper,
[...autoCompleteProps, 'exceptUserInternalId'],
{
features: [addLink, formUpdaterTrigger()],
},
)

export default {
fieldType: 'agent',
Expand Down
Expand Up @@ -481,6 +481,35 @@ describe('Form - Field - AutoComplete - Query', () => {
},
])
})

it('supports passing additional query parameters', async () => {
const wrapper = renderComponent(FormKit, {
...wrapperParameters,
props: {
...testProps,
debounceInterval: 0,
additionalQueryParams: {
limit: 2,
},
},
})

await wrapper.events.click(wrapper.getByLabelText('Select…'))

mockAutocompleteSearchUserQuery({
autocompleteSearchUser: testOptions,
})

await wrapper.events.type(wrapper.getByRole('searchbox'), '*')

const calls = await waitForAutocompleteSearchUserQueryCalls()

expect(calls.at(-1)?.variables).toEqual({
input: expect.objectContaining({
limit: 2,
}),
})
})
})

describe('Form - Field - AutoComplete - Initial Options', () => {
Expand Down
Expand Up @@ -43,6 +43,8 @@ describe('Out of Office page', () => {
describe('when enabled', () => {
beforeEach(() => {
mockAccount({
id: '123',
internalId: 1,
firstname: 'John',
lastname: 'Doe',
outOfOffice: true,
Expand Down Expand Up @@ -172,8 +174,6 @@ describe('Out of Office page', () => {
it('cannot set replacement agent to blank', async () => {
const view = await visitView('/personal-setting/out-of-office')

await vi.dynamicImportSettled()

const input = view.getByLabelText('Replacement agent')
const button = getByRole(input, 'button')
await view.events.click(button)
Expand All @@ -183,6 +183,30 @@ describe('Out of Office page', () => {
expect(input).toBeDescribedBy('This field is required.')
})

it('cannot set replacement agent to themselves', async () => {
const view = await visitView('/personal-setting/out-of-office')

const inputAgent = view.getByLabelText('Replacement agent')

await view.events.click(inputAgent)

const filterElement = getByRole(inputAgent, 'searchbox')

mockAutocompleteSearchAgentQuery({
autocompleteSearchAgent: agentAutocompleteOptions,
})

await view.events.type(filterElement, '*')

const calls = await waitForAutocompleteSearchAgentQueryCalls()

expect(calls.at(-1)?.variables).toEqual({
input: expect.objectContaining({
exceptInternalId: 1,
}),
})
})

it('can disable Out of Office', async () => {
const view = await visitView('/personal-setting/out-of-office')

Expand All @@ -204,7 +228,6 @@ describe('Out of Office page', () => {

it('can disable Out of Office and unset settings', async () => {
const view = await visitView('/personal-setting/out-of-office')
await vi.dynamicImportSettled()

const inputActivated = view.getByLabelText('Active')
await view.events.click(inputActivated)
Expand Down Expand Up @@ -252,7 +275,6 @@ describe('Out of Office page', () => {

it('loads current Out of Office settings', async () => {
const view = await visitView('/personal-setting/out-of-office')
await vi.dynamicImportSettled()

expect(view.getByLabelText('Reason for absence')).toHaveValue('')
expect(view.getByLabelText('Start and end date')).toHaveValue('')
Expand Down Expand Up @@ -283,7 +305,6 @@ describe('Out of Office page', () => {

it('can set replacement agent', async () => {
const view = await visitView('/personal-setting/out-of-office')
await vi.dynamicImportSettled()

const inputAgent = view.getByLabelText('Replacement agent')

Expand Down
Expand Up @@ -59,6 +59,7 @@ const schema = defineFormSchema([
props: {
clearable: true,
belongsToObjectField: 'outOfOfficeReplacement',
exceptUserInternalId: user.value?.internalId,
},
},
{
Expand Down
Expand Up @@ -7,7 +7,7 @@ import * as VueCompositionApi from 'vue';
export type ReactiveFunction<TParam> = () => TParam;

export const AutocompleteSearchAgentDocument = gql`
query autocompleteSearchAgent($input: AutocompleteSearchInput!) {
query autocompleteSearchAgent($input: AutocompleteSearchUserInput!) {
autocompleteSearchAgent(input: $input) {
value
label
Expand Down
@@ -1,4 +1,4 @@
query autocompleteSearchAgent($input: AutocompleteSearchInput!) {
query autocompleteSearchAgent($input: AutocompleteSearchUserInput!) {
autocompleteSearchAgent(input: $input) {
value
label
Expand Down
Expand Up @@ -7,7 +7,7 @@ import * as VueCompositionApi from 'vue';
export type ReactiveFunction<TParam> = () => TParam;

export const AutocompleteSearchUserDocument = gql`
query autocompleteSearchUser($input: AutocompleteSearchInput!) {
query autocompleteSearchUser($input: AutocompleteSearchUserInput!) {
autocompleteSearchUser(input: $input) {
value
label
Expand Down
@@ -1,4 +1,4 @@
query autocompleteSearchUser($input: AutocompleteSearchInput!) {
query autocompleteSearchUser($input: AutocompleteSearchUserInput!) {
autocompleteSearchUser(input: $input) {
value
label
Expand Down
20 changes: 16 additions & 4 deletions app/frontend/shared/graphql/types.ts
Expand Up @@ -249,6 +249,8 @@ export type AutocompleteSearchRecipientEntry = {
export type AutocompleteSearchRecipientInput = {
/** User contact type option, i.e. email or phone */
contact?: InputMaybe<EnumUserContact>;
/** Optional user ID to be filtered out from results */
exceptInternalId?: InputMaybe<Scalars['Int']['input']>;
/** Limit for the amount of entries */
limit?: InputMaybe<Scalars['Int']['input']>;
/** Query from the autocomplete field */
Expand All @@ -268,6 +270,16 @@ export type AutocompleteSearchUserEntry = {
value: Scalars['Int']['output'];
};

/** The default fields for user autocomplete searches. */
export type AutocompleteSearchUserInput = {
/** Optional user ID to be filtered out from results */
exceptInternalId?: InputMaybe<Scalars['Int']['input']>;
/** Limit for the amount of entries */
limit?: InputMaybe<Scalars['Int']['input']>;
/** Query from the autocomplete field */
query: Scalars['String']['input'];
};

/** Avatar for users */
export type Avatar = {
__typename?: 'Avatar';
Expand Down Expand Up @@ -2020,7 +2032,7 @@ export type Queries = {

/** All available queries */
export type QueriesAutocompleteSearchAgentArgs = {
input: AutocompleteSearchInput;
input: AutocompleteSearchUserInput;
};


Expand Down Expand Up @@ -2056,7 +2068,7 @@ export type QueriesAutocompleteSearchTagArgs = {

/** All available queries */
export type QueriesAutocompleteSearchUserArgs = {
input: AutocompleteSearchInput;
input: AutocompleteSearchUserInput;
};


Expand Down Expand Up @@ -3754,14 +3766,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, outOfOffice?: boolean | null, outOfOfficeStartAt?: string | null, outOfOfficeEndAt?: string | null, preferences?: any | null, hasSecondaryOrganizations?: boolean | null, outOfOfficeReplacement?: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, login?: string | null, phone?: string | null, email?: string | null } | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, renderedLink?: string | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, renderedLink?: string | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string } }> | null } | null } | null, errors?: Array<{ __typename?: 'UserError', message: string, field?: string | null }> | null } | null };

export type AutocompleteSearchAgentQueryVariables = Exact<{
input: AutocompleteSearchInput;
input: AutocompleteSearchUserInput;
}>;


export type AutocompleteSearchAgentQuery = { __typename?: 'Queries', autocompleteSearchAgent: Array<{ __typename?: 'AutocompleteSearchUserEntry', value: number, label: string, labelPlaceholder?: Array<string> | null, heading?: string | null, headingPlaceholder?: Array<string> | null, disabled?: boolean | null, icon?: string | null, user: { __typename?: 'User', vip?: boolean | null, outOfOffice?: boolean | null, active?: boolean | null, id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, image?: string | null, outOfOfficeStartAt?: string | null, outOfOfficeEndAt?: string | null, preferences?: any | null, hasSecondaryOrganizations?: boolean | null, outOfOfficeReplacement?: { __typename?: 'User', id: string, internalId: number, firstname?: string | null, lastname?: string | null, fullname?: string | null, login?: string | null, phone?: string | null, email?: string | null } | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, renderedLink?: string | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string } }> | null, organization?: { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null, objectAttributeValues?: Array<{ __typename?: 'ObjectAttributeValue', value?: any | null, renderedLink?: string | null, attribute: { __typename?: 'ObjectManagerFrontendAttribute', name: string, display: string } }> | null } | null } }> };

export type AutocompleteSearchUserQueryVariables = Exact<{
input: AutocompleteSearchInput;
input: AutocompleteSearchUserInput;
}>;


Expand Down
12 changes: 10 additions & 2 deletions app/graphql/gql/queries/autocomplete_search/user.rb
Expand Up @@ -5,7 +5,7 @@ class AutocompleteSearch::User < BaseQuery

description 'Search for users'

argument :input, Gql::Types::Input::AutocompleteSearch::InputType, required: true, description: 'The input object for the autocomplete search'
argument :input, Gql::Types::Input::AutocompleteSearch::UserInputType, required: true, description: 'The input object for the autocomplete search'

type [Gql::Types::AutocompleteSearch::UserEntryType], null: false

Expand All @@ -16,7 +16,9 @@ def resolve(input:)

return [] if query.strip.empty?

post_process(find_users(query:, limit:), input: input)
users = find_users(query:, limit:)
users = reject_user(users, input:)
post_process(users, input:)
end

def find_users(query:, limit:)
Expand All @@ -27,6 +29,12 @@ def find_users(query:, limit:)
)
end

def reject_user(results, input:)
return results if input[:except_internal_id].blank?

results.reject { |user| user.id == input[:except_internal_id] }
end

def post_process(results, input:)
results.map { |user| coerce_to_result(user) }
end
Expand Down
@@ -1,7 +1,7 @@
# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/

module Gql::Types::Input::AutocompleteSearch
class RecipientInputType < InputType
class RecipientInputType < UserInputType

description 'The default fields for recipient autocomplete searches.'

Expand Down
10 changes: 10 additions & 0 deletions app/graphql/gql/types/input/autocomplete_search/user_input_type.rb
@@ -0,0 +1,10 @@
# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/

module Gql::Types::Input::AutocompleteSearch
class UserInputType < InputType

description 'The default fields for user autocomplete searches.'

argument :except_internal_id, Integer, required: false, description: 'Optional user ID to be filtered out from results'
end
end

0 comments on commit 629f0e1

Please sign in to comment.