Browse Source

added multi-publish

feature/advanced-setting
PhilReact 8 months ago
parent
commit
f8c678b740
  1. 23
      src/assets/svgs/CircleSVG.tsx
  2. 23
      src/assets/svgs/EmptyCircleSVG.tsx
  3. 214
      src/components/common/MultiplePublish/MultiplePublish.tsx
  4. 46
      src/components/common/useModal.tsx
  5. 8
      src/components/modals/EditStoreModal.tsx
  6. 22
      src/pages/Cart/Cart.tsx
  7. 23
      src/pages/ProductManager/ProductManager.tsx
  8. 188
      src/wrappers/GlobalWrapper.tsx

23
src/assets/svgs/CircleSVG.tsx

@ -0,0 +1,23 @@
import { IconTypes } from "./IconTypes";
export const CircleSVG: React.FC<IconTypes> = ({
color,
height,
width,
className,
onClickFunc,
}) => {
return (
<svg
onClick={onClickFunc}
className={className}
fill={color}
xmlns="http://www.w3.org/2000/svg"
height={height}
viewBox="0 -960 960 960"
width={width}
>
<path d="m424-296 282-282-56-56-226 226-114-114-56 56 170 170Zm56 216q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z" />
</svg>
);
};

23
src/assets/svgs/EmptyCircleSVG.tsx

@ -0,0 +1,23 @@
import { IconTypes } from "./IconTypes";
export const EmptyCircleSVG: React.FC<IconTypes> = ({
color,
height,
width,
className,
onClickFunc,
}) => {
return (
<svg onClick={onClickFunc}
className={className}
fill={color}
height={height}
width={width} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" ><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
);
};

214
src/components/common/MultiplePublish/MultiplePublish.tsx

