mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
build: decouple package deps and introduce yarn workspaces (#7415)
* feat: decouple package deps and introduce yarn workspaces * update root directory * fix * fix scripts * fix lint * update path in scripts * remove yarn.lock files from packages * ignore workspace * dummy * dummy * remove comment check * revert workflow changes * ignore ws when installing gh actions * remove log * update path * fix * fix typo
This commit is contained in:
parent
b7d7ccc929
commit
d6cd8b78f1
567 changed files with 5066 additions and 8648 deletions
|
@ -1,538 +0,0 @@
|
|||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
import OpenColor from "open-color";
|
||||
|
||||
import { Dialog } from "./Dialog";
|
||||
import { t } from "../i18n";
|
||||
import Trans from "./Trans";
|
||||
|
||||
import { LibraryItems, LibraryItem, UIAppState } from "../types";
|
||||
import { exportToCanvas, exportToSvg } from "../packages/utils";
|
||||
import {
|
||||
EDITOR_LS_KEYS,
|
||||
EXPORT_DATA_TYPES,
|
||||
EXPORT_SOURCE,
|
||||
MIME_TYPES,
|
||||
VERSIONS,
|
||||
} from "../constants";
|
||||
import { ExportedLibraryData } from "../data/types";
|
||||
import { canvasToBlob, resizeImageFile } from "../data/blob";
|
||||
import { chunk } from "../utils";
|
||||
import DialogActionButton from "./DialogActionButton";
|
||||
import { CloseIcon } from "./icons";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { EditorLocalStorage } from "../data/EditorLocalStorage";
|
||||
|
||||
import "./PublishLibrary.scss";
|
||||
|
||||
interface PublishLibraryDataParams {
|
||||
authorName: string;
|
||||
githubHandle: string;
|
||||
name: string;
|
||||
description: string;
|
||||
twitterHandle: string;
|
||||
website: string;
|
||||
}
|
||||
|
||||
const generatePreviewImage = async (libraryItems: LibraryItems) => {
|
||||
const MAX_ITEMS_PER_ROW = 6;
|
||||
const BOX_SIZE = 128;
|
||||
const BOX_PADDING = Math.round(BOX_SIZE / 16);
|
||||
const BORDER_WIDTH = Math.max(Math.round(BOX_SIZE / 64), 2);
|
||||
|
||||
const rows = chunk(libraryItems, MAX_ITEMS_PER_ROW);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
canvas.width =
|
||||
rows[0].length * BOX_SIZE +
|
||||
(rows[0].length + 1) * (BOX_PADDING * 2) -
|
||||
BOX_PADDING * 2;
|
||||
canvas.height =
|
||||
rows.length * BOX_SIZE +
|
||||
(rows.length + 1) * (BOX_PADDING * 2) -
|
||||
BOX_PADDING * 2;
|
||||
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
ctx.fillStyle = OpenColor.white;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// draw items
|
||||
// ---------------------------------------------------------------------------
|
||||
for (const [index, item] of libraryItems.entries()) {
|
||||
const itemCanvas = await exportToCanvas({
|
||||
elements: item.elements,
|
||||
files: null,
|
||||
maxWidthOrHeight: BOX_SIZE,
|
||||
});
|
||||
|
||||
const { width, height } = itemCanvas;
|
||||
|
||||
// draw item
|
||||
// -------------------------------------------------------------------------
|
||||
const rowOffset =
|
||||
Math.floor(index / MAX_ITEMS_PER_ROW) * (BOX_SIZE + BOX_PADDING * 2);
|
||||
const colOffset =
|
||||
(index % MAX_ITEMS_PER_ROW) * (BOX_SIZE + BOX_PADDING * 2);
|
||||
|
||||
ctx.drawImage(
|
||||
itemCanvas,
|
||||
colOffset + (BOX_SIZE - width) / 2 + BOX_PADDING,
|
||||
rowOffset + (BOX_SIZE - height) / 2 + BOX_PADDING,
|
||||
);
|
||||
|
||||
// draw item border
|
||||
// -------------------------------------------------------------------------
|
||||
ctx.lineWidth = BORDER_WIDTH;
|
||||
ctx.strokeStyle = OpenColor.gray[4];
|
||||
ctx.strokeRect(
|
||||
colOffset + BOX_PADDING / 2,
|
||||
rowOffset + BOX_PADDING / 2,
|
||||
BOX_SIZE + BOX_PADDING,
|
||||
BOX_SIZE + BOX_PADDING,
|
||||
);
|
||||
}
|
||||
|
||||
return await resizeImageFile(
|
||||
new File([await canvasToBlob(canvas)], "preview", { type: MIME_TYPES.png }),
|
||||
{
|
||||
outputType: MIME_TYPES.jpg,
|
||||
maxWidthOrHeight: 5000,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const SingleLibraryItem = ({
|
||||
libItem,
|
||||
appState,
|
||||
index,
|
||||
onChange,
|
||||
onRemove,
|
||||
}: {
|
||||
libItem: LibraryItem;
|
||||
appState: UIAppState;
|
||||
index: number;
|
||||
onChange: (val: string, index: number) => void;
|
||||
onRemove: (id: string) => void;
|
||||
}) => {
|
||||
const svgRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const node = svgRef.current;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
const svg = await exportToSvg({
|
||||
elements: libItem.elements,
|
||||
appState: {
|
||||
...appState,
|
||||
viewBackgroundColor: OpenColor.white,
|
||||
exportBackground: true,
|
||||
},
|
||||
files: null,
|
||||
});
|
||||
node.innerHTML = svg.outerHTML;
|
||||
})();
|
||||
}, [libItem.elements, appState]);
|
||||
|
||||
return (
|
||||
<div className="single-library-item">
|
||||
{libItem.status === "published" && (
|
||||
<span className="single-library-item-status">
|
||||
{t("labels.statusPublished")}
|
||||
</span>
|
||||
)}
|
||||
<div ref={svgRef} className="single-library-item__svg" />
|
||||
<ToolButton
|
||||
aria-label={t("buttons.remove")}
|
||||
type="button"
|
||||
icon={CloseIcon}
|
||||
className="single-library-item--remove"
|
||||
onClick={onRemove.bind(null, libItem.id)}
|
||||
title={t("buttons.remove")}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
margin: "0.8rem 0",
|
||||
width: "100%",
|
||||
fontSize: "14px",
|
||||
fontWeight: 500,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: "0.5em 0" }}>
|
||||
<span style={{ fontWeight: 500, color: OpenColor.gray[6] }}>
|
||||
{t("publishDialog.itemName")}
|
||||
</span>
|
||||
<span aria-hidden="true" className="required">
|
||||
*
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
style={{ width: "80%", padding: "0.2rem" }}
|
||||
defaultValue={libItem.name}
|
||||
placeholder="Item name"
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value, index);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<span className="error">{libItem.error}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PublishLibrary = ({
|
||||
onClose,
|
||||
libraryItems,
|
||||
appState,
|
||||
onSuccess,
|
||||
onError,
|
||||
updateItemsInStorage,
|
||||
onRemove,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
libraryItems: LibraryItems;
|
||||
appState: UIAppState;
|
||||
onSuccess: (data: {
|
||||
url: string;
|
||||
authorName: string;
|
||||
items: LibraryItems;
|
||||
}) => void;
|
||||
|
||||
onError: (error: Error) => void;
|
||||
updateItemsInStorage: (items: LibraryItems) => void;
|
||||
onRemove: (id: string) => void;
|
||||
}) => {
|
||||
const [libraryData, setLibraryData] = useState<PublishLibraryDataParams>({
|
||||
authorName: "",
|
||||
githubHandle: "",
|
||||
name: "",
|
||||
description: "",
|
||||
twitterHandle: "",
|
||||
website: "",
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const data = EditorLocalStorage.get<PublishLibraryDataParams>(
|
||||
EDITOR_LS_KEYS.PUBLISH_LIBRARY,
|
||||
);
|
||||
if (data) {
|
||||
setLibraryData(data);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [clonedLibItems, setClonedLibItems] = useState<LibraryItems>(
|
||||
libraryItems.slice(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setClonedLibItems(libraryItems.slice());
|
||||
}, [libraryItems]);
|
||||
|
||||
const onInputChange = (event: any) => {
|
||||
setLibraryData({
|
||||
...libraryData,
|
||||
[event.target.name]: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
const erroredLibItems: LibraryItem[] = [];
|
||||
let isError = false;
|
||||
clonedLibItems.forEach((libItem) => {
|
||||
let error = "";
|
||||
if (!libItem.name) {
|
||||
error = t("publishDialog.errors.required");
|
||||
isError = true;
|
||||
}
|
||||
erroredLibItems.push({ ...libItem, error });
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
setClonedLibItems(erroredLibItems);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const previewImage = await generatePreviewImage(clonedLibItems);
|
||||
|
||||
const libContent: ExportedLibraryData = {
|
||||
type: EXPORT_DATA_TYPES.excalidrawLibrary,
|
||||
version: VERSIONS.excalidrawLibrary,
|
||||
source: EXPORT_SOURCE,
|
||||
libraryItems: clonedLibItems,
|
||||
};
|
||||
const content = JSON.stringify(libContent, null, 2);
|
||||
const lib = new Blob([content], { type: "application/json" });
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("excalidrawLib", lib);
|
||||
formData.append("previewImage", previewImage);
|
||||
formData.append("previewImageType", previewImage.type);
|
||||
formData.append("title", libraryData.name);
|
||||
formData.append("authorName", libraryData.authorName);
|
||||
formData.append("githubHandle", libraryData.githubHandle);
|
||||
formData.append("name", libraryData.name);
|
||||
formData.append("description", libraryData.description);
|
||||
formData.append("twitterHandle", libraryData.twitterHandle);
|
||||
formData.append("website", libraryData.website);
|
||||
|
||||
fetch(`${import.meta.env.VITE_APP_LIBRARY_BACKEND}/submit`, {
|
||||
method: "post",
|
||||
body: formData,
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.ok) {
|
||||
return response.json().then(({ url }) => {
|
||||
// flush data from local storage
|
||||
EditorLocalStorage.delete(EDITOR_LS_KEYS.PUBLISH_LIBRARY);
|
||||
onSuccess({
|
||||
url,
|
||||
authorName: libraryData.authorName,
|
||||
items: clonedLibItems,
|
||||
});
|
||||
});
|
||||
}
|
||||
return response
|
||||
.json()
|
||||
.catch(() => {
|
||||
throw new Error(response.statusText || "something went wrong");
|
||||
})
|
||||
.then((error) => {
|
||||
throw new Error(
|
||||
error.message || response.statusText || "something went wrong",
|
||||
);
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
console.error(err);
|
||||
onError(err);
|
||||
setIsSubmitting(false);
|
||||
},
|
||||
)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
onError(err);
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
const renderLibraryItems = () => {
|
||||
const items: ReactNode[] = [];
|
||||
clonedLibItems.forEach((libItem, index) => {
|
||||
items.push(
|
||||
<div className="single-library-item-wrapper" key={index}>
|
||||
<SingleLibraryItem
|
||||
libItem={libItem}
|
||||
appState={appState}
|
||||
index={index}
|
||||
onChange={(val, index) => {
|
||||
const items = clonedLibItems.slice();
|
||||
items[index].name = val;
|
||||
setClonedLibItems(items);
|
||||
}}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
return <div className="selected-library-items">{items}</div>;
|
||||
};
|
||||
|
||||
const onDialogClose = useCallback(() => {
|
||||
updateItemsInStorage(clonedLibItems);
|
||||
EditorLocalStorage.set(EDITOR_LS_KEYS.PUBLISH_LIBRARY, libraryData);
|
||||
onClose();
|
||||
}, [clonedLibItems, onClose, updateItemsInStorage, libraryData]);
|
||||
|
||||
const shouldRenderForm = !!libraryItems.length;
|
||||
|
||||
const containsPublishedItems = libraryItems.some(
|
||||
(item) => item.status === "published",
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onCloseRequest={onDialogClose}
|
||||
title={t("publishDialog.title")}
|
||||
className="publish-library"
|
||||
>
|
||||
{shouldRenderForm ? (
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="publish-library-note">
|
||||
<Trans
|
||||
i18nKey="publishDialog.noteDescription"
|
||||
link={(el) => (
|
||||
<a
|
||||
href="https://libraries.excalidraw.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{el}
|
||||
</a>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<span className="publish-library-note">
|
||||
<Trans
|
||||
i18nKey="publishDialog.noteGuidelines"
|
||||
link={(el) => (
|
||||
<a
|
||||
href="https://github.com/excalidraw/excalidraw-libraries#guidelines"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{el}
|
||||
</a>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div className="publish-library-note">
|
||||
{t("publishDialog.noteItems")}
|
||||
</div>
|
||||
{containsPublishedItems && (
|
||||
<span className="publish-library-note publish-library-warning">
|
||||
{t("publishDialog.republishWarning")}
|
||||
</span>
|
||||
)}
|
||||
{renderLibraryItems()}
|
||||
<div className="publish-library__fields">
|
||||
<label>
|
||||
<div>
|
||||
<span>{t("publishDialog.libraryName")}</span>
|
||||
<span aria-hidden="true" className="required">
|
||||
*
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
value={libraryData.name}
|
||||
onChange={onInputChange}
|
||||
placeholder={t("publishDialog.placeholder.libraryName")}
|
||||
/>
|
||||
</label>
|
||||
<label style={{ alignItems: "flex-start" }}>
|
||||
<div>
|
||||
<span>{t("publishDialog.libraryDesc")}</span>
|
||||
<span aria-hidden="true" className="required">
|
||||
*
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
name="description"
|
||||
rows={4}
|
||||
required
|
||||
value={libraryData.description}
|
||||
onChange={onInputChange}
|
||||
placeholder={t("publishDialog.placeholder.libraryDesc")}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<div>
|
||||
<span>{t("publishDialog.authorName")}</span>
|
||||
<span aria-hidden="true" className="required">
|
||||
*
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="authorName"
|
||||
required
|
||||
value={libraryData.authorName}
|
||||
onChange={onInputChange}
|
||||
placeholder={t("publishDialog.placeholder.authorName")}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t("publishDialog.githubUsername")}</span>
|
||||
<input
|
||||
type="text"
|
||||
name="githubHandle"
|
||||
value={libraryData.githubHandle}
|
||||
onChange={onInputChange}
|
||||
placeholder={t("publishDialog.placeholder.githubHandle")}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t("publishDialog.twitterUsername")}</span>
|
||||
<input
|
||||
type="text"
|
||||
name="twitterHandle"
|
||||
value={libraryData.twitterHandle}
|
||||
onChange={onInputChange}
|
||||
placeholder={t("publishDialog.placeholder.twitterHandle")}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>{t("publishDialog.website")}</span>
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
pattern="https?://.+"
|
||||
title={t("publishDialog.errors.website")}
|
||||
value={libraryData.website}
|
||||
onChange={onInputChange}
|
||||
placeholder={t("publishDialog.placeholder.website")}
|
||||
/>
|
||||
</label>
|
||||
<span className="publish-library-note">
|
||||
<Trans
|
||||
i18nKey="publishDialog.noteLicense"
|
||||
link={(el) => (
|
||||
<a
|
||||
href="https://github.com/excalidraw/excalidraw-libraries/blob/main/LICENSE"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{el}
|
||||
</a>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="publish-library__buttons">
|
||||
<DialogActionButton
|
||||
label={t("buttons.cancel")}
|
||||
onClick={onDialogClose}
|
||||
data-testid="cancel-clear-canvas-button"
|
||||
/>
|
||||
<DialogActionButton
|
||||
type="submit"
|
||||
label={t("buttons.submit")}
|
||||
actionType="primary"
|
||||
isLoading={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<p style={{ padding: "1em", textAlign: "center", fontWeight: 500 }}>
|
||||
{t("publishDialog.atleastOneLibItem")}
|
||||
</p>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublishLibrary;
|
Loading…
Add table
Add a link
Reference in a new issue