-
Notifications
You must be signed in to change notification settings - Fork 70
feat(cc-widgets): added-address-book #537
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
ed4d2df
feat(cc-wudgets): added-address-book
adhmenon 976eec0
feat(cc-wudgets): added-snaps
adhmenon 0422294
feat(cc-wudgets): adjusted-queues-method
adhmenon 7f17031
feat(cc-wudgets): removed-whitespaces
adhmenon 5c16926
feat(cc-wudgets): added-consult-transfer-props
adhmenon 975000a
feat(cc-wudgets): fixed-styles
adhmenon 92a1f83
feat(cc-widgets): added-basic-playwright-for-address-book
adhmenon 87ea791
feat(cc-widgets): resolved-comments
adhmenon 7f3a283
feat(cc-widgets): moved-hooks
adhmenon 4f1a670
Merge pull request #1 from adhmenon/address-book-playwright
adhmenon 63b12ac
feat(cc-widgets): fixed-styling
adhmenon da59673
feat(cc-widgets): resolved-comments
adhmenon cf5c6a6
Merge branch 'ccwidgets' of https://github.com/webex/widgets into add…
adhmenon c5b131c
feat(cc-widgets): resolved-more-comments
adhmenon 8865991
feat(cc-widgets): removed-old-code
adhmenon f590dd8
feat(cc-widgets): fixed-logging
adhmenon 83c7f7c
feat(cc-widgets): resolved-further-comments
adhmenon 0316502
feat(cc-widgets): removed-unknowns
adhmenon a4ee310
feat(cc-widgets): fixed-digital-list
adhmenon f925eac
feat(cc-widgets): added-test-fixtures
adhmenon aee0b46
feat(cc-widgets): removed-unknowns
adhmenon 84936b8
feat(cc-widgets): removed-dead-code
adhmenon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
337 changes: 337 additions & 0 deletions
337
...nents/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,337 @@ | ||
| import {useCallback, useEffect, useRef, useState} from 'react'; | ||
| import { | ||
| AddressBookEntry, | ||
| ContactServiceQueue, | ||
| EntryPointRecord, | ||
| ILogger, | ||
| FetchPaginatedList, | ||
| PaginatedListParams, | ||
| TransformPaginatedData, | ||
| } from '@webex/cc-store'; | ||
| import { | ||
| CategoryType, | ||
| UseConsultTransferParams, | ||
| CATEGORY_DIAL_NUMBER, | ||
| CATEGORY_ENTRY_POINT, | ||
| CATEGORY_QUEUES, | ||
| CATEGORY_AGENTS, | ||
| } from '../../task.types'; | ||
| import {debounce} from './call-control-custom.utils'; | ||
| import {DEFAULT_PAGE_SIZE} from '../../constants'; | ||
|
|
||
| /** | ||
| * React hook to load, transform and manage paginated data with optional search. | ||
| * | ||
| * @template T - The item type returned by the provided `fetchFunction` (raw API/entity). | ||
| * @template U - The transformed item type stored internally and returned to consumers. | ||
| * @param fetchFunction - Fetcher that returns a paginated list of items of type T. | ||
| * @param transformFunction - Mapper that converts each T into U for UI consumption. | ||
| * @param categoryName - Human-readable name used for logging/telemetry. | ||
| * @param logger - Optional logger instance for diagnostics. | ||
| * @returns An object containing the transformed data (U[]), pagination state and helpers. | ||
| */ | ||
| export const usePaginatedData = <T, U>( | ||
| fetchFunction: FetchPaginatedList<T> | undefined, | ||
| transformFunction: TransformPaginatedData<T, U>, | ||
| categoryName: string, | ||
| logger?: ILogger | ||
| ) => { | ||
| const MODULE = 'cc-components#consult-transfer-popover-hooks.ts'; | ||
| const [data, setData] = useState<U[]>([]); | ||
| const [page, setPage] = useState(0); | ||
| const [hasMore, setHasMore] = useState(true); | ||
| const [loading, setLoading] = useState(false); | ||
|
|
||
| const loadData = useCallback( | ||
| async (currentPage = 0, search = '', reset = false) => { | ||
| if (!fetchFunction) { | ||
| setData([]); | ||
| setHasMore(false); | ||
| return; | ||
| } | ||
|
|
||
| setLoading(true); | ||
| try { | ||
| const apiParams: PaginatedListParams = { | ||
| page: currentPage, | ||
| pageSize: DEFAULT_PAGE_SIZE, | ||
| }; | ||
|
|
||
| if (search && search.trim()) { | ||
| apiParams.search = search; | ||
| } | ||
|
|
||
| logger?.info(`CC-Components: Loading ${categoryName}`, { | ||
| module: MODULE, | ||
| method: 'usePaginatedData#loadData', | ||
| }); | ||
| const response = await fetchFunction(apiParams); | ||
|
|
||
| if (!response || !response.data) { | ||
| logger?.error(`CC-Components: No data received from fetch function for ${categoryName}`, { | ||
| module: MODULE, | ||
| method: 'usePaginatedData#loadData', | ||
| }); | ||
| if (reset || currentPage === 0) { | ||
| setData([]); | ||
| } | ||
| setHasMore(false); | ||
| return; | ||
| } | ||
|
|
||
| logger?.info(`CC-Components: Loaded ${response.data.length} ${categoryName}`, { | ||
| module: MODULE, | ||
| method: 'usePaginatedData#loadData', | ||
| }); | ||
|
|
||
| const transformedEntries = response.data.map((entry, index) => transformFunction(entry, currentPage, index)); | ||
|
|
||
| if (reset || currentPage === 0) { | ||
| setData(transformedEntries); | ||
| } else { | ||
| setData((prev) => [...prev, ...transformedEntries]); | ||
| } | ||
|
|
||
| const newPage = response.meta?.page ?? currentPage; | ||
| const totalPages = response.meta?.totalPages ?? 0; | ||
|
|
||
| setPage(newPage); | ||
| setHasMore(totalPages > 0 && newPage < totalPages - 1); | ||
|
|
||
| logger?.info('CC-Components: Pagination state updated', { | ||
| module: MODULE, | ||
| method: 'usePaginatedData#loadData', | ||
| }); | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| logger?.error(`CC-Components: Error loading ${categoryName}`, { | ||
| module: MODULE, | ||
| method: 'usePaginatedData#loadData', | ||
| error: errorMessage, | ||
| }); | ||
| if (reset || currentPage === 0) { | ||
| setData([]); | ||
| } | ||
| setHasMore(false); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }, | ||
| [fetchFunction, transformFunction, logger, categoryName] | ||
| ); | ||
|
|
||
| const reset = useCallback(() => { | ||
| setData([]); | ||
| setPage(0); | ||
| setHasMore(true); | ||
| }, []); | ||
|
|
||
| return {data, page, hasMore, loading, loadData, reset}; | ||
| }; | ||
|
|
||
| export function useConsultTransferPopover({ | ||
| showDialNumberTab, | ||
| showEntryPointTab, | ||
| getAddressBookEntries, | ||
| getEntryPoints, | ||
| getQueues, | ||
| logger, | ||
|
adhmenon marked this conversation as resolved.
|
||
| }: UseConsultTransferParams) { | ||
| const [selectedCategory, setSelectedCategory] = useState<CategoryType>(CATEGORY_AGENTS); | ||
| const [searchQuery, setSearchQuery] = useState(''); | ||
| const loadMoreRef = useRef<HTMLDivElement>(null); | ||
|
|
||
| const { | ||
| data: dialNumbers, | ||
| page: dialNumbersPage, | ||
| hasMore: hasMoreDialNumbers, | ||
| loading: loadingDialNumbers, | ||
| loadData: loadDialNumbers, | ||
| reset: resetDialNumbers, | ||
| } = usePaginatedData<AddressBookEntry, AddressBookEntry>( | ||
| getAddressBookEntries, | ||
| (entry) => ({ | ||
| id: entry.id, | ||
| name: entry.name, | ||
| number: entry.number, | ||
| organizationId: entry.organizationId, | ||
| version: entry.version, | ||
| createdTime: entry.createdTime, | ||
| lastUpdatedTime: entry.lastUpdatedTime, | ||
| }), | ||
| CATEGORY_DIAL_NUMBER, | ||
| logger | ||
| ); | ||
|
|
||
| const { | ||
| data: entryPoints, | ||
| page: entryPointsPage, | ||
| hasMore: hasMoreEntryPoints, | ||
| loading: loadingEntryPoints, | ||
| loadData: loadEntryPoints, | ||
| reset: resetEntryPoints, | ||
| } = usePaginatedData<EntryPointRecord, {id: string; name: string}>( | ||
| getEntryPoints, | ||
| (entry) => ({id: entry.id, name: entry.name}), | ||
| CATEGORY_ENTRY_POINT, | ||
| logger | ||
| ); | ||
|
|
||
| const { | ||
| data: queuesData, | ||
| page: queuesPage, | ||
| hasMore: hasMoreQueues, | ||
| loading: loadingQueues, | ||
| loadData: loadQueues, | ||
| reset: resetQueues, | ||
| } = usePaginatedData<ContactServiceQueue, {id: string; name: string; description?: string}>( | ||
| getQueues, | ||
| (entry) => ({id: entry.id, name: entry.name, description: entry.description}), | ||
| CATEGORY_QUEUES, | ||
| logger | ||
| ); | ||
|
|
||
| const loadNextPage = useCallback(() => { | ||
| if (!canLoadCategory(selectedCategory)) return; | ||
| const nextPage = currentPageForCategory(selectedCategory) + 1; | ||
| loadCategory(selectedCategory, nextPage, searchQuery); | ||
| }, [ | ||
| selectedCategory, | ||
| hasMoreDialNumbers, | ||
| hasMoreEntryPoints, | ||
| hasMoreQueues, | ||
| loadingDialNumbers, | ||
| loadingEntryPoints, | ||
| loadingQueues, | ||
| dialNumbersPage, | ||
| entryPointsPage, | ||
| queuesPage, | ||
| searchQuery, | ||
| loadDialNumbers, | ||
| loadEntryPoints, | ||
| loadQueues, | ||
| ]); | ||
|
|
||
| const debouncedSearchRef = useRef<ReturnType<typeof debounce>>(); | ||
| if (!debouncedSearchRef.current) { | ||
| const triggerSearch = (query: string, category: CategoryType) => { | ||
| if (query.length === 0 || query.length >= 2) { | ||
| loadCategory(category, 0, query, true); | ||
| } | ||
| }; | ||
| debouncedSearchRef.current = debounce(triggerSearch, 500); | ||
| } | ||
|
|
||
| useEffect(() => { | ||
| return () => { | ||
| debouncedSearchRef.current = undefined; | ||
| }; | ||
| }, []); | ||
|
|
||
| const handleSearchChange = useCallback( | ||
| (value: string) => { | ||
| setSearchQuery(value); | ||
| if (selectedCategory !== CATEGORY_AGENTS) { | ||
| debouncedSearchRef.current?.(value, selectedCategory); | ||
| } | ||
| }, | ||
| [selectedCategory] | ||
| ); | ||
|
|
||
| const handleCategoryChange = useCallback( | ||
| (category: CategoryType) => { | ||
| setSelectedCategory(category); | ||
| setSearchQuery(''); | ||
| resetDialNumbers(); | ||
| resetEntryPoints(); | ||
| resetQueues(); | ||
| }, | ||
| [resetDialNumbers, resetEntryPoints, resetQueues] | ||
| ); | ||
|
|
||
| const createCategoryClickHandler = (category: CategoryType) => () => handleCategoryChange(category); | ||
| const handleAgentsClick = createCategoryClickHandler(CATEGORY_AGENTS); | ||
| const handleQueuesClick = createCategoryClickHandler(CATEGORY_QUEUES); | ||
| const handleDialNumberClick = createCategoryClickHandler(CATEGORY_DIAL_NUMBER); | ||
| const handleEntryPointClick = createCategoryClickHandler(CATEGORY_ENTRY_POINT); | ||
|
|
||
| // Helper: determines if the given category can load next page now | ||
| const canLoadCategory = (category: CategoryType): boolean => { | ||
| if (category === CATEGORY_DIAL_NUMBER) return hasMoreDialNumbers && !loadingDialNumbers; | ||
| if (category === CATEGORY_ENTRY_POINT) return hasMoreEntryPoints && !loadingEntryPoints; | ||
| if (category === CATEGORY_QUEUES) return hasMoreQueues && !loadingQueues; | ||
| return false; | ||
| }; | ||
|
|
||
| // Helper: gets current page number for the given category | ||
| const currentPageForCategory = (category: CategoryType): number => { | ||
| if (category === CATEGORY_DIAL_NUMBER) return dialNumbersPage; | ||
| if (category === CATEGORY_ENTRY_POINT) return entryPointsPage; | ||
| if (category === CATEGORY_QUEUES) return queuesPage; | ||
| return 0; | ||
| }; | ||
|
|
||
| // Helper: invokes appropriate loader for the given category | ||
| const loadCategory = (category: CategoryType, page: number, search: string, reset = false) => { | ||
| switch (category) { | ||
| case CATEGORY_DIAL_NUMBER: | ||
| loadDialNumbers(page, search, reset); | ||
| break; | ||
| case CATEGORY_ENTRY_POINT: | ||
| loadEntryPoints(page, search, reset); | ||
| break; | ||
| case CATEGORY_QUEUES: | ||
| loadQueues(page, search, reset); | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| const loadMoreElement = loadMoreRef.current; | ||
| if (!loadMoreElement) return; | ||
|
adhmenon marked this conversation as resolved.
|
||
| const observer = new IntersectionObserver( | ||
| ([entry]) => { | ||
| if (entry?.isIntersecting) { | ||
| loadNextPage(); | ||
| } | ||
| }, | ||
| {threshold: 1.0} | ||
| ); | ||
| observer.observe(loadMoreElement); | ||
| return () => { | ||
| observer.unobserve(loadMoreElement); | ||
| }; | ||
| }, [loadNextPage]); | ||
|
|
||
| useEffect(() => { | ||
| if (selectedCategory === CATEGORY_DIAL_NUMBER && showDialNumberTab && dialNumbers.length === 0) { | ||
| loadCategory(CATEGORY_DIAL_NUMBER, 0, '', true); | ||
| } else if (selectedCategory === CATEGORY_ENTRY_POINT && showEntryPointTab && entryPoints.length === 0) { | ||
| loadCategory(CATEGORY_ENTRY_POINT, 0, '', true); | ||
| } else if (selectedCategory === CATEGORY_QUEUES && queuesData.length === 0) { | ||
| loadCategory(CATEGORY_QUEUES, 0, '', true); | ||
| } | ||
| }, [selectedCategory]); | ||
|
|
||
| return { | ||
| selectedCategory, | ||
| searchQuery, | ||
| loadMoreRef, | ||
| dialNumbers, | ||
| hasMoreDialNumbers, | ||
| loadingDialNumbers, | ||
| entryPoints, | ||
| hasMoreEntryPoints, | ||
| loadingEntryPoints, | ||
| queuesData, | ||
| hasMoreQueues, | ||
| loadingQueues, | ||
| handleSearchChange, | ||
| handleAgentsClick, | ||
| handleQueuesClick, | ||
| handleDialNumberClick, | ||
| handleEntryPointClick, | ||
| }; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.