diff --git a/CHANGELOG.md b/CHANGELOG.md index c609e53..6263c1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Fixed + +- Implement cost center autocomplete, lazyload in organization autocomplete and maxHeight prop + ## [0.3.2] - 2024-08-22 ### Fixed - shows more options in the cost centers dropdown + ### Removed - [ENGINEERS-1247] - Disable cypress tests in PR level diff --git a/react/components/CostCentersAutocomplete.tsx b/react/components/CostCentersAutocomplete.tsx new file mode 100644 index 0000000..bdcbe66 --- /dev/null +++ b/react/components/CostCentersAutocomplete.tsx @@ -0,0 +1,90 @@ +import React, { useEffect, useState } from 'react' +import { useQuery } from 'react-apollo' +import { AutocompleteInput } from 'vtex.styleguide' +import { useIntl } from 'react-intl' + +import { messages } from './customers-admin' +import GET_COST_CENTER_BY_ORG from '../queries/costCentersByOrg.gql' +import { SEARCH_TERM_DELAY_MS } from '../constants/debounceDelay' + +interface Props { + onChange: (value: { value: string | null; label: string }) => void + organizationId?: string +} + +const initialState = { + search: '', + page: 1, + pageSize: 25, + sortOrder: 'ASC', + sortedBy: 'name', +} + +const CostCenterAutocomplete = ({ onChange, organizationId }: Props) => { + const { formatMessage } = useIntl() + const [costCenterTextInput, setCostCenterTextInput] = useState('') + const [debouncedSearchTerm, setDebouncedSearchTerm] = + useState(costCenterTextInput) + + const { data, loading, refetch } = useQuery(GET_COST_CENTER_BY_ORG, { + variables: { + ...initialState, + id: organizationId, + }, + ssr: false, + notifyOnNetworkStatusChange: true, + skip: !organizationId, + }) + + const onClear = () => { + setCostCenterTextInput('') + onChange({ value: null, label: '' }) + } + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedSearchTerm(costCenterTextInput) + }, SEARCH_TERM_DELAY_MS) // 500ms delay + + return () => { + clearTimeout(handler) + } + }, [costCenterTextInput]) + + useEffect(() => { + if (debouncedSearchTerm) { + refetch({ + ...initialState, + id: organizationId, + search: debouncedSearchTerm, + }) + } else if (debouncedSearchTerm === '') { + onClear() + } + }, [debouncedSearchTerm]) + + const options = { + maxHeight: 200, + onSelect: onChange, + loading, + value: data?.getCostCentersByOrganizationId?.data?.map( + (costCenter: { id: string; name: string }) => ({ + value: costCenter.id, + label: costCenter.name, + }) + ), + } + + const input = { + onChange: (_term: string) => { + setCostCenterTextInput(_term) + }, + onClear, + placeholder: formatMessage(messages.costCenter), + value: costCenterTextInput, + } + + return +} + +export default CostCenterAutocomplete diff --git a/react/components/OrganizationsAutocomplete.tsx b/react/components/OrganizationsAutocomplete.tsx index 7359d64..9ae4489 100644 --- a/react/components/OrganizationsAutocomplete.tsx +++ b/react/components/OrganizationsAutocomplete.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useMemo, useCallback } from 'react' import { useQuery } from 'react-apollo' import { AutocompleteInput } from 'vtex.styleguide' import { useIntl } from 'react-intl' @@ -6,6 +6,7 @@ import { useIntl } from 'react-intl' import { messages } from './customers-admin' import GET_ORGANIZATIONS from '../queries/listOrganizations.gql' import GET_ORGANIZATION_BY_ID from '../queries/getOrganization.graphql' +import { SEARCH_TERM_DELAY_MS } from '../constants/debounceDelay' const initialState = { status: ['active', 'on-hold', 'inactive'], @@ -24,62 +25,97 @@ interface Props { const OrganizationsAutocomplete = ({ onChange, organizationId }: Props) => { const { formatMessage } = useIntl() const [term, setTerm] = useState('') - const [hasChanged, setHasChanged] = useState(false) - const [values, setValues] = useState([] as any) + const [debouncedTerm, setDebouncedTerm] = useState(term) + + const [values, setValues] = useState>( + [] + ) + const { data, loading, refetch } = useQuery(GET_ORGANIZATIONS, { variables: initialState, ssr: false, notifyOnNetworkStatusChange: true, }) - const { data: organization } = useQuery(GET_ORGANIZATION_BY_ID, { - variables: { id: organizationId }, - ssr: false, - fetchPolicy: 'network-only', - notifyOnNetworkStatusChange: true, - skip: !organizationId, - }) + const { data: organization, loading: orgLoading } = useQuery( + GET_ORGANIZATION_BY_ID, + { + variables: { id: organizationId }, + ssr: false, + fetchPolicy: 'network-only', + notifyOnNetworkStatusChange: true, + skip: !organizationId, + } + ) - const options = { - onSelect: (value: any) => onChange(value), - loading, - value: values, - } + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedTerm(term) + }, SEARCH_TERM_DELAY_MS) // 500ms delay - const onClear = () => { - if (!hasChanged) return - setTerm('') - onChange({ value: null, label: '' }) - } + return () => clearTimeout(handler) + }, [term]) + + useEffect(() => { + if (debouncedTerm.length > 2) { + refetch({ + ...initialState, + search: debouncedTerm, + }) + } else if (debouncedTerm === '') { + refetch({ + ...initialState, + search: '', + }) + } + }, [debouncedTerm, refetch]) useEffect(() => { - if (!organization) { + if (!organization?.getOrganizationById) { return } const { name, id } = organization.getOrganizationById setTerm(name) - setHasChanged(true) onChange({ value: id, label: name }) - }, [organization]) + }, [organization, onChange]) useEffect(() => { if (data?.getOrganizations?.data) { setValues( - data.getOrganizations.data.map((item: any) => { - return { + data.getOrganizations.data.map( + (item: { id: string; name: string }) => ({ value: item.id, label: item.name, - } - }) + }) + ) ) } }, [data]) + const onClear = useCallback(() => { + setTerm('') + + refetch({ + ...initialState, + search: '', + }) + onChange({ value: null, label: '' }) + }, [onChange, refetch]) + + const options = useMemo( + () => ({ + maxHeight: 250, + onSelect: onChange, + loading, + value: values, + }), + [loading, values, onChange, orgLoading] + ) + useEffect(() => { if (term && term.length > 1) { - setHasChanged(true) refetch({ ...initialState, search: term, @@ -89,14 +125,15 @@ const OrganizationsAutocomplete = ({ onChange, organizationId }: Props) => { } }, [term]) - const input = { - onChange: (_term: string) => { - setTerm(_term) - }, - onClear, - placeholder: formatMessage(messages.searchOrganizations), - value: term, - } + const input = useMemo( + () => ({ + onChange: (_term: string) => setTerm(_term), + onClear, + placeholder: formatMessage(messages.searchOrganizations), + value: term, + }), + [term, onClear, formatMessage] + ) return } diff --git a/react/components/customers-admin.tsx b/react/components/customers-admin.tsx index b032451..0971c80 100644 --- a/react/components/customers-admin.tsx +++ b/react/components/customers-admin.tsx @@ -20,6 +20,7 @@ import GET_COST from '../queries/costCentersByOrg.gql' import GET_ORGANIZATIONS from '../queries/getOrganizationsByEmail.graphql' import ADD_USER from '../mutations/addUser.gql' import DELETE_USER from '../mutations/deleteUser.gql' +import CostCenterAutocomplete from './CostCentersAutocomplete' export const messages = defineMessages({ b2bInfo: { @@ -455,15 +456,22 @@ const UserEdit: FC = (props: any) => { )} {state.orgId && ( - - { - setState({ ...state, costId }) - }} - /> + + + + + + {formatMessage(messages.costCenter)} + + { + setState({ ...state, costId: event.value }) + }} + /> + + + )} diff --git a/react/constants/debounceDelay.ts b/react/constants/debounceDelay.ts new file mode 100644 index 0000000..029db33 --- /dev/null +++ b/react/constants/debounceDelay.ts @@ -0,0 +1 @@ +export const SEARCH_TERM_DELAY_MS = 500 diff --git a/react/package.json b/react/package.json index 24ffd6a..819fec0 100644 --- a/react/package.json +++ b/react/package.json @@ -18,7 +18,7 @@ "vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.132.4/public/@types/vtex.render-runtime", "vtex.storefront-permissions": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.storefront-permissions@1.23.0/public/@types/vtex.storefront-permissions", "vtex.storefront-permissions-components": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.storefront-permissions-components@0.2.0/public/@types/vtex.storefront-permissions-components", - "vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.3/public/@types/vtex.styleguide" + "vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.13/public/@types/vtex.styleguide" }, "dependencies": { "faker": "^4.1.0", diff --git a/react/yarn.lock b/react/yarn.lock index 2b20dac..e0c8167 100644 --- a/react/yarn.lock +++ b/react/yarn.lock @@ -500,9 +500,9 @@ typescript@3.9.7: version "1.23.0" resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.storefront-permissions@1.23.0/public/@types/vtex.storefront-permissions#2a12e48a1b630d6d9cd64115572bcae55a7aa756" -"vtex.styleguide@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.3/public/@types/vtex.styleguide": - version "9.146.3" - resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.3/public/@types/vtex.styleguide#05558160f29cd8f4aefe419844a4bd66e2b3fdbb" +"vtex.styleguide@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.13/public/@types/vtex.styleguide": + version "9.146.13" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.146.13/public/@types/vtex.styleguide#f4ccbc54621bf5114ddd115b6032ae320f2eba55" zen-observable-ts@^0.8.21: version "0.8.21"