UI improvement. (#521)

* don't remove service provider sid and filteredAccountSid when logout

* support fetching applications with pagination

* applications wip

* support pagination for voip carriers

* wip

* support phone number pagination

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
This commit is contained in:
Hoan Luu Huu
2025-05-28 18:28:52 +07:00
committed by GitHub
parent 19620116b5
commit 844eec953c
12 changed files with 383 additions and 134 deletions

View File

@@ -92,10 +92,6 @@ export const DISABLE_ADDITIONAL_SPEECH_VENDORS: boolean =
export const AWS_REGION: string = export const AWS_REGION: string =
window.JAMBONZ?.AWS_REGION || import.meta.env.VITE_APP_AWS_REGION; window.JAMBONZ?.AWS_REGION || import.meta.env.VITE_APP_AWS_REGION;
export const ENABLE_PHONE_NUMBER_LAZY_LOAD: boolean =
window.JAMBONZ?.ENABLE_PHONE_NUMBER_LAZY_LOAD === "true" ||
JSON.parse(import.meta.env.VITE_APP_ENABLE_PHONE_NUMBER_LAZY_LOAD || "false");
export const DEFAULT_SERVICE_PROVIDER_SID: string = export const DEFAULT_SERVICE_PROVIDER_SID: string =
window.JAMBONZ?.DEFAULT_SERVICE_PROVIDER_SID || window.JAMBONZ?.DEFAULT_SERVICE_PROVIDER_SID ||
import.meta.env.VITE_APP_DEFAULT_SERVICE_PROVIDER_SID; import.meta.env.VITE_APP_DEFAULT_SERVICE_PROVIDER_SID;

View File

@@ -97,6 +97,8 @@ import type {
SpeechSupportedLanguagesAndVoices, SpeechSupportedLanguagesAndVoices,
AppEnv, AppEnv,
PhoneNumberQuery, PhoneNumberQuery,
ApplicationQuery,
VoipCarrierQuery,
} from "./types"; } from "./types";
import { Availability, StatusCodes } from "./types"; import { Availability, StatusCodes } from "./types";
import { JaegerRoot } from "./jaeger-types"; import { JaegerRoot } from "./jaeger-types";
@@ -821,6 +823,28 @@ export const getAppEnvSchema = (url: string) => {
return getFetch<AppEnv>(`${API_APP_ENV}?url=${url}`); return getFetch<AppEnv>(`${API_APP_ENV}?url=${url}`);
}; };
export const getApplications = (
sid: string,
query: Partial<ApplicationQuery>,
) => {
const qryStr = getQuery<Partial<ApplicationQuery>>(query);
return getFetch<PagedResponse<Application>>(
`${API_ACCOUNTS}/${sid}/Applications?${qryStr}`,
);
};
export const getSPVoipCarriers = (
sid: string,
query: Partial<VoipCarrierQuery>,
) => {
const qryStr = getQuery<Partial<VoipCarrierQuery>>(query);
return getFetch<PagedResponse<Carrier>>(
`${API_SERVICE_PROVIDERS}/${sid}/VoipCarriers?${qryStr}`,
);
};
/** Wrappers for APIs that can have a mock dev server response */ /** Wrappers for APIs that can have a mock dev server response */
export const getMe = () => { export const getMe = () => {
@@ -903,7 +927,7 @@ export const getPrice = () => {
export const getPhoneNumbers = (query: Partial<PhoneNumberQuery>) => { export const getPhoneNumbers = (query: Partial<PhoneNumberQuery>) => {
const qryStr = getQuery<Partial<PhoneNumberQuery>>(query); const qryStr = getQuery<Partial<PhoneNumberQuery>>(query);
return getFetch<PhoneNumber[]>(`${API_PHONE_NUMBERS}?${qryStr}`); return getFetch<PagedResponse<PhoneNumber>>(`${API_PHONE_NUMBERS}?${qryStr}`);
}; };
export const getSpeechSupportedLanguagesAndVoices = ( export const getSpeechSupportedLanguagesAndVoices = (

View File

@@ -557,12 +557,14 @@ export interface Client {
export interface PageQuery { export interface PageQuery {
page: number; page: number;
page_size?: number;
count: number; count: number;
start?: string; start?: string;
days?: number; days?: number;
} }
export interface PhoneNumberQuery extends PageQuery { export interface PhoneNumberQuery extends PageQuery {
service_provider_sid?: string;
account_sid?: string; account_sid?: string;
filter?: string; filter?: string;
} }
@@ -572,6 +574,15 @@ export interface CallQuery extends PageQuery {
answered?: string; answered?: string;
} }
export interface ApplicationQuery extends PageQuery {
name?: string;
}
export interface VoipCarrierQuery extends PageQuery {
name?: string;
account_sid?: string;
}
export interface GoogleCustomVoicesQuery { export interface GoogleCustomVoicesQuery {
speech_credential_sid?: string; speech_credential_sid?: string;
label?: string; label?: string;

View File

@@ -11,7 +11,11 @@ import {
toastSuccess, toastSuccess,
toastError, toastError,
} from "src/store"; } from "src/store";
import { getActiveSP, setActiveSP } from "src/store/localStore"; import {
getActiveSP,
removeAccountFilter,
setActiveSP,
} from "src/store/localStore";
import { postServiceProviders } from "src/api"; import { postServiceProviders } from "src/api";
import type { NaviItem } from "./items"; import type { NaviItem } from "./items";
@@ -166,6 +170,7 @@ export const Navi = ({
onChange={(e) => { onChange={(e) => {
setSid(e.target.value); setSid(e.target.value);
setActiveSP(e.target.value); setActiveSP(e.target.value);
removeAccountFilter();
navigate(ROUTE_LOGIN); navigate(ROUTE_LOGIN);
}} }}
disabled={user?.scope !== USER_ADMIN} disabled={user?.scope !== USER_ADMIN}

View File

@@ -68,10 +68,10 @@ export const Alerts = () => {
}; };
useMemo(() => { useMemo(() => {
setAccountSid(getAccountFilter() || accountSid);
if (!accountSid && user?.account_sid) setAccountSid(user?.account_sid);
if (getQueryFilter()) { if (getQueryFilter()) {
const [date] = getQueryFilter().split("/"); const [date] = getQueryFilter().split("/");
setAccountSid(getAccountFilter() || accountSid);
if (!accountSid && user?.account_sid) setAccountSid(user?.account_sid);
setDateFilter(date); setDateFilter(date);
} }
}, [accountSid]); }, [accountSid]);

View File

@@ -1,8 +1,12 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, useRef } from "react";
import { H1, M, Button, Icon } from "@jambonz/ui-kit"; import { H1, M, Button, Icon, ButtonGroup, MS } from "@jambonz/ui-kit";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { deleteApplication, useServiceProviderData, useApiData } from "src/api"; import {
deleteApplication,
useServiceProviderData,
getApplications,
} from "src/api";
import { import {
ROUTE_INTERNAL_APPLICATIONS, ROUTE_INTERNAL_APPLICATIONS,
ROUTE_INTERNAL_ACCOUNTS, ROUTE_INTERNAL_ACCOUNTS,
@@ -13,20 +17,17 @@ import {
Spinner, Spinner,
AccountFilter, AccountFilter,
SearchFilter, SearchFilter,
Pagination,
SelectFilter,
} from "src/components"; } from "src/components";
import { DeleteApplication } from "./delete"; import { DeleteApplication } from "./delete";
import { toastError, toastSuccess, useSelectState } from "src/store"; import { toastError, toastSuccess, useSelectState } from "src/store";
import { import { isUserAccountScope, hasLength, hasValue } from "src/utils";
isUserAccountScope,
hasLength,
hasValue,
useFilteredResults,
} from "src/utils";
import type { Application, Account } from "src/api/types"; import type { Application, Account } from "src/api/types";
import { ScopedAccess } from "src/components/scoped-access"; import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types"; import { Scope } from "src/store/types";
import { USER_ACCOUNT } from "src/api/constants"; import { PER_PAGE_SELECTION, USER_ACCOUNT } from "src/api/constants";
import { getAccountFilter, setLocation } from "src/store/localStore"; import { getAccountFilter, setLocation } from "src/store/localStore";
export const Applications = () => { export const Applications = () => {
@@ -34,14 +35,51 @@ export const Applications = () => {
const [accounts] = useServiceProviderData<Account[]>("Accounts"); const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [accountSid, setAccountSid] = useState(""); const [accountSid, setAccountSid] = useState("");
const [application, setApplication] = useState<Application | null>(null); const [application, setApplication] = useState<Application | null>(null);
const [apiUrl, setApiUrl] = useState(""); const [applications, setApplications] = useState<Application[] | null>(null);
const [applications, refetch] = useApiData<Application[]>(apiUrl);
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
const filteredApplications = useFilteredResults<Application>( const [applicationsTotal, setApplicationsTotal] = useState(0);
filter, const [pageNumber, setPageNumber] = useState(1);
applications, const [perPageFilter, setPerPageFilter] = useState("25");
); const [maxPageNumber, setMaxPageNumber] = useState(1);
// Track previous values to detect changes
const prevValuesRef = useRef({
accountSid: "",
filter: "",
pageNumber: 1,
perPageFilter: "25",
});
const fetchApplications = (resetPage = false) => {
// Don't fetch if no account is selected
if (!accountSid) return;
setApplications(null);
// Calculate the correct page to use
const currentPage = resetPage ? 1 : pageNumber;
// If we're resetting the page, also update the state
if (resetPage && pageNumber !== 1) {
setPageNumber(1);
}
getApplications(accountSid, {
page: currentPage,
page_size: Number(perPageFilter),
...(filter && { name: filter }),
})
.then(({ json }) => {
setApplications(json.data);
setApplicationsTotal(json.total);
setMaxPageNumber(Math.ceil(json.total / Number(perPageFilter)));
})
.catch((error) => {
setApplications([]);
toastError(error.msg);
});
};
const handleDelete = () => { const handleDelete = () => {
if (application) { if (application) {
@@ -53,8 +91,7 @@ export const Applications = () => {
} }
deleteApplication(application.application_sid) deleteApplication(application.application_sid)
.then(() => { .then(() => {
// getApplications(); fetchApplications(false);
refetch();
setApplication(null); setApplication(null);
toastSuccess( toastSuccess(
<> <>
@@ -68,18 +105,44 @@ export const Applications = () => {
} }
}; };
// Set initial account
useEffect(() => { useEffect(() => {
setLocation();
if (user?.account_sid && user.scope === USER_ACCOUNT) { if (user?.account_sid && user.scope === USER_ACCOUNT) {
setAccountSid(user?.account_sid); setAccountSid(user?.account_sid);
} else { } else {
setAccountSid(getAccountFilter() || accountSid); setAccountSid(
getAccountFilter() || accountSid || accounts?.[0]?.account_sid || "",
);
} }
setLocation();
}, [user, accounts]);
if (accountSid) { // This single effect handles all data fetching triggers
setApiUrl(`Accounts/${accountSid}/Applications`); useEffect(() => {
} const accSid = accountSid || getAccountFilter() || "";
}, [accountSid, user]);
if (!accSid) return;
// Determine if the change requires a page reset
const prevValues = prevValuesRef.current;
const isFilterChange =
prevValues.accountSid !== accountSid || prevValues.filter !== filter;
const isPageSizeChange =
prevValues.perPageFilter !== perPageFilter &&
prevValues.perPageFilter !== ""; // Skip initial render
// Update ref with current values for next comparison
prevValuesRef.current = {
accountSid: accSid,
filter,
pageNumber,
perPageFilter,
};
// Fetch data with page reset if needed
fetchApplications(isFilterChange || isPageSizeChange);
}, [accountSid, filter, pageNumber, perPageFilter]);
return ( return (
<> <>
@@ -100,6 +163,7 @@ export const Applications = () => {
<SearchFilter <SearchFilter
placeholder="Filter applications" placeholder="Filter applications"
filter={[filter, setFilter]} filter={[filter, setFilter]}
delay={1000}
/> />
<ScopedAccess user={user} scope={Scope.service_provider}> <ScopedAccess user={user} scope={Scope.service_provider}>
<AccountFilter <AccountFilter
@@ -108,12 +172,12 @@ export const Applications = () => {
/> />
</ScopedAccess> </ScopedAccess>
</section> </section>
<Section {...(hasLength(filteredApplications) && { slim: true })}> <Section {...(hasLength(applications) && { slim: true })}>
<div className="list"> <div className="list">
{!hasValue(applications) && hasLength(accounts) ? ( {!hasValue(applications) && hasLength(accounts) ? (
<Spinner /> <Spinner />
) : hasLength(filteredApplications) ? ( ) : hasLength(applications) ? (
filteredApplications applications
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.map((application) => { .map((application) => {
return ( return (
@@ -189,6 +253,26 @@ export const Applications = () => {
</Button> </Button>
</Section> </Section>
)} )}
<footer>
<ButtonGroup>
<MS>
Total: {applicationsTotal} record
{applicationsTotal === 1 ? "" : "s"}
</MS>
{hasLength(applications) && (
<Pagination
pageNumber={pageNumber}
setPageNumber={setPageNumber}
maxPageNumber={maxPageNumber}
/>
)}
<SelectFilter
id="page_filter"
filter={[perPageFilter, setPerPageFilter]}
options={PER_PAGE_SELECTION}
/>
</ButtonGroup>
</footer>
{application && ( {application && (
<DeleteApplication <DeleteApplication
application={application} application={application}

View File

@@ -1,11 +1,12 @@
import React, { useState, useMemo, useEffect } from "react"; import React, { useState, useEffect, useRef } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Button, H1, Icon, M } from "@jambonz/ui-kit"; import { Button, ButtonGroup, H1, Icon, M, MS } from "@jambonz/ui-kit";
import { import {
deleteCarrier, deleteCarrier,
deleteSipGateway, deleteSipGateway,
deleteSmppGateway, deleteSmppGateway,
getFetch, getFetch,
getSPVoipCarriers,
useApiData, useApiData,
useServiceProviderData, useServiceProviderData,
} from "src/api"; } from "src/api";
@@ -17,20 +18,18 @@ import {
Section, Section,
Spinner, Spinner,
SearchFilter, SearchFilter,
Pagination,
SelectFilter,
} from "src/components"; } from "src/components";
import { ScopedAccess } from "src/components/scoped-access"; import { ScopedAccess } from "src/components/scoped-access";
import { Gateways } from "./gateways"; import { Gateways } from "./gateways";
import { import { isUserAccountScope, hasLength, hasValue } from "src/utils";
isUserAccountScope,
hasLength,
hasValue,
useFilteredResults,
} from "src/utils";
import { import {
API_SIP_GATEWAY, API_SIP_GATEWAY,
API_SMPP_GATEWAY, API_SMPP_GATEWAY,
CARRIER_REG_OK, CARRIER_REG_OK,
ENABLE_HOSTED_SYSTEM, ENABLE_HOSTED_SYSTEM,
PER_PAGE_SELECTION,
USER_ACCOUNT, USER_ACCOUNT,
} from "src/api/constants"; } from "src/api/constants";
import { DeleteCarrier } from "./delete"; import { DeleteCarrier } from "./delete";
@@ -49,32 +48,55 @@ export const Carriers = () => {
const user = useSelectState("user"); const user = useSelectState("user");
const [userData] = useApiData<CurrentUserData>("Users/me"); const [userData] = useApiData<CurrentUserData>("Users/me");
const currentServiceProvider = useSelectState("currentServiceProvider"); const currentServiceProvider = useSelectState("currentServiceProvider");
const [apiUrl, setApiUrl] = useState("");
const [carrier, setCarrier] = useState<Carrier | null>(null); const [carrier, setCarrier] = useState<Carrier | null>(null);
const [carriers, refetch] = useApiData<Carrier[]>(apiUrl); const [carriers, setCarriers] = useState<Carrier[] | null>(null);
const [accounts] = useServiceProviderData<Account[]>("Accounts"); const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [accountSid, setAccountSid] = useState(""); const [accountSid, setAccountSid] = useState("");
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
const carriersFiltered = useMemo(() => { const [carriersTotal, setCarriersTotal] = useState(0);
setAccountSid(getAccountFilter()); const [pageNumber, setPageNumber] = useState(1);
if (user?.account_sid && user?.scope === USER_ACCOUNT) { const [perPageFilter, setPerPageFilter] = useState("25");
setAccountSid(user?.account_sid); const [maxPageNumber, setMaxPageNumber] = useState(1);
// Add a ref to track previous values
const prevValuesRef = useRef({
serviceProviderId: "",
accountSid: "",
filter: "",
pageNumber: 1,
perPageFilter: "25",
});
const fetchCarriers = (resetPage = false) => {
if (!currentServiceProvider) return;
setCarriers(null);
// Calculate the correct page to use
const currentPage = resetPage ? 1 : pageNumber;
// If we're resetting the page, also update the state
if (resetPage && pageNumber !== 1) {
setPageNumber(1);
} }
return carriers getSPVoipCarriers(currentServiceProvider.service_provider_sid, {
? carriers.filter((carrier) => page: currentPage,
accountSid page_size: Number(perPageFilter),
? carrier.account_sid === accountSid ...(filter && { name: filter }),
: carrier.account_sid === null, ...(accountSid && { account_sid: accountSid }),
) })
: []; .then(({ json }) => {
}, [accountSid, carrier, carriers]); setCarriers(json.data);
setCarriersTotal(json.total);
const filteredCarriers = useFilteredResults<Carrier>( setMaxPageNumber(Math.ceil(json.total / Number(perPageFilter)));
filter, })
carriersFiltered, .catch((error) => {
); setCarriers([]);
toastError(error.msg);
});
};
const handleDelete = () => { const handleDelete = () => {
if (carrier) { if (carrier) {
@@ -113,7 +135,7 @@ export const Carriers = () => {
); );
}); });
setCarrier(null); setCarrier(null);
refetch(); fetchCarriers(false);
toastSuccess( toastSuccess(
<> <>
Deleted Carrier <strong>{carrier.name}</strong> Deleted Carrier <strong>{carrier.name}</strong>
@@ -126,14 +148,45 @@ export const Carriers = () => {
} }
}; };
// Initial account setup
useEffect(() => { useEffect(() => {
setLocation(); if (user?.account_sid && user?.scope === USER_ACCOUNT) {
if (currentServiceProvider) { setAccountSid(user?.account_sid);
setApiUrl( } else {
`ServiceProviders/${currentServiceProvider.service_provider_sid}/VoipCarriers`, setAccountSid(getAccountFilter());
);
} }
}, [user, currentServiceProvider, accountSid]); setLocation();
}, [user, accounts]);
// Combined effect for all data fetching
useEffect(() => {
if (!currentServiceProvider) return;
const prevValues = prevValuesRef.current;
const currentSPId = currentServiceProvider.service_provider_sid;
// Determine if we should reset pagination
const isFilterOrProviderChange =
prevValues.serviceProviderId !== currentSPId ||
prevValues.accountSid !== accountSid ||
prevValues.filter !== filter;
const isPageSizeChange =
prevValues.perPageFilter !== perPageFilter &&
prevValues.perPageFilter !== "25"; // Skip initial render
// Update ref for next comparison
prevValuesRef.current = {
serviceProviderId: currentSPId,
accountSid,
filter,
pageNumber,
perPageFilter,
};
// Fetch data with page reset if filters changed
fetchCarriers(isFilterOrProviderChange || isPageSizeChange);
}, [currentServiceProvider, accountSid, filter, pageNumber, perPageFilter]);
return ( return (
<> <>
@@ -159,6 +212,7 @@ export const Carriers = () => {
<SearchFilter <SearchFilter
placeholder="Filter carriers" placeholder="Filter carriers"
filter={[filter, setFilter]} filter={[filter, setFilter]}
delay={1000}
/> />
<ScopedAccess user={user} scope={Scope.service_provider}> <ScopedAccess user={user} scope={Scope.service_provider}>
<AccountFilter <AccountFilter
@@ -169,12 +223,12 @@ export const Carriers = () => {
/> />
</ScopedAccess> </ScopedAccess>
</section> </section>
<Section {...(hasLength(filteredCarriers) && { slim: true })}> <Section {...(hasLength(carriers) && { slim: true })}>
<div className="list"> <div className="list">
{!hasValue(carriers) && hasLength(accounts) ? ( {!hasValue(carriers) && hasLength(accounts) ? (
<Spinner /> <Spinner />
) : hasLength(filteredCarriers) ? ( ) : hasLength(carriers) ? (
filteredCarriers.map((carrier) => ( carriers.map((carrier) => (
<div className="item" key={carrier.voip_carrier_sid}> <div className="item" key={carrier.voip_carrier_sid}>
<div className="item__info"> <div className="item__info">
<div className="item__title"> <div className="item__title">
@@ -274,6 +328,26 @@ export const Carriers = () => {
Add carrier Add carrier
</Button> </Button>
</Section> </Section>
<footer>
<ButtonGroup>
<MS>
Total: {carriersTotal} record
{carriersTotal === 1 ? "" : "s"}
</MS>
{hasLength(carriers) && (
<Pagination
pageNumber={pageNumber}
setPageNumber={setPageNumber}
maxPageNumber={maxPageNumber}
/>
)}
<SelectFilter
id="page_filter"
filter={[perPageFilter, setPerPageFilter]}
options={PER_PAGE_SELECTION}
/>
</ButtonGroup>
</footer>
{carrier && ( {carrier && (
<DeleteCarrier <DeleteCarrier
carrier={carrier} carrier={carrier}

View File

@@ -17,6 +17,7 @@ import { Scope } from "src/store/types";
import { hasLength, hasValue, useFilteredResults } from "src/utils"; import { hasLength, hasValue, useFilteredResults } from "src/utils";
import ClientsDelete from "./delete"; import ClientsDelete from "./delete";
import { USER_ACCOUNT } from "src/api/constants"; import { USER_ACCOUNT } from "src/api/constants";
import { getAccountFilter } from "src/store/localStore";
export const Clients = () => { export const Clients = () => {
const user = useSelectState("user"); const user = useSelectState("user");
@@ -32,6 +33,7 @@ export const Clients = () => {
const [client, setClient] = useState<Client | null>(); const [client, setClient] = useState<Client | null>();
const tmpFilteredClients = useMemo(() => { const tmpFilteredClients = useMemo(() => {
setAccountSid(getAccountFilter() || accountSid);
if (user?.account_sid && user?.scope === USER_ACCOUNT) { if (user?.account_sid && user?.scope === USER_ACCOUNT) {
setAccountSid(user?.account_sid); setAccountSid(user?.account_sid);
return clients; return clients;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useState, useRef } from "react";
import { Button, ButtonGroup, H1, Icon, MS } from "@jambonz/ui-kit"; import { Button, ButtonGroup, H1, Icon, MS } from "@jambonz/ui-kit";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -16,28 +16,26 @@ import {
ApplicationFilter, ApplicationFilter,
SearchFilter, SearchFilter,
AccountFilter, AccountFilter,
Pagination,
SelectFilter,
} from "src/components"; } from "src/components";
import { import {
ROUTE_INTERNAL_ACCOUNTS, ROUTE_INTERNAL_ACCOUNTS,
ROUTE_INTERNAL_CARRIERS, ROUTE_INTERNAL_CARRIERS,
ROUTE_INTERNAL_PHONE_NUMBERS, ROUTE_INTERNAL_PHONE_NUMBERS,
} from "src/router/routes"; } from "src/router/routes";
import { import { hasLength, hasValue, formatPhoneNumber } from "src/utils";
hasLength,
hasValue,
formatPhoneNumber,
useFilteredResults,
} from "src/utils";
import { DeletePhoneNumber } from "./delete"; import { DeletePhoneNumber } from "./delete";
import type { Account, PhoneNumber, Carrier, Application } from "src/api/types"; import type { Account, PhoneNumber, Carrier, Application } from "src/api/types";
import { ENABLE_PHONE_NUMBER_LAZY_LOAD, USER_ACCOUNT } from "src/api/constants"; import { PER_PAGE_SELECTION, USER_ACCOUNT } from "src/api/constants";
import { ScopedAccess } from "src/components/scoped-access"; import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types"; import { Scope } from "src/store/types";
import { getAccountFilter, setLocation } from "src/store/localStore"; import { getAccountFilter, setLocation } from "src/store/localStore";
export const PhoneNumbers = () => { export const PhoneNumbers = () => {
const user = useSelectState("user"); const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");
const [accounts] = useServiceProviderData<Account[]>("Accounts"); const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [applications] = useServiceProviderData<Application[]>("Applications"); const [applications] = useServiceProviderData<Application[]>("Applications");
const [carriers] = useServiceProviderData<Carrier[]>("VoipCarriers"); const [carriers] = useServiceProviderData<Carrier[]>("VoipCarriers");
@@ -51,35 +49,48 @@ export const PhoneNumbers = () => {
const [applyMassEdit, setApplyMassEdit] = useState(false); const [applyMassEdit, setApplyMassEdit] = useState(false);
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
const [accountSid, setAccountSid] = useState(""); const [accountSid, setAccountSid] = useState("");
const [phoneNumbersTotal, setphoneNumbersTotal] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
const [perPageFilter, setPerPageFilter] = useState("25");
const [maxPageNumber, setMaxPageNumber] = useState(1);
const phoneNumbersFiltered = useMemo(() => { // Add ref to track previous values
setAccountSid(getAccountFilter()); const prevValuesRef = useRef({
return phoneNumbers serviceProviderId: "",
? phoneNumbers.filter( accountSid: "",
(phn) => !accountSid || phn.account_sid === accountSid, filter: "",
) pageNumber: 1,
: []; perPageFilter: "25",
}, [phoneNumbers, ...[!ENABLE_PHONE_NUMBER_LAZY_LOAD && accountSid]]); });
const filteredPhoneNumbers = !ENABLE_PHONE_NUMBER_LAZY_LOAD const fetchPhoneNumbers = (resetPage = false) => {
? useFilteredResults<PhoneNumber>(filter, phoneNumbersFiltered) setPhoneNumbers(null);
: phoneNumbersFiltered;
const refetch = () => { // Calculate the correct page to use
getPhoneNumbers( const currentPage = resetPage ? 1 : pageNumber;
!ENABLE_PHONE_NUMBER_LAZY_LOAD
? {} // If we're resetting the page, also update the state
: { if (resetPage && pageNumber !== 1) {
...(accountSid && { account_sid: accountSid }), setPageNumber(1);
...(filter && { filter }), }
},
) const accSid = accountSid || getAccountFilter() || "";
getPhoneNumbers({
page: currentPage,
page_size: Number(perPageFilter),
...(accSid && { account_sid: accSid }),
...(filter && { filter }),
})
.then(({ json }) => { .then(({ json }) => {
if (json) { if (json) {
setPhoneNumbers(json); setPhoneNumbers(json.data);
setphoneNumbersTotal(json.total);
setMaxPageNumber(Math.ceil(json.total / Number(perPageFilter)));
} }
}) })
.catch((error) => { .catch((error) => {
setPhoneNumbers([]);
toastError(error.msg); toastError(error.msg);
}); });
}; };
@@ -95,9 +106,11 @@ export const PhoneNumbers = () => {
}), }),
) )
.then(() => { .then(() => {
refetch(); fetchPhoneNumbers(false);
setApplicationSid(""); setApplicationSid("");
setApplyMassEdit(false); setApplyMassEdit(false);
setSelectAll(false);
setSelectedPhoneNumbers([]);
toastSuccess("Number routing updated successfully"); toastSuccess("Number routing updated successfully");
}) })
.catch((error) => { .catch((error) => {
@@ -111,7 +124,7 @@ export const PhoneNumbers = () => {
if (phoneNumber) { if (phoneNumber) {
deletePhoneNumber(phoneNumber.phone_number_sid) deletePhoneNumber(phoneNumber.phone_number_sid)
.then(() => { .then(() => {
refetch(); fetchPhoneNumbers(false);
setPhoneNumber(null); setPhoneNumber(null);
toastSuccess( toastSuccess(
<> <>
@@ -125,21 +138,43 @@ export const PhoneNumbers = () => {
} }
}; };
// Initial account setup
useEffect(() => { useEffect(() => {
setLocation();
if (user?.account_sid && user.scope === USER_ACCOUNT) { if (user?.account_sid && user.scope === USER_ACCOUNT) {
setAccountSid(user?.account_sid); setAccountSid(user?.account_sid);
} else {
setAccountSid(getAccountFilter() || accountSid);
} }
setLocation();
}, [user]); }, [user]);
// Combined effect for all data fetching
useEffect(() => { useEffect(() => {
if (ENABLE_PHONE_NUMBER_LAZY_LOAD) { const prevValues = prevValuesRef.current;
setPhoneNumbers([]); const currentSPId = currentServiceProvider?.service_provider_sid;
return;
}
refetch(); // Detect changes that require page reset
}, []); const isFilterOrProviderChange =
prevValues.serviceProviderId !== currentSPId ||
prevValues.accountSid !== accountSid ||
prevValues.filter !== filter;
const isPageSizeChange =
prevValues.perPageFilter !== perPageFilter &&
prevValues.perPageFilter !== "25"; // Skip initial render
// Update ref for next comparison
prevValuesRef.current = {
serviceProviderId: currentSPId || "",
accountSid,
filter,
pageNumber,
perPageFilter,
};
// Fetch data with appropriate reset parameter
fetchPhoneNumbers(isFilterOrProviderChange || isPageSizeChange);
}, [currentServiceProvider, accountSid, filter, pageNumber, perPageFilter]);
return ( return (
<> <>
@@ -160,6 +195,7 @@ export const PhoneNumbers = () => {
<SearchFilter <SearchFilter
placeholder="Filter phone numbers" placeholder="Filter phone numbers"
filter={[filter, setFilter]} filter={[filter, setFilter]}
delay={1000}
/> />
<ScopedAccess user={user} scope={Scope.service_provider}> <ScopedAccess user={user} scope={Scope.service_provider}>
<AccountFilter <AccountFilter
@@ -168,25 +204,12 @@ export const PhoneNumbers = () => {
defaultOption defaultOption
/> />
</ScopedAccess> </ScopedAccess>
{ENABLE_PHONE_NUMBER_LAZY_LOAD && (
<ButtonGroup>
<Button
small
onClick={() => {
setPhoneNumbers(null);
refetch();
}}
>
Search
</Button>
</ButtonGroup>
)}
</section> </section>
<Section {...(hasLength(filteredPhoneNumbers) && { slim: true })}> <Section {...(hasLength(phoneNumbers) && { slim: true })}>
<div className="list"> <div className="list">
{!hasValue(phoneNumbers) ? ( {!hasValue(phoneNumbers) ? (
<Spinner /> <Spinner />
) : hasLength(filteredPhoneNumbers) ? ( ) : hasLength(phoneNumbers) ? (
<> <>
<div className="item item--actions"> <div className="item item--actions">
{accountSid ? ( {accountSid ? (
@@ -200,7 +223,7 @@ export const PhoneNumbers = () => {
onChange={(e) => { onChange={(e) => {
if (e.target.checked) { if (e.target.checked) {
setSelectAll(true); setSelectAll(true);
setSelectedPhoneNumbers(filteredPhoneNumbers); setSelectedPhoneNumbers(phoneNumbers);
} else { } else {
setSelectAll(false); setSelectAll(false);
setSelectedPhoneNumbers([]); setSelectedPhoneNumbers([]);
@@ -224,10 +247,8 @@ export const PhoneNumbers = () => {
<Button <Button
small small
onClick={() => { onClick={() => {
handleMassEdit();
setSelectAll(false);
setApplyMassEdit(true); setApplyMassEdit(true);
setSelectedPhoneNumbers([]); handleMassEdit();
}} }}
> >
Apply Apply
@@ -249,7 +270,7 @@ export const PhoneNumbers = () => {
</MS> </MS>
)} )}
</div> </div>
{filteredPhoneNumbers.map((phoneNumber) => { {phoneNumbers.map((phoneNumber) => {
return ( return (
<div className="item" key={phoneNumber.phone_number_sid}> <div className="item" key={phoneNumber.phone_number_sid}>
<div className="item__info"> <div className="item__info">
@@ -385,6 +406,26 @@ export const PhoneNumbers = () => {
</Button> </Button>
)} )}
</Section> </Section>
<footer>
<ButtonGroup>
<MS>
Total: {phoneNumbersTotal} record
{phoneNumbersTotal === 1 ? "" : "s"}
</MS>
{hasLength(phoneNumbers) && (
<Pagination
pageNumber={pageNumber}
setPageNumber={setPageNumber}
maxPageNumber={maxPageNumber}
/>
)}
<SelectFilter
id="page_filter"
filter={[perPageFilter, setPerPageFilter]}
options={PER_PAGE_SELECTION}
/>
</ButtonGroup>
</footer>
{phoneNumber && ( {phoneNumber && (
<DeletePhoneNumber <DeletePhoneNumber
phoneNumber={phoneNumber} phoneNumber={phoneNumber}

View File

@@ -87,10 +87,10 @@ export const RecentCalls = () => {
}; };
useMemo(() => { useMemo(() => {
setAccountSid(getAccountFilter() || accountSid);
if (!accountSid && user?.account_sid) setAccountSid(user?.account_sid);
if (getQueryFilter()) { if (getQueryFilter()) {
const [date, direction, status] = getQueryFilter().split("/"); const [date, direction, status] = getQueryFilter().split("/");
setAccountSid(getAccountFilter() || accountSid);
if (!accountSid && user?.account_sid) setAccountSid(user?.account_sid);
setDateFilter(date); setDateFilter(date);
setDirectionFilter(direction); setDirectionFilter(direction);
setStatusFilter(status); setStatusFilter(status);

View File

@@ -27,6 +27,7 @@ import { ENABLE_HOSTED_SYSTEM, USER_ACCOUNT } from "src/api/constants";
import type { UserData } from "src/store/types"; import type { UserData } from "src/store/types";
import { toastError } from "src/store"; import { toastError } from "src/store";
import { import {
clearLocalStorage,
removeLocationBeforeOauth, removeLocationBeforeOauth,
removeOauthState, removeOauthState,
} from "src/store/localStore"; } from "src/store/localStore";
@@ -163,7 +164,7 @@ export const useProvideAuth = (): AuthStateContext => {
postLogout() postLogout()
.then((response) => { .then((response) => {
if (response.status === StatusCodes.NO_CONTENT) { if (response.status === StatusCodes.NO_CONTENT) {
localStorage.clear(); clearLocalStorage();
sessionStorage.clear(); sessionStorage.clear();
sessionStorage.setItem(SESS_FLASH_MSG, MSG_LOGGED_OUT); sessionStorage.setItem(SESS_FLASH_MSG, MSG_LOGGED_OUT);
window.location.href = ROUTE_LOGIN; window.location.href = ROUTE_LOGIN;

View File

@@ -123,7 +123,18 @@ export const checkLocation = () => {
if (currentLocation !== storedLocation) { if (currentLocation !== storedLocation) {
localStorage.removeItem(storeQueryFilter); localStorage.removeItem(storeQueryFilter);
localStorage.removeItem(storeAccountFilter); // Keep storeAccountFilter in different location that user can search for same account
// in different location
// localStorage.removeItem(storeAccountFilter);
return; return;
} }
}; };
export const clearLocalStorage = () => {
const toKeep = [storeActiveSP, storeAccountFilter];
Object.keys(localStorage).forEach((key) => {
if (!toKeep.includes(key)) {
localStorage.removeItem(key);
}
});
};