mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-07-04 19:21:58 +00:00
Feature/persist state (#191)
* persist SP implementation * add account filter check after refresh * store recent calls and alerts filter --------- Co-authored-by: EgleHelms <e.helms@cognigy.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { Icons } from "src/components/icons";
|
||||
|
||||
import type { Account } from "src/api/types";
|
||||
import { hasLength, sortLocaleName } from "src/utils";
|
||||
import { setAccountFilter } from "src/store/localStore";
|
||||
|
||||
export type AccountFilterProps = {
|
||||
label?: string;
|
||||
@@ -43,7 +44,10 @@ export const AccountFilter = ({
|
||||
id="account_filter"
|
||||
name="account_filter"
|
||||
value={accountSid}
|
||||
onChange={(e) => setAccountSid(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setAccountSid(e.target.value);
|
||||
setAccountFilter(e.target.value);
|
||||
}}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { classNames } from "jambonz-ui";
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
import type { SelectorOption } from "./forms/selector";
|
||||
import { setQueryFilter } from "src/store/localStore";
|
||||
import { createFilterString } from "src/utils";
|
||||
|
||||
type SelectFilterProps = {
|
||||
id: string;
|
||||
@@ -38,6 +40,11 @@ export const SelectFilter = ({
|
||||
value={filterValue}
|
||||
onChange={(e) => {
|
||||
setFilterValue(e.target.value);
|
||||
const advancedFilter = createFilterString(
|
||||
e.target.value,
|
||||
label as string
|
||||
);
|
||||
setQueryFilter(advancedFilter);
|
||||
|
||||
if (handleSelect) {
|
||||
handleSelect(e);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
toastSuccess,
|
||||
toastError,
|
||||
} from "src/store";
|
||||
import { getActiveSP, setActiveSP } from "src/store/localStore";
|
||||
import { postServiceProviders } from "src/api";
|
||||
|
||||
import type { NaviItem } from "./items";
|
||||
@@ -98,6 +99,7 @@ export const Navi = ({
|
||||
);
|
||||
dispatch({ type: "serviceProviders" });
|
||||
setSid(json.sid);
|
||||
setActiveSP(json.sid);
|
||||
setName("");
|
||||
setModal(false);
|
||||
})
|
||||
@@ -113,6 +115,7 @@ export const Navi = ({
|
||||
|
||||
/** Subscribe to change events on the service provider <select> */
|
||||
useEffect(() => {
|
||||
setSid(getActiveSP());
|
||||
if (sid) {
|
||||
const serviceProvider = serviceProviders.find(
|
||||
(sp) => sp.service_provider_sid === sid
|
||||
@@ -153,8 +156,11 @@ export const Navi = ({
|
||||
<div className="smsel smsel--navi">
|
||||
<div>
|
||||
<select
|
||||
value={currentServiceProvider?.service_provider_sid}
|
||||
onChange={(e) => setSid(e.target.value)}
|
||||
value={sid || currentServiceProvider?.service_provider_sid}
|
||||
onChange={(e) => {
|
||||
setSid(e.target.value);
|
||||
setActiveSP(e.target.value);
|
||||
}}
|
||||
disabled={user?.scope !== USER_ADMIN}
|
||||
>
|
||||
{currentServiceProvider ? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { ButtonGroup, H1, M, MS } from "jambonz-ui";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@@ -22,6 +22,11 @@ import {
|
||||
import type { Account, Alert, PageQuery } from "src/api/types";
|
||||
import { ScopedAccess } from "src/components/scoped-access";
|
||||
import { Scope } from "src/store/types";
|
||||
import {
|
||||
getAccountFilter,
|
||||
getQueryFilter,
|
||||
setLocation,
|
||||
} from "src/store/localStore";
|
||||
|
||||
export const Alerts = () => {
|
||||
const user = useSelectState("user");
|
||||
@@ -57,7 +62,16 @@ export const Alerts = () => {
|
||||
});
|
||||
};
|
||||
|
||||
useMemo(() => {
|
||||
if (getQueryFilter()) {
|
||||
const [date] = getQueryFilter().split("/");
|
||||
setAccountSid(getAccountFilter());
|
||||
setDateFilter(date);
|
||||
}
|
||||
}, [accountSid]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (user?.account_sid && user.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import type {
|
||||
} from "src/api/types";
|
||||
import { MSG_REQUIRED_FIELDS, MSG_WEBHOOK_FIELDS } from "src/constants";
|
||||
import { isUserAccountScope, useRedirect } from "src/utils";
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
|
||||
type ApplicationFormProps = {
|
||||
application?: UseApiDataMap<Application>;
|
||||
@@ -172,6 +173,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
.then(() => {
|
||||
toastSuccess("Application created successfully");
|
||||
navigate(ROUTE_INTERNAL_APPLICATIONS);
|
||||
setAccountFilter(accountSid);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
@@ -180,6 +182,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (application && application.data) {
|
||||
setApplicationName(application.data.name);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ 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 { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
|
||||
export const Applications = () => {
|
||||
const user = useSelectState("user");
|
||||
@@ -68,10 +69,13 @@ export const Applications = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (user?.account_sid && user.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
}
|
||||
|
||||
setAccountSid(getAccountFilter());
|
||||
|
||||
if (accountSid) {
|
||||
setApiUrl(`Accounts/${accountSid}/Applications`);
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ import type {
|
||||
Smpp,
|
||||
Application,
|
||||
} from "src/api/types";
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
|
||||
type CarrierFormProps = {
|
||||
carrier?: UseApiDataMap<Carrier>;
|
||||
@@ -532,6 +533,7 @@ export const CarrierForm = ({
|
||||
|
||||
toastSuccess("Carrier created successfully");
|
||||
navigate(ROUTE_INTERNAL_CARRIERS);
|
||||
setAccountFilter(accountSid);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
@@ -541,6 +543,7 @@ export const CarrierForm = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (predefinedName && hasLength(predefinedCarriers)) {
|
||||
const predefinedCarrierSid = predefinedCarriers.find(
|
||||
(a) => a.name === predefinedName
|
||||
|
||||
@@ -35,6 +35,7 @@ import { DeleteCarrier } from "./delete";
|
||||
|
||||
import type { Account, Carrier, SipGateway, SmppGateway } from "src/api/types";
|
||||
import { Scope } from "src/store/types";
|
||||
import { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
|
||||
export const Carriers = () => {
|
||||
const user = useSelectState("user");
|
||||
@@ -47,6 +48,7 @@ export const Carriers = () => {
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const carriersFiltered = useMemo(() => {
|
||||
setAccountSid(getAccountFilter());
|
||||
if (user?.scope === USER_ACCOUNT) {
|
||||
return carriers;
|
||||
}
|
||||
@@ -116,6 +118,7 @@ export const Carriers = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (accountSid) {
|
||||
setApiUrl(`Accounts/${accountSid}/VoipCarriers`);
|
||||
} else if (currentServiceProvider) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
Carrier,
|
||||
UseApiDataMap,
|
||||
} from "src/api/types";
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
|
||||
type PhoneNumberFormProps = {
|
||||
phoneNumber?: UseApiDataMap<PhoneNumber>;
|
||||
@@ -104,6 +105,7 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
|
||||
.then(() => {
|
||||
toastSuccess("Phone number created successfully");
|
||||
navigate(ROUTE_INTERNAL_PHONE_NUMBERS);
|
||||
setAccountFilter(accountSid);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
@@ -112,6 +114,7 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (phoneNumber && phoneNumber.data) {
|
||||
setPhoneNumberNum(phoneNumber.data.number);
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import type { Account, PhoneNumber, Carrier, Application } from "src/api/types";
|
||||
import { 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");
|
||||
@@ -52,6 +53,7 @@ export const PhoneNumbers = () => {
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
|
||||
const phoneNumbersFiltered = useMemo(() => {
|
||||
setAccountSid(getAccountFilter());
|
||||
return phoneNumbers
|
||||
? phoneNumbers.filter(
|
||||
(phn) => !accountSid || phn.account_sid === accountSid
|
||||
@@ -106,6 +108,7 @@ export const PhoneNumbers = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (user?.account_sid && user.scope === USER_ACCOUNT) {
|
||||
setAccountSid(user?.account_sid);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { ButtonGroup, H1, M, MS } from "jambonz-ui";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@@ -22,6 +22,11 @@ import { DetailsItem } from "./details";
|
||||
import type { Account, CallQuery, RecentCall } from "src/api/types";
|
||||
import { ScopedAccess } from "src/components/scoped-access";
|
||||
import { Scope } from "src/store/types";
|
||||
import {
|
||||
getAccountFilter,
|
||||
getQueryFilter,
|
||||
setLocation,
|
||||
} from "src/store/localStore";
|
||||
|
||||
const directionSelection = [
|
||||
{ name: "either", value: "io" },
|
||||
@@ -73,7 +78,18 @@ export const RecentCalls = () => {
|
||||
});
|
||||
};
|
||||
|
||||
useMemo(() => {
|
||||
if (getQueryFilter()) {
|
||||
const [date, direction, status] = getQueryFilter().split("/");
|
||||
setAccountSid(getAccountFilter());
|
||||
setDateFilter(date);
|
||||
setDirectionFilter(direction);
|
||||
setStatusFilter(status);
|
||||
}
|
||||
}, [accountSid]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (accountSid) {
|
||||
handleFilterChange();
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import { CredentialStatus } from "./status";
|
||||
|
||||
import type { RegionVendors, GoogleServiceKey, Vendor } from "src/vendor/types";
|
||||
import type { Account, SpeechCredential, UseApiDataMap } from "src/api/types";
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
|
||||
type SpeechServiceFormProps = {
|
||||
credential?: UseApiDataMap<SpeechCredential>;
|
||||
@@ -176,6 +177,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
.then(() => {
|
||||
toastSuccess("Speech credential created successfully");
|
||||
navigate(ROUTE_INTERNAL_SPEECH);
|
||||
setAccountFilter(accountSid);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
@@ -185,6 +187,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (credential && credential.data) {
|
||||
if (credential.data.vendor) {
|
||||
setVendor(credential.data.vendor);
|
||||
|
||||
@@ -25,6 +25,7 @@ import { CredentialStatus } from "./status";
|
||||
import type { SpeechCredential, Account } from "src/api/types";
|
||||
import { ScopedAccess } from "src/components/scoped-access";
|
||||
import { Scope } from "src/store/types";
|
||||
import { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
|
||||
export const SpeechServices = () => {
|
||||
const user = useSelectState("user");
|
||||
@@ -37,6 +38,7 @@ export const SpeechServices = () => {
|
||||
const [filter] = useState("");
|
||||
|
||||
const credentialsFiltered = useMemo(() => {
|
||||
setAccountSid(getAccountFilter());
|
||||
if (user?.scope === USER_ACCOUNT) {
|
||||
return credentials;
|
||||
}
|
||||
@@ -83,6 +85,7 @@ export const SpeechServices = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (accountSid) {
|
||||
setApiUrl(`Accounts/${accountSid}/SpeechCredentials`);
|
||||
} else if (currentServiceProvider) {
|
||||
|
||||
@@ -37,6 +37,7 @@ import type {
|
||||
Account,
|
||||
} from "src/api/types";
|
||||
import type { IMessage } from "src/store/types";
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
|
||||
type UserFormProps = {
|
||||
user?: UseApiDataMap<User>;
|
||||
@@ -126,6 +127,7 @@ export const UserForm = ({ user }: UserFormProps) => {
|
||||
.then(() => {
|
||||
toastSuccess("User created successfully");
|
||||
navigate(ROUTE_INTERNAL_USERS);
|
||||
setAccountFilter(accountSid);
|
||||
})
|
||||
.catch((error: { msg: IMessage }) => {
|
||||
toastError(error.msg);
|
||||
@@ -165,6 +167,7 @@ export const UserForm = ({ user }: UserFormProps) => {
|
||||
|
||||
/** Set current user data values if applicable -- e.g. "edit mode" */
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (user && user.data) {
|
||||
setName(user.data.name);
|
||||
setForceChange(!!user.data.force_change);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { H1, Button, Icon } from "jambonz-ui";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
@@ -30,6 +30,7 @@ import type { Account, User } from "src/api/types";
|
||||
import { useSelectState } from "src/store";
|
||||
import { ScopedAccess } from "src/components/scoped-access";
|
||||
import { Scope } from "src/store/types";
|
||||
import { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
|
||||
export const Users = () => {
|
||||
const user = useSelectState("user");
|
||||
@@ -41,6 +42,7 @@ export const Users = () => {
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
|
||||
const usersFiltered = useMemo(() => {
|
||||
setAccountSid(getAccountFilter());
|
||||
const serviceProviderUsers = users?.filter((e) => {
|
||||
return (
|
||||
e.scope === USER_ADMIN ||
|
||||
@@ -72,6 +74,10 @@ export const Users = () => {
|
||||
sortUsersAlpha
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="mast">
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* The key used to store active Service Provider in localStorage
|
||||
*/
|
||||
const storeActiveSP = "activeServiceProvider";
|
||||
|
||||
/**
|
||||
* Methods to get/set the token from local storage
|
||||
*/
|
||||
export const getActiveSP = () => {
|
||||
return localStorage.getItem(storeActiveSP) || "";
|
||||
};
|
||||
|
||||
export const setActiveSP = (sid: string) => {
|
||||
localStorage.setItem(storeActiveSP, sid);
|
||||
};
|
||||
|
||||
/**
|
||||
* The key used to store active Filter in localStorage
|
||||
*/
|
||||
const storeAccountFilter = "accountFilter";
|
||||
|
||||
/**
|
||||
* Methods to get/set the account selected in the filter from local storage
|
||||
*/
|
||||
export const getAccountFilter = () => {
|
||||
checkLocation();
|
||||
return localStorage.getItem(storeAccountFilter) || "";
|
||||
};
|
||||
|
||||
export const setAccountFilter = (accountSid: string) => {
|
||||
localStorage.setItem(storeAccountFilter, accountSid);
|
||||
};
|
||||
|
||||
export const removeAccountFilter = () => {
|
||||
return localStorage.removeItem(storeAccountFilter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Methods to get/set the RecentCalls and Alerts selected filters from local storage
|
||||
*/
|
||||
|
||||
const storeQueryFilter = "advancedFilter";
|
||||
|
||||
export const setQueryFilter = (combinedFilterString: string) => {
|
||||
return localStorage.setItem(storeQueryFilter, combinedFilterString);
|
||||
};
|
||||
|
||||
export const getQueryFilter = () => {
|
||||
checkLocation();
|
||||
return localStorage.getItem(storeQueryFilter) || "";
|
||||
};
|
||||
|
||||
export const removeAdvancedFilter = () => {
|
||||
return localStorage.removeItem(storeQueryFilter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Methods to get/set the location from local storage
|
||||
*/
|
||||
|
||||
const storeLocation = "location";
|
||||
|
||||
export const setLocation = () => {
|
||||
return localStorage.setItem(
|
||||
storeLocation,
|
||||
window.location.pathname.split("/")[2]
|
||||
);
|
||||
};
|
||||
|
||||
export const getLocation = () => {
|
||||
return localStorage.getItem(storeLocation);
|
||||
};
|
||||
|
||||
export const checkLocation = () => {
|
||||
const currentLocation = window.location.pathname.split("/")[2];
|
||||
const storedLocation = getLocation();
|
||||
|
||||
if (currentLocation !== storedLocation) {
|
||||
localStorage.removeItem(storeQueryFilter);
|
||||
localStorage.removeItem(storeAccountFilter);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
UserScopes,
|
||||
} from "src/api/types";
|
||||
import type { UserData } from "src/store/types";
|
||||
import { getQueryFilter } from "src/store/localStore";
|
||||
|
||||
export const hasValue = <Type>(
|
||||
variable: Type | null | undefined
|
||||
@@ -211,6 +212,19 @@ export const filterScopeOptions = (
|
||||
return optionArray;
|
||||
};
|
||||
|
||||
export const createFilterString = (filterValue: string, label: string) => {
|
||||
const filterString = getQueryFilter().split("/");
|
||||
|
||||
if (label === "Date") {
|
||||
filterString.splice(0, 0, filterValue);
|
||||
} else if (label === "Direction") {
|
||||
filterString.splice(1, 1, filterValue);
|
||||
} else if (label === "Status") {
|
||||
filterString.splice(2, 2, filterValue);
|
||||
}
|
||||
return filterString.join("/");
|
||||
};
|
||||
|
||||
export {
|
||||
withSuspense,
|
||||
useMobileMedia,
|
||||
|
||||
Reference in New Issue
Block a user