refactor: move excalidraw-app outside src (#6987)

* refactor: move excalidraw-app outside src

* move some tests to excal app and fix some

* fix tests

* fix

* port remaining tests

* fix

* update snap

* move tests inside test folder

* fix

* fix
This commit is contained in:
Aakansha Doshi
2023-09-21 09:28:48 +05:30
committed by GitHub
parent 0a588a880b
commit 741d5f1a18
63 changed files with 638 additions and 415 deletions

View File

@ -0,0 +1,25 @@
import React from "react";
import { Footer } from "../../src/packages/excalidraw/index";
import { EncryptedIcon } from "./EncryptedIcon";
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
import { isExcalidrawPlusSignedUser } from "../app_constants";
export const AppFooter = React.memo(() => {
return (
<Footer>
<div
style={{
display: "flex",
gap: ".5rem",
alignItems: "center",
}}
>
{isExcalidrawPlusSignedUser ? (
<ExcalidrawPlusAppLink />
) : (
<EncryptedIcon />
)}
</div>
</Footer>
);
});

View File

@ -0,0 +1,45 @@
import React from "react";
import { PlusPromoIcon } from "../../src/components/icons";
import { MainMenu } from "../../src/packages/excalidraw/index";
import { LanguageList } from "./LanguageList";
export const AppMainMenu: React.FC<{
setCollabDialogShown: (toggle: boolean) => any;
isCollaborating: boolean;
isCollabEnabled: boolean;
}> = React.memo((props) => {
return (
<MainMenu>
<MainMenu.DefaultItems.LoadScene />
<MainMenu.DefaultItems.SaveToActiveFile />
<MainMenu.DefaultItems.Export />
<MainMenu.DefaultItems.SaveAsImage />
{props.isCollabEnabled && (
<MainMenu.DefaultItems.LiveCollaborationTrigger
isCollaborating={props.isCollaborating}
onSelect={() => props.setCollabDialogShown(true)}
/>
)}
<MainMenu.DefaultItems.Help />
<MainMenu.DefaultItems.ClearCanvas />
<MainMenu.Separator />
<MainMenu.ItemLink
icon={PlusPromoIcon}
href={`${
import.meta.env.VITE_APP_PLUS_LP
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`}
className="ExcalidrawPlus"
>
Excalidraw+
</MainMenu.ItemLink>
<MainMenu.DefaultItems.Socials />
<MainMenu.Separator />
<MainMenu.DefaultItems.ToggleTheme />
<MainMenu.ItemCustom>
<LanguageList style={{ width: "100%" }} />
</MainMenu.ItemCustom>
<MainMenu.DefaultItems.ChangeCanvasBackground />
</MainMenu>
);
});

View File

@ -0,0 +1,73 @@
import React from "react";
import { PlusPromoIcon } from "../../src/components/icons";
import { useI18n } from "../../src/i18n";
import { WelcomeScreen } from "../../src/packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants";
import { POINTER_EVENTS } from "../../src/constants";
export const AppWelcomeScreen: React.FC<{
setCollabDialogShown: (toggle: boolean) => any;
isCollabEnabled: boolean;
}> = React.memo((props) => {
const { t } = useI18n();
let headingContent;
if (isExcalidrawPlusSignedUser) {
headingContent = t("welcomeScreen.app.center_heading_plus")
.split(/(Excalidraw\+)/)
.map((bit, idx) => {
if (bit === "Excalidraw+") {
return (
<a
style={{ pointerEvents: POINTER_EVENTS.inheritFromUI }}
href={`${
import.meta.env.VITE_APP_PLUS_APP
}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
key={idx}
>
Excalidraw+
</a>
);
}
return bit;
});
} else {
headingContent = t("welcomeScreen.app.center_heading");
}
return (
<WelcomeScreen>
<WelcomeScreen.Hints.MenuHint>
{t("welcomeScreen.app.menuHint")}
</WelcomeScreen.Hints.MenuHint>
<WelcomeScreen.Hints.ToolbarHint />
<WelcomeScreen.Hints.HelpHint />
<WelcomeScreen.Center>
<WelcomeScreen.Center.Logo />
<WelcomeScreen.Center.Heading>
{headingContent}
</WelcomeScreen.Center.Heading>
<WelcomeScreen.Center.Menu>
<WelcomeScreen.Center.MenuItemLoadScene />
<WelcomeScreen.Center.MenuItemHelp />
{props.isCollabEnabled && (
<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
onSelect={() => props.setCollabDialogShown(true)}
/>
)}
{!isExcalidrawPlusSignedUser && (
<WelcomeScreen.Center.MenuItemLink
href={`${
import.meta.env.VITE_APP_PLUS_LP
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`}
shortcut={null}
icon={PlusPromoIcon}
>
Try Excalidraw Plus!
</WelcomeScreen.Center.MenuItemLink>
)}
</WelcomeScreen.Center.Menu>
</WelcomeScreen.Center>
</WelcomeScreen>
);
});

View File

@ -0,0 +1,21 @@
import { shield } from "../../src/components/icons";
import { Tooltip } from "../../src/components/Tooltip";
import { useI18n } from "../../src/i18n";
export const EncryptedIcon = () => {
const { t } = useI18n();
return (
<a
className="encrypted-icon tooltip"
href="https://blog.excalidraw.com/end-to-end-encryption/"
target="_blank"
rel="noopener noreferrer"
aria-label={t("encrypted.link")}
>
<Tooltip label={t("encrypted.tooltip")} long={true}>
{shield}
</Tooltip>
</a>
);
};

View File

@ -0,0 +1,19 @@
import { isExcalidrawPlusSignedUser } from "../app_constants";
export const ExcalidrawPlusAppLink = () => {
if (!isExcalidrawPlusSignedUser) {
return null;
}
return (
<a
href={`${
import.meta.env.VITE_APP_PLUS_APP
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
target="_blank"
rel="noreferrer"
className="plus-button"
>
Go to Excalidraw+
</a>
);
};

View File

@ -0,0 +1,120 @@
import React from "react";
import { Card } from "../../src/components/Card";
import { ToolButton } from "../../src/components/ToolButton";
import { serializeAsJSON } from "../../src/data/json";
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
import { FileId, NonDeletedExcalidrawElement } from "../../src/element/types";
import { AppState, BinaryFileData, BinaryFiles } from "../../src/types";
import { nanoid } from "nanoid";
import { useI18n } from "../../src/i18n";
import { encryptData, generateEncryptionKey } from "../../src/data/encryption";
import { isInitializedImageElement } from "../../src/element/typeChecks";
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
import { encodeFilesForUpload } from "../data/FileManager";
import { MIME_TYPES } from "../../src/constants";
import { trackEvent } from "../../src/analytics";
import { getFrame } from "../../src/utils";
import { ExcalidrawLogo } from "../../src/components/ExcalidrawLogo";
export const exportToExcalidrawPlus = async (
elements: readonly NonDeletedExcalidrawElement[],
appState: Partial<AppState>,
files: BinaryFiles,
) => {
const firebase = await loadFirebaseStorage();
const id = `${nanoid(12)}`;
const encryptionKey = (await generateEncryptionKey())!;
const encryptedData = await encryptData(
encryptionKey,
serializeAsJSON(elements, appState, files, "database"),
);
const blob = new Blob(
[encryptedData.iv, new Uint8Array(encryptedData.encryptedBuffer)],
{
type: MIME_TYPES.binary,
},
);
await firebase
.storage()
.ref(`/migrations/scenes/${id}`)
.put(blob, {
customMetadata: {
data: JSON.stringify({ version: 2, name: appState.name }),
created: Date.now().toString(),
},
});
const filesMap = new Map<FileId, BinaryFileData>();
for (const element of elements) {
if (isInitializedImageElement(element) && files[element.fileId]) {
filesMap.set(element.fileId, files[element.fileId]);
}
}
if (filesMap.size) {
const filesToUpload = await encodeFilesForUpload({
files: filesMap,
encryptionKey,
maxBytes: FILE_UPLOAD_MAX_BYTES,
});
await saveFilesToFirebase({
prefix: `/migrations/files/scenes/${id}`,
files: filesToUpload,
});
}
window.open(
`${
import.meta.env.VITE_APP_PLUS_APP
}/import?excalidraw=${id},${encryptionKey}`,
);
};
export const ExportToExcalidrawPlus: React.FC<{
elements: readonly NonDeletedExcalidrawElement[];
appState: Partial<AppState>;
files: BinaryFiles;
onError: (error: Error) => void;
}> = ({ elements, appState, files, onError }) => {
const { t } = useI18n();
return (
<Card color="primary">
<div className="Card-icon">
<ExcalidrawLogo
style={{
[`--color-logo-icon` as any]: "#fff",
width: "2.8rem",
height: "2.8rem",
}}
/>
</div>
<h2>Excalidraw+</h2>
<div className="Card-details">
{t("exportDialog.excalidrawplus_description")}
</div>
<ToolButton
className="Card-button"
type="button"
title={t("exportDialog.excalidrawplus_button")}
aria-label={t("exportDialog.excalidrawplus_button")}
showAriaLabel={true}
onClick={async () => {
try {
trackEvent("export", "eplus", `ui (${getFrame()})`);
await exportToExcalidrawPlus(elements, appState, files);
} catch (error: any) {
console.error(error);
if (error.name !== "AbortError") {
onError(new Error(t("exportDialog.excalidrawplus_exportError")));
}
}
}}
/>
</Card>
);
};

View File

@ -0,0 +1,45 @@
import oc from "open-color";
import React from "react";
import { THEME } from "../../src/constants";
import { Theme } from "../../src/element/types";
// https://github.com/tholman/github-corners
export const GitHubCorner = React.memo(
({ theme, dir }: { theme: Theme; dir: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="0 0 250 250"
className="rtl-mirror"
style={{
marginTop: "calc(var(--space-factor) * -1)",
[dir === "rtl" ? "marginLeft" : "marginRight"]:
"calc(var(--space-factor) * -1)",
}}
>
<a
href="https://github.com/excalidraw/excalidraw"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub repository"
>
<path
d="M0 0l115 115h15l12 27 108 108V0z"
fill={theme === THEME.LIGHT ? oc.gray[6] : oc.gray[7]}
/>
<path
className="octo-arm"
d="M128 109c-15-9-9-19-9-19 3-7 2-11 2-11-1-7 3-2 3-2 4 5 2 11 2 11-3 10 5 15 9 16"
style={{ transformOrigin: "130px 106px" }}
fill={theme === THEME.LIGHT ? oc.white : "var(--default-bg-color)"}
/>
<path
className="octo-body"
d="M115 115s4 2 5 0l14-14c3-2 6-3 8-3-8-11-15-24 2-41 5-5 10-7 16-7 1-2 3-7 12-11 0 0 5 3 7 16 4 2 8 5 12 9s7 8 9 12c14 3 17 7 17 7-4 8-9 11-11 11 0 6-2 11-7 16-16 16-30 10-41 2 0 3-1 7-5 11l-12 11c-1 1 1 5 1 5z"
fill={theme === THEME.LIGHT ? oc.white : "var(--default-bg-color)"}
/>
</a>
</svg>
),
);

View File

@ -0,0 +1,26 @@
import { useSetAtom } from "jotai";
import React from "react";
import { appLangCodeAtom } from "..";
import { useI18n } from "../../src/i18n";
import { languages } from "../../src/i18n";
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
const { t, langCode } = useI18n();
const setLangCode = useSetAtom(appLangCodeAtom);
return (
<select
className="dropdown-select dropdown-select__language"
onChange={({ target }) => setLangCode(target.value)}
value={langCode}
aria-label={t("buttons.selectLanguage")}
style={style}
>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.label}
</option>
))}
</select>
);
};