Skip to content

Commit 0997d10

Browse files
authored
feat(cc-widgets): added-address-book (#537)
1 parent 3f7237d commit 0997d10

36 files changed

+4038
-2523
lines changed

packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-list-item.tsx

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,22 @@ const ConsultTransferListComponent: React.FC<ConsultTransferListComponentProps>
1616

1717
return (
1818
<ListItemBase className={classnames('call-control-list-item', className)} size={50} isPadded aria-label={title}>
19-
<ListItemBaseSection position="start">
19+
<ListItemBaseSection position="start" className="call-control-list-item-start">
2020
<AvatarNext size={32} initials={initials} title={title} />
2121
</ListItemBaseSection>
22-
<ListItemBaseSection
23-
position="middle"
24-
style={{
25-
flex: 1,
26-
display: 'flex',
27-
flexDirection: 'column',
28-
marginLeft: '8px',
29-
minWidth: 0,
30-
overflow: 'hidden',
31-
}}
32-
>
33-
<Text tagName="p" type="body-primary" style={{margin: 0, lineHeight: '1.2'}}>
22+
<ListItemBaseSection position="middle" className="call-control-list-item-middle">
23+
<Text tagName="div" type="body-primary" className="call-control-list-item-title">
3424
{title}
3525
</Text>
3626
{subtitle && (
37-
<Text tagName="p" type="body-secondary" style={{margin: 0, lineHeight: '1.2'}}>
27+
<Text tagName="div" type="body-secondary" className="call-control-list-item-subtitle">
3828
{subtitle}
3929
</Text>
4030
)}
4131
</ListItemBaseSection>
42-
<ListItemBaseSection position="end">
32+
<ListItemBaseSection position="end" className="call-control-list-item-end">
4333
<div className="hover-button">
44-
<ButtonCircle onPress={handleButtonPress} size={28} color="join">
34+
<ButtonCircle onPress={handleButtonPress} size={32} color="join">
4535
<Icon name={buttonIcon} />
4636
</ButtonCircle>
4737
</div>
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
import {useCallback, useEffect, useRef, useState} from 'react';
2+
import {
3+
AddressBookEntry,
4+
ContactServiceQueue,
5+
EntryPointRecord,
6+
ILogger,
7+
FetchPaginatedList,
8+
PaginatedListParams,
9+
TransformPaginatedData,
10+
} from '@webex/cc-store';
11+
import {
12+
CategoryType,
13+
UseConsultTransferParams,
14+
CATEGORY_DIAL_NUMBER,
15+
CATEGORY_ENTRY_POINT,
16+
CATEGORY_QUEUES,
17+
CATEGORY_AGENTS,
18+
} from '../../task.types';
19+
import {debounce} from './call-control-custom.utils';
20+
import {DEFAULT_PAGE_SIZE} from '../../constants';
21+
22+
/**
23+
* React hook to load, transform and manage paginated data with optional search.
24+
*
25+
* @template T - The item type returned by the provided `fetchFunction` (raw API/entity).
26+
* @template U - The transformed item type stored internally and returned to consumers.
27+
* @param fetchFunction - Fetcher that returns a paginated list of items of type T.
28+
* @param transformFunction - Mapper that converts each T into U for UI consumption.
29+
* @param categoryName - Human-readable name used for logging/telemetry.
30+
* @param logger - Optional logger instance for diagnostics.
31+
* @returns An object containing the transformed data (U[]), pagination state and helpers.
32+
*/
33+
export const usePaginatedData = <T, U>(
34+
fetchFunction: FetchPaginatedList<T> | undefined,
35+
transformFunction: TransformPaginatedData<T, U>,
36+
categoryName: string,
37+
logger?: ILogger
38+
) => {
39+
const MODULE = 'cc-components#consult-transfer-popover-hooks.ts';
40+
const [data, setData] = useState<U[]>([]);
41+
const [page, setPage] = useState(0);
42+
const [hasMore, setHasMore] = useState(true);
43+
const [loading, setLoading] = useState(false);
44+
45+
const loadData = useCallback(
46+
async (currentPage = 0, search = '', reset = false) => {
47+
if (!fetchFunction) {
48+
setData([]);
49+
setHasMore(false);
50+
return;
51+
}
52+
53+
setLoading(true);
54+
try {
55+
const apiParams: PaginatedListParams = {
56+
page: currentPage,
57+
pageSize: DEFAULT_PAGE_SIZE,
58+
};
59+
60+
if (search && search.trim()) {
61+
apiParams.search = search;
62+
}
63+
64+
logger?.info(`CC-Components: Loading ${categoryName}`, {
65+
module: MODULE,
66+
method: 'usePaginatedData#loadData',
67+
});
68+
const response = await fetchFunction(apiParams);
69+
70+
if (!response || !response.data) {
71+
logger?.error(`CC-Components: No data received from fetch function for ${categoryName}`, {
72+
module: MODULE,
73+
method: 'usePaginatedData#loadData',
74+
});
75+
if (reset || currentPage === 0) {
76+
setData([]);
77+
}
78+
setHasMore(false);
79+
return;
80+
}
81+
82+
logger?.info(`CC-Components: Loaded ${response.data.length} ${categoryName}`, {
83+
module: MODULE,
84+
method: 'usePaginatedData#loadData',
85+
});
86+
87+
const transformedEntries = response.data.map((entry, index) => transformFunction(entry, currentPage, index));
88+
89+
if (reset || currentPage === 0) {
90+
setData(transformedEntries);
91+
} else {
92+
setData((prev) => [...prev, ...transformedEntries]);
93+
}
94+
95+
const newPage = response.meta?.page ?? currentPage;
96+
const totalPages = response.meta?.totalPages ?? 0;
97+
98+
setPage(newPage);
99+
setHasMore(totalPages > 0 && newPage < totalPages - 1);
100+
101+
logger?.info('CC-Components: Pagination state updated', {
102+
module: MODULE,
103+
method: 'usePaginatedData#loadData',
104+
});
105+
} catch (error) {
106+
const errorMessage = error instanceof Error ? error.message : String(error);
107+
logger?.error(`CC-Components: Error loading ${categoryName}`, {
108+
module: MODULE,
109+
method: 'usePaginatedData#loadData',
110+
error: errorMessage,
111+
});
112+
if (reset || currentPage === 0) {
113+
setData([]);
114+
}
115+
setHasMore(false);
116+
} finally {
117+
setLoading(false);
118+
}
119+
},
120+
[fetchFunction, transformFunction, logger, categoryName]
121+
);
122+
123+
const reset = useCallback(() => {
124+
setData([]);
125+
setPage(0);
126+
setHasMore(true);
127+
}, []);
128+
129+
return {data, page, hasMore, loading, loadData, reset};
130+
};
131+
132+
export function useConsultTransferPopover({
133+
showDialNumberTab,
134+
showEntryPointTab,
135+
getAddressBookEntries,
136+
getEntryPoints,
137+
getQueues,
138+
logger,
139+
}: UseConsultTransferParams) {
140+
const [selectedCategory, setSelectedCategory] = useState<CategoryType>(CATEGORY_AGENTS);
141+
const [searchQuery, setSearchQuery] = useState('');
142+
const loadMoreRef = useRef<HTMLDivElement>(null);
143+
144+
const {
145+
data: dialNumbers,
146+
page: dialNumbersPage,
147+
hasMore: hasMoreDialNumbers,
148+
loading: loadingDialNumbers,
149+
loadData: loadDialNumbers,
150+
reset: resetDialNumbers,
151+
} = usePaginatedData<AddressBookEntry, AddressBookEntry>(
152+
getAddressBookEntries,
153+
(entry) => ({
154+
id: entry.id,
155+
name: entry.name,
156+
number: entry.number,
157+
organizationId: entry.organizationId,
158+
version: entry.version,
159+
createdTime: entry.createdTime,
160+
lastUpdatedTime: entry.lastUpdatedTime,
161+
}),
162+
CATEGORY_DIAL_NUMBER,
163+
logger
164+
);
165+
166+
const {
167+
data: entryPoints,
168+
page: entryPointsPage,
169+
hasMore: hasMoreEntryPoints,
170+
loading: loadingEntryPoints,
171+
loadData: loadEntryPoints,
172+
reset: resetEntryPoints,
173+
} = usePaginatedData<EntryPointRecord, {id: string; name: string}>(
174+
getEntryPoints,
175+
(entry) => ({id: entry.id, name: entry.name}),
176+
CATEGORY_ENTRY_POINT,
177+
logger
178+
);
179+
180+
const {
181+
data: queuesData,
182+
page: queuesPage,
183+
hasMore: hasMoreQueues,
184+
loading: loadingQueues,
185+
loadData: loadQueues,
186+
reset: resetQueues,
187+
} = usePaginatedData<ContactServiceQueue, {id: string; name: string; description?: string}>(
188+
getQueues,
189+
(entry) => ({id: entry.id, name: entry.name, description: entry.description}),
190+
CATEGORY_QUEUES,
191+
logger
192+
);
193+
194+
const loadNextPage = useCallback(() => {
195+
if (!canLoadCategory(selectedCategory)) return;
196+
const nextPage = currentPageForCategory(selectedCategory) + 1;
197+
loadCategory(selectedCategory, nextPage, searchQuery);
198+
}, [
199+
selectedCategory,
200+
hasMoreDialNumbers,
201+
hasMoreEntryPoints,
202+
hasMoreQueues,
203+
loadingDialNumbers,
204+
loadingEntryPoints,
205+
loadingQueues,
206+
dialNumbersPage,
207+
entryPointsPage,
208+
queuesPage,
209+
searchQuery,
210+
loadDialNumbers,
211+
loadEntryPoints,
212+
loadQueues,
213+
]);
214+
215+
const debouncedSearchRef = useRef<ReturnType<typeof debounce>>();
216+
if (!debouncedSearchRef.current) {
217+
const triggerSearch = (query: string, category: CategoryType) => {
218+
if (query.length === 0 || query.length >= 2) {
219+
loadCategory(category, 0, query, true);
220+
}
221+
};
222+
debouncedSearchRef.current = debounce(triggerSearch, 500);
223+
}
224+
225+
useEffect(() => {
226+
return () => {
227+
debouncedSearchRef.current = undefined;
228+
};
229+
}, []);
230+
231+
const handleSearchChange = useCallback(
232+
(value: string) => {
233+
setSearchQuery(value);
234+
if (selectedCategory !== CATEGORY_AGENTS) {
235+
debouncedSearchRef.current?.(value, selectedCategory);
236+
}
237+
},
238+
[selectedCategory]
239+
);
240+
241+
const handleCategoryChange = useCallback(
242+
(category: CategoryType) => {
243+
setSelectedCategory(category);
244+
setSearchQuery('');
245+
resetDialNumbers();
246+
resetEntryPoints();
247+
resetQueues();
248+
},
249+
[resetDialNumbers, resetEntryPoints, resetQueues]
250+
);
251+
252+
const createCategoryClickHandler = (category: CategoryType) => () => handleCategoryChange(category);
253+
const handleAgentsClick = createCategoryClickHandler(CATEGORY_AGENTS);
254+
const handleQueuesClick = createCategoryClickHandler(CATEGORY_QUEUES);
255+
const handleDialNumberClick = createCategoryClickHandler(CATEGORY_DIAL_NUMBER);
256+
const handleEntryPointClick = createCategoryClickHandler(CATEGORY_ENTRY_POINT);
257+
258+
// Helper: determines if the given category can load next page now
259+
const canLoadCategory = (category: CategoryType): boolean => {
260+
if (category === CATEGORY_DIAL_NUMBER) return hasMoreDialNumbers && !loadingDialNumbers;
261+
if (category === CATEGORY_ENTRY_POINT) return hasMoreEntryPoints && !loadingEntryPoints;
262+
if (category === CATEGORY_QUEUES) return hasMoreQueues && !loadingQueues;
263+
return false;
264+
};
265+
266+
// Helper: gets current page number for the given category
267+
const currentPageForCategory = (category: CategoryType): number => {
268+
if (category === CATEGORY_DIAL_NUMBER) return dialNumbersPage;
269+
if (category === CATEGORY_ENTRY_POINT) return entryPointsPage;
270+
if (category === CATEGORY_QUEUES) return queuesPage;
271+
return 0;
272+
};
273+
274+
// Helper: invokes appropriate loader for the given category
275+
const loadCategory = (category: CategoryType, page: number, search: string, reset = false) => {
276+
switch (category) {
277+
case CATEGORY_DIAL_NUMBER:
278+
loadDialNumbers(page, search, reset);
279+
break;
280+
case CATEGORY_ENTRY_POINT:
281+
loadEntryPoints(page, search, reset);
282+
break;
283+
case CATEGORY_QUEUES:
284+
loadQueues(page, search, reset);
285+
break;
286+
default:
287+
break;
288+
}
289+
};
290+
291+
useEffect(() => {
292+
const loadMoreElement = loadMoreRef.current;
293+
if (!loadMoreElement) return;
294+
const observer = new IntersectionObserver(
295+
([entry]) => {
296+
if (entry?.isIntersecting) {
297+
loadNextPage();
298+
}
299+
},
300+
{threshold: 1.0}
301+
);
302+
observer.observe(loadMoreElement);
303+
return () => {
304+
observer.unobserve(loadMoreElement);
305+
};
306+
}, [loadNextPage]);
307+
308+
useEffect(() => {
309+
if (selectedCategory === CATEGORY_DIAL_NUMBER && showDialNumberTab && dialNumbers.length === 0) {
310+
loadCategory(CATEGORY_DIAL_NUMBER, 0, '', true);
311+
} else if (selectedCategory === CATEGORY_ENTRY_POINT && showEntryPointTab && entryPoints.length === 0) {
312+
loadCategory(CATEGORY_ENTRY_POINT, 0, '', true);
313+
} else if (selectedCategory === CATEGORY_QUEUES && queuesData.length === 0) {
314+
loadCategory(CATEGORY_QUEUES, 0, '', true);
315+
}
316+
}, [selectedCategory]);
317+
318+
return {
319+
selectedCategory,
320+
searchQuery,
321+
loadMoreRef,
322+
dialNumbers,
323+
hasMoreDialNumbers,
324+
loadingDialNumbers,
325+
entryPoints,
326+
hasMoreEntryPoints,
327+
loadingEntryPoints,
328+
queuesData,
329+
hasMoreQueues,
330+
loadingQueues,
331+
handleSearchChange,
332+
handleAgentsClick,
333+
handleQueuesClick,
334+
handleDialNumberClick,
335+
handleEntryPointClick,
336+
};
337+
}

0 commit comments

Comments
 (0)