Merge branch 'master' into mrazator/canvas-partitioning

# Conflicts:
#	yarn.lock
This commit is contained in:
dwelle 2023-08-06 22:10:19 +02:00
commit a502e49f78
28 changed files with 322 additions and 142 deletions

5
.codesandbox/Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM node:18-bullseye
# Vite wants to open the browser using `open`, so we
# need to install those utils.
RUN apt update -y && apt install -y xdg-utils

View File

@ -27,7 +27,10 @@
"start": {
"name": "Start Excalidraw",
"command": "yarn start",
"runAtStart": true
"runAtStart": true,
"preview": {
"port": 3000
}
},
"test": {
"name": "Run Tests",
@ -37,7 +40,11 @@
"install-deps": {
"name": "Install Dependencies",
"command": "yarn install",
"restartOn": { "files": ["yarn.lock"] }
"restartOn": {
"files": ["yarn.lock"],
"branch": false,
"resume": false
}
}
}
}

View File

@ -1,5 +1,5 @@
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries

26
.github/workflows/test-coverage-pr.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Test Coverage PR
on:
pull_request:
jobs:
coverage:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v2
- name: "Install Node"
uses: actions/setup-node@v2
with:
node-version: "18.x"
- name: "Install Deps"
run: yarn --frozen-lockfile
- name: "Test Coverage"
run: yarn test:coverage
- name: "Report Coverage"
if: always() # Also generate the report if tests are failing
uses: davelosert/vitest-coverage-report-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

2
.nvmrc
View File

@ -1 +1 @@
14
18

View File

@ -69,6 +69,10 @@ It's also a good idea to consider if your change should include additional tests
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
:::note
Some checks, such as the `lint` and `test`, require approval from the maintainers to run.
They will appear as `Expected — Waiting for status to be reported` in the PR checks when they are waiting for approval.
:::
## Translating

View File

@ -6611,19 +6611,19 @@ semver@7.0.0:
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
semver@^5.4.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
version "5.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7:
version "7.3.7"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
dependencies:
lru-cache "^6.0.0"

View File

