LibraryMenuHeaderContent.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import React, { useCallback, useState } from "react";
  2. import { saveLibraryAsJSON } from "../data/json";
  3. import Library, { libraryItemsAtom } from "../data/library";
  4. import { t } from "../i18n";
  5. import { AppState, LibraryItem, LibraryItems } from "../types";
  6. import {
  7. DotsIcon,
  8. ExportIcon,
  9. LoadIcon,
  10. publishIcon,
  11. TrashIcon,
  12. } from "./icons";
  13. import { ToolButton } from "./ToolButton";
  14. import { fileOpen } from "../data/filesystem";
  15. import { muteFSAbortError } from "../utils";
  16. import { atom, useAtom } from "jotai";
  17. import { jotaiScope } from "../jotai";
  18. import ConfirmDialog from "./ConfirmDialog";
  19. import PublishLibrary from "./PublishLibrary";
  20. import { Dialog } from "./Dialog";
  21. import DropdownMenu from "./dropdownMenu/DropdownMenu";
  22. export const isLibraryMenuOpenAtom = atom(false);
  23. const getSelectedItems = (
  24. libraryItems: LibraryItems,
  25. selectedItems: LibraryItem["id"][],
  26. ) => libraryItems.filter((item) => selectedItems.includes(item.id));
  27. export const LibraryMenuHeader: React.FC<{
  28. setAppState: React.Component<any, AppState>["setState"];
  29. selectedItems: LibraryItem["id"][];
  30. library: Library;
  31. onRemoveFromLibrary: () => void;
  32. resetLibrary: () => void;
  33. onSelectItems: (items: LibraryItem["id"][]) => void;
  34. appState: AppState;
  35. }> = ({
  36. setAppState,
  37. selectedItems,
  38. library,
  39. onRemoveFromLibrary,
  40. resetLibrary,
  41. onSelectItems,
  42. appState,
  43. }) => {
  44. const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
  45. const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom(
  46. isLibraryMenuOpenAtom,
  47. );
  48. const renderRemoveLibAlert = useCallback(() => {
  49. const content = selectedItems.length
  50. ? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
  51. : t("alerts.resetLibrary");
  52. const title = selectedItems.length
  53. ? t("confirmDialog.removeItemsFromLib")
  54. : t("confirmDialog.resetLibrary");
  55. return (
  56. <ConfirmDialog
  57. onConfirm={() => {
  58. if (selectedItems.length) {
  59. onRemoveFromLibrary();
  60. } else {
  61. resetLibrary();
  62. }
  63. setShowRemoveLibAlert(false);
  64. }}
  65. onCancel={() => {
  66. setShowRemoveLibAlert(false);
  67. }}
  68. title={title}
  69. >
  70. <p>{content}</p>
  71. </ConfirmDialog>
  72. );
  73. }, [selectedItems, onRemoveFromLibrary, resetLibrary]);
  74. const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
  75. const itemsSelected = !!selectedItems.length;
  76. const items = itemsSelected
  77. ? libraryItemsData.libraryItems.filter((item) =>
  78. selectedItems.includes(item.id),
  79. )
  80. : libraryItemsData.libraryItems;
  81. const resetLabel = itemsSelected
  82. ? t("buttons.remove")
  83. : t("buttons.resetLibrary");
  84. const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
  85. useState(false);
  86. const [publishLibSuccess, setPublishLibSuccess] = useState<null | {
  87. url: string;
  88. authorName: string;
  89. }>(null);
  90. const renderPublishSuccess = useCallback(() => {
  91. return (
  92. <Dialog
  93. onCloseRequest={() => setPublishLibSuccess(null)}
  94. title={t("publishSuccessDialog.title")}
  95. className="publish-library-success"
  96. small={true}
  97. >
  98. <p>
  99. {t("publishSuccessDialog.content", {
  100. authorName: publishLibSuccess!.authorName,
  101. })}{" "}
  102. <a
  103. href={publishLibSuccess?.url}
  104. target="_blank"
  105. rel="noopener noreferrer"
  106. >
  107. {t("publishSuccessDialog.link")}
  108. </a>
  109. </p>
  110. <ToolButton
  111. type="button"
  112. title={t("buttons.close")}
  113. aria-label={t("buttons.close")}
  114. label={t("buttons.close")}
  115. onClick={() => setPublishLibSuccess(null)}
  116. data-testid="publish-library-success-close"
  117. className="publish-library-success-close"
  118. />
  119. </Dialog>
  120. );
  121. }, [setPublishLibSuccess, publishLibSuccess]);
  122. const onPublishLibSuccess = useCallback(
  123. (data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
  124. setShowPublishLibraryDialog(false);
  125. setPublishLibSuccess({ url: data.url, authorName: data.authorName });
  126. const nextLibItems = libraryItems.slice();
  127. nextLibItems.forEach((libItem) => {
  128. if (selectedItems.includes(libItem.id)) {
  129. libItem.status = "published";
  130. }
  131. });
  132. library.setLibrary(nextLibItems);
  133. },
  134. [setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library],
  135. );
  136. const onLibraryImport = async () => {
  137. try {
  138. await library.updateLibrary({
  139. libraryItems: fileOpen({
  140. description: "Excalidraw library files",
  141. // ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
  142. // gets resolved. Else, iOS users cannot open `.excalidraw` files.
  143. /*
  144. extensions: [".json", ".excalidrawlib"],
  145. */
  146. }),
  147. merge: true,
  148. openLibraryMenu: true,
  149. });
  150. } catch (error: any) {
  151. if (error?.name === "AbortError") {
  152. console.warn(error);
  153. return;
  154. }
  155. setAppState({ errorMessage: t("errors.importLibraryError") });
  156. }
  157. };
  158. const onLibraryExport = async () => {
  159. const libraryItems = itemsSelected
  160. ? items
  161. : await library.getLatestLibrary();
  162. saveLibraryAsJSON(libraryItems)
  163. .catch(muteFSAbortError)
  164. .catch((error) => {
  165. setAppState({ errorMessage: error.message });
  166. });
  167. };
  168. const renderLibraryMenu = () => {
  169. return (
  170. <DropdownMenu open={isLibraryMenuOpen}>
  171. <DropdownMenu.Trigger
  172. className="Sidebar__dropdown-btn"
  173. onToggle={() => setIsLibraryMenuOpen(!isLibraryMenuOpen)}
  174. >
  175. {DotsIcon}
  176. </DropdownMenu.Trigger>
  177. <DropdownMenu.Content
  178. onClickOutside={() => setIsLibraryMenuOpen(false)}
  179. onSelect={() => setIsLibraryMenuOpen(false)}
  180. className="library-menu"
  181. >
  182. {!itemsSelected && (
  183. <DropdownMenu.Item
  184. onSelect={onLibraryImport}
  185. icon={LoadIcon}
  186. data-testid="lib-dropdown--load"
  187. >
  188. {t("buttons.load")}
  189. </DropdownMenu.Item>
  190. )}
  191. {!!items.length && (
  192. <DropdownMenu.Item
  193. onSelect={onLibraryExport}
  194. icon={ExportIcon}
  195. data-testid="lib-dropdown--export"
  196. >
  197. {t("buttons.export")}
  198. </DropdownMenu.Item>
  199. )}
  200. {!!items.length && (
  201. <DropdownMenu.Item
  202. onSelect={() => setShowRemoveLibAlert(true)}
  203. icon={TrashIcon}
  204. >
  205. {resetLabel}
  206. </DropdownMenu.Item>
  207. )}
  208. {itemsSelected && (
  209. <DropdownMenu.Item
  210. icon={publishIcon}
  211. onSelect={() => setShowPublishLibraryDialog(true)}
  212. data-testid="lib-dropdown--remove"
  213. >
  214. {t("buttons.publishLibrary")}
  215. </DropdownMenu.Item>
  216. )}
  217. </DropdownMenu.Content>
  218. </DropdownMenu>
  219. );
  220. };
  221. return (
  222. <div style={{ position: "relative" }}>
  223. {renderLibraryMenu()}
  224. {selectedItems.length > 0 && (
  225. <div className="library-actions-counter">{selectedItems.length}</div>
  226. )}
  227. {showRemoveLibAlert && renderRemoveLibAlert()}
  228. {showPublishLibraryDialog && (
  229. <PublishLibrary
  230. onClose={() => setShowPublishLibraryDialog(false)}
  231. libraryItems={getSelectedItems(
  232. libraryItemsData.libraryItems,
  233. selectedItems,
  234. )}
  235. appState={appState}
  236. onSuccess={(data) =>
  237. onPublishLibSuccess(data, libraryItemsData.libraryItems)
  238. }
  239. onError={(error) => window.alert(error)}
  240. updateItemsInStorage={() =>
  241. library.setLibrary(libraryItemsData.libraryItems)
  242. }
  243. onRemove={(id: string) =>
  244. onSelectItems(selectedItems.filter((_id) => _id !== id))
  245. }
  246. />
  247. )}
  248. {publishLibSuccess && renderPublishSuccess()}
  249. </div>
  250. );
  251. };