@ -0,0 +1,214 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Box,
Button,
CircularProgress,
Modal,
Typography,
useTheme,
} from "@mui/material";
import React, { useCallback, useEffect, useState, useRef } from "react";
import { CircleSVG } from "../../../assets/svgs/CircleSVG";
import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG";
import { styled } from "@mui/system";
interface Publish {
resources: any[];
action: string;
}
interface MultiplePublishProps {
publishes: Publish;
isOpen: boolean;
onSubmit: ()=> void
onError: (message?: string)=> void
}
export const MultiplePublish = ({ publishes, isOpen, onSubmit, onError}: MultiplePublishProps) => {
const theme = useTheme();
const listOfSuccessfulPublishesRef = useRef([])
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
any[]
>([]);
const [listOfUnsuccessfulPublishes, setListOfUnSuccessfulPublishes] = useState<
any[]
>([]);
const [currentlyInPublish, setCurrentlyInPublish] = useState(null);
const hasStarted = useRef(false);
const publish = useCallback(async (pub: any) => {
const lengthOfResources = pub?.resources?.length
const lengthOfTimeout = lengthOfResources * 30000
return await qortalRequestWithTimeout(pub, lengthOfTimeout);
}, []);
const [isPublishing, setIsPublishing] = useState(true)
const handlePublish = useCallback(
async (pub: any) => {
try {
setCurrentlyInPublish(pub?.identifier);
setIsPublishing(true)
const res = await publish(pub);
onSubmit()
setListOfUnSuccessfulPublishes([])
} catch (error: any) {
const unsuccessfulPublishes = error?.error?.unsuccessfulPublishes || []
if(error?.error === 'User declined request'){
onError()
return
}
if(error?.error === 'The request timed out'){
onError("The request timed out")
return
}
if(unsuccessfulPublishes?.length > 0){
setListOfUnSuccessfulPublishes(unsuccessfulPublishes)
}
} finally {
setIsPublishing(false)
}
},
[publish]
);
const retry = ()=> {
let newlistOfMultiplePublishes: any[] = [];
listOfUnsuccessfulPublishes?.forEach((item)=> {
const findPub = publishes?.resources.find((res: any)=> res?.identifier === item.identifier)
if(findPub){
newlistOfMultiplePublishes.push(findPub)
}
})
const multiplePublish = {
...publishes,
resources: newlistOfMultiplePublishes
};
handlePublish(multiplePublish)
}
const startPublish = useCallback(
async (pubs: any) => {
await handlePublish(pubs);
},
[handlePublish, onSubmit, listOfSuccessfulPublishes, publishes]
);
useEffect(() => {
if (publishes && !hasStarted.current) {
hasStarted.current = true;
startPublish(publishes);
}
}, [startPublish, publishes, listOfSuccessfulPublishes]);
return (
<Modal
open={isOpen}
aria-labelledby="modal-title"
aria-describedby="modal-description"
sx={{
zIndex: 10001
}}
>
<ModalBody
sx={{
minHeight: "50vh",
}}
>
{publishes?.resources?.map((publish: any) => {
const unpublished = listOfUnsuccessfulPublishes.map(item => item?.identifier)
return (
<Box
sx={{
display: "flex",
gap: "20px",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography>{publish?.identifier}</Typography>
{!isPublishing && hasStarted.current ? (
<>
{!unpublished.includes(publish.identifier) ? (
<CircleSVG
color={theme.palette.text.primary}
height="24px"
width="24px"
/>
) : (
<EmptyCircleSVG
color={theme.palette.text.primary}
height="24px"
width="24px"
/>
)}
</>
): <CircularProgress size={16} color="secondary"/>}
</Box>
);
})}
{!isPublishing && listOfUnsuccessfulPublishes.length > 0 && (
<>
<Typography sx={{
marginTop: '20px',
fontSize: '16px'
}}>Some files were not published. Please try again. It's important that all the files get published. Maybe wait a couple minutes if the error keeps occurring</Typography>
<Button variant="contained" onClick={()=> {
retry()
}}>Try again</Button>
</>
)}
</ModalBody>
</Modal>
);
};
export const ModalBody = styled(Box)(({ theme }) => ({
position: "absolute",
backgroundColor: theme.palette.background.default,
borderRadius: "4px",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "75%",
maxWidth: "900px",
padding: "15px 35px",
display: "flex",
flexDirection: "column",
gap: "17px",
overflowY: "auto",
maxHeight: "95vh",
boxShadow:
theme.palette.mode === "dark"
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
"&::-webkit-scrollbar-track": {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar-track:hover": {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar": {
width: "16px",
height: "10px",
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
},
"&::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
},
}));

46
src/components/common/useModal.tsx

@ -0,0 +1,46 @@
import { useRef, useState } from 'react';
interface State {
isShow: boolean;
}
export const useModal = () => {
const [state, setState] = useState<State>({
isShow: false,
});
const promiseConfig = useRef<any>(null);
const show = async () => {
return new Promise((resolve, reject) => {
promiseConfig.current = {
resolve,
reject,
};
setState({
isShow: true,
});
});
};
const hide = () => {
setState({
isShow: false,
});
};
const onOk = (payload?:any) => {
const { resolve } = promiseConfig.current;
hide();
resolve(payload);
};
const onCancel = () => {
const { reject } = promiseConfig.current;
hide();
reject();
};
return {
show,
onOk,
onCancel,
isShow: state.isShow
};
};

8
src/components/modals/EditStoreModal.tsx

@ -140,8 +140,9 @@ const MyModal: React.FC<MyModalProps> = ({ open, onClose, onPublish }) => {
setLogo(currentStore?.logo || null);
setLocation(currentStore?.location || "");
setShipsTo(currentStore?.shipsTo || "");
setSupportedCoinsSelected(currentStore?.supportedCoins || ["QORT"]);
setArrrWalletAddress(currentStore?.foreignCoins?.ARRR || "");
const selectedCoinsList = [...new Set([...(currentStore?.supportedCoins || []), 'QORT'])];
setSupportedCoinsSelected(selectedCoinsList)
setArrrWalletAddress(currentStore?.foreignCoins?.ARRR || "")
}
}, [currentStore, storeId, open]);
@ -233,15 +234,12 @@ const MyModal: React.FC<MyModalProps> = ({ open, onClose, onPublish }) => {
}
} catch (error) {
console.error(error);
navigate("/");
dispatch(
setNotification({
msg: "Error when creating the data container. Please try again!",
alertType: "error",
})
);
dispatch(updateRecentlyVisitedStoreId(""));
dispatch(clearDataCotainer());
}
};

22
src/pages/Cart/Cart.tsx

