onboarding-flow.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import { WidgetConfig, WidgetProps } from "@medusajs/admin";
  2. import { useAdminCustomPost, useAdminCustomQuery } from "medusa-react";
  3. import React, { useEffect, useState } from "react";
  4. import { useNavigate } from "react-router-dom";
  5. import { OnboardingState } from "../../../models/onboarding";
  6. import {
  7. AdminOnboardingUpdateStateReq,
  8. OnboardingStateRes,
  9. UpdateOnboardingStateInput,
  10. } from "../../../types/onboarding";
  11. import OrderDetail from "../../components/onboarding-flow/orders/order-detail";
  12. import OrdersList from "../../components/onboarding-flow/orders/orders-list";
  13. import ProductDetail from "../../components/onboarding-flow/products/product-detail";
  14. import ProductsList from "../../components/onboarding-flow/products/products-list";
  15. import Accordion from "../../components/shared/accordion";
  16. import Button from "../../components/shared/button";
  17. import { Container } from "../../components/shared/container";
  18. import GetStartedIcon from "../../components/shared/icons/get-started-icon";
  19. type STEP_ID =
  20. | "create_product"
  21. | "preview_product"
  22. | "create_order"
  23. | "setup_finished";
  24. export type StepContentProps = WidgetProps & {
  25. onNext?: Function;
  26. isComplete?: boolean;
  27. data?: OnboardingState;
  28. };
  29. type Step = {
  30. id: STEP_ID;
  31. title: string;
  32. component: React.FC<StepContentProps>;
  33. onNext?: Function;
  34. };
  35. const STEP_FLOW: STEP_ID[] = [
  36. "create_product",
  37. "preview_product",
  38. "create_order",
  39. "setup_finished",
  40. ];
  41. const QUERY_KEY = ["onboarding_state"];
  42. const OnboardingFlow = (props: WidgetProps) => {
  43. const { data, isLoading } = useAdminCustomQuery<
  44. undefined,
  45. OnboardingStateRes
  46. >("/onboarding", QUERY_KEY);
  47. const { mutate } = useAdminCustomPost<
  48. AdminOnboardingUpdateStateReq,
  49. OnboardingStateRes
  50. >("/onboarding", QUERY_KEY);
  51. const navigate = useNavigate();
  52. const currentStep: STEP_ID | undefined = data?.status
  53. ?.current_step as STEP_ID;
  54. const [openStep, setOpenStep] = useState(currentStep);
  55. const [completed, setCompleted] = useState(false);
  56. useEffect(() => {
  57. setOpenStep(currentStep);
  58. if (currentStep === STEP_FLOW[STEP_FLOW.length - 1]) setCompleted(true);
  59. }, [currentStep]);
  60. if (
  61. !isLoading &&
  62. data?.status?.is_complete &&
  63. !localStorage.getItem("override_onboarding_finish")
  64. )
  65. return null;
  66. const updateServerState = (
  67. payload: UpdateOnboardingStateInput,
  68. onSuccess: () => void = () => {}
  69. ) => {
  70. mutate(payload, { onSuccess });
  71. };
  72. const onStart = () => {
  73. updateServerState({ current_step: STEP_FLOW[0] });
  74. navigate(`/a/products`);
  75. };
  76. const setStepComplete = ({
  77. step_id,
  78. extraData,
  79. onComplete,
  80. }: {
  81. step_id: STEP_ID;
  82. extraData?: UpdateOnboardingStateInput;
  83. onComplete?: () => void;
  84. }) => {
  85. const next = STEP_FLOW[STEP_FLOW.findIndex((step) => step === step_id) + 1];
  86. updateServerState({ current_step: next, ...extraData }, onComplete);
  87. };
  88. const goToProductView = (product: any) => {
  89. setStepComplete({
  90. step_id: "create_product",
  91. extraData: { product_id: product.id },
  92. onComplete: () => navigate(`/a/products/${product.id}`),
  93. });
  94. };
  95. const goToOrders = () => {
  96. setStepComplete({
  97. step_id: "preview_product",
  98. onComplete: () => navigate(`/a/orders`),
  99. });
  100. };
  101. const goToOrderView = (order: any) => {
  102. setStepComplete({
  103. step_id: "create_order",
  104. onComplete: () => navigate(`/a/orders/${order.id}`),
  105. });
  106. };
  107. const onComplete = () => {
  108. setCompleted(true);
  109. };
  110. const onHide = () => {
  111. updateServerState({ is_complete: true });
  112. };
  113. const Steps: Step[] = [
  114. {
  115. id: "create_product",
  116. title: "Create Product",
  117. component: ProductsList,
  118. onNext: goToProductView,
  119. },
  120. {
  121. id: "preview_product",
  122. title: "Preview Product",
  123. component: ProductDetail,
  124. onNext: goToOrders,
  125. },
  126. {
  127. id: "create_order",
  128. title: "Create an Order",
  129. component: OrdersList,
  130. onNext: goToOrderView,
  131. },
  132. {
  133. id: "setup_finished",
  134. title: "Setup Finished: Start developing with Medusa",
  135. component: OrderDetail,
  136. },
  137. ];
  138. const isStepComplete = (step_id: STEP_ID) =>
  139. STEP_FLOW.indexOf(currentStep) > STEP_FLOW.indexOf(step_id);
  140. return (
  141. <>
  142. <Container>
  143. <Accordion
  144. type="single"
  145. className="my-3"
  146. value={openStep}
  147. onValueChange={(value) => setOpenStep(value as STEP_ID)}
  148. >
  149. <div className="flex items-center">
  150. <div className="mr-5">
  151. <GetStartedIcon />
  152. </div>
  153. {!completed ? (
  154. <>
  155. <div>
  156. <h1 className="font-semibold text-lg">Get started</h1>
  157. <p>
  158. Learn the basics of Medusa by creating your first order.
  159. </p>
  160. </div>
  161. <div className="ml-auto flex items-start gap-2">
  162. {!!currentStep ? (
  163. <>
  164. {currentStep === STEP_FLOW[STEP_FLOW.length - 1] ? (
  165. <Button
  166. variant="primary"
  167. size="small"
  168. onClick={() => onComplete()}
  169. >
  170. Complete Setup
  171. </Button>
  172. ) : (
  173. <Button
  174. variant="secondary"
  175. size="small"
  176. onClick={() => onHide()}
  177. >
  178. Cancel Setup
  179. </Button>
  180. )}
  181. </>
  182. ) : (
  183. <>
  184. <Button
  185. variant="secondary"
  186. size="small"
  187. onClick={() => onHide()}
  188. >
  189. Close
  190. </Button>
  191. <Button
  192. variant="primary"
  193. size="small"
  194. onClick={() => onStart()}
  195. >
  196. Begin setup
  197. </Button>
  198. </>
  199. )}
  200. </div>
  201. </>
  202. ) : (
  203. <>
  204. <div>
  205. <h1 className="font-semibold text-lg">
  206. Thank you for completing the setup guide!
  207. </h1>
  208. <p>
  209. This whole experience was built using our new{" "}
  210. <strong>widgets</strong> feature.
  211. <br /> You can find out more details and build your own by
  212. following{" "}
  213. <a
  214. href="https://docs.medusajs.com/admin/onboarding?ref=onboarding"
  215. target="_blank"
  216. className="text-blue-500 font-semibold"
  217. >
  218. our guide
  219. </a>
  220. .
  221. </p>
  222. </div>
  223. <div className="ml-auto flex items-start gap-2">
  224. <Button
  225. variant="secondary"
  226. size="small"
  227. onClick={() => onHide()}
  228. >
  229. Close
  230. </Button>
  231. </div>
  232. </>
  233. )}
  234. </div>
  235. {
  236. <div className="mt-5">
  237. {(!completed ? Steps : Steps.slice(-1)).map((step) => {
  238. const isComplete = isStepComplete(step.id);
  239. const isCurrent = currentStep === step.id;
  240. return (
  241. <Accordion.Item
  242. title={step.title}
  243. value={step.id}
  244. headingSize="medium"
  245. active={isCurrent}
  246. complete={isComplete}
  247. disabled={!isComplete && !isCurrent}
  248. key={step.id}
  249. {...(!isComplete &&
  250. !isCurrent && {
  251. customTrigger: <></>,
  252. })}
  253. >
  254. <div className="py-3 px-11 text-gray-500">
  255. <step.component
  256. onNext={step.onNext}
  257. isComplete={isComplete}
  258. data={data?.status}
  259. {...props}
  260. />
  261. </div>
  262. </Accordion.Item>
  263. );
  264. })}
  265. </div>
  266. }
  267. </Accordion>
  268. </Container>
  269. </>
  270. );
  271. };
  272. export const config: WidgetConfig = {
  273. zone: [
  274. "product.list.before",
  275. "product.details.before",
  276. "order.list.before",
  277. "order.details.before",
  278. ],
  279. };
  280. export default OnboardingFlow;