add response interceptors for refreshTokens method
This commit is contained in:
committed by
Daniel Hutzel
parent
76cbf7f9ca
commit
938abb6387
13
media-store/app/.vscode/launch.json
vendored
Normal file
13
media-store/app/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Chrome",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceRoot}/src"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
"@umijs/hooks": "^1.9.3",
|
||||
"antd": "^4.8.2",
|
||||
"axios": "^0.20.0",
|
||||
"events": "^3.2.0",
|
||||
"lodash": "^4.17.20",
|
||||
"moment": "^2.29.1",
|
||||
"react": "^16.14.0",
|
||||
@@ -18,9 +19,9 @@
|
||||
"react-scripts": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"start": "react-scripts start --no-cache",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"test": "react-scripts test --no-cache",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
@import "~antd/dist/antd.css";
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
section.ant-layout {
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Layout
|
||||
*/
|
||||
.site-layout .site-layout-background {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -36,7 +55,3 @@
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu.ant-menu-sub.ant-menu-vertical {
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ import React from "react";
|
||||
import "antd/dist/antd.css";
|
||||
import "./App.css";
|
||||
import { Layout } from "antd";
|
||||
import { MyRouter } from "./Router";
|
||||
import { GlobalContextProvider } from "./GlobalContext";
|
||||
import { MyRouter } from "./components/Router";
|
||||
import { AppStateContextProvider } from "./contexts/AppStateContext";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<Layout style={{ height: "100%" }}>
|
||||
<GlobalContextProvider>
|
||||
<AppStateContextProvider>
|
||||
<MyRouter />
|
||||
</GlobalContextProvider>
|
||||
</AppStateContextProvider>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import React, { useMemo, createContext, useContext, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { isEmpty, isArray } from "lodash";
|
||||
|
||||
const globalContext = {
|
||||
error: {},
|
||||
loading: true,
|
||||
user: {
|
||||
ID: undefined,
|
||||
roles: [],
|
||||
email: undefined,
|
||||
token: undefined,
|
||||
},
|
||||
locale: undefined,
|
||||
invoicedItems: [],
|
||||
notifications: [],
|
||||
};
|
||||
const GlobalContext = createContext(globalContext);
|
||||
const useGlobals = () => useContext(GlobalContext);
|
||||
const AVAILABLE_LOCALES = ["en", "fr", "de"];
|
||||
|
||||
const isValidUser = (user) => {
|
||||
return (
|
||||
!isEmpty(user) &&
|
||||
user.ID &&
|
||||
user.roles &&
|
||||
user.email &&
|
||||
user.token &&
|
||||
isArray(user.roles)
|
||||
);
|
||||
};
|
||||
|
||||
const resetAxiosParams = () => {
|
||||
delete axios.defaults.headers.common["Authorization"];
|
||||
delete axios.defaults.userEntity;
|
||||
axios.defaults.tracksEntity = "Tracks";
|
||||
};
|
||||
|
||||
const setAxiosParams = (user) => {
|
||||
axios.defaults.headers.common["Authorization"] = `Basic ${user.token}`;
|
||||
axios.defaults.userID = user.ID;
|
||||
if (user.roles.includes("customer")) {
|
||||
axios.defaults.userEntity = `Customers/${user.ID}`;
|
||||
axios.defaults.tracksEntity = "MarkedTracks";
|
||||
} else {
|
||||
axios.defaults.userEntity = `Employees/${user.ID}`;
|
||||
axios.defaults.tracksEntity = "Tracks";
|
||||
}
|
||||
};
|
||||
|
||||
const useUserData = () => {
|
||||
const getUserDataFromLS = () => {
|
||||
let userFromLS;
|
||||
try {
|
||||
userFromLS = JSON.parse(localStorage.getItem("user"));
|
||||
} catch (e) {}
|
||||
if (isValidUser(userFromLS)) {
|
||||
setAxiosParams(userFromLS);
|
||||
return userFromLS;
|
||||
} else {
|
||||
localStorage.removeItem("user");
|
||||
resetAxiosParams();
|
||||
}
|
||||
};
|
||||
|
||||
const setUserDataToLS = (value) => {
|
||||
if (isValidUser(value)) {
|
||||
localStorage.setItem("user", JSON.stringify(value));
|
||||
setAxiosParams(value);
|
||||
} else {
|
||||
localStorage.removeItem("user");
|
||||
resetAxiosParams();
|
||||
}
|
||||
};
|
||||
|
||||
const setLocaleToLS = (value) => {
|
||||
localStorage.setItem("locale", value);
|
||||
axios.defaults.headers.common["Accept-language"] = value;
|
||||
};
|
||||
|
||||
const getLocaleFromLS = () => {
|
||||
const localeFromLS = localStorage.getItem("locale");
|
||||
const selectedLocale =
|
||||
localeFromLS &&
|
||||
localeFromLS !== "undefined" &&
|
||||
AVAILABLE_LOCALES.includes(localeFromLS)
|
||||
? localeFromLS
|
||||
: "en";
|
||||
axios.defaults.headers.common["Accept-language"] = selectedLocale;
|
||||
|
||||
return selectedLocale;
|
||||
};
|
||||
|
||||
return { getUserDataFromLS, setUserDataToLS, setLocaleToLS, getLocaleFromLS };
|
||||
};
|
||||
|
||||
const GlobalContextProvider = ({ children }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState({});
|
||||
const [invoicedItems, setInvoicedItems] = useState([]);
|
||||
const [user, setUser] = useState(null);
|
||||
const [locale, setLocale] = useState(undefined);
|
||||
const {
|
||||
getUserDataFromLS,
|
||||
setUserDataToLS,
|
||||
getLocaleFromLS,
|
||||
setLocaleToLS,
|
||||
} = useUserData();
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
error: error,
|
||||
loading: loading,
|
||||
invoicedItems: invoicedItems,
|
||||
user: user ? user : getUserDataFromLS(),
|
||||
locale: locale ? locale : getLocaleFromLS(),
|
||||
setLoading,
|
||||
setError,
|
||||
setInvoicedItems,
|
||||
setUser: (userParam) => {
|
||||
setUserDataToLS(userParam);
|
||||
setUser(userParam);
|
||||
},
|
||||
setLocale: (localeParam) => {
|
||||
setLocaleToLS(localeParam);
|
||||
setLocale(localeParam);
|
||||
},
|
||||
}),
|
||||
[locale, user, loading, error, invoicedItems]
|
||||
);
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={value}>{children}</GlobalContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { GlobalContextProvider, useGlobals };
|
||||
21
media-store/app/src/api/axiosInstance.js
Normal file
21
media-store/app/src/api/axiosInstance.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import axios from "axios";
|
||||
import { getUserFromLS, getLocaleFromLS } from "../util/localStorageService";
|
||||
import {
|
||||
changeUserDefaults,
|
||||
changeLocaleDefaults,
|
||||
} from "./changeAxiosDefaults";
|
||||
import { API } from "../util/constants";
|
||||
import { responseErrorInterceptor } from "./responseErrorInterceptor";
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: API,
|
||||
timeout: 1000,
|
||||
});
|
||||
const user = getUserFromLS();
|
||||
const locale = getLocaleFromLS();
|
||||
changeUserDefaults(user);
|
||||
changeLocaleDefaults(locale);
|
||||
|
||||
axiosInstance.interceptors.response.use(null, responseErrorInterceptor);
|
||||
|
||||
export { axiosInstance, changeLocaleDefaults, changeUserDefaults };
|
||||
@@ -1,15 +1,10 @@
|
||||
import { isEmpty } from "lodash";
|
||||
import axios from "axios";
|
||||
import { axiosInstance } from "./axiosInstance";
|
||||
|
||||
// in dev mode using provided api
|
||||
// in prod mode using proxy
|
||||
const API =
|
||||
process.env.NODE_ENV === "development" ? "http://localhost:4004/" : "api/";
|
||||
|
||||
const BROWSE_TRACKS_SERVICE = `${API}browse-tracks`;
|
||||
const INVOICES_SERVICE = `${API}browse-invoices`;
|
||||
const USER_SERVICE = `${API}users`;
|
||||
const MANAGE_STORE = `${API}manage-store`;
|
||||
const BROWSE_TRACKS_SERVICE = "browse-tracks";
|
||||
const INVOICES_SERVICE = "browse-invoices";
|
||||
const USER_SERVICE = "users";
|
||||
const MANAGE_STORE = "manage-store";
|
||||
|
||||
const constructGenresQuery = (genreIds) => {
|
||||
return !isEmpty(genreIds)
|
||||
@@ -29,26 +24,31 @@ const fetchTacks = ({
|
||||
}`;
|
||||
};
|
||||
|
||||
return axios.get(`${BROWSE_TRACKS_SERVICE}/${axios.defaults.tracksEntity}`, {
|
||||
params: {},
|
||||
paramsSerializer: () => serializeTracksUrl(),
|
||||
});
|
||||
return axiosInstance.get(
|
||||
`${BROWSE_TRACKS_SERVICE}/${axiosInstance.defaults.tracksEntity}`,
|
||||
{
|
||||
params: {},
|
||||
paramsSerializer: () => serializeTracksUrl(),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const countTracks = ({ genreIds = [], substr = "" } = {}) => {
|
||||
return axios.get(
|
||||
`${BROWSE_TRACKS_SERVICE}/${axios.defaults.tracksEntity}/$count?$filter=${
|
||||
const tracksEntity = axiosInstance.defaults.tracksEntity;
|
||||
|
||||
return axiosInstance.get(
|
||||
`${BROWSE_TRACKS_SERVICE}/${tracksEntity}/$count?$filter=${
|
||||
`contains(name,'${substr}')` + constructGenresQuery(genreIds)
|
||||
}`
|
||||
);
|
||||
};
|
||||
|
||||
const fetchGenres = () => {
|
||||
return axios.get(`${BROWSE_TRACKS_SERVICE}/Genres`);
|
||||
return axiosInstance.get(`${BROWSE_TRACKS_SERVICE}/Genres`);
|
||||
};
|
||||
|
||||
const invoice = (tracks) => {
|
||||
return axios.post(
|
||||
return axiosInstance.post(
|
||||
`${INVOICES_SERVICE}/invoice`,
|
||||
{
|
||||
tracks: tracks.map(({ unitPrice, ID }) => ({
|
||||
@@ -63,12 +63,14 @@ const invoice = (tracks) => {
|
||||
};
|
||||
|
||||
const fetchPerson = () => {
|
||||
return axios.get(`${USER_SERVICE}/${axios.defaults.userEntity}`);
|
||||
return axiosInstance.get(
|
||||
`${USER_SERVICE}/${axiosInstance.defaults.userEntity}`
|
||||
);
|
||||
};
|
||||
|
||||
const confirmPerson = (person) => {
|
||||
return axios.put(
|
||||
`${USER_SERVICE}/${axios.defaults.userEntity}`,
|
||||
return axiosInstance.put(
|
||||
`${USER_SERVICE}/${axiosInstance.defaults.userEntity}`,
|
||||
{
|
||||
...person,
|
||||
},
|
||||
@@ -79,13 +81,13 @@ const confirmPerson = (person) => {
|
||||
};
|
||||
|
||||
const fetchInvoices = () => {
|
||||
return axios.get(
|
||||
return axiosInstance.get(
|
||||
`${INVOICES_SERVICE}/Invoices?$expand=invoiceItems($expand=track($expand=album($expand=artist)))`
|
||||
);
|
||||
};
|
||||
|
||||
const cancelInvoice = (ID) => {
|
||||
return axios.post(
|
||||
return axiosInstance.post(
|
||||
`${INVOICES_SERVICE}/cancelInvoice`,
|
||||
{
|
||||
ID,
|
||||
@@ -97,61 +99,71 @@ const cancelInvoice = (ID) => {
|
||||
};
|
||||
|
||||
const fetchAlbumsByName = (substr = "", top) => {
|
||||
return axios.get(
|
||||
return axiosInstance.get(
|
||||
`${BROWSE_TRACKS_SERVICE}/Albums?$filter=${`contains(title,'${substr}')&$top=${top}`}`
|
||||
);
|
||||
};
|
||||
|
||||
const addTrack = (data) => {
|
||||
return axios.post(`${MANAGE_STORE}/Tracks`, data, {
|
||||
headers: { "content-type": "application/json" },
|
||||
return axiosInstance.post(`${MANAGE_STORE}/Tracks`, data, {
|
||||
headers: { "content-type": "application/json;IEEE754Compatible=true" },
|
||||
});
|
||||
};
|
||||
|
||||
const addArtist = (data) => {
|
||||
return axios.post(`${MANAGE_STORE}/Artists`, data, {
|
||||
return axiosInstance.post(`${MANAGE_STORE}/Artists`, data, {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
const addAlbum = (data) => {
|
||||
return axios.post(`${MANAGE_STORE}/Albums`, data, {
|
||||
return axiosInstance.post(`${MANAGE_STORE}/Albums`, data, {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
const fetchArtistsByName = (substr = "", top) => {
|
||||
return axios.get(
|
||||
return axiosInstance.get(
|
||||
`${MANAGE_STORE}/Artists?$filter=${`contains(name,'${substr}')&$top=${top}`}`
|
||||
);
|
||||
};
|
||||
|
||||
const login = (data) => {
|
||||
return axios.post(`${USER_SERVICE}/login`, data, {
|
||||
return axiosInstance.post(`${USER_SERVICE}/login`, data, {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
const updateTrack = (track) => {
|
||||
return axios.put(
|
||||
return axiosInstance.put(
|
||||
`${MANAGE_STORE}/Tracks/${track.ID}`,
|
||||
{
|
||||
...track,
|
||||
},
|
||||
{
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: { "content-type": "application/json;IEEE754Compatible=true" },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const getTrack = (ID) => {
|
||||
return axios.get(
|
||||
`${BROWSE_TRACKS_SERVICE}/${axios.defaults.tracksEntity}/${ID}?$expand=genre,album($expand=artist)`
|
||||
return axiosInstance.get(
|
||||
`${BROWSE_TRACKS_SERVICE}/${axiosInstance.defaults.tracksEntity}/${ID}?$expand=genre,album($expand=artist)`
|
||||
);
|
||||
};
|
||||
|
||||
const deleteTrack = (ID) => {
|
||||
return axios.delete(`${MANAGE_STORE}/Tracks(${ID})`);
|
||||
return axiosInstance.delete(`${MANAGE_STORE}/Tracks(${ID})`);
|
||||
};
|
||||
|
||||
const refreshTokens = (refreshToken) => {
|
||||
return axiosInstance.post(
|
||||
`${USER_SERVICE}/refreshTokens`,
|
||||
{ refreshToken },
|
||||
{
|
||||
headers: { "content-type": "application/json" },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -172,4 +184,5 @@ export {
|
||||
updateTrack,
|
||||
getTrack,
|
||||
deleteTrack,
|
||||
refreshTokens,
|
||||
};
|
||||
27
media-store/app/src/api/changeAxiosDefaults.js
Normal file
27
media-store/app/src/api/changeAxiosDefaults.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { axiosInstance } from "./axiosInstance";
|
||||
|
||||
function changeUserDefaults(currentUser) {
|
||||
if (currentUser) {
|
||||
axiosInstance.defaults.headers.common[
|
||||
"Authorization"
|
||||
] = `Basic ${currentUser.accessToken}`;
|
||||
axiosInstance.defaults.userID = currentUser.ID;
|
||||
if (currentUser.roles.includes("customer")) {
|
||||
axiosInstance.defaults.userEntity = `Customers/${currentUser.ID}`;
|
||||
axiosInstance.defaults.tracksEntity = "MarkedTracks";
|
||||
} else {
|
||||
axiosInstance.defaults.userEntity = `Employees/${currentUser.ID}`;
|
||||
axiosInstance.defaults.tracksEntity = "Tracks";
|
||||
}
|
||||
} else {
|
||||
axiosInstance.defaults.tracksEntity = "Tracks";
|
||||
}
|
||||
}
|
||||
|
||||
function changeLocaleDefaults(locale) {
|
||||
if (locale) {
|
||||
axiosInstance.defaults.headers.common["Accept-language"] = locale;
|
||||
}
|
||||
}
|
||||
|
||||
export { changeLocaleDefaults, changeUserDefaults };
|
||||
64
media-store/app/src/api/responseErrorInterceptor.js
Normal file
64
media-store/app/src/api/responseErrorInterceptor.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { emitter } from "../util/EventEmitter";
|
||||
import { axiosInstance } from "./axiosInstance";
|
||||
import { refreshTokens } from "./calls";
|
||||
import { getUserFromLS } from "../util/localStorageService";
|
||||
|
||||
let isRefreshing = false;
|
||||
let subscribers = [];
|
||||
|
||||
function responseErrorInterceptor(error) {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.response && error.response.status === 401) {
|
||||
if (originalRequest.url === "users/login") {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// if users/refreshTokens request failed
|
||||
if (isRefreshing && originalRequest.url === "users/refreshTokens") {
|
||||
subscribers.forEach((request) => request.reject(error));
|
||||
subscribers = [];
|
||||
isRefreshing = false;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// if got a 401 error we sending users/refreshTokens request
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
|
||||
refreshTokens(getUserFromLS().refreshToken)
|
||||
.then((response) => {
|
||||
emitter.emit("UPDATE_USER", response.data);
|
||||
subscribers.forEach((request) =>
|
||||
request.resolve(response.data.accessToken)
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
subscribers.forEach((request) => request.reject(error));
|
||||
})
|
||||
.finally(() => {
|
||||
subscribers = [];
|
||||
isRefreshing = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// holding requests which should be sended after users/refreshTokens complete
|
||||
// otherwise if users/refreshTokens failed an error will be thrown
|
||||
return new Promise((resolve, reject) => {
|
||||
subscribers.push({
|
||||
resolve: (newAccessToken) => {
|
||||
originalRequest.headers.Authorization = "Basic " + newAccessToken;
|
||||
resolve(axiosInstance(originalRequest));
|
||||
},
|
||||
reject: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
export { responseErrorInterceptor };
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { Breadcrumb, Spin } from "antd";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useGlobals } from "./GlobalContext";
|
||||
import { useAppState } from "../hooks/useAppState";
|
||||
|
||||
const names = {
|
||||
"/": "Browse / Tracks",
|
||||
@@ -13,7 +13,7 @@ const names = {
|
||||
|
||||
const CurrentPageHeader = () => {
|
||||
const location = useLocation();
|
||||
const { loading } = useGlobals();
|
||||
const { loading } = useAppState();
|
||||
|
||||
return (
|
||||
<Breadcrumb
|
||||
@@ -39,7 +39,6 @@ const Editable = ({ value, onChange, type }) => {
|
||||
fontWeight: 600,
|
||||
outline: "none",
|
||||
border: "none",
|
||||
borderRadius: 6,
|
||||
backgroundColor: "white",
|
||||
padding: "0 2px",
|
||||
}}
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
LoginOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { useGlobals } from "./GlobalContext";
|
||||
import { useAppState } from "../hooks/useAppState";
|
||||
import { setLocaleToLS } from "../util/localStorageService";
|
||||
import { changeLocaleDefaults } from "../api/axiosInstance";
|
||||
import { emitter } from "../util/EventEmitter";
|
||||
import "./Header.css";
|
||||
|
||||
const { SubMenu } = Menu;
|
||||
@@ -19,12 +22,14 @@ const RELOAD_LOCATION_NUMBER = 0;
|
||||
const Header = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const { user, invoicedItems, setUser, locale, setLocale } = useGlobals();
|
||||
const { user, invoicedItems, locale, setLocale } = useAppState();
|
||||
const currentKey = [keys.find((key) => key === location.pathname)];
|
||||
const haveInvoicedItems = !isEmpty(invoicedItems);
|
||||
const invoicedItemsLength = invoicedItems.length;
|
||||
|
||||
const onChangeLocale = (value) => {
|
||||
setLocaleToLS(value);
|
||||
changeLocaleDefaults(value);
|
||||
setLocale(value);
|
||||
history.go(RELOAD_LOCATION_NUMBER);
|
||||
};
|
||||
@@ -39,6 +44,12 @@ const Header = () => {
|
||||
</Menu.Item>
|
||||
));
|
||||
|
||||
const onUserLogout = () => {
|
||||
emitter.emit("UPDATE_USER", undefined);
|
||||
|
||||
history.push("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -107,10 +118,7 @@ const Header = () => {
|
||||
|
||||
{!!user ? (
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setUser(undefined);
|
||||
history.push("/");
|
||||
}}
|
||||
onClick={onUserLogout}
|
||||
danger
|
||||
icon={<LogoutOutlined style={{ fontSize: 16 }} />}
|
||||
></Menu.Item>
|
||||
@@ -6,16 +6,19 @@ import {
|
||||
Redirect,
|
||||
} from "react-router-dom";
|
||||
import { isEmpty } from "lodash";
|
||||
import { TracksContainer } from "./pages/tracks/TracksPage";
|
||||
import { TracksContainer } from "../pages/tracks/TracksPage";
|
||||
import { CurrentPageHeader } from "./CurrentPageHeader";
|
||||
import { Header } from "./Header";
|
||||
import { PersonPage } from "./pages/person/PersonPage";
|
||||
import { ErrorPage } from "./pages/ErrorPage";
|
||||
import { Login } from "./pages/login/Login";
|
||||
import { withRestrictions, withRestrictedSection } from "./withRestrictions";
|
||||
import { InvoicePage } from "./pages/invoice/InvoicePage";
|
||||
import { ManageStore } from "./pages/manage-store/ManageStore";
|
||||
import { MyInvoices } from "./pages/person/MyInvoices";
|
||||
import { Header } from "../components/Header";
|
||||
import { PersonPage } from "../pages/person/PersonPage";
|
||||
import { ErrorPage } from "../pages/ErrorPage";
|
||||
import { Login } from "../pages/login/Login";
|
||||
import {
|
||||
withRestrictions,
|
||||
withRestrictedSection,
|
||||
} from "../hocs/withRestrictions";
|
||||
import { InvoicePage } from "../pages/invoice/InvoicePage";
|
||||
import { ManageStore } from "../pages/manage-store/ManageStore";
|
||||
import { MyInvoices } from "../pages/person/MyInvoices";
|
||||
|
||||
const needCustomer = ({ user }) => !!user && user.roles.includes("customer");
|
||||
|
||||
72
media-store/app/src/contexts/AppStateContext.js
Normal file
72
media-store/app/src/contexts/AppStateContext.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { useMemo, createContext, useState, useEffect } from "react";
|
||||
import {
|
||||
getUserFromLS,
|
||||
getLocaleFromLS,
|
||||
setUserToLS,
|
||||
} from "../util/localStorageService";
|
||||
import { changeUserDefaults } from "../api/axiosInstance";
|
||||
import { emitter } from "../util/EventEmitter";
|
||||
|
||||
let counter = 0;
|
||||
|
||||
const globalContext = {
|
||||
error: {},
|
||||
loading: true,
|
||||
user: {
|
||||
ID: undefined,
|
||||
roles: [],
|
||||
email: undefined,
|
||||
accessToken: undefined,
|
||||
refreshToken: undefined,
|
||||
},
|
||||
locale: undefined,
|
||||
invoicedItems: [],
|
||||
notifications: [],
|
||||
};
|
||||
const AppStateContext = createContext(globalContext);
|
||||
|
||||
const AppStateContextProvider = ({ children }) => {
|
||||
const [invoicedItems, setInvoicedItems] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState({});
|
||||
const [user, setUser] = useState(getUserFromLS());
|
||||
const [locale, setLocale] = useState(getLocaleFromLS());
|
||||
|
||||
useEffect(() => {
|
||||
const updateUser = (newUser) => {
|
||||
console.log("USER_UPDATE WAS TRIGGERED", counter++);
|
||||
changeUserDefaults(newUser);
|
||||
setUserToLS(newUser);
|
||||
setUser(newUser);
|
||||
};
|
||||
emitter.on("UPDATE_USER", updateUser);
|
||||
console.log("listener was registered");
|
||||
return () => {
|
||||
emitter.removeListener("UPDATE_USER", updateUser);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
error: error,
|
||||
loading: loading,
|
||||
invoicedItems: invoicedItems,
|
||||
user: user,
|
||||
locale: locale,
|
||||
setLoading,
|
||||
setError,
|
||||
setInvoicedItems,
|
||||
setUser: setUser,
|
||||
setLocale: setLocale,
|
||||
}),
|
||||
[locale, user, loading, error, invoicedItems]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppStateContext.Provider value={value}>
|
||||
{children}
|
||||
</AppStateContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { AppStateContextProvider, AppStateContext };
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { useGlobals } from "./GlobalContext";
|
||||
import { useAppState } from "../hooks/useAppState";
|
||||
|
||||
const withRestrictions = (Component, isUserMeetRestrictions) => {
|
||||
return (props) => {
|
||||
const { user, invoicedItems } = useGlobals();
|
||||
const { user, invoicedItems } = useAppState();
|
||||
return isUserMeetRestrictions({ user, invoicedItems }) ? (
|
||||
<Component {...props} />
|
||||
) : (
|
||||
@@ -15,7 +15,7 @@ const withRestrictions = (Component, isUserMeetRestrictions) => {
|
||||
|
||||
const withRestrictedSection = (Component, isUserMeetRestrictions) => {
|
||||
return (props) => {
|
||||
const { user, invoicedItems } = useGlobals();
|
||||
const { user, invoicedItems } = useAppState();
|
||||
return (
|
||||
isUserMeetRestrictions({ user, invoicedItems }) && (
|
||||
<Component {...props} />
|
||||
22
media-store/app/src/hooks/useAbortableEffect.js
Normal file
22
media-store/app/src/hooks/useAbortableEffect.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
function useAbortableEffect(effect, dependencies) {
|
||||
const status = {}; // mutable status object
|
||||
useEffect(() => {
|
||||
status.aborted = false;
|
||||
// pass the mutable object to the effect callback
|
||||
// store the returned value for cleanup
|
||||
const cleanUpFn = effect(status);
|
||||
return () => {
|
||||
// mutate the object to signal the consumer
|
||||
// this effect is cleaning up
|
||||
status.aborted = true;
|
||||
if (typeof cleanUpFn === "function") {
|
||||
// run the cleanup function
|
||||
cleanUpFn();
|
||||
}
|
||||
};
|
||||
}, [...dependencies]);
|
||||
}
|
||||
|
||||
export { useAbortableEffect };
|
||||
6
media-store/app/src/hooks/useAppState.js
Normal file
6
media-store/app/src/hooks/useAppState.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useContext } from "react";
|
||||
import { AppStateContext } from "../contexts/AppStateContext";
|
||||
|
||||
const useAppState = () => useContext(AppStateContext);
|
||||
|
||||
export { useAppState };
|
||||
42
media-store/app/src/hooks/useErrors.js
Normal file
42
media-store/app/src/hooks/useErrors.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useAppState } from "./useAppState";
|
||||
import { emitter } from "../util/EventEmitter";
|
||||
import { message } from "antd";
|
||||
import { MESSAGE_TIMEOUT } from "../util/constants";
|
||||
|
||||
const useErrors = () => {
|
||||
const history = useHistory();
|
||||
const { setError } = useAppState();
|
||||
|
||||
const handleError = (error) => {
|
||||
console.error("Error", error);
|
||||
|
||||
if (error.response) {
|
||||
if (error.response.status === 401) {
|
||||
emitter.emit("UPDATE_USER", undefined);
|
||||
message.error("You are unauthorized, try login again", MESSAGE_TIMEOUT);
|
||||
}
|
||||
|
||||
const { status, statusText, data } = error.response;
|
||||
setError({
|
||||
status,
|
||||
statusText,
|
||||
message: data.error ? data.error.message : data,
|
||||
});
|
||||
} else {
|
||||
setError({
|
||||
status: "",
|
||||
statusText: "Error",
|
||||
message: "Something went wrong",
|
||||
});
|
||||
}
|
||||
|
||||
history.push("/error");
|
||||
};
|
||||
|
||||
return {
|
||||
handleError,
|
||||
};
|
||||
};
|
||||
|
||||
export { useErrors };
|
||||
@@ -1,18 +0,0 @@
|
||||
@import "~antd/dist/antd.css";
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
section.ant-layout {
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Layout
|
||||
*/
|
||||
.site-layout .site-layout-background {
|
||||
background: #fff;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { isEmpty } from "lodash";
|
||||
import { Result, Button } from "antd";
|
||||
import { useGlobals } from "../GlobalContext";
|
||||
import { useAppState } from "../hooks/useAppState";
|
||||
|
||||
const ErrorPage = () => {
|
||||
const { error, setError } = useGlobals();
|
||||
const { error, setError } = useAppState();
|
||||
const history = useHistory();
|
||||
|
||||
const onGoHome = () => {
|
||||
useEffect(() => {
|
||||
setError({});
|
||||
history.replace("/");
|
||||
}, []);
|
||||
|
||||
const onGoHome = () => {
|
||||
history.push("/");
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
import { Table, Button, message } from "antd";
|
||||
import { useGlobals } from "../../GlobalContext";
|
||||
import { useAppState } from "../../hooks/useAppState";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { invoice } from "../../api-service";
|
||||
import { useErrors } from "../../useErrors";
|
||||
import { invoice } from "../../api/calls";
|
||||
import { useErrors } from "../../hooks/useErrors";
|
||||
import { MESSAGE_TIMEOUT } from "../../util/constants";
|
||||
|
||||
import "./InvoicePage.css";
|
||||
|
||||
const columns = [
|
||||
@@ -24,12 +26,11 @@ const columns = [
|
||||
dataIndex: "unitPrice",
|
||||
},
|
||||
];
|
||||
const MESSAGE_TIMEOUT = 2;
|
||||
|
||||
const InvoicePage = () => {
|
||||
const history = useHistory();
|
||||
const { handleError } = useErrors();
|
||||
const { invoicedItems, setInvoicedItems, setLoading } = useGlobals();
|
||||
const { invoicedItems, setInvoicedItems, setLoading } = useAppState();
|
||||
|
||||
const data = invoicedItems.map(({ ID: key, ...otherProps }) => ({
|
||||
key,
|
||||
@@ -45,12 +46,12 @@ const InvoicePage = () => {
|
||||
}))
|
||||
)
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
setInvoicedItems([]);
|
||||
message.success("Invoice successfully completed", MESSAGE_TIMEOUT);
|
||||
history.push("/person");
|
||||
})
|
||||
.catch(handleError);
|
||||
.catch(handleError)
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
const onCancel = () => {
|
||||
setInvoicedItems([]);
|
||||
@@ -58,7 +59,7 @@ const InvoicePage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ borderRadius: 6, backgroundColor: "white", padding: 10 }}>
|
||||
<div style={{ backgroundColor: "white", padding: 10 }}>
|
||||
<Table
|
||||
bordered={false}
|
||||
pagination={false}
|
||||
@@ -73,17 +74,12 @@ const InvoicePage = () => {
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
style={{ borderRadius: 6 }}
|
||||
onClick={onBuy}
|
||||
>
|
||||
<Button type="primary" size="large" onClick={onBuy}>
|
||||
Buy
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
style={{ borderRadius: 6, marginLeft: 5 }}
|
||||
style={{ marginLeft: 5 }}
|
||||
onClick={onCancel}
|
||||
danger
|
||||
>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
import { Form, Input, Button, Checkbox, message } from "antd";
|
||||
import { login } from "../../api-service";
|
||||
import { login } from "../../api/calls";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useGlobals } from "../../GlobalContext";
|
||||
import { useErrors } from "../../useErrors";
|
||||
import { useAppState } from "../../hooks/useAppState";
|
||||
import { useErrors } from "../../hooks/useErrors";
|
||||
import { MESSAGE_TIMEOUT } from "../../util/constants";
|
||||
import { emitter } from "../../util/EventEmitter";
|
||||
|
||||
const layout = {
|
||||
labelCol: {
|
||||
@@ -19,12 +21,11 @@ const tailLayout = {
|
||||
span: 8,
|
||||
},
|
||||
};
|
||||
const MESSAGE_TIMEOUT = 2;
|
||||
|
||||
const Login = () => {
|
||||
const [form] = Form.useForm();
|
||||
const history = useHistory();
|
||||
const { setLoading, setUser } = useGlobals();
|
||||
const { setLoading } = useAppState();
|
||||
const { handleError } = useErrors();
|
||||
|
||||
const onFinish = (values) => {
|
||||
@@ -32,25 +33,19 @@ const Login = () => {
|
||||
setLoading(true);
|
||||
login({ email: values.email, password: values.password })
|
||||
.then((response) => {
|
||||
const { ID, email, level, token, roles } = response.data;
|
||||
setUser({
|
||||
ID,
|
||||
roles,
|
||||
email,
|
||||
level,
|
||||
token,
|
||||
});
|
||||
emitter.emit("UPDATE_USER", response.data);
|
||||
history.push("/");
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response.status === 401) {
|
||||
console.log(error);
|
||||
if (error.response && error.response.status === 401) {
|
||||
form.resetFields();
|
||||
message.error("Invalid credentials!", MESSAGE_TIMEOUT);
|
||||
} else {
|
||||
handleError(error);
|
||||
}
|
||||
})
|
||||
.then(() => setLoading(false));
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const onFinishFailed = (errorInfo) => {
|
||||
@@ -78,7 +73,7 @@ const Login = () => {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input style={{ borderRadius: 6 }} />
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
@@ -91,7 +86,7 @@ const Login = () => {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password style={{ borderRadius: 6 }} />
|
||||
<Input.Password style={{}} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item {...tailLayout} name="remember" valuePropName="checked">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Form, Input, Select } from "antd";
|
||||
import { useSearch } from "@umijs/hooks";
|
||||
import { useErrors } from "../../useErrors";
|
||||
import { fetchArtistsByName } from "../../api-service";
|
||||
import { useErrors } from "../../hooks/useErrors";
|
||||
import { fetchArtistsByName } from "../../api/calls";
|
||||
|
||||
const REQUIRED = [
|
||||
{
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.ant-select.ant-select-single.ant-select-show-arrow.ant-select-show-search
|
||||
> div,
|
||||
.ant-form-item-control-input-content > input {
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ import { Form, Radio, Button, message } from "antd";
|
||||
import { TrackForm } from "./TrackForm";
|
||||
import { AddArtistForm } from "./AddArtistForm";
|
||||
import { AddAlbumForm } from "./AddAlbumForm";
|
||||
import { useErrors } from "../../useErrors";
|
||||
import { useGlobals } from "../../GlobalContext";
|
||||
import { addTrack, addArtist, addAlbum } from "../../api-service";
|
||||
import { useErrors } from "../../hooks/useErrors";
|
||||
import { useAppState } from "../../hooks/useAppState";
|
||||
import { addTrack, addArtist, addAlbum } from "../../api/calls";
|
||||
import { MESSAGE_TIMEOUT } from "../../util/constants";
|
||||
import "./ManageStore.css";
|
||||
|
||||
const FORM_TYPES = {
|
||||
@@ -14,8 +15,6 @@ const FORM_TYPES = {
|
||||
album: "album",
|
||||
playlist: "",
|
||||
};
|
||||
const DEFAULT_MEDIA_TYPE_ID = 1;
|
||||
const MESSAGE_TIMEOUT = 2;
|
||||
|
||||
const chooseForm = (type) => {
|
||||
return (
|
||||
@@ -28,7 +27,7 @@ const chooseForm = (type) => {
|
||||
const ManageStore = () => {
|
||||
const [form] = Form.useForm();
|
||||
const { handleError } = useErrors();
|
||||
const { setLoading } = useGlobals();
|
||||
const { setLoading } = useAppState();
|
||||
const [formType, setFormType] = useState("track");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -53,8 +52,8 @@ const ManageStore = () => {
|
||||
name: data.name,
|
||||
composer: data.composer,
|
||||
album: { ID: data.albumID },
|
||||
mediaType: { ID: DEFAULT_MEDIA_TYPE_ID },
|
||||
genre: { ID: data.genreID },
|
||||
unitPrice: data.unitPrice.toString(),
|
||||
});
|
||||
break;
|
||||
case FORM_TYPES.artist:
|
||||
@@ -68,11 +67,11 @@ const ManageStore = () => {
|
||||
|
||||
promise
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
message.success("Entity successfully created", MESSAGE_TIMEOUT);
|
||||
form.resetFields();
|
||||
})
|
||||
.catch(handleError);
|
||||
.catch(handleError)
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -95,13 +94,9 @@ const ManageStore = () => {
|
||||
>
|
||||
<Form.Item label="Entity" name="type">
|
||||
<Radio.Group onChange={onChangeForm}>
|
||||
<Radio.Button value="track" style={{ borderRadius: "6px 0 0 6px" }}>
|
||||
Track
|
||||
</Radio.Button>
|
||||
<Radio.Button value="track">Track</Radio.Button>
|
||||
<Radio.Button value="album">Album</Radio.Button>
|
||||
<Radio.Button value="artist" style={{ borderRadius: "0 6px 6px 0" }}>
|
||||
Artist
|
||||
</Radio.Button>
|
||||
<Radio.Button value="artist">Artist</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{formElement}
|
||||
@@ -111,7 +106,6 @@ const ManageStore = () => {
|
||||
span: 14,
|
||||
offset: 4,
|
||||
}}
|
||||
style={{ borderRadius: 6 }}
|
||||
>
|
||||
<Button onClick={() => form.submit()}>Create</Button>
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Form, Input, Select } from "antd";
|
||||
import { Form, Input, Select, InputNumber } from "antd";
|
||||
import { head } from "lodash";
|
||||
import { useSearch } from "@umijs/hooks";
|
||||
import { useGlobals } from "../../GlobalContext";
|
||||
import { fetchAlbumsByName, fetchGenres } from "../../api-service";
|
||||
import { useErrors } from "../../useErrors";
|
||||
import { useAppState } from "../../hooks/useAppState";
|
||||
import { fetchAlbumsByName, fetchGenres } from "../../api/calls";
|
||||
import { useErrors } from "../../hooks/useErrors";
|
||||
|
||||
const ALBUMS_LIMIT = 10;
|
||||
const REQUIRED = [
|
||||
@@ -13,6 +13,10 @@ const REQUIRED = [
|
||||
message: "This filed is required!",
|
||||
},
|
||||
];
|
||||
const PRICE_INPUT_RULE = {
|
||||
pattern: /^(?:\d*\.\d\d)$/,
|
||||
message: "Price should have precision 2 and dot separator!",
|
||||
};
|
||||
|
||||
const getAlbums = function (value) {
|
||||
return fetchAlbumsByName(value, ALBUMS_LIMIT)
|
||||
@@ -28,17 +32,15 @@ const TrackForm = ({ initialAlbumTitle }) => {
|
||||
onChange: onChangeAlbumInput,
|
||||
cancel: onAlbumCancel,
|
||||
} = useSearch(getAlbums.bind({ handleError }));
|
||||
const { setLoading } = useGlobals();
|
||||
const { setLoading } = useAppState();
|
||||
const [genres, setGenres] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
Promise.all([fetchGenres(), onChangeAlbumInput(initialAlbumTitle)])
|
||||
.then((responses) => {
|
||||
setGenres(head(responses).data.value);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(handleError);
|
||||
.then((responses) => setGenres(head(responses).data.value))
|
||||
.catch(handleError)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -76,6 +78,18 @@ const TrackForm = ({ initialAlbumTitle }) => {
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Unit price"
|
||||
name="unitPrice"
|
||||
precision={2}
|
||||
rules={REQUIRED}
|
||||
>
|
||||
<InputNumber
|
||||
precision={2}
|
||||
decimalSeparator="."
|
||||
parser={(value) => value.replace(/\$\s?|(,*)/g, "")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { Button, message, Divider, Tag, Collapse, Table, Spin } from "antd";
|
||||
import moment from "moment";
|
||||
import { useErrors } from "../../useErrors";
|
||||
import { useGlobals } from "../../GlobalContext";
|
||||
import { cancelInvoice, fetchInvoices } from "../../api-service";
|
||||
import { useErrors } from "../../hooks/useErrors";
|
||||
import { useAppState } from "../../hooks/useAppState";
|
||||
import { cancelInvoice, fetchInvoices } from "../../api/calls";
|
||||
import { MESSAGE_TIMEOUT } from "../../util/constants";
|
||||
|
||||
const { Panel } = Collapse;
|
||||
const MESSAGE_TIMEOUT = 2;
|
||||
const INVOICE_STATUS = {
|
||||
2: {
|
||||
tagTitle: "Shipped",
|
||||
@@ -64,7 +64,7 @@ const chooseStatus = (utcNowTimestamp, invoiceDate, statusFromDb) => {
|
||||
};
|
||||
|
||||
const ExtraHeader = ({ ID, invoiceDate, status: initialStatus }) => {
|
||||
const { loading, setLoading } = useGlobals();
|
||||
const { loading, setLoading } = useAppState();
|
||||
const { handleError } = useErrors();
|
||||
const [loadingHeaderId, setLoadingHeaderId] = useState();
|
||||
const [status, setStatus] = useState(initialStatus);
|
||||
@@ -83,11 +83,11 @@ const ExtraHeader = ({ ID, invoiceDate, status: initialStatus }) => {
|
||||
cancelInvoice(ID)
|
||||
.then(() => {
|
||||
message.success("Invoice successfully cancelled", MESSAGE_TIMEOUT);
|
||||
setLoading(false);
|
||||
setLoadingHeaderId(undefined);
|
||||
setStatus(CANCELLED_STATUS);
|
||||
})
|
||||
.catch(handleError);
|
||||
.catch(handleError)
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -108,20 +108,15 @@ const ExtraHeader = ({ ID, invoiceDate, status: initialStatus }) => {
|
||||
|
||||
const MyInvoices = () => {
|
||||
const { handleError } = useErrors();
|
||||
const { setLoading } = useGlobals();
|
||||
const { setLoading } = useAppState();
|
||||
const [invoices, setInvoices] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetchInvoices()
|
||||
.then((response) => {
|
||||
const {
|
||||
data: { value },
|
||||
} = response;
|
||||
setInvoices(value);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(handleError);
|
||||
.then(({ data: { value } }) => setInvoices(value))
|
||||
.catch(handleError)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const genExtra = useCallback(
|
||||
@@ -183,9 +178,7 @@ const MyInvoices = () => {
|
||||
{invoiceElements && (
|
||||
<>
|
||||
<Divider orientation="left">My invoices</Divider>
|
||||
<Collapse style={{ borderRadius: 6 }} expandIconPosition="left">
|
||||
{invoiceElements}
|
||||
</Collapse>
|
||||
<Collapse expandIconPosition="left">{invoiceElements}</Collapse>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { Card, Button, message } from "antd";
|
||||
import { omit } from "lodash";
|
||||
import { fetchPerson, confirmPerson } from "../../api-service";
|
||||
import { useErrors } from "../../useErrors";
|
||||
import { useGlobals } from "../../GlobalContext";
|
||||
import { Editable } from "../../Editable";
|
||||
import { fetchPerson, confirmPerson } from "../../api/calls";
|
||||
import { useErrors } from "../../hooks/useErrors";
|
||||
import { useAppState } from "../../hooks/useAppState";
|
||||
import { Editable } from "../../components/Editable";
|
||||
import { MESSAGE_TIMEOUT } from "../../util/constants";
|
||||
import { useAbortableEffect } from "../../hooks/useAbortableEffect";
|
||||
|
||||
const MESSAGE_TIMEOUT = 2;
|
||||
const PERSON_PROP = {
|
||||
address: "Address ",
|
||||
city: "City ",
|
||||
@@ -22,7 +23,7 @@ const PERSON_PROP = {
|
||||
};
|
||||
|
||||
const PersonPage = ({ myInvoicesSection }) => {
|
||||
const { setLoading } = useGlobals();
|
||||
const { setLoading } = useAppState();
|
||||
const { handleError } = useErrors();
|
||||
const [initialPerson, setInitialPerson] = useState({});
|
||||
const [person, setPerson] = useState({
|
||||
@@ -39,30 +40,31 @@ const PersonPage = ({ myInvoicesSection }) => {
|
||||
company: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
useAbortableEffect((status) => {
|
||||
setLoading(true);
|
||||
|
||||
fetchPerson()
|
||||
.then((response) => {
|
||||
let { data: personData } = response;
|
||||
.then(({ data: personData }) => {
|
||||
personData = omit(personData, "@odata.context", "ID");
|
||||
console.log("personData", personData);
|
||||
setInitialPerson(personData);
|
||||
setPerson(personData);
|
||||
setLoading(false);
|
||||
if (!status.aborted) {
|
||||
setInitialPerson(personData);
|
||||
setPerson(personData);
|
||||
}
|
||||
})
|
||||
.catch(handleError);
|
||||
.catch(handleError)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const onConfirmChanges = () => {
|
||||
setLoading(true);
|
||||
confirmPerson(person)
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
setInitialPerson(person);
|
||||
message.success("Person successfully updated", MESSAGE_TIMEOUT);
|
||||
})
|
||||
.catch(handleError);
|
||||
.catch(handleError)
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
const isPersonChanged = useMemo(() => {
|
||||
const keysOne = Object.keys(initialPerson);
|
||||
@@ -100,10 +102,7 @@ const PersonPage = ({ myInvoicesSection }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
style={{ borderRadius: 6 }}
|
||||
title={`${person.lastName} ${person.firstName}`}
|
||||
>
|
||||
<Card title={`${person.lastName} ${person.firstName}`}>
|
||||
{personProperties}
|
||||
<div>
|
||||
Email: <span style={{ fontWeight: 600 }}>{person.email}</span>
|
||||
@@ -111,7 +110,7 @@ const PersonPage = ({ myInvoicesSection }) => {
|
||||
{isPersonChanged && (
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ margin: 10, borderRadius: 6 }}
|
||||
style={{ margin: 10 }}
|
||||
onClick={onConfirmChanges}
|
||||
>
|
||||
Confirm changes
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import { Modal, message } from "antd";
|
||||
import { DeleteOutlined } from "@ant-design/icons";
|
||||
import { deleteTrack } from "../../api-service";
|
||||
import { useErrors } from "../../useErrors";
|
||||
|
||||
const MESSAGE_TIMEOUT = 2;
|
||||
import { deleteTrack } from "../../api/calls";
|
||||
import { useErrors } from "../../hooks/useErrors";
|
||||
import { MESSAGE_TIMEOUT } from "../../util/constants";
|
||||
|
||||
const DeleteAction = ({ ID, onDeleteTrack }) => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import React from "react";
|
||||
import { Button, Modal, Form, message } from "antd";
|
||||
import { EditOutlined, LoadingOutlined } from "@ant-design/icons";
|
||||
import { useErrors } from "../../useErrors";
|
||||
import { useErrors } from "../../hooks/useErrors";
|
||||
import { TrackForm } from "../manage-store/TrackForm";
|
||||
import { updateTrack, getTrack } from "../../api-service";
|
||||
import { updateTrack, getTrack } from "../../api/calls";
|
||||
import { MESSAGE_TIMEOUT } from "../../util/constants";
|
||||
|
||||
const MESSAGE_TIMEOUT = 2;
|
||||
|
||||
const EditAction = ({ ID, name, composer, genre, album, afterTrackUpdate }) => {
|
||||
const EditAction = ({
|
||||
ID,
|
||||
name,
|
||||
composer,
|
||||
genre,
|
||||
unitPrice,
|
||||
album,
|
||||
afterTrackUpdate,
|
||||
}) => {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [confirmLoading, setConfirmLoading] = React.useState(false);
|
||||
const [updateLoading, setUpdateLoading] = React.useState(false);
|
||||
@@ -26,6 +33,7 @@ const EditAction = ({ ID, name, composer, genre, album, afterTrackUpdate }) => {
|
||||
composer: value.composer,
|
||||
album: { ID: value.albumID },
|
||||
genre: { ID: value.genreID },
|
||||
unitPrice: value.unitPrice.toString(),
|
||||
})
|
||||
.then(() => {
|
||||
message.success("Track successfully updated!", MESSAGE_TIMEOUT);
|
||||
@@ -99,6 +107,7 @@ const EditAction = ({ ID, name, composer, genre, album, afterTrackUpdate }) => {
|
||||
composer: composer,
|
||||
genreID: genre.ID,
|
||||
albumID: album.ID,
|
||||
unitPrice: unitPrice,
|
||||
}}
|
||||
>
|
||||
<TrackForm initialAlbumTitle={album.title} />
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { Card, Button } from "antd";
|
||||
import { PlusOutlined, MinusOutlined } from "@ant-design/icons";
|
||||
import { useGlobals } from "../../GlobalContext";
|
||||
import { withRestrictedSection } from "../../withRestrictions";
|
||||
import { useAppState } from "../../hooks/useAppState";
|
||||
import { withRestrictedSection } from "../../hocs/withRestrictions";
|
||||
import { EditAction } from "./EditAction";
|
||||
import { DeleteAction } from "./DeleteAction";
|
||||
import "./Track.css";
|
||||
|
||||
const RestrictedButton = withRestrictedSection(
|
||||
Button,
|
||||
({ user }) => !!user && user.roles.includes("customer")
|
||||
);
|
||||
const RestrictedButton = withRestrictedSection(Button, ({ user }) => {
|
||||
return !!user && user.roles.includes("customer");
|
||||
});
|
||||
|
||||
const RestrictedEditAction = withRestrictedSection(
|
||||
EditAction,
|
||||
@@ -28,7 +27,7 @@ const Track = ({
|
||||
onDeleteTrack,
|
||||
}) => {
|
||||
const trackElement = useRef();
|
||||
const { setInvoicedItems, invoicedItems } = useGlobals();
|
||||
const { setInvoicedItems, invoicedItems } = useAppState();
|
||||
const [isInvoiced, setIsInvoiced] = useState(isInvoicedProp);
|
||||
const [track, setTrack] = useState(initialTrack);
|
||||
|
||||
@@ -70,10 +69,10 @@ const Track = ({
|
||||
composer={track.composer}
|
||||
album={track.album}
|
||||
genre={track.genre}
|
||||
unitPrice={track.unitPrice}
|
||||
afterTrackUpdate={(value) => setTrack(value)}
|
||||
/>,
|
||||
]}
|
||||
style={{ borderRadius: 6 }}
|
||||
title={track.name}
|
||||
bordered={false}
|
||||
>
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
div.ant-select.ant-select-multiple.ant-select-show-search > div,
|
||||
div.ant-select-dropdown.ant-select-dropdown-placement-bottomLeft {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.ant-select > div.ant-select-selector {
|
||||
padding: 5px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.ant-pagination-prev > .ant-pagination-item-link,
|
||||
.ant-pagination-next > .ant-pagination-item-link,
|
||||
.ant-pagination-item {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { debounce } from "lodash";
|
||||
import { Input, Col, Row, Select, Pagination } from "antd";
|
||||
import { Track } from "./Track";
|
||||
import "./TracksPage.css";
|
||||
import { useGlobals } from "../../GlobalContext";
|
||||
import { useErrors } from "../../useErrors";
|
||||
import { fetchTacks, countTracks, fetchGenres } from "../../api-service";
|
||||
import { useAppState } from "../../hooks/useAppState";
|
||||
import { useErrors } from "../../hooks/useErrors";
|
||||
import { fetchTacks, countTracks, fetchGenres } from "../../api/calls";
|
||||
import { useAbortableEffect } from "../../hooks/useAbortableEffect";
|
||||
|
||||
let counter = 0;
|
||||
|
||||
const { Search } = Input;
|
||||
const { Option } = Select;
|
||||
@@ -24,7 +27,7 @@ const renderGenres = (genres) =>
|
||||
));
|
||||
|
||||
const TracksContainer = () => {
|
||||
const { setLoading, invoicedItems } = useGlobals();
|
||||
const { setLoading, invoicedItems } = useAppState();
|
||||
const { handleError } = useErrors();
|
||||
const [state, setState] = useState({
|
||||
tracks: [],
|
||||
@@ -40,16 +43,17 @@ const TracksContainer = () => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
useAbortableEffect((status) => {
|
||||
setLoading(true);
|
||||
|
||||
const countTracksReq = countTracks();
|
||||
const getTracksRequest = fetchTacks();
|
||||
const getGenresReq = fetchGenres();
|
||||
|
||||
console.log("calling requests", counter++);
|
||||
Promise.all([countTracksReq, getTracksRequest, getGenresReq])
|
||||
.then((responses) => {
|
||||
const [
|
||||
.then(
|
||||
([
|
||||
{ data: totalItems },
|
||||
{
|
||||
data: { value: tracks },
|
||||
@@ -57,16 +61,19 @@ const TracksContainer = () => {
|
||||
{
|
||||
data: { value: genres },
|
||||
},
|
||||
] = responses;
|
||||
setState({
|
||||
...state,
|
||||
tracks,
|
||||
genres,
|
||||
pagination: { ...state.pagination, totalItems },
|
||||
});
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(handleError);
|
||||
]) => {
|
||||
if (!status.aborted) {
|
||||
setState({
|
||||
...state,
|
||||
tracks,
|
||||
genres,
|
||||
pagination: { ...state.pagination, totalItems },
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(handleError)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const onSearch = debounce(
|
||||
@@ -85,21 +92,15 @@ const TracksContainer = () => {
|
||||
genreIds: options.genreIds,
|
||||
}),
|
||||
])
|
||||
.then((responses) => {
|
||||
const [
|
||||
{
|
||||
data: { value: tracks },
|
||||
},
|
||||
{ data: totalItems },
|
||||
] = responses;
|
||||
.then(([{ data: { value: tracks } }, { data: totalItems }]) =>
|
||||
setState({
|
||||
...state,
|
||||
tracks,
|
||||
pagination: { ...state.pagination, totalItems },
|
||||
});
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(handleError);
|
||||
})
|
||||
)
|
||||
.catch(handleError)
|
||||
.finally(() => setLoading(false));
|
||||
},
|
||||
DEBOUNCE_TIMER,
|
||||
DEBOUNCE_OPTIONS
|
||||
@@ -132,15 +133,15 @@ const TracksContainer = () => {
|
||||
$skip: (pageNumber - 1) * state.pagination.pageSize,
|
||||
};
|
||||
fetchTacks(options)
|
||||
.then((response) => {
|
||||
.then((response) =>
|
||||
setState({
|
||||
...state,
|
||||
tracks: response.data.value,
|
||||
pagination: { ...state.pagination, currentPage: pageNumber },
|
||||
});
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(handleError);
|
||||
})
|
||||
)
|
||||
.catch(handleError)
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
const deleteTrack = (ID) => {
|
||||
setState({
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
const reducersFactory = (initialState, handlers) => {
|
||||
return (state = initialState, action) => {
|
||||
const handler = handlers[action.type];
|
||||
|
||||
if (handler) {
|
||||
return handler(state, action);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
};
|
||||
|
||||
export { reducersFactory };
|
||||
@@ -1,47 +0,0 @@
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useGlobals } from "./GlobalContext";
|
||||
import { message } from "antd";
|
||||
|
||||
const MESSAGE_TIMEOUT = 2;
|
||||
|
||||
const useErrors = () => {
|
||||
const history = useHistory();
|
||||
const { setError, setUser, setLoading } = useGlobals();
|
||||
|
||||
const handleError = (error) => {
|
||||
console.error("Error", error);
|
||||
|
||||
console.log("error", error);
|
||||
if (error.response) {
|
||||
if (error.response.status === 401 || error.response.status === 403) {
|
||||
setUser(undefined);
|
||||
setLoading(false);
|
||||
// message.error("You are unauthorized, try login again", MESSAGE_TIMEOUT);
|
||||
// history.push("/login");
|
||||
// return;
|
||||
}
|
||||
|
||||
setError({
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
message: error.response.data.error
|
||||
? error.response.data.error.message
|
||||
: error.response.data,
|
||||
});
|
||||
} else {
|
||||
setError({
|
||||
status: "",
|
||||
statusText: "Network error",
|
||||
message: "Please, check your connection",
|
||||
});
|
||||
}
|
||||
|
||||
history.push("/error");
|
||||
};
|
||||
|
||||
return {
|
||||
handleError,
|
||||
};
|
||||
};
|
||||
|
||||
export { useErrors };
|
||||
5
media-store/app/src/util/EventEmitter.js
Normal file
5
media-store/app/src/util/EventEmitter.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import EventEmitter from "events";
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
export { emitter };
|
||||
8
media-store/app/src/util/constants.js
Normal file
8
media-store/app/src/util/constants.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export const AVAILABLE_LOCALES = ["en", "fr", "de"];
|
||||
|
||||
export const MESSAGE_TIMEOUT = 2;
|
||||
|
||||
// in dev mode using provided api
|
||||
// in prod mode using proxy
|
||||
export const API =
|
||||
process.env.NODE_ENV === "development" ? "http://localhost:4004/" : "api/";
|
||||
35
media-store/app/src/util/localStorageService.js
Normal file
35
media-store/app/src/util/localStorageService.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { isValidUser } from "./validateUser";
|
||||
import { AVAILABLE_LOCALES } from "./constants";
|
||||
|
||||
const setUserToLS = (user) => {
|
||||
if (user) {
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
} else {
|
||||
localStorage.removeItem("user");
|
||||
}
|
||||
};
|
||||
|
||||
const getUserFromLS = () => {
|
||||
let userFromLS;
|
||||
try {
|
||||
userFromLS = JSON.parse(localStorage.getItem("user"));
|
||||
if (isValidUser(userFromLS)) {
|
||||
return userFromLS;
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const getLocaleFromLS = () => {
|
||||
const localeFromLS = localStorage.getItem("locale");
|
||||
return localeFromLS &&
|
||||
localeFromLS !== "undefined" &&
|
||||
AVAILABLE_LOCALES.includes(localeFromLS)
|
||||
? localeFromLS
|
||||
: "en";
|
||||
};
|
||||
|
||||
const setLocaleToLS = (locale) => {
|
||||
localStorage.setItem("locale", locale);
|
||||
};
|
||||
|
||||
export { setLocaleToLS, getLocaleFromLS, getUserFromLS, setUserToLS };
|
||||
20
media-store/app/src/util/validateUser.js
Normal file
20
media-store/app/src/util/validateUser.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { isArray, isEmpty, isString, isNumber } from "lodash";
|
||||
|
||||
const CUSTOMER_ROLE = "customer";
|
||||
const EMPLOYEE_ROLE = "employee";
|
||||
|
||||
const isValidUser = (user) => {
|
||||
return (
|
||||
!isEmpty(user) &&
|
||||
isNumber(user.ID) &&
|
||||
isArray(user.roles) &&
|
||||
!!user.roles.some(
|
||||
(role) => role === CUSTOMER_ROLE || role === EMPLOYEE_ROLE
|
||||
) &&
|
||||
isString(user.email) &&
|
||||
isString(user.accessToken) &&
|
||||
isString(user.refreshToken)
|
||||
);
|
||||
};
|
||||
|
||||
export { isValidUser };
|
||||
Reference in New Issue
Block a user