@ -70,6 +70,7 @@
"@types/resize-observer-browser": "0.1.7",
"@types/socket.io-client": "1.4.36",
"@vitejs/plugin-react": "3.1.0",
"@vitest/coverage-v8": "0.33.0",
"@vitest/ui": "0.32.2",
"chai": "4.3.6",
"dotenv": "16.0.1",
@ -101,8 +102,8 @@
"private": true,
"scripts": {
"build-node": "node ./scripts/build-node.js",
"build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true REACT_APP_DISABLE_TRACKING=true vite build",
"build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
"build:version": "node ./scripts/build-version.js",
"build": "yarn build:app && yarn build:version",
"fix:code": "yarn test:code --fix",
@ -114,14 +115,15 @@
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
"start": "vite",
"start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
"test:app": "vitest --config vitest.config.ts",
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
"test:other": "yarn prettier --list-different",
"test:typecheck": "tsc",
"test:update": "yarn test:app --update --watch=false",
"test": "yarn test:app",
"test:coverage": "vitest --coverage --watchAll",
"test:coverage": "vitest --coverage",
"test:coverage:watch": "vitest --coverage --watch",
"test:ui": "yarn test --ui",
"autorelease": "node scripts/autorelease.js",
"prerelease": "node scripts/prerelease.js",

20
public/service-worker.js Normal file
View File

@ -0,0 +1,20 @@
// Since we migrated to Vite, the service worker strategy changed, in CRA it was a custom service worker named service-worker.js and in Vite its sw.js handled by vite-plugin-pwa
// Due to this the existing CRA users were not able to migrate to Vite or any new changes post Vite unless browser is hard refreshed
// Hence adding a self destroying worker so all CRA service workers are destroyed and migrated to Vite
// We should remove this code after sometime when we are confident that
// all users have migrated to Vite
self.addEventListener("install", () => {
self.skipWaiting();
});
self.addEventListener("activate", () => {
self.registration
.unregister()
.then(() => {
return self.clients.matchAll();
})
.then((clients) => {
clients.forEach((client) => client.navigate(client.url));
});
});

View File

@ -297,7 +297,6 @@ import {
getApproxMinLineWidth,
getBoundTextElement,
getContainerCenter,
getContainerDims,
getContainerElement,
getDefaultLineHeight,
getLineHeightInPx,
@ -3464,9 +3463,8 @@ class App extends React.Component<AppProps, AppState> {
lineHeight,
);
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
const containerDims = getContainerDims(container);
const newHeight = Math.max(containerDims.height, minHeight);
const newWidth = Math.max(containerDims.width, minWidth);
const newHeight = Math.max(container.height, minHeight);
const newWidth = Math.max(container.width, minWidth);
mutateElement(container, { height: newHeight, width: newWidth });
sceneX = container.x + newWidth / 2;
sceneY = container.y + newHeight / 2;
@ -5309,7 +5307,7 @@ class App extends React.Component<AppProps, AppState> {
width: embedLink.aspectRatio.w,
height: embedLink.aspectRatio.h,
link,
validated: undefined,
validated: null,
});
this.scene.replaceAllElements([
@ -5497,7 +5495,7 @@ class App extends React.Component<AppProps, AppState> {
}
private createGenericElementOnPointerDown = (
elementType: ExcalidrawGenericElement["type"],
elementType: ExcalidrawGenericElement["type"] | "embeddable",
pointerDownState: PointerDownState,
): void => {
const [gridX, gridY] = getGridPoint(
@ -5511,8 +5509,7 @@ class App extends React.Component<AppProps, AppState> {
y: gridY,
});
const element = newElement({
type: elementType,
const baseElementAttributes = {
x: gridX,
y: gridY,
strokeColor: this.state.currentItemStrokeColor,
@ -5525,8 +5522,21 @@ class App extends React.Component<AppProps, AppState> {
roundness: this.getCurrentItemRoundness(elementType),
locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null,
...(elementType === "embeddable" ? { validated: false } : {}),
});
} as const;
let element;
if (elementType === "embeddable") {
element = newEmbeddableElement({
type: "embeddable",
validated: null,
...baseElementAttributes,
});
} else {
element = newElement({
type: elementType,
...baseElementAttributes,
});
}
if (element.type === "selection") {
this.setState({

View File

@ -77,8 +77,8 @@ export const EyeDropper: React.FC<{
colorPreviewDiv.style.left = `${clientX + 20}px`;
const pixel = ctx.getImageData(
clientX * window.devicePixelRatio - appState.offsetLeft,
clientY * window.devicePixelRatio - appState.offsetTop,
(clientX - appState.offsetLeft) * window.devicePixelRatio,
(clientY - appState.offsetTop) * window.devicePixelRatio,
1,
1,
).data;

View File

@ -117,6 +117,7 @@ export const FRAME_STYLE = {
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
export const MIN_FONT_SIZE = 1;
export const DEFAULT_FONT_SIZE = 20;
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
export const DEFAULT_TEXT_ALIGN = "left";
@ -239,6 +240,8 @@ export const VERSIONS = {
} as const;
export const BOUND_TEXT_PADDING = 5;
export const ARROW_LABEL_WIDTH_FRACTION = 0.7;
export const ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO = 11;
export const VERTICAL_ALIGN = {
TOP: "top",

View File

@ -286,7 +286,7 @@ const restoreElement = (
return restoreElementWithProperties(element, {});
case "embeddable":
return restoreElementWithProperties(element, {
validated: undefined,
validated: null,
});
case "frame":
return restoreElementWithProperties(element, {

View File

@ -269,11 +269,11 @@ export class LinearElementEditor {
};
}),
);
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
handleBindTextResize(element, false);
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
handleBindTextResize(element, false);
}
// suggest bindings for first and last point if selected

View File

@ -134,7 +134,7 @@ export const newElement = (
export const newEmbeddableElement = (
opts: {
type: "embeddable";
validated: boolean | undefined;
validated: ExcalidrawEmbeddableElement["validated"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawEmbeddableElement> => {
return {

View File

@ -1,4 +1,4 @@
import { SHIFT_LOCKING_ANGLE } from "../constants";
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
import { rescalePoints } from "../points";
import {
@ -204,8 +204,6 @@ const rescalePointsInElement = (
}
: {};
const MIN_FONT_SIZE = 1;
const measureFontSizeFromWidth = (
element: NonDeleted<ExcalidrawTextElement>,
nextWidth: number,
@ -589,24 +587,42 @@ export const resizeSingleElement = (
});
}
if (
isArrowElement(element) &&
boundTextElement &&
shouldMaintainAspectRatio
) {
const fontSize =
(resizedElement.width / element.width) * boundTextElement.fontSize;
if (fontSize < MIN_FONT_SIZE) {
return;
}
boundTextFont.fontSize = fontSize;
}
if (
resizedElement.width !== 0 &&
resizedElement.height !== 0 &&
Number.isFinite(resizedElement.x) &&
Number.isFinite(resizedElement.y)
) {
mutateElement(element, resizedElement);
updateBoundElements(element, {
newSize: { width: resizedElement.width, height: resizedElement.height },
});
mutateElement(element, resizedElement);
if (boundTextElement && boundTextFont != null) {
mutateElement(boundTextElement, {
fontSize: boundTextFont.fontSize,
baseline: boundTextFont.baseline,
});
}
handleBindTextResize(element, transformHandleDirection);
handleBindTextResize(
element,
transformHandleDirection,
shouldMaintainAspectRatio,
);
}
};
@ -722,12 +738,8 @@ export const resizeMultipleElements = (
fontSize?: ExcalidrawTextElement["fontSize"];
baseline?: ExcalidrawTextElement["baseline"];
scale?: ExcalidrawImageElement["scale"];
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
};
boundText: {
element: ExcalidrawTextElementWithContainer;
fontSize: ExcalidrawTextElement["fontSize"];
baseline: ExcalidrawTextElement["baseline"];
} | null;
}[] = [];
for (const { orig, latest } of targetElements) {
@ -798,50 +810,39 @@ export const resizeMultipleElements = (
}
}
let boundText: typeof elementsAndUpdates[0]["boundText"] = null;
const boundTextElement = getBoundTextElement(latest);
if (boundTextElement || isTextElement(orig)) {
const updatedElement = {
...latest,
width,
height,
};
const metrics = measureFontSizeFromWidth(
boundTextElement ?? (orig as ExcalidrawTextElement),
boundTextElement
? getBoundTextMaxWidth(updatedElement)
: updatedElement.width,
boundTextElement
? getBoundTextMaxHeight(updatedElement, boundTextElement)
: updatedElement.height,
);
if (isTextElement(orig)) {
const metrics = measureFontSizeFromWidth(orig, width, height);
if (!metrics) {
return;
}
if (isTextElement(orig)) {
update.fontSize = metrics.size;
update.baseline = metrics.baseline;
}
if (boundTextElement) {
boundText = {
element: boundTextElement,
fontSize: metrics.size,
baseline: metrics.baseline,
};
}
update.fontSize = metrics.size;
update.baseline = metrics.baseline;
}
elementsAndUpdates.push({ element: latest, update, boundText });
const boundTextElement = pointerDownState.originalElements.get(
getBoundTextElementId(orig) ?? "",
) as ExcalidrawTextElementWithContainer | undefined;
if (boundTextElement) {
const newFontSize = boundTextElement.fontSize * scale;
if (newFontSize < MIN_FONT_SIZE) {
return;
}
update.boundTextFontSize = newFontSize;
}
elementsAndUpdates.push({
element: latest,
update,
});
}
const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
for (const { element, update, boundText } of elementsAndUpdates) {
for (const {
element,
update: { boundTextFontSize, ...update },
} of elementsAndUpdates) {
const { width, height, angle } = update;
mutateElement(element, update, false);
@ -851,17 +852,17 @@ export const resizeMultipleElements = (
newSize: { width, height },
});
if (boundText) {
const { element: boundTextElement, ...boundTextUpdates } = boundText;
const boundTextElement = getBoundTextElement(element);
if (boundTextElement && boundTextFontSize) {
mutateElement(
boundTextElement,
{
...boundTextUpdates,
fontSize: boundTextFontSize,
angle: isLinearElement(element) ? undefined : angle,
},
false,
);
handleBindTextResize(element, transformHandleType);
handleBindTextResize(element, transformHandleType, true);
}
}

View File

@ -10,6 +10,8 @@ import {
} from "./types";
import { mutateElement } from "./mutateElement";
import {
ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO,
ARROW_LABEL_WIDTH_FRACTION,
BOUND_TEXT_PADDING,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
@ -65,7 +67,7 @@ export const redrawTextBoundingBox = (
boundTextUpdates.text = textElement.text;
if (container) {
maxWidth = getBoundTextMaxWidth(container);
maxWidth = getBoundTextMaxWidth(container, textElement);
boundTextUpdates.text = wrapText(
textElement.originalText,
getFontString(textElement),
@ -83,13 +85,12 @@ export const redrawTextBoundingBox = (
boundTextUpdates.baseline = metrics.baseline;
if (container) {
const containerDims = getContainerDims(container);
const maxContainerHeight = getBoundTextMaxHeight(
container,
textElement as ExcalidrawTextElementWithContainer,
);
let nextHeight = containerDims.height;
let nextHeight = container.height;
if (metrics.height > maxContainerHeight) {
nextHeight = computeContainerDimensionForBoundText(
metrics.height,
@ -155,6 +156,7 @@ export const bindTextToShapeAfterDuplication = (
export const handleBindTextResize = (
container: NonDeletedExcalidrawElement,
transformHandleType: MaybeTransformHandleType,
shouldMaintainAspectRatio = false,
) => {
const boundTextElementId = getBoundTextElementId(container);
if (!boundTextElementId) {
@ -175,15 +177,17 @@ export const handleBindTextResize = (
let text = textElement.text;
let nextHeight = textElement.height;
let nextWidth = textElement.width;
const containerDims = getContainerDims(container);
const maxWidth = getBoundTextMaxWidth(container);
const maxHeight = getBoundTextMaxHeight(
container,
textElement as ExcalidrawTextElementWithContainer,
);
let containerHeight = containerDims.height;
let containerHeight = container.height;
let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") {
if (
shouldMaintainAspectRatio ||
(transformHandleType !== "n" && transformHandleType !== "s")
) {
if (text) {
text = wrapText(
textElement.originalText,
@ -207,7 +211,7 @@ export const handleBindTextResize = (
container.type,
);
const diff = containerHeight - containerDims.height;
const diff = containerHeight - container.height;
// fix the y coord when resizing from ne/nw/n
const updatedY =
!isArrowElement(container) &&
@ -687,16 +691,6 @@ export const getContainerElement = (
return null;
};
export const getContainerDims = (element: ExcalidrawElement) => {
const MIN_WIDTH = 300;
if (isArrowElement(element)) {
const width = Math.max(element.width, MIN_WIDTH);
const height = element.height;
return { width, height };
}
return { width: element.width, height: element.height };
};
export const getContainerCenter = (
container: ExcalidrawElement,
appState: AppState,
@ -887,12 +881,19 @@ export const computeContainerDimensionForBoundText = (
return dimension + padding;
};
export const getBoundTextMaxWidth = (container: ExcalidrawElement) => {
const width = getContainerDims(container).width;
export const getBoundTextMaxWidth = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElement | null = getBoundTextElement(
container,
),
) => {
const { width } = container;
if (isArrowElement(container)) {
return width - BOUND_TEXT_PADDING * 8 * 2;
const minWidth =
(boundTextElement?.fontSize ?? DEFAULT_FONT_SIZE) *
ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO;
return Math.max(ARROW_LABEL_WIDTH_FRACTION * width, minWidth);
}
if (container.type === "ellipse") {
// The width of the largest rectangle inscribed inside an ellipse is
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
@ -911,7 +912,7 @@ export const getBoundTextMaxHeight = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer,
) => {
const height = getContainerDims(container).height;
const { height } = container;
if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
if (containerHeight <= 0) {

View File

@ -23,7 +23,6 @@ import { AppState } from "../types";
import { mutateElement } from "./mutateElement";
import {
getBoundTextElementId,
getContainerDims,
getContainerElement,
getTextElementAngle,
getTextWidth,
@ -177,20 +176,19 @@ export const textWysiwyg = ({
updatedTextElement,
editable,
);
const containerDims = getContainerDims(container);
let originalContainerData;
if (propertiesUpdated) {
originalContainerData = updateOriginalContainerCache(
container.id,
containerDims.height,
container.height,
);
} else {
originalContainerData = originalContainerCache[container.id];
if (!originalContainerData) {
originalContainerData = updateOriginalContainerCache(
container.id,
containerDims.height,
container.height,
);
}
}
@ -214,7 +212,7 @@ export const textWysiwyg = ({
// autoshrink container height until original container height
// is reached when text is removed
!isArrowElement(container) &&
containerDims.height > originalContainerData.height &&
container.height > originalContainerData.height &&
textElementHeight < maxHeight
) {
const targetContainerHeight = computeContainerDimensionForBoundText(

View File

@ -86,15 +86,15 @@ export type ExcalidrawEllipseElement = _ExcalidrawElementBase & {
export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase &
Readonly<{
type: "embeddable";
/**
* indicates whether the embeddable src (url) has been validated for rendering.
* nullish value indicates that the validation is pending. We reset the
* null value indicates that the validation is pending. We reset the
* value on each restore (or url change) so that we can guarantee
* the validation came from a trusted source (the editor). Also because we
* may not have access to host-app supplied url validator during restore.
*/
validated?: boolean;
type: "embeddable";
validated: boolean | null;
}>;
export type ExcalidrawImageElement = _ExcalidrawElementBase &
@ -123,7 +123,6 @@ export type ExcalidrawFrameElement = _ExcalidrawElementBase & {
export type ExcalidrawGenericElement =
| ExcalidrawSelectionElement
| ExcalidrawRectangleElement
| ExcalidrawEmbeddableElement
| ExcalidrawDiamondElement
| ExcalidrawEllipseElement;
@ -138,7 +137,8 @@ export type ExcalidrawElement =
| ExcalidrawLinearElement
| ExcalidrawFreeDrawElement
| ExcalidrawImageElement
| ExcalidrawFrameElement;
| ExcalidrawFrameElement
| ExcalidrawEmbeddableElement;
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
isDeleted: boolean;

View File

@ -6,12 +6,11 @@ const SentryEnvHostnameMap: { [key: string]: string } = {
"vercel.app": "staging",
};
const REACT_APP_DISABLE_SENTRY =
import.meta.env.VITE_APP_DISABLE_SENTRY === "true";
const SENTRY_DISABLED = import.meta.env.VITE_APP_DISABLE_SENTRY === "true";
// Disable Sentry locally or inside the Docker to avoid noise/respect privacy
const onlineEnv =
!REACT_APP_DISABLE_SENTRY &&
!SENTRY_DISABLED &&
Object.keys(SentryEnvHostnameMap).find(
(item) => window.location.hostname.indexOf(item) >= 0,
);

View File

@ -2,8 +2,8 @@ const path = require("path");
const webpack = require("webpack");
const autoprefixer = require("autoprefixer");
const { parseEnvVariables } = require("./env");
const outputDir = process.env.EXAMPLE === "true" ? "example/public" : "dist";
module.exports = {
mode: "development",
devtool: false,
@ -17,7 +17,6 @@ module.exports = {
filename: "[name].js",
chunkFilename: "excalidraw-assets-dev/[name]-[contenthash].js",
assetModuleFilename: "excalidraw-assets-dev/[name][ext]",
publicPath: "",
},
resolve: {
@ -45,7 +44,7 @@ module.exports = {
{
test: /\.(ts|tsx|js|jsx|mjs)$/,
exclude:
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
/node_modules[\\/](?!(browser-fs-access|canvas-roundrect-polyfill))/,
use: [
{
loader: "import-meta-loader",

View File

@ -1,10 +1,10 @@
const path = require("path");
const webpack = require("webpack");
const autoprefixer = require("autoprefixer");
const { parseEnvVariables } = require("./env");
const TerserPlugin = require("terser-webpack-plugin");
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const autoprefixer = require("autoprefixer");
const webpack = require("webpack");
const { parseEnvVariables } = require("./env");
module.exports = {
mode: "production",
@ -47,8 +47,7 @@ module.exports = {
{
test: /\.(ts|tsx|js|jsx|mjs)$/,
exclude:
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
/node_modules[\\/](?!(browser-fs-access|canvas-roundrect-polyfill))/,
use: [
{
loader: "import-meta-loader",

View File

@ -34,6 +34,7 @@ export const rectangleFixture: ExcalidrawElement = {
export const embeddableFixture: ExcalidrawElement = {
...elementBase,
type: "embeddable",
validated: null,
};
export const ellipseFixture: ExcalidrawElement = {
...elementBase,

View File

@ -15,7 +15,11 @@ import fs from "fs";
import util from "util";
import path from "path";
import { getMimeType } from "../../data/blob";
import { newFreeDrawElement, newImageElement } from "../../element/newElement";
import {
newEmbeddableElement,
newFreeDrawElement,
newImageElement,
} from "../../element/newElement";
import { Point } from "../../types";
import { getSelectedElements } from "../../scene/selection";
import { isLinearElementType } from "../../element/typeChecks";
@ -178,14 +182,20 @@ export class API {
case "rectangle":
case "diamond":
case "ellipse":
case "embeddable":
element = newElement({
type: type as "rectangle" | "diamond" | "ellipse" | "embeddable",
type: type as "rectangle" | "diamond" | "ellipse",
width,
height,
...base,
});
break;
case "embeddable":
element = newEmbeddableElement({
type: "embeddable",
...base,
validated: null,
});
break;
case "text":
const fontSize = rest.fontSize ?? appState.currentItemFontSize;
const fontFamily = rest.fontFamily ?? appState.currentItemFontFamily;

View File

@ -1143,7 +1143,7 @@ describe("Test Linear Elements", () => {
height: 500,
});
const arrow = UI.createElement("arrow", {
x: 210,
x: -10,
y: 250,
width: 400,
height: 1,
@ -1167,8 +1167,8 @@ describe("Test Linear Elements", () => {
expect(
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
).toMatchInlineSnapshot(`
"Online whiteboard collaboration
made easy"
"Online whiteboard
collaboration made easy"
`);
const handleBindTextResizeSpy = vi.spyOn(
textElementUtils,
@ -1180,7 +1180,7 @@ describe("Test Linear Elements", () => {
mouse.moveTo(200, 0);
mouse.upAt(200, 0);
expect(arrow.width).toBe(170);
expect(arrow.width).toBe(200);
expect(rect.x).toBe(200);
expect(rect.y).toBe(0);
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(

View File

@ -61,7 +61,7 @@ export default defineConfig({
workbox: {
// Don't push fonts and locales to app precache
globIgnores: ["fonts.css", "**/locales/**"],
globIgnores: ["fonts.css", "**/locales/**", "service-worker.js"],
runtimeCaching: [
{
urlPattern: new RegExp("/.+.(ttf|woff2|otf)"),

View File

@ -5,5 +5,12 @@ export default defineConfig({
setupFiles: ["./src/setupTests.ts"],
globals: true,
environment: "jsdom",
coverage: {
reporter: ["text", "json-summary", "json"],
lines: 70,
branches: 70,
functions: 68,
statements: 70,
},
},
});

100
yarn.lock
View File

@ -2,7 +2,7 @@
# yarn lockfile v1
"@ampproject/remapping@^2.2.0":
"@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==
@ -1260,6 +1260,11 @@
"@babel/helper-validator-identifier" "^7.22.5"
to-fast-properties "^2.0.0"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@braintree/sanitize-url@6.0.2":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f"
@ -1812,6 +1817,11 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@istanbuljs/schema@^0.1.2":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
"@jest/expect-utils@^29.5.0":
version "29.5.0"
resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.5.0.tgz#f74fad6b6e20f924582dc8ecbf2cb800fe43a036"
@ -1875,7 +1885,7 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
version "0.3.18"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6"
integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==
@ -2500,7 +2510,7 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==
@ -2786,6 +2796,23 @@
magic-string "^0.27.0"
react-refresh "^0.14.0"
"@vitest/coverage-v8@0.33.0":
version "0.33.0"
resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-0.33.0.tgz#dfcb36cf51624a89d33ab0962a6eec8a41346ef2"
integrity sha512-Rj5IzoLF7FLj6yR7TmqsfRDSeaFki6NAJ/cQexqhbWkHEV2htlVGrmuOde3xzvFsCbLCagf4omhcIaVmfU8Okg==
dependencies:
"@ampproject/remapping" "^2.2.1"
"@bcoe/v8-coverage" "^0.2.3"
istanbul-lib-coverage "^3.2.0"
istanbul-lib-report "^3.0.0"
istanbul-lib-source-maps "^4.0.1"
istanbul-reports "^3.1.5"
magic-string "^0.30.1"
picocolors "^1.0.0"
std-env "^3.3.3"
test-exclude "^6.0.0"
v8-to-istanbul "^9.1.0"
"@vitest/expect@0.34.1":
version "0.34.1"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.34.1.tgz#2ba6cb96695f4b4388c6d955423a81afc79b8da0"
@ -3491,7 +3518,7 @@ confusing-browser-globals@^1.0.11:
resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81"
integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==
convert-source-map@^1.7.0:
convert-source-map@^1.6.0, convert-source-map@^1.7.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
@ -4536,7 +4563,7 @@ glob-parent@^5.1.2, glob-parent@~5.1.2:
dependencies:
is-glob "^4.0.1"
glob@^7.1.3, glob@^7.1.6:
glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@ -4671,6 +4698,11 @@ html-encoding-sniffer@^3.0.0:
dependencies:
whatwg-encoding "^2.0.0"
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
http-parser-js@>=0.5.1:
version "0.5.8"
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3"
@ -5060,6 +5092,37 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3"
integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==
istanbul-lib-report@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d"
integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==
dependencies:
istanbul-lib-coverage "^3.0.0"
make-dir "^4.0.0"
supports-color "^7.1.0"
istanbul-lib-source-maps@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551"
integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==
dependencies:
debug "^4.1.1"
istanbul-lib-coverage "^3.0.0"
source-map "^0.6.1"
istanbul-reports@^3.1.5:
version "3.1.6"
resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.6.tgz#2544bcab4768154281a2f0870471902704ccaa1a"
integrity sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==
dependencies:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
jake@^10.8.5:
version "10.8.5"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46"
@ -5477,6 +5540,13 @@ magic-string@^0.30.1:
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
make-dir@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==
dependencies:
semver "^7.5.3"
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@ -6420,7 +6490,7 @@ semver@^7.2.1, semver@^7.3.7:
dependencies:
lru-cache "^6.0.0"
semver@^7.3.4, semver@^7.5.0:
semver@^7.3.4, semver@^7.5.0, semver@^7.5.3:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
@ -6799,6 +6869,15 @@ terser@^5.0.0:
commander "^2.20.0"
source-map-support "~0.5.20"
test-exclude@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
dependencies:
"@istanbuljs/schema" "^0.1.2"
glob "^7.1.4"
minimatch "^3.0.4"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@ -7096,6 +7175,15 @@ v8-compile-cache@^2.0.3:
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
v8-to-istanbul@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265"
integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==
dependencies:
"@jridgewell/trace-mapping" "^0.3.12"
"@types/istanbul-lib-coverage" "^2.0.1"
convert-source-map "^1.6.0"
vite-node@0.34.1:
version "0.34.1"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.34.1.tgz#144900ca4bd54cc419c501d671350bcbc07eb1ee"