feat: factor out url library init & switch to updateLibrary API (#5115)

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
This commit is contained in:
David Luzar 2022-05-11 15:08:54 +02:00 committed by GitHub
parent 2537b225ac
commit cad6097d60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 394 additions and 235 deletions

View file

@ -17,7 +17,6 @@ import {
ExportedLibraryData,
ImportedLibraryData,
} from "./types";
import Library from "./library";
/**
* Strips out files which are only referenced by deleted elements
@ -147,15 +146,3 @@ export const saveLibraryAsJSON = async (libraryItems: LibraryItems) => {
},
);
};
export const importLibraryFromJSON = async (library: Library) => {
const blob = await fileOpen({
description: "Excalidraw library files",
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
/*
extensions: [".json", ".excalidrawlib"],
*/
});
await library.importLibrary(blob);
};

View file

@ -1,10 +1,18 @@
import { loadLibraryFromBlob } from "./blob";
import { LibraryItems, LibraryItem } from "../types";
import {
LibraryItems,
LibraryItem,
ExcalidrawImperativeAPI,
LibraryItemsSource,
} from "../types";
import { restoreLibraryItems } from "./restore";
import type App from "../components/App";
import { ImportedDataState } from "./types";
import { atom } from "jotai";
import { jotaiStore } from "../jotai";
import { AbortError } from "../errors";
import { t } from "../i18n";
import { useEffect, useRef } from "react";
import { URL_HASH_KEYS, URL_QUERY_KEYS, APP_NAME, EVENT } from "../constants";
export const libraryItemsAtom = atom<{
status: "loading" | "loaded";
@ -102,36 +110,6 @@ class Library {
return this.setLibrary([]);
};
/**
* imports library (from blob or libraryItems), merging with current library
* (attempting to remove duplicates)
*/
importLibrary(
library:
| Blob
| Required<ImportedDataState>["libraryItems"]
| Promise<Required<ImportedDataState>["libraryItems"]>,
defaultStatus: LibraryItem["status"] = "unpublished",
): Promise<LibraryItems> {
return this.setLibrary(
() =>
new Promise<LibraryItems>(async (resolve, reject) => {
try {
let libraryItems: LibraryItems;
if (library instanceof Blob) {
libraryItems = await loadLibraryFromBlob(library, defaultStatus);
} else {
libraryItems = restoreLibraryItems(await library, defaultStatus);
}
resolve(mergeLibraryItems(this.lastLibraryItems, libraryItems));
} catch (error) {
reject(error);
}
}),
);
}
/**
* @returns latest cloned libraryItems. Awaits all in-progress updates first.
*/
@ -151,6 +129,65 @@ class Library {
});
};
// NOTE this is a high-level public API (exposed on ExcalidrawAPI) with
// a slight overhead (always restoring library items). For internal use
// where merging isn't needed, use `library.setLibrary()` directly.
updateLibrary = async ({
libraryItems,
prompt = false,
merge = false,
openLibraryMenu = false,
defaultStatus = "unpublished",
}: {
libraryItems: LibraryItemsSource;
merge?: boolean;
prompt?: boolean;
openLibraryMenu?: boolean;
defaultStatus?: "unpublished" | "published";
}): Promise<LibraryItems> => {
if (openLibraryMenu) {
this.app.setState({ isLibraryOpen: true });
}
return this.setLibrary(() => {
return new Promise<LibraryItems>(async (resolve, reject) => {
try {
const source = await (typeof libraryItems === "function"
? libraryItems(this.lastLibraryItems)
: libraryItems);
let nextItems;
if (source instanceof Blob) {
nextItems = await loadLibraryFromBlob(source, defaultStatus);
} else {
nextItems = restoreLibraryItems(source, defaultStatus);
}
if (
!prompt ||
window.confirm(
t("alerts.confirmAddLibrary", {
numShapes: nextItems.length,
}),
)
) {
if (merge) {
resolve(mergeLibraryItems(this.lastLibraryItems, nextItems));
} else {
resolve(nextItems);
}
} else {
reject(new AbortError());
}
} catch (error: any) {
reject(error);
}
});
}).finally(() => {
this.app.focusContainer();
});
};
setLibrary = (
/**
* LibraryItems that will replace current items. Can be a function which
@ -204,3 +241,102 @@ class Library {
}
export default Library;
export const parseLibraryTokensFromUrl = () => {
const libraryUrl =
// current
new URLSearchParams(window.location.hash.slice(1)).get(
URL_HASH_KEYS.addLibrary,
) ||
// legacy, kept for compat reasons
new URLSearchParams(window.location.search).get(URL_QUERY_KEYS.addLibrary);
const idToken = libraryUrl
? new URLSearchParams(window.location.hash.slice(1)).get("token")
: null;
return libraryUrl ? { libraryUrl, idToken } : null;
};
export const useHandleLibrary = ({
excalidrawAPI,
getInitialLibraryItems,
}: {
excalidrawAPI: ExcalidrawImperativeAPI | null;
getInitialLibraryItems?: () => LibraryItemsSource;
}) => {
const getInitialLibraryRef = useRef(getInitialLibraryItems);
useEffect(() => {
if (!excalidrawAPI) {
return;
}
const importLibraryFromURL = ({
libraryUrl,
idToken,
}: {
libraryUrl: string;
idToken: string | null;
}) => {
if (window.location.hash.includes(URL_HASH_KEYS.addLibrary)) {
const hash = new URLSearchParams(window.location.hash.slice(1));
hash.delete(URL_HASH_KEYS.addLibrary);
window.history.replaceState({}, APP_NAME, `#${hash.toString()}`);
} else if (window.location.search.includes(URL_QUERY_KEYS.addLibrary)) {
const query = new URLSearchParams(window.location.search);
query.delete(URL_QUERY_KEYS.addLibrary);
window.history.replaceState({}, APP_NAME, `?${query.toString()}`);
}
excalidrawAPI.updateLibrary({
libraryItems: new Promise<Blob>(async (resolve, reject) => {
try {
const request = await fetch(decodeURIComponent(libraryUrl));
const blob = await request.blob();
resolve(blob);
} catch (error: any) {
reject(error);
}
}),
prompt: idToken !== excalidrawAPI.id,
merge: true,
defaultStatus: "published",
openLibraryMenu: true,
});
};
const onHashChange = (event: HashChangeEvent) => {
event.preventDefault();
const libraryUrlTokens = parseLibraryTokensFromUrl();
if (libraryUrlTokens) {
event.stopImmediatePropagation();
// If hash changed and it contains library url, import it and replace
// the url to its previous state (important in case of collaboration
// and similar).
// Using history API won't trigger another hashchange.
window.history.replaceState({}, "", event.oldURL);
importLibraryFromURL(libraryUrlTokens);
}
};
// -------------------------------------------------------------------------
// ------ init load --------------------------------------------------------
if (getInitialLibraryRef.current) {
excalidrawAPI.updateLibrary({
libraryItems: getInitialLibraryRef.current(),
});
}
const libraryUrlTokens = parseLibraryTokensFromUrl();
if (libraryUrlTokens) {
importLibraryFromURL(libraryUrlTokens);
}
// --------------------------------------------------------- init load -----
window.addEventListener(EVENT.HASHCHANGE, onHashChange);
return () => {
window.removeEventListener(EVENT.HASHCHANGE, onHashChange);
};
}, [excalidrawAPI]);
};

View file

@ -1,5 +1,10 @@
import { ExcalidrawElement } from "../element/types";
import { AppState, BinaryFiles, LibraryItems, LibraryItems_v1 } from "../types";
import {
AppState,
BinaryFiles,
LibraryItems,
LibraryItems_anyVersion,
} from "../types";
import type { cleanAppStateForExport } from "../appState";
import { VERSIONS } from "../constants";
@ -19,7 +24,7 @@ export interface ImportedDataState {
elements?: readonly ExcalidrawElement[] | null;
appState?: Readonly<Partial<AppState>> | null;
scrollToContent?: boolean;
libraryItems?: LibraryItems | LibraryItems_v1;
libraryItems?: LibraryItems_anyVersion;
files?: BinaryFiles;
}