@ -81,6 +81,8 @@ import { AcceptedCoin } from "../StoreList/StoreList-styles";
import { ARRRSVG } from "../../assets/svgs/ARRRSVG";
import { setPreferredCoin } from "../../state/features/storeSlice";
import { CoinFilter } from "../Store/Store/Store";
import { useModal } from "../../components/common/useModal";
import { MultiplePublish } from "../../components/common/MultiplePublish/MultiplePublish";
/* Currency must be replaced in the order confirmation email by proper currency */
@ -92,7 +94,8 @@ interface CountryProps {
export const Cart = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const {isShow, onCancel, onOk, show} = useModal()
const [publishes, setPublishes] = useState<any>(null);
const uid = new ShortUniqueId({
length: 10,
});
@ -754,7 +757,9 @@ export const Cart = () => {
encrypt: true,
publicKeys: [resAddress.publicKey, usernamePublicKey],
};
await qortalRequest(multiplePublish);
setPublishes(multiplePublish)
await show()
// await qortalRequest(multiplePublish);
// Clear this cart state from global carts redux
dispatch(removeCartFromCarts({ storeId }));
// Clear cart local state
@ -787,6 +792,7 @@ export const Cart = () => {
);
} finally {
dispatch(setIsLoadingGlobal(false));
setPublishes(null)
}
};
@ -828,6 +834,18 @@ export const Cart = () => {
return (
<>
{isShow && (
<MultiplePublish
isOpen={isShow}
onError={(messageNotification)=> {
onCancel()
}}
onSubmit={() => {
onOk()
}}
publishes={publishes}
/>
)}
<ReusableModal
open={isOpen}
customStyles={{

23
src/pages/ProductManager/ProductManager.tsx

@ -57,10 +57,15 @@ import {
STORE_BASE,
} from "../../constants/identifiers";
import { resetOrders } from "../../state/features/orderSlice";
import { useModal } from "../../components/common/useModal";
import { MultiplePublish } from "../../components/common/MultiplePublish/MultiplePublish";
const uid = new ShortUniqueId({ length: 10 });
export const ProductManager = () => {
const {isShow, onCancel, onOk, show} = useModal()
const [publishes, setPublishes] = useState<any>(null);
const theme = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();
@ -362,7 +367,9 @@ export const ProductManager = () => {
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
resources: [...publishMultipleCatalogues, publishDataContainer],
};
await qortalRequest(multiplePublish);
setPublishes(multiplePublish)
await show()
// await qortalRequest(multiplePublish);
// Clear productsToSave from Redux store
dispatch(clearAllProductsToSave());
@ -431,6 +438,8 @@ export const ProductManager = () => {
dispatch(setNotification(notificationObj));
throw new Error("Failed to send message");
} finally {
setPublishes(null)
}
}
@ -656,6 +665,18 @@ export const ProductManager = () => {
{/* Confirm Remove Product from productsToSave in global state */}
<Modal />
{isShow && (
<MultiplePublish
isOpen={isShow}
onError={(messageNotification)=> {
onCancel()
}}
onSubmit={() => {
onOk()
}}
publishes={publishes}
/>
)}
</ProductManagerContainer>
);
};

188
src/wrappers/GlobalWrapper.tsx

@ -48,6 +48,8 @@ import {
import QortalLogo from "../assets/img/Q-AppsLogo.webp";
import { DownloadCircleSVG } from "../assets/svgs/DownloadCircleSVG";
import { UAParser } from "ua-parser-js";
import { useModal } from "../components/common/useModal";
import { MultiplePublish } from "../components/common/MultiplePublish/MultiplePublish";
interface Props {
children: React.ReactNode;
@ -65,7 +67,7 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const [isCreatingShop, setIsCreatingShop] = useState(false)
// Determine which OS they're on
const parser = new UAParser();
@ -109,7 +111,8 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
useState<boolean>(false);
const [retryDataContainer, setRetryDataContainer] = useState<boolean>(false);
const [showDownloadModal, setShowDownloadModal] = useState<boolean>(false);
const {isShow, onCancel, onOk, show} = useModal()
const [publishes, setPublishes] = useState<any>(null);
useEffect(() => {
if (!user?.name) return;
@ -269,104 +272,7 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
}
}, []);
const handlePublishDataContainer = React.useCallback(async () => {
try {
const dataContainerToBase64 = await objectToBase64(storedDataContainer);
// Publish Data Container to QDN
const resourceResponse = await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: storedDataContainer?.owner,
service: "DOCUMENT",
data64: dataContainerToBase64,
identifier: `${storedDataContainer?.storeId}-${DATA_CONTAINER_BASE}`,
filename: "datacontainer.json",
});
if (isSuccessful(resourceResponse)) {
await new Promise<void>((res, rej) => {
setTimeout(() => {
res();
}, 1000);
});
dispatch(
setDataContainer({
...storedDataContainer,
id: `${storedDataContainer?.storeId}-${DATA_CONTAINER_BASE}`,
})
);
dispatch(
setNotification({
msg: "Shop successfully created",
alertType: "success",
})
);
setCloseCreateStoreModal(true);
setRetryDataContainer(false);
setOpenDataContainer(false);
} else {
setOpenDataContainer(true);
}
} catch (error) {
console.error(error);
dispatch(
setNotification({
msg: "You must create a data container in order to create a shop!",
alertType: "error",
})
);
// Try again after 8 seconds automatically
setOpenDataContainer(true);
let interval: number | undefined = undefined;
const dataContainerToBase64 = await objectToBase64(storedDataContainer);
interval = window.setInterval(async () => {
try {
const resourceResponse = await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: storedDataContainer?.owner,
service: "DOCUMENT",
data64: dataContainerToBase64,
identifier: `${storedDataContainer?.storeId}-${DATA_CONTAINER_BASE}`,
filename: "datacontainer.json",
});
if (isSuccessful(resourceResponse)) {
await new Promise<void>((res, rej) => {
setTimeout(() => {
res();
}, 1000);
});
dispatch(
setDataContainer({
...storedDataContainer,
id: `${storedDataContainer?.storeId}-${DATA_CONTAINER_BASE}`,
})
);
dispatch(
setNotification({
msg: "Shop successfully created",
alertType: "success",
})
);
setCloseCreateStoreModal(true);
setRetryDataContainer(false);
setOpenDataContainer(false);
clearInterval(interval);
}
} catch (error) {
console.error(error);
setRetryDataContainer(false);
// clear interval
if (interval) {
clearInterval(interval);
}
dispatch(
setNotification({
msg: "You must create a data container in order to create a shop!",
alertType: "error",
})
);
}
}, 8000);
}
}, [storedDataContainer]);
// If they successfully create a store but not a data container, keep the data-container information in the state.
// Wait 8 seconds and try again automatically. If it fails again, then tell them to republish again.
@ -382,6 +288,8 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
foreignCoins,
supportedCoins,
}: onPublishParam) => {
if(isCreatingShop) return
setIsCreatingShop(true)
if (!user || !user.name)
throw new Error("Cannot publish: You do not have a Qortal name");
if (!title) throw new Error("A title is required");
@ -429,7 +337,7 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
`**coins:QORTtrue,ARRR${supportedCoins.includes("ARRR")}**` +
description.slice(0, 180);
const resourceResponse = await qortalRequest({
const resourceStore = {
action: "PUBLISH_QDN_RESOURCE",
name: name,
service: "STORE",
@ -438,13 +346,9 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
title,
description: metadescription,
identifier: identifier,
});
if (isSuccessful(resourceResponse)) {
await new Promise<void>((res, rej) => {
setTimeout(() => {
res();
}, 1000);
});
}
const createdAt = Date.now();
const dataContainer = {
storeId: identifier,
@ -472,15 +376,43 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
updated: createdAt,
};
const dataContainerToBase64 = await objectToBase64(dataContainer);
// Publish Data Container to QDN
const resourceDatacontainer = {
action: "PUBLISH_QDN_RESOURCE",
name: name,
service: "DOCUMENT",
data64: dataContainerToBase64,
identifier: `${identifier}-${DATA_CONTAINER_BASE}`,
filename: "datacontainer.json",
}
const multiplePublish = {
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
resources: [resourceStore, resourceDatacontainer],
};
setPublishes(multiplePublish)
await show()
dispatch(setCurrentStore(storefullObj));
dispatch(addToHashMapStores(storefullObj));
dispatch(addToStores(storefullObj));
dispatch(addToAllMyStores(storeData));
setStoredDataContainer(dataContainer);
setRetryDataContainer(true);
} else {
throw new Error("Failed to create store");
}
dispatch(
setDataContainer({
...dataContainer,
id: `${identifier}-${DATA_CONTAINER_BASE}`,
})
);
dispatch(
setNotification({
msg: "Shop successfully created",
alertType: "success",
})
);
setCloseCreateStoreModal(true);
setOpenDataContainer(false);
} catch (error: any) {
let notificationObj: any = null;
if (typeof error === "string") {
@ -507,6 +439,8 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
console.error(error);
throw new Error("An unknown error occurred");
}
} finally {
setIsCreatingShop(false)
}
},
[user]
@ -821,24 +755,22 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
}
}, [recentlyVisitedStoreId, myStores, userOwnDataContainer]);
// Handle publishing of data container when creating a store, or if it fails too (Need this to be able to call it from the reusable modal)
useEffect(() => {
const publishDataContainer = async () => {
// Publish Data Container to QDN here
await handlePublishDataContainer();
};
if (
retryDataContainer &&
storedDataContainer &&
Object.keys(storedDataContainer).length > 0
) {
publishDataContainer();
}
// We only want to run this when retryDataContainer changes, or else storedDataContainer will be cleared beforehand.
}, [retryDataContainer]);
return (
<>
{isShow && (
<MultiplePublish
isOpen={isShow}
onError={()=> {
onCancel()
}}
onSubmit={() => {
onOk()
}}
publishes={publishes}
/>
)}
{isLoadingGlobal && <PageLoader />}
{isOpenCreateStoreModal && user?.name && (
<CreateStoreModal

Loading…
Cancel
Save