mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2025-12-19 05:37:43 +00:00
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:
@@ -92,10 +92,6 @@ export const DISABLE_ADDITIONAL_SPEECH_VENDORS: boolean =
|
||||
export const AWS_REGION: string =
|
||||
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 =
|
||||
window.JAMBONZ?.DEFAULT_SERVICE_PROVIDER_SID ||
|
||||
import.meta.env.VITE_APP_DEFAULT_SERVICE_PROVIDER_SID;
|
||||
|
||||
@@ -97,6 +97,8 @@ import type {
|
||||
SpeechSupportedLanguagesAndVoices,
|
||||
AppEnv,
|
||||
PhoneNumberQuery,
|
||||
ApplicationQuery,
|
||||
VoipCarrierQuery,
|
||||
} from "./types";
|
||||
import { Availability, StatusCodes } from "./types";
|
||||
import { JaegerRoot } from "./jaeger-types";
|
||||
@@ -821,6 +823,28 @@ export const getAppEnvSchema = (url: string) => {
|
||||
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 */
|
||||
|
||||
export const getMe = () => {
|
||||
@@ -903,7 +927,7 @@ export const getPrice = () => {
|
||||
export const getPhoneNumbers = (query: Partial<PhoneNumberQuery>) => {
|
||||
const qryStr = getQuery<Partial<PhoneNumberQuery>>(query);
|
||||
|
||||
return getFetch<PhoneNumber[]>(`${API_PHONE_NUMBERS}?${qryStr}`);
|
||||
return getFetch<PagedResponse<PhoneNumber>>(`${API_PHONE_NUMBERS}?${qryStr}`);
|
||||
};
|
||||
|
||||
export const getSpeechSupportedLanguagesAndVoices = (
|
||||
|
||||
@@ -557,12 +557,14 @@ export interface Client {
|
||||
|
||||
export interface PageQuery {
|
||||
page: number;
|
||||
page_size?: number;
|
||||
count: number;
|
||||
start?: string;
|
||||
days?: number;
|
||||
}
|
||||
|
||||
export interface PhoneNumberQuery extends PageQuery {
|
||||
service_provider_sid?: string;
|
||||
account_sid?: string;
|
||||
filter?: string;
|
||||
}
|
||||
@@ -572,6 +574,15 @@ export interface CallQuery extends PageQuery {
|
||||
answered?: string;
|
||||
}
|
||||
|
||||
export interface ApplicationQuery extends PageQuery {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface VoipCarrierQuery extends PageQuery {
|
||||
name?: string;
|
||||
account_sid?: string;
|
||||
}
|
||||
|
||||
export interface GoogleCustomVoicesQuery {
|
||||
speech_credential_sid?: string;
|
||||
label?: string;
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
toastSuccess,
|
||||
toastError,
|
||||
} from "src/store";
|
||||
import { getActiveSP, setActiveSP } from "src/store/localStore";
|
||||
import {
|
||||
getActiveSP,
|
||||
removeAccountFilter,
|
||||
setActiveSP,
|
||||
} from "src/store/localStore";
|
||||
import { postServiceProviders } from "src/api";
|
||||
|
||||
import type { NaviItem } from "./items";
|
||||
@@ -166,6 +170,7 @@ export const Navi = ({
|
||||
onChange={(e) => {
|
||||
setSid(e.target.value);
|
||||
setActiveSP(e.target.value);
|
||||
removeAccountFilter();
|
||||
navigate(ROUTE_LOGIN);
|
||||
}}
|
||||
disabled={user?.scope !== USER_ADMIN}
|
||||
|
||||
@@ -68,10 +68,10 @@ export const Alerts = () => {
|
||||
};
|
||||
|
||||
useMemo(() => {
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
if (!accountSid && user?.account_sid) setAccountSid(user?.account_sid);
|
||||
if (getQueryFilter()) {
|
||||
const [date] = getQueryFilter().split("/");
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
if (!accountSid && user?.account_sid) setAccountSid(user?.account_sid);
|
||||
setDateFilter(date);
|
||||
}
|
||||
}, [accountSid]);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { H1, M, Button, Icon } from "@jambonz/ui-kit";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { H1, M, Button, Icon, ButtonGroup, MS } from "@jambonz/ui-kit";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { deleteApplication, useServiceProviderData, useApiData } from "src/api";
|
||||
import {
|
||||
deleteApplication,
|
||||
useServiceProviderData,
|
||||
getApplications,
|
||||
} from "src/api";
|
||||
import {
|
||||
ROUTE_INTERNAL_APPLICATIONS,
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
@@ -13,20 +17,17 @@ import {
|
||||
Spinner,
|
||||
AccountFilter,
|
||||
SearchFilter,
|
||||
Pagination,
|
||||
SelectFilter,
|
||||
} from "src/components";
|
||||
import { DeleteApplication } from "./delete";
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import {
|
||||
isUserAccountScope,
|
||||
hasLength,
|
||||
hasValue,
|
||||
useFilteredResults,
|
||||
} from "src/utils";
|
||||
import { isUserAccountScope, hasLength, hasValue } from "src/utils";
|
||||
|
||||
import type { Application, Account } from "src/api/types";
|
||||
import { ScopedAccess } from "src/components/scoped-access";
|
||||
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";
|
||||
|
||||
export const Applications = () => {
|
||||
@@ -34,14 +35,51 @@ export const Applications = () => {
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [application, setApplication] = useState<Application | null>(null);
|
||||
const [apiUrl, setApiUrl] = useState("");
|
||||
const [applications, refetch] = useApiData<Application[]>(apiUrl);
|
||||
const [applications, setApplications] = useState<Application[] | null>(null);
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const filteredApplications = useFilteredResults<Application>(
|
||||
filter,
|
||||
applications,
|
||||
);
|
||||
const [applicationsTotal, setApplicationsTotal] = useState(0);
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
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 = () => {
|
||||
if (application) {
|
||||
@@ -53,8 +91,7 @@ export const Applications = () => {
|
||||
}
|
||||
deleteApplication(application.application_sid)
|
||||
.then(() => {
|
||||
// getApplications();
|
||||
refetch();
|
||||
fetchApplications(false);
|
||||
setApplication(null);
|
||||
toastSuccess(
|
||||
<>
|
||||
@@ -68,18 +105,44 @@ export const Applications = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial account
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (user?.account_sid && user.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
} else {
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
setAccountSid(
|
||||
getAccountFilter() || accountSid || accounts?.[0]?.account_sid || "",
|
||||
);
|
||||
}
|
||||
setLocation();
|
||||
}, [user, accounts]);
|
||||
|
||||
if (accountSid) {
|
||||
setApiUrl(`Accounts/${accountSid}/Applications`);
|
||||
}
|
||||
}, [accountSid, user]);
|
||||
// This single effect handles all data fetching triggers
|
||||
useEffect(() => {
|
||||
const accSid = accountSid || getAccountFilter() || "";
|
||||
|
||||
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 (
|
||||
<>
|
||||
@@ -100,6 +163,7 @@ export const Applications = () => {
|
||||
<SearchFilter
|
||||
placeholder="Filter applications"
|
||||
filter={[filter, setFilter]}
|
||||
delay={1000}
|
||||
/>
|
||||
<ScopedAccess user={user} scope={Scope.service_provider}>
|
||||
<AccountFilter
|
||||
@@ -108,12 +172,12 @@ export const Applications = () => {
|
||||
/>
|
||||
</ScopedAccess>
|
||||
</section>
|
||||
<Section {...(hasLength(filteredApplications) && { slim: true })}>
|
||||
<Section {...(hasLength(applications) && { slim: true })}>
|
||||
<div className="list">
|
||||
{!hasValue(applications) && hasLength(accounts) ? (
|
||||
<Spinner />
|
||||
) : hasLength(filteredApplications) ? (
|
||||
filteredApplications
|
||||
) : hasLength(applications) ? (
|
||||
applications
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((application) => {
|
||||
return (
|
||||
@@ -189,6 +253,26 @@ export const Applications = () => {
|
||||
</Button>
|
||||
</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 && (
|
||||
<DeleteApplication
|
||||
application={application}
|
||||
|
||||
@@ -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 { Button, H1, Icon, M } from "@jambonz/ui-kit";
|
||||
import { Button, ButtonGroup, H1, Icon, M, MS } from "@jambonz/ui-kit";
|
||||
import {
|
||||
deleteCarrier,
|
||||
deleteSipGateway,
|
||||
deleteSmppGateway,
|
||||
getFetch,
|
||||
getSPVoipCarriers,
|
||||
useApiData,
|
||||
useServiceProviderData,
|
||||
} from "src/api";
|
||||
@@ -17,20 +18,18 @@ import {
|
||||
Section,
|
||||
Spinner,
|
||||
SearchFilter,
|
||||
Pagination,
|
||||
SelectFilter,
|
||||
} from "src/components";
|
||||
import { ScopedAccess } from "src/components/scoped-access";
|
||||
import { Gateways } from "./gateways";
|
||||
import {
|
||||
isUserAccountScope,
|
||||
hasLength,
|
||||
hasValue,
|
||||
useFilteredResults,
|
||||
} from "src/utils";
|
||||
import { isUserAccountScope, hasLength, hasValue } from "src/utils";
|
||||
import {
|
||||
API_SIP_GATEWAY,
|
||||
API_SMPP_GATEWAY,
|
||||
CARRIER_REG_OK,
|
||||
ENABLE_HOSTED_SYSTEM,
|
||||
PER_PAGE_SELECTION,
|
||||
USER_ACCOUNT,
|
||||
} from "src/api/constants";
|
||||
import { DeleteCarrier } from "./delete";
|
||||
@@ -49,32 +48,55 @@ export const Carriers = () => {
|
||||
const user = useSelectState("user");
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [apiUrl, setApiUrl] = useState("");
|
||||
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 [accountSid, setAccountSid] = useState("");
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const carriersFiltered = useMemo(() => {
|
||||
setAccountSid(getAccountFilter());
|
||||
if (user?.account_sid && user?.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
const [carriersTotal, setCarriersTotal] = useState(0);
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [perPageFilter, setPerPageFilter] = useState("25");
|
||||
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
|
||||
? carriers.filter((carrier) =>
|
||||
accountSid
|
||||
? carrier.account_sid === accountSid
|
||||
: carrier.account_sid === null,
|
||||
)
|
||||
: [];
|
||||
}, [accountSid, carrier, carriers]);
|
||||
|
||||
const filteredCarriers = useFilteredResults<Carrier>(
|
||||
filter,
|
||||
carriersFiltered,
|
||||
);
|
||||
getSPVoipCarriers(currentServiceProvider.service_provider_sid, {
|
||||
page: currentPage,
|
||||
page_size: Number(perPageFilter),
|
||||
...(filter && { name: filter }),
|
||||
...(accountSid && { account_sid: accountSid }),
|
||||
})
|
||||
.then(({ json }) => {
|
||||
setCarriers(json.data);
|
||||
setCarriersTotal(json.total);
|
||||
setMaxPageNumber(Math.ceil(json.total / Number(perPageFilter)));
|
||||
})
|
||||
.catch((error) => {
|
||||
setCarriers([]);
|
||||
toastError(error.msg);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (carrier) {
|
||||
@@ -113,7 +135,7 @@ export const Carriers = () => {
|
||||
);
|
||||
});
|
||||
setCarrier(null);
|
||||
refetch();
|
||||
fetchCarriers(false);
|
||||
toastSuccess(
|
||||
<>
|
||||
Deleted Carrier <strong>{carrier.name}</strong>
|
||||
@@ -126,14 +148,45 @@ export const Carriers = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Initial account setup
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (currentServiceProvider) {
|
||||
setApiUrl(
|
||||
`ServiceProviders/${currentServiceProvider.service_provider_sid}/VoipCarriers`,
|
||||
);
|
||||
if (user?.account_sid && user?.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
} else {
|
||||
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 (
|
||||
<>
|
||||
@@ -159,6 +212,7 @@ export const Carriers = () => {
|
||||
<SearchFilter
|
||||
placeholder="Filter carriers"
|
||||
filter={[filter, setFilter]}
|
||||
delay={1000}
|
||||
/>
|
||||
<ScopedAccess user={user} scope={Scope.service_provider}>
|
||||
<AccountFilter
|
||||
@@ -169,12 +223,12 @@ export const Carriers = () => {
|
||||
/>
|
||||
</ScopedAccess>
|
||||
</section>
|
||||
<Section {...(hasLength(filteredCarriers) && { slim: true })}>
|
||||
<Section {...(hasLength(carriers) && { slim: true })}>
|
||||
<div className="list">
|
||||
{!hasValue(carriers) && hasLength(accounts) ? (
|
||||
<Spinner />
|
||||
) : hasLength(filteredCarriers) ? (
|
||||
filteredCarriers.map((carrier) => (
|
||||
) : hasLength(carriers) ? (
|
||||
carriers.map((carrier) => (
|
||||
<div className="item" key={carrier.voip_carrier_sid}>
|
||||
<div className="item__info">
|
||||
<div className="item__title">
|
||||
@@ -274,6 +328,26 @@ export const Carriers = () => {
|
||||
Add carrier
|
||||
</Button>
|
||||
</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 && (
|
||||
<DeleteCarrier
|
||||
carrier={carrier}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Scope } from "src/store/types";
|
||||
import { hasLength, hasValue, useFilteredResults } from "src/utils";
|
||||
import ClientsDelete from "./delete";
|
||||
import { USER_ACCOUNT } from "src/api/constants";
|
||||
import { getAccountFilter } from "src/store/localStore";
|
||||
|
||||
export const Clients = () => {
|
||||
const user = useSelectState("user");
|
||||
@@ -32,6 +33,7 @@ export const Clients = () => {
|
||||
const [client, setClient] = useState<Client | null>();
|
||||
|
||||
const tmpFilteredClients = useMemo(() => {
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
if (user?.account_sid && user?.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
return clients;
|
||||
|
||||
@@ -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 { Link } from "react-router-dom";
|
||||
|
||||
@@ -16,28 +16,26 @@ import {
|
||||
ApplicationFilter,
|
||||
SearchFilter,
|
||||
AccountFilter,
|
||||
Pagination,
|
||||
SelectFilter,
|
||||
} from "src/components";
|
||||
import {
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
ROUTE_INTERNAL_CARRIERS,
|
||||
ROUTE_INTERNAL_PHONE_NUMBERS,
|
||||
} from "src/router/routes";
|
||||
import {
|
||||
hasLength,
|
||||
hasValue,
|
||||
formatPhoneNumber,
|
||||
useFilteredResults,
|
||||
} from "src/utils";
|
||||
import { hasLength, hasValue, formatPhoneNumber } from "src/utils";
|
||||
import { DeletePhoneNumber } from "./delete";
|
||||
|
||||
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 { Scope } from "src/store/types";
|
||||
import { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
|
||||
export const PhoneNumbers = () => {
|
||||
const user = useSelectState("user");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [applications] = useServiceProviderData<Application[]>("Applications");
|
||||
const [carriers] = useServiceProviderData<Carrier[]>("VoipCarriers");
|
||||
@@ -51,35 +49,48 @@ export const PhoneNumbers = () => {
|
||||
const [applyMassEdit, setApplyMassEdit] = useState(false);
|
||||
const [filter, setFilter] = 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(() => {
|
||||
setAccountSid(getAccountFilter());
|
||||
return phoneNumbers
|
||||
? phoneNumbers.filter(
|
||||
(phn) => !accountSid || phn.account_sid === accountSid,
|
||||
)
|
||||
: [];
|
||||
}, [phoneNumbers, ...[!ENABLE_PHONE_NUMBER_LAZY_LOAD && accountSid]]);
|
||||
// Add ref to track previous values
|
||||
const prevValuesRef = useRef({
|
||||
serviceProviderId: "",
|
||||
accountSid: "",
|
||||
filter: "",
|
||||
pageNumber: 1,
|
||||
perPageFilter: "25",
|
||||
});
|
||||
|
||||
const filteredPhoneNumbers = !ENABLE_PHONE_NUMBER_LAZY_LOAD
|
||||
? useFilteredResults<PhoneNumber>(filter, phoneNumbersFiltered)
|
||||
: phoneNumbersFiltered;
|
||||
const fetchPhoneNumbers = (resetPage = false) => {
|
||||
setPhoneNumbers(null);
|
||||
|
||||
const refetch = () => {
|
||||
getPhoneNumbers(
|
||||
!ENABLE_PHONE_NUMBER_LAZY_LOAD
|
||||
? {}
|
||||
: {
|
||||
...(accountSid && { account_sid: accountSid }),
|
||||
...(filter && { filter }),
|
||||
},
|
||||
)
|
||||
// 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);
|
||||
}
|
||||
|
||||
const accSid = accountSid || getAccountFilter() || "";
|
||||
|
||||
getPhoneNumbers({
|
||||
page: currentPage,
|
||||
page_size: Number(perPageFilter),
|
||||
...(accSid && { account_sid: accSid }),
|
||||
...(filter && { filter }),
|
||||
})
|
||||
.then(({ json }) => {
|
||||
if (json) {
|
||||
setPhoneNumbers(json);
|
||||
setPhoneNumbers(json.data);
|
||||
setphoneNumbersTotal(json.total);
|
||||
setMaxPageNumber(Math.ceil(json.total / Number(perPageFilter)));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setPhoneNumbers([]);
|
||||
toastError(error.msg);
|
||||
});
|
||||
};
|
||||
@@ -95,9 +106,11 @@ export const PhoneNumbers = () => {
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
refetch();
|
||||
fetchPhoneNumbers(false);
|
||||
setApplicationSid("");
|
||||
setApplyMassEdit(false);
|
||||
setSelectAll(false);
|
||||
setSelectedPhoneNumbers([]);
|
||||
toastSuccess("Number routing updated successfully");
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -111,7 +124,7 @@ export const PhoneNumbers = () => {
|
||||
if (phoneNumber) {
|
||||
deletePhoneNumber(phoneNumber.phone_number_sid)
|
||||
.then(() => {
|
||||
refetch();
|
||||
fetchPhoneNumbers(false);
|
||||
setPhoneNumber(null);
|
||||
toastSuccess(
|
||||
<>
|
||||
@@ -125,21 +138,43 @@ export const PhoneNumbers = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Initial account setup
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (user?.account_sid && user.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
} else {
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
}
|
||||
setLocation();
|
||||
}, [user]);
|
||||
|
||||
// Combined effect for all data fetching
|
||||
useEffect(() => {
|
||||
if (ENABLE_PHONE_NUMBER_LAZY_LOAD) {
|
||||
setPhoneNumbers([]);
|
||||
return;
|
||||
}
|
||||
const prevValues = prevValuesRef.current;
|
||||
const currentSPId = currentServiceProvider?.service_provider_sid;
|
||||
|
||||
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 (
|
||||
<>
|
||||
@@ -160,6 +195,7 @@ export const PhoneNumbers = () => {
|
||||
<SearchFilter
|
||||
placeholder="Filter phone numbers"
|
||||
filter={[filter, setFilter]}
|
||||
delay={1000}
|
||||
/>
|
||||
<ScopedAccess user={user} scope={Scope.service_provider}>
|
||||
<AccountFilter
|
||||
@@ -168,25 +204,12 @@ export const PhoneNumbers = () => {
|
||||
defaultOption
|
||||
/>
|
||||
</ScopedAccess>
|
||||
{ENABLE_PHONE_NUMBER_LAZY_LOAD && (
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
small
|
||||
onClick={() => {
|
||||
setPhoneNumbers(null);
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</section>
|
||||
<Section {...(hasLength(filteredPhoneNumbers) && { slim: true })}>
|
||||
<Section {...(hasLength(phoneNumbers) && { slim: true })}>
|
||||
<div className="list">
|
||||
{!hasValue(phoneNumbers) ? (
|
||||
<Spinner />
|
||||
) : hasLength(filteredPhoneNumbers) ? (
|
||||
) : hasLength(phoneNumbers) ? (
|
||||
<>
|
||||
<div className="item item--actions">
|
||||
{accountSid ? (
|
||||
@@ -200,7 +223,7 @@ export const PhoneNumbers = () => {
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectAll(true);
|
||||
setSelectedPhoneNumbers(filteredPhoneNumbers);
|
||||
setSelectedPhoneNumbers(phoneNumbers);
|
||||
} else {
|
||||
setSelectAll(false);
|
||||
setSelectedPhoneNumbers([]);
|
||||
@@ -224,10 +247,8 @@ export const PhoneNumbers = () => {
|
||||
<Button
|
||||
small
|
||||
onClick={() => {
|
||||
handleMassEdit();
|
||||
setSelectAll(false);
|
||||
setApplyMassEdit(true);
|
||||
setSelectedPhoneNumbers([]);
|
||||
handleMassEdit();
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
@@ -249,7 +270,7 @@ export const PhoneNumbers = () => {
|
||||
</MS>
|
||||
)}
|
||||
</div>
|
||||
{filteredPhoneNumbers.map((phoneNumber) => {
|
||||
{phoneNumbers.map((phoneNumber) => {
|
||||
return (
|
||||
<div className="item" key={phoneNumber.phone_number_sid}>
|
||||
<div className="item__info">
|
||||
@@ -385,6 +406,26 @@ export const PhoneNumbers = () => {
|
||||
</Button>
|
||||
)}
|
||||
</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 && (
|
||||
<DeletePhoneNumber
|
||||
phoneNumber={phoneNumber}
|
||||
|
||||
@@ -87,10 +87,10 @@ export const RecentCalls = () => {
|
||||
};
|
||||
|
||||
useMemo(() => {
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
if (!accountSid && user?.account_sid) setAccountSid(user?.account_sid);
|
||||
if (getQueryFilter()) {
|
||||
const [date, direction, status] = getQueryFilter().split("/");
|
||||
setAccountSid(getAccountFilter() || accountSid);
|
||||
if (!accountSid && user?.account_sid) setAccountSid(user?.account_sid);
|
||||
setDateFilter(date);
|
||||
setDirectionFilter(direction);
|
||||
setStatusFilter(status);
|
||||
|
||||
@@ -27,6 +27,7 @@ import { ENABLE_HOSTED_SYSTEM, USER_ACCOUNT } from "src/api/constants";
|
||||
import type { UserData } from "src/store/types";
|
||||
import { toastError } from "src/store";
|
||||
import {
|
||||
clearLocalStorage,
|
||||
removeLocationBeforeOauth,
|
||||
removeOauthState,
|
||||
} from "src/store/localStore";
|
||||
@@ -163,7 +164,7 @@ export const useProvideAuth = (): AuthStateContext => {
|
||||
postLogout()
|
||||
.then((response) => {
|
||||
if (response.status === StatusCodes.NO_CONTENT) {
|
||||
localStorage.clear();
|
||||
clearLocalStorage();
|
||||
sessionStorage.clear();
|
||||
sessionStorage.setItem(SESS_FLASH_MSG, MSG_LOGGED_OUT);
|
||||
window.location.href = ROUTE_LOGIN;
|
||||
|
||||
@@ -123,7 +123,18 @@ export const checkLocation = () => {
|
||||
|
||||
if (currentLocation !== storedLocation) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
export const clearLocalStorage = () => {
|
||||
const toKeep = [storeActiveSP, storeAccountFilter];
|
||||
Object.keys(localStorage).forEach((key) => {
|
||||
if (!toKeep.includes(key)) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user