mirror of
https://github.com/excalidraw/excalidraw
synced 2025-07-25 13:58:22 +08:00
Merge branch 'master' into ryan-di/freedraw-width
# Conflicts: # packages/excalidraw/package.json
This commit is contained in:
2
.github/workflows/autorelease-excalidraw.yml
vendored
2
.github/workflows/autorelease-excalidraw.yml
vendored
@ -24,4 +24,4 @@ jobs:
|
||||
- name: Auto release
|
||||
run: |
|
||||
yarn add @actions/core -W
|
||||
yarn autorelease
|
||||
yarn release --tag=next --non-interactive
|
||||
|
55
.github/workflows/autorelease-preview.yml
vendored
55
.github/workflows/autorelease-preview.yml
vendored
@ -1,55 +0,0 @@
|
||||
name: Auto release excalidraw preview
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
Auto-release-excalidraw-preview:
|
||||
name: Auto release preview
|
||||
if: github.event.comment.body == '@excalibot trigger release' && github.event.issue.pull_request
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: React to release comment
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: "+1"
|
||||
- name: Get PR SHA
|
||||
id: sha
|
||||
uses: actions/github-script@v4
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
const { owner, repo, number } = context.issue;
|
||||
const pr = await github.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: number,
|
||||
});
|
||||
return pr.data.head.sha
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ steps.sha.outputs.result }}
|
||||
fetch-depth: 2
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Set up publish access
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Auto release preview
|
||||
id: "autorelease"
|
||||
run: |
|
||||
yarn add @actions/core -W
|
||||
yarn autorelease preview ${{ github.event.issue.number }}
|
||||
- name: Post comment post release
|
||||
if: always()
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: "@${{ github.event.comment.user.login }} ${{ steps.autorelease.outputs.result }}"
|
@ -28,32 +28,12 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the
|
||||
|
||||
## Releasing
|
||||
|
||||
### Create a test release
|
||||
|
||||
You can create a test release by posting the below comment in your pull request:
|
||||
|
||||
```bash
|
||||
@excalibot trigger release
|
||||
```
|
||||
|
||||
Once the version is released `@excalibot` will post a comment with the release version.
|
||||
|
||||
### Creating a production release
|
||||
|
||||
To release the next stable version follow the below steps:
|
||||
|
||||
```bash
|
||||
yarn prerelease:excalidraw
|
||||
yarn release --tag=latest --version=0.19.0
|
||||
```
|
||||
|
||||
You need to pass the `version` for which you want to create the release. This will make the changes needed before making the release like updating `package.json`, `changelog` and more.
|
||||
|
||||
The next step is to run the `release` script:
|
||||
|
||||
```bash
|
||||
yarn release:excalidraw
|
||||
```
|
||||
|
||||
This will publish the package.
|
||||
|
||||
Right now there are two steps to create a production release but once this works fine these scripts will be combined and more automation will be done.
|
||||
You will need to pass the `latest` tag with `version` for which you want to create the release. This will make the changes needed before publishing the packages into NPM, like updating dependencies of all `@excalidraw/*` packages, generating new entries in `CHANGELOG.md` and more.
|
||||
|
@ -33,6 +33,7 @@ const ExcalidrawScope = {
|
||||
initialData,
|
||||
useI18n: ExcalidrawComp.useI18n,
|
||||
convertToExcalidrawElements: ExcalidrawComp.convertToExcalidrawElements,
|
||||
CaptureUpdateAction: ExcalidrawComp.CaptureUpdateAction,
|
||||
};
|
||||
|
||||
export default ExcalidrawScope;
|
||||
|
@ -3,7 +3,8 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm && yarn copy:assets",
|
||||
"build:packages": "yarn --cwd ../../ build:packages",
|
||||
"build:workspace": "yarn build:packages && yarn copy:assets",
|
||||
"copy:assets": "cp -r ../../packages/excalidraw/dist/prod/fonts ./public",
|
||||
"dev": "yarn build:workspace && next dev -p 3005",
|
||||
"build": "yarn build:workspace && next build",
|
||||
|
@ -17,6 +17,6 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 5002",
|
||||
"build:preview": "yarn build && yarn preview",
|
||||
"build:package": "yarn workspace @excalidraw/excalidraw run build:esm"
|
||||
"build:packages": "yarn --cwd ../../ build:packages"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"outputDirectory": "dist",
|
||||
"installCommand": "yarn install",
|
||||
"buildCommand": "yarn build:package && yarn build"
|
||||
"buildCommand": "yarn build:packages && yarn build"
|
||||
}
|
||||
|
15
package.json
15
package.json
@ -52,13 +52,17 @@
|
||||
"build-node": "node ./scripts/build-node.js",
|
||||
"build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker",
|
||||
"build:app": "yarn --cwd ./excalidraw-app build:app",
|
||||
"build:package": "yarn --cwd ./packages/excalidraw build:esm",
|
||||
"build:common": "yarn --cwd ./packages/common build:esm",
|
||||
"build:element": "yarn --cwd ./packages/element build:esm",
|
||||
"build:excalidraw": "yarn --cwd ./packages/excalidraw build:esm",
|
||||
"build:math": "yarn --cwd ./packages/math build:esm",
|
||||
"build:packages": "yarn build:common && yarn build:math && yarn build:element && yarn build:excalidraw",
|
||||
"build:version": "yarn --cwd ./excalidraw-app build:version",
|
||||
"build": "yarn --cwd ./excalidraw-app build",
|
||||
"build:preview": "yarn --cwd ./excalidraw-app build:preview",
|
||||
"start": "yarn --cwd ./excalidraw-app start",
|
||||
"start:production": "yarn --cwd ./excalidraw-app start:production",
|
||||
"start:example": "yarn build:package && yarn --cwd ./examples/with-script-in-browser start",
|
||||
"start:example": "yarn build:packages && yarn --cwd ./examples/with-script-in-browser start",
|
||||
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
|
||||
"test:app": "vitest",
|
||||
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
||||
@ -76,9 +80,10 @@
|
||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||
"prepare": "husky install",
|
||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||
"autorelease": "node scripts/autorelease.js",
|
||||
"prerelease:excalidraw": "node scripts/prerelease.js",
|
||||
"release:excalidraw": "node scripts/release.js",
|
||||
"release": "node scripts/release.js",
|
||||
"release:test": "node scripts/release.js --tag=test",
|
||||
"release:next": "node scripts/release.js --tag=next",
|
||||
"release:latest": "node scripts/release.js --tag=latest",
|
||||
"rm:build": "rimraf --glob excalidraw-app/build excalidraw-app/dist excalidraw-app/dev-dist packages/*/dist packages/*/build examples/*/build examples/*/dist",
|
||||
"rm:node_modules": "rimraf --glob node_modules excalidraw-app/node_modules packages/*/node_modules",
|
||||
"clean-install": "yarn rm:node_modules && yarn install"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@excalidraw/common",
|
||||
"version": "0.1.0",
|
||||
"version": "0.18.0",
|
||||
"type": "module",
|
||||
"types": "./dist/types/common/src/index.d.ts",
|
||||
"main": "./dist/prod/index.js",
|
||||
@ -13,7 +13,10 @@
|
||||
"default": "./dist/prod/index.js"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./dist/types/common/src/*.d.ts"
|
||||
"types": "./dist/types/common/src/*.d.ts",
|
||||
"development": "./dist/dev/index.js",
|
||||
"production": "./dist/prod/index.js",
|
||||
"default": "./dist/prod/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@excalidraw/element",
|
||||
"version": "0.1.0",
|
||||
"version": "0.18.0",
|
||||
"type": "module",
|
||||
"types": "./dist/types/element/src/index.d.ts",
|
||||
"main": "./dist/prod/index.js",
|
||||
@ -13,7 +13,10 @@
|
||||
"default": "./dist/prod/index.js"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./dist/types/element/src/*.d.ts"
|
||||
"types": "./dist/types/element/src/*.d.ts",
|
||||
"development": "./dist/dev/index.js",
|
||||
"production": "./dist/prod/index.js",
|
||||
"default": "./dist/prod/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@ -52,5 +55,9 @@
|
||||
"scripts": {
|
||||
"gen:types": "rimraf types && tsc",
|
||||
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/common": "0.18.0",
|
||||
"@excalidraw/math": "0.18.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { updateBoundElements } from "./binding";
|
||||
import { getCommonBoundingBox } from "./bounds";
|
||||
import { getMaximumGroups } from "./groups";
|
||||
import { getSelectedElementsByGroup } from "./groups";
|
||||
|
||||
import type { Scene } from "./Scene";
|
||||
|
||||
@ -16,11 +18,12 @@ export const alignElements = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
alignment: Alignment,
|
||||
scene: Scene,
|
||||
appState: Readonly<AppState>,
|
||||
): ExcalidrawElement[] => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||
const groups: ExcalidrawElement[][] = getSelectedElementsByGroup(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
appState,
|
||||
);
|
||||
const selectionBoundingBox = getCommonBoundingBox(selectedElements);
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { getCommonBoundingBox } from "./bounds";
|
||||
import { newElementWith } from "./mutateElement";
|
||||
|
||||
import { getMaximumGroups } from "./groups";
|
||||
import { getSelectedElementsByGroup } from "./groups";
|
||||
|
||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||
|
||||
@ -14,6 +16,7 @@ export const distributeElements = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
distribution: Distribution,
|
||||
appState: Readonly<AppState>,
|
||||
): ExcalidrawElement[] => {
|
||||
const [start, mid, end, extent] =
|
||||
distribution.axis === "x"
|
||||
@ -21,7 +24,11 @@ export const distributeElements = (
|
||||
: (["minY", "midY", "maxY", "height"] as const);
|
||||
|
||||
const bounds = getCommonBoundingBox(selectedElements);
|
||||
const groups = getMaximumGroups(selectedElements, elementsMap)
|
||||
const groups = getSelectedElementsByGroup(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
appState,
|
||||
)
|
||||
.map((group) => [group, getCommonBoundingBox(group)] as const)
|
||||
.sort((a, b) => a[1][mid] - b[1][mid]);
|
||||
|
||||
|
@ -23,7 +23,7 @@ type IframeDataWithSandbox = MarkRequired<IframeData, "sandbox">;
|
||||
const embeddedLinkCache = new Map<string, IframeDataWithSandbox>();
|
||||
|
||||
const RE_YOUTUBE =
|
||||
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
|
||||
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)/;
|
||||
|
||||
const RE_VIMEO =
|
||||
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}\.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
|
||||
@ -56,6 +56,35 @@ const RE_REDDIT =
|
||||
const RE_REDDIT_EMBED =
|
||||
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
|
||||
|
||||
const parseYouTubeTimestamp = (url: string): number => {
|
||||
let timeParam: string | null | undefined;
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url.startsWith("http") ? url : `https://${url}`);
|
||||
timeParam =
|
||||
urlObj.searchParams.get("t") || urlObj.searchParams.get("start");
|
||||
} catch (error) {
|
||||
const timeMatch = url.match(/[?&#](?:t|start)=([^&#\s]+)/);
|
||||
timeParam = timeMatch?.[1];
|
||||
}
|
||||
|
||||
if (!timeParam) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (/^\d+$/.test(timeParam)) {
|
||||
return parseInt(timeParam, 10);
|
||||
}
|
||||
|
||||
const timeMatch = timeParam.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
|
||||
if (!timeMatch) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const [, hours = "0", minutes = "0", seconds = "0"] = timeMatch;
|
||||
return parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds);
|
||||
};
|
||||
|
||||
const ALLOWED_DOMAINS = new Set([
|
||||
"youtube.com",
|
||||
"youtu.be",
|
||||
@ -113,7 +142,8 @@ export const getEmbedLink = (
|
||||
let aspectRatio = { w: 560, h: 840 };
|
||||
const ytLink = link.match(RE_YOUTUBE);
|
||||
if (ytLink?.[2]) {
|
||||
const time = ytLink[3] ? `&start=${ytLink[3]}` : ``;
|
||||
const startTime = parseYouTubeTimestamp(originalLink);
|
||||
const time = startTime > 0 ? `&start=${startTime}` : ``;
|
||||
const isPortrait = link.includes("shorts");
|
||||
type = "video";
|
||||
switch (ytLink[1]) {
|
||||
|
@ -7,6 +7,8 @@ import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
|
||||
import { isBoundToContainer } from "./typeChecks";
|
||||
|
||||
import { makeNextSelectedElementIds, getSelectedElements } from "./selection";
|
||||
|
||||
import type {
|
||||
@ -402,3 +404,78 @@ export const getNewGroupIdsForDuplication = (
|
||||
|
||||
return copy;
|
||||
};
|
||||
|
||||
// given a list of selected elements, return the element grouped by their immediate group selected state
|
||||
// in the case if only one group is selected and all elements selected are within the group, it will respect group hierarchy in accordance to their nested grouping order
|
||||
export const getSelectedElementsByGroup = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
appState: Readonly<AppState>,
|
||||
): ExcalidrawElement[][] => {
|
||||
const selectedGroupIds = getSelectedGroupIds(appState);
|
||||
const unboundElements = selectedElements.filter(
|
||||
(element) => !isBoundToContainer(element),
|
||||
);
|
||||
const groups: Map<string, ExcalidrawElement[]> = new Map();
|
||||
const elements: Map<string, ExcalidrawElement[]> = new Map();
|
||||
|
||||
// helper function to add an element to the elements map
|
||||
const addToElementsMap = (element: ExcalidrawElement) => {
|
||||
// elements
|
||||
const currentElementMembers = elements.get(element.id) || [];
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (boundTextElement) {
|
||||
currentElementMembers.push(boundTextElement);
|
||||
}
|
||||
elements.set(element.id, [...currentElementMembers, element]);
|
||||
};
|
||||
|
||||
// helper function to add an element to the groups map
|
||||
const addToGroupsMap = (element: ExcalidrawElement, groupId: string) => {
|
||||
// groups
|
||||
const currentGroupMembers = groups.get(groupId) || [];
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (boundTextElement) {
|
||||
currentGroupMembers.push(boundTextElement);
|
||||
}
|
||||
groups.set(groupId, [...currentGroupMembers, element]);
|
||||
};
|
||||
|
||||
// helper function to handle the case where a single group is selected
|
||||
// and all elements selected are within the group, it will respect group hierarchy in accordance to
|
||||
// their nested grouping order
|
||||
const handleSingleSelectedGroupCase = (
|
||||
element: ExcalidrawElement,
|
||||
selectedGroupId: GroupId,
|
||||
) => {
|
||||
const indexOfSelectedGroupId = element.groupIds.indexOf(selectedGroupId, 0);
|
||||
const nestedGroupCount = element.groupIds.slice(
|
||||
0,
|
||||
indexOfSelectedGroupId,
|
||||
).length;
|
||||
return nestedGroupCount > 0
|
||||
? addToGroupsMap(element, element.groupIds[indexOfSelectedGroupId - 1])
|
||||
: addToElementsMap(element);
|
||||
};
|
||||
|
||||
const isAllInSameGroup = selectedElements.every((element) =>
|
||||
isSelectedViaGroup(appState, element),
|
||||
);
|
||||
|
||||
unboundElements.forEach((element) => {
|
||||
const selectedGroupId = getSelectedGroupIdForElement(
|
||||
element,
|
||||
appState.selectedGroupIds,
|
||||
);
|
||||
if (!selectedGroupId) {
|
||||
addToElementsMap(element);
|
||||
} else if (selectedGroupIds.length === 1 && isAllInSameGroup) {
|
||||
handleSingleSelectedGroupCase(element, selectedGroupId);
|
||||
} else {
|
||||
addToGroupsMap(element, selectedGroupId);
|
||||
}
|
||||
});
|
||||
return Array.from(groups.values()).concat(Array.from(elements.values()));
|
||||
};
|
||||
|
@ -589,4 +589,424 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].x).toEqual(250);
|
||||
expect(API.getSelectedElements()[3].x).toEqual(150);
|
||||
});
|
||||
|
||||
const createGroupAndSelectInEditGroupMode = () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down();
|
||||
mouse.up(100, 100);
|
||||
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(0, 0);
|
||||
mouse.up(100, 100);
|
||||
|
||||
// select the first element.
|
||||
// The second rectangle is already reselected because it was the last element created
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
API.executeAction(actionGroup);
|
||||
mouse.reset();
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.doubleClick();
|
||||
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.click();
|
||||
mouse.moveTo(100, 100);
|
||||
mouse.click();
|
||||
});
|
||||
};
|
||||
|
||||
it("aligns elements within a group while in group edit mode correctly to the top", () => {
|
||||
createGroupAndSelectInEditGroupMode();
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
|
||||
API.executeAction(actionAlignTop);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(0);
|
||||
});
|
||||
it("aligns elements within a group while in group edit mode correctly to the bottom", () => {
|
||||
createGroupAndSelectInEditGroupMode();
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
|
||||
API.executeAction(actionAlignBottom);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
});
|
||||
it("aligns elements within a group while in group edit mode correctly to the left", () => {
|
||||
createGroupAndSelectInEditGroupMode();
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
|
||||
API.executeAction(actionAlignLeft);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(0);
|
||||
});
|
||||
it("aligns elements within a group while in group edit mode correctly to the right", () => {
|
||||
createGroupAndSelectInEditGroupMode();
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
|
||||
API.executeAction(actionAlignRight);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
});
|
||||
it("aligns elements within a group while in group edit mode correctly to the vertical center", () => {
|
||||
createGroupAndSelectInEditGroupMode();
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
|
||||
API.executeAction(actionAlignVerticallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(50);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(50);
|
||||
});
|
||||
it("aligns elements within a group while in group edit mode correctly to the horizontal center", () => {
|
||||
createGroupAndSelectInEditGroupMode();
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
|
||||
API.executeAction(actionAlignHorizontallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(50);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(50);
|
||||
});
|
||||
|
||||
const createNestedGroupAndSelectInEditGroupMode = () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down();
|
||||
mouse.up(100, 100);
|
||||
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(0, 0);
|
||||
mouse.up(100, 100);
|
||||
|
||||
// Select the first element.
|
||||
// The second rectangle is already reselected because it was the last element created
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
API.executeAction(actionGroup);
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(200, 200);
|
||||
// create third element
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(0, 0);
|
||||
mouse.up(100, 100);
|
||||
|
||||
// third element is already selected, select the initial group and group together
|
||||
mouse.reset();
|
||||
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
API.executeAction(actionGroup);
|
||||
|
||||
// double click to enter edit mode
|
||||
mouse.doubleClick();
|
||||
|
||||
// select nested group and other element within the group
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(200, 200);
|
||||
mouse.click();
|
||||
});
|
||||
};
|
||||
|
||||
it("aligns element and nested group while in group edit mode correctly to the top", () => {
|
||||
createNestedGroupAndSelectInEditGroupMode();
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignTop);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(0);
|
||||
});
|
||||
it("aligns element and nested group while in group edit mode correctly to the bottom", () => {
|
||||
createNestedGroupAndSelectInEditGroupMode();
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignBottom);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(200);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
});
|
||||
it("aligns element and nested group while in group edit mode correctly to the left", () => {
|
||||
createNestedGroupAndSelectInEditGroupMode();
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignLeft);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(0);
|
||||
});
|
||||
it("aligns element and nested group while in group edit mode correctly to the right", () => {
|
||||
createNestedGroupAndSelectInEditGroupMode();
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignRight);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(200);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
});
|
||||
it("aligns element and nested group while in group edit mode correctly to the vertical center", () => {
|
||||
createNestedGroupAndSelectInEditGroupMode();
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignVerticallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(50);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(150);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(100);
|
||||
});
|
||||
it("aligns elements and nested group within a group while in group edit mode correctly to the horizontal center", () => {
|
||||
createNestedGroupAndSelectInEditGroupMode();
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignHorizontallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(50);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(150);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(100);
|
||||
});
|
||||
|
||||
const createAndSelectSingleGroup = () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down();
|
||||
mouse.up(100, 100);
|
||||
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(0, 0);
|
||||
mouse.up(100, 100);
|
||||
|
||||
// Select the first element.
|
||||
// The second rectangle is already reselected because it was the last element created
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
API.executeAction(actionGroup);
|
||||
};
|
||||
|
||||
it("aligns elements within a single-selected group correctly to the top", () => {
|
||||
createAndSelectSingleGroup();
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
|
||||
API.executeAction(actionAlignTop);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(0);
|
||||
});
|
||||
it("aligns elements within a single-selected group correctly to the bottom", () => {
|
||||
createAndSelectSingleGroup();
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
|
||||
API.executeAction(actionAlignBottom);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
});
|
||||
it("aligns elements within a single-selected group correctly to the left", () => {
|
||||
createAndSelectSingleGroup();
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
|
||||
API.executeAction(actionAlignLeft);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(0);
|
||||
});
|
||||
it("aligns elements within a single-selected group correctly to the right", () => {
|
||||
createAndSelectSingleGroup();
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
|
||||
API.executeAction(actionAlignRight);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
});
|
||||
it("aligns elements within a single-selected group correctly to the vertical center", () => {
|
||||
createAndSelectSingleGroup();
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
|
||||
API.executeAction(actionAlignVerticallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(50);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(50);
|
||||
});
|
||||
it("aligns elements within a single-selected group correctly to the horizontal center", () => {
|
||||
createAndSelectSingleGroup();
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
|
||||
API.executeAction(actionAlignHorizontallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(50);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(50);
|
||||
});
|
||||
|
||||
const createAndSelectSingleGroupWithNestedGroup = () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down();
|
||||
mouse.up(100, 100);
|
||||
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(0, 0);
|
||||
mouse.up(100, 100);
|
||||
|
||||
// Select the first element.
|
||||
// The second rectangle is already reselected because it was the last element created
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
API.executeAction(actionGroup);
|
||||
|
||||
mouse.reset();
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(200, 200);
|
||||
mouse.up(100, 100);
|
||||
|
||||
// Add group to current selection
|
||||
mouse.restorePosition(10, 0);
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
// Create the nested group
|
||||
API.executeAction(actionGroup);
|
||||
};
|
||||
it("aligns elements within a single-selected group containing a nested group correctly to the top", () => {
|
||||
createAndSelectSingleGroupWithNestedGroup();
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignTop);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(0);
|
||||
});
|
||||
it("aligns elements within a single-selected group containing a nested group correctly to the bottom", () => {
|
||||
createAndSelectSingleGroupWithNestedGroup();
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignBottom);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(200);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
});
|
||||
it("aligns elements within a single-selected group containing a nested group correctly to the left", () => {
|
||||
createAndSelectSingleGroupWithNestedGroup();
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignLeft);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(0);
|
||||
});
|
||||
it("aligns elements within a single-selected group containing a nested group correctly to the right", () => {
|
||||
createAndSelectSingleGroupWithNestedGroup();
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignRight);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(200);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
});
|
||||
it("aligns elements within a single-selected group containing a nested group correctly to the vertical center", () => {
|
||||
createAndSelectSingleGroupWithNestedGroup();
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignVerticallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(50);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(150);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(100);
|
||||
});
|
||||
it("aligns elements within a single-selected group containing a nested group correctly to the horizontal center", () => {
|
||||
createAndSelectSingleGroupWithNestedGroup();
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignHorizontallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(50);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(150);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(100);
|
||||
});
|
||||
});
|
||||
|
153
packages/element/tests/embeddable.test.ts
Normal file
153
packages/element/tests/embeddable.test.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { getEmbedLink } from "../src/embeddable";
|
||||
|
||||
describe("YouTube timestamp parsing", () => {
|
||||
it("should parse YouTube URLs with timestamp in seconds", () => {
|
||||
const testCases = [
|
||||
{
|
||||
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=90",
|
||||
expectedStart: 90,
|
||||
},
|
||||
{
|
||||
url: "https://youtu.be/dQw4w9WgXcQ?t=120",
|
||||
expectedStart: 120,
|
||||
},
|
||||
{
|
||||
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&start=150",
|
||||
expectedStart: 150,
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(({ url, expectedStart }) => {
|
||||
const result = getEmbedLink(url);
|
||||
expect(result).toBeTruthy();
|
||||
expect(result?.type).toBe("video");
|
||||
if (result?.type === "video" || result?.type === "generic") {
|
||||
expect(result.link).toContain(`start=${expectedStart}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse YouTube URLs with timestamp in time format", () => {
|
||||
const testCases = [
|
||||
{
|
||||
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1m30s",
|
||||
expectedStart: 90, // 1*60 + 30
|
||||
},
|
||||
{
|
||||
url: "https://youtu.be/dQw4w9WgXcQ?t=2m45s",
|
||||
expectedStart: 165, // 2*60 + 45
|
||||
},
|
||||
{
|
||||
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1h2m3s",
|
||||
expectedStart: 3723, // 1*3600 + 2*60 + 3
|
||||
},
|
||||
{
|
||||
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=45s",
|
||||
expectedStart: 45,
|
||||
},
|
||||
{
|
||||
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=5m",
|
||||
expectedStart: 300, // 5*60
|
||||
},
|
||||
{
|
||||
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=2h",
|
||||
expectedStart: 7200, // 2*3600
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(({ url, expectedStart }) => {
|
||||
const result = getEmbedLink(url);
|
||||
expect(result).toBeTruthy();
|
||||
expect(result?.type).toBe("video");
|
||||
if (result?.type === "video" || result?.type === "generic") {
|
||||
expect(result.link).toContain(`start=${expectedStart}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle YouTube URLs without timestamps", () => {
|
||||
const testCases = [
|
||||
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"https://youtu.be/dQw4w9WgXcQ",
|
||||
"https://www.youtube.com/embed/dQw4w9WgXcQ",
|
||||
];
|
||||
|
||||
testCases.forEach((url) => {
|
||||
const result = getEmbedLink(url);
|
||||
expect(result).toBeTruthy();
|
||||
expect(result?.type).toBe("video");
|
||||
if (result?.type === "video" || result?.type === "generic") {
|
||||
expect(result.link).not.toContain("start=");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle YouTube shorts URLs with timestamps", () => {
|
||||
const url = "https://www.youtube.com/shorts/dQw4w9WgXcQ?t=30";
|
||||
const result = getEmbedLink(url);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result?.type).toBe("video");
|
||||
if (result?.type === "video" || result?.type === "generic") {
|
||||
expect(result.link).toContain("start=30");
|
||||
}
|
||||
// Shorts should have portrait aspect ratio
|
||||
expect(result?.intrinsicSize).toEqual({ w: 315, h: 560 });
|
||||
});
|
||||
|
||||
it("should handle playlist URLs with timestamps", () => {
|
||||
const url =
|
||||
"https://www.youtube.com/playlist?list=PLrAXtmRdnEQy1KbG5lbfgQ0-PKQY6FKYZ&t=60";
|
||||
const result = getEmbedLink(url);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result?.type).toBe("video");
|
||||
if (result?.type === "video" || result?.type === "generic") {
|
||||
expect(result.link).toContain("start=60");
|
||||
expect(result.link).toContain("list=PLrAXtmRdnEQy1KbG5lbfgQ0-PKQY6FKYZ");
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle malformed or edge case timestamps", () => {
|
||||
const testCases = [
|
||||
{
|
||||
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=abc",
|
||||
expectedStart: 0, // Invalid timestamp should default to 0
|
||||
},
|
||||
{
|
||||
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=",
|
||||
expectedStart: 0, // Empty timestamp should default to 0
|
||||
},
|
||||
{
|
||||
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=0",
|
||||
expectedStart: 0, // Zero timestamp should be handled
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(({ url, expectedStart }) => {
|
||||
const result = getEmbedLink(url);
|
||||
expect(result).toBeTruthy();
|
||||
expect(result?.type).toBe("video");
|
||||
if (result?.type === "video" || result?.type === "generic") {
|
||||
if (expectedStart === 0) {
|
||||
expect(result.link).not.toContain("start=");
|
||||
} else {
|
||||
expect(result.link).toContain(`start=${expectedStart}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve other URL parameters", () => {
|
||||
const url =
|
||||
"https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=90&feature=youtu.be&list=PLtest";
|
||||
const result = getEmbedLink(url);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result?.type).toBe("video");
|
||||
if (result?.type === "video" || result?.type === "generic") {
|
||||
expect(result.link).toContain("start=90");
|
||||
expect(result.link).toContain("enablejsapi=1");
|
||||
}
|
||||
});
|
||||
});
|
@ -10,6 +10,8 @@ import { alignElements } from "@excalidraw/element";
|
||||
|
||||
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||
|
||||
import { getSelectedElementsByGroup } from "@excalidraw/element";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type { Alignment } from "@excalidraw/element";
|
||||
@ -38,7 +40,11 @@ export const alignActionsPredicate = (
|
||||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
return (
|
||||
selectedElements.length > 1 &&
|
||||
getSelectedElementsByGroup(
|
||||
selectedElements,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
appState as Readonly<AppState>,
|
||||
).length > 1 &&
|
||||
// TODO enable aligning frames when implemented properly
|
||||
!selectedElements.some((el) => isFrameLikeElement(el))
|
||||
);
|
||||
@ -52,7 +58,12 @@ const alignSelectedElements = (
|
||||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
|
||||
const updatedElements = alignElements(selectedElements, alignment, app.scene);
|
||||
const updatedElements = alignElements(
|
||||
selectedElements,
|
||||
alignment,
|
||||
app.scene,
|
||||
appState,
|
||||
);
|
||||
|
||||
const updatedElementsMap = arrayToMap(updatedElements);
|
||||
|
||||
|
@ -10,6 +10,8 @@ import { distributeElements } from "@excalidraw/element";
|
||||
|
||||
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||
|
||||
import { getSelectedElementsByGroup } from "@excalidraw/element";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type { Distribution } from "@excalidraw/element";
|
||||
@ -31,7 +33,11 @@ import type { AppClassProperties, AppState } from "../types";
|
||||
const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
return (
|
||||
selectedElements.length > 1 &&
|
||||
getSelectedElementsByGroup(
|
||||
selectedElements,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
appState as Readonly<AppState>,
|
||||
).length > 2 &&
|
||||
// TODO enable distributing frames when implemented properly
|
||||
!selectedElements.some((el) => isFrameLikeElement(el))
|
||||
);
|
||||
@ -49,6 +55,7 @@ const distributeSelectedElements = (
|
||||
selectedElements,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
distribution,
|
||||
appState,
|
||||
);
|
||||
|
||||
const updatedElementsMap = arrayToMap(updatedElements);
|
||||
|
@ -595,6 +595,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
* insert to DOM before user initially scrolls to them) */
|
||||
private initializedEmbeds = new Set<ExcalidrawIframeLikeElement["id"]>();
|
||||
|
||||
private handleToastClose = () => {
|
||||
this.setToast(null);
|
||||
};
|
||||
|
||||
private elementsPendingErasure: ElementsPendingErasure = new Set();
|
||||
|
||||
public flowChartCreator: FlowChartCreator = new FlowChartCreator();
|
||||
@ -1709,14 +1713,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
/>
|
||||
</ElementCanvasButtons>
|
||||
)}
|
||||
|
||||
{this.state.toast !== null && (
|
||||
<Toast
|
||||
message={this.state.toast.message}
|
||||
onClose={() => this.setToast(null)}
|
||||
onClose={this.handleToastClose}
|
||||
duration={this.state.toast.duration}
|
||||
closable={this.state.toast.closable}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.contextMenu && (
|
||||
<ContextMenu
|
||||
items={this.state.contextMenu.items}
|
||||
|
@ -108,6 +108,7 @@ $verticalBreakpoint: 861px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,6 +59,8 @@ import { useStableCallback } from "../../hooks/useStableCallback";
|
||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||
import { useStable } from "../../hooks/useStable";
|
||||
|
||||
import { Ellipsify } from "../Ellipsify";
|
||||
|
||||
import * as defaultItems from "./defaultCommandPaletteItems";
|
||||
|
||||
import "./CommandPalette.scss";
|
||||
@ -964,7 +966,7 @@ const CommandItem = ({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{command.label}
|
||||
<Ellipsify>{command.label}</Ellipsify>
|
||||
</div>
|
||||
{showShortcut && command.shortcut && (
|
||||
<CommandShortcutHint shortcut={command.shortcut} />
|
||||
|
18
packages/excalidraw/components/Ellipsify.tsx
Normal file
18
packages/excalidraw/components/Ellipsify.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
export const Ellipsify = ({
|
||||
children,
|
||||
...rest
|
||||
}: { children: React.ReactNode } & React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
{...rest}
|
||||
style={{
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
...rest.style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
@ -7,6 +7,7 @@ export const InlineIcon = ({ icon }: { icon: React.ReactNode }) => {
|
||||
display: "inline-block",
|
||||
lineHeight: 0,
|
||||
verticalAlign: "middle",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
|
@ -19,6 +19,8 @@
|
||||
border-radius: var(--border-radius-lg);
|
||||
position: relative;
|
||||
transition: box-shadow 0.5s ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.zen-mode {
|
||||
box-shadow: none;
|
||||
@ -100,6 +102,7 @@
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius-md);
|
||||
flex: 1 0 auto;
|
||||
|
||||
@media screen and (min-width: 1921px) {
|
||||
height: 2.25rem;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { useDevice } from "../App";
|
||||
|
||||
import { Ellipsify } from "../Ellipsify";
|
||||
|
||||
import type { JSX } from "react";
|
||||
|
||||
const MenuItemContent = ({
|
||||
@ -18,7 +20,7 @@ const MenuItemContent = ({
|
||||
<>
|
||||
{icon && <div className="dropdown-menu-item__icon">{icon}</div>}
|
||||
<div style={textStyle} className="dropdown-menu-item__text">
|
||||
{children}
|
||||
<Ellipsify>{children}</Ellipsify>
|
||||
</div>
|
||||
{shortcut && !device.editor.isMobile && (
|
||||
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
||||
|
@ -281,6 +281,7 @@ export { Sidebar } from "./components/Sidebar/Sidebar";
|
||||
export { Button } from "./components/Button";
|
||||
export { Footer };
|
||||
export { MainMenu };
|
||||
export { Ellipsify } from "./components/Ellipsify";
|
||||
export { useDevice } from "./components/App";
|
||||
export { WelcomeScreen };
|
||||
export { LiveCollaborationTrigger };
|
||||
|
@ -66,12 +66,22 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"repository": "https://github.com/excalidraw/excalidraw",
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/packages/excalidraw",
|
||||
"scripts": {
|
||||
"gen:types": "rimraf types && tsc",
|
||||
"build:esm": "rimraf dist && node ../../scripts/buildPackage.js && yarn gen:types"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2 || ^18.2.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@excalidraw/common": "0.18.0",
|
||||
"@excalidraw/element": "0.18.0",
|
||||
"@excalidraw/math": "0.18.0",
|
||||
"@excalidraw/laser-pointer": "1.3.2",
|
||||
"@excalidraw/mermaid-to-excalidraw": "1.1.2",
|
||||
"@excalidraw/random-username": "1.1.0",
|
||||
@ -124,12 +134,5 @@
|
||||
"harfbuzzjs": "0.3.6",
|
||||
"jest-diff": "29.7.0",
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
"repository": "https://github.com/excalidraw/excalidraw",
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/packages/excalidraw",
|
||||
"scripts": {
|
||||
"gen:types": "rimraf types && tsc",
|
||||
"build:esm": "rimraf dist && node ../../scripts/buildPackage.js && yarn gen:types"
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,12 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
||||
>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
Click me
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<a
|
||||
@ -26,8 +30,12 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
||||
>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
Excalidraw blog
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<div
|
||||
@ -87,8 +95,12 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
Help
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__shortcut"
|
||||
@ -137,8 +149,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
Open
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__shortcut"
|
||||
@ -174,8 +190,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
Save to...
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
@ -230,8 +250,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
Export image...
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__shortcut"
|
||||
@ -279,8 +303,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
Find on canvas
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__shortcut"
|
||||
@ -336,8 +364,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
Help
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__shortcut"
|
||||
@ -373,8 +405,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
Reset the canvas
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
@ -418,8 +454,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
GitHub
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@ -464,8 +504,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
Follow us
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@ -504,8 +548,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
Discord chat
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@ -541,8 +589,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
>
|
||||
<span
|
||||
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
|
||||
>
|
||||
Dark mode
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu-item__shortcut"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@excalidraw/math",
|
||||
"version": "0.1.0",
|
||||
"version": "0.18.0",
|
||||
"type": "module",
|
||||
"types": "./dist/types/math/src/index.d.ts",
|
||||
"main": "./dist/prod/index.js",
|
||||
@ -13,7 +13,10 @@
|
||||
"default": "./dist/prod/index.js"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./dist/types/math/src/*.d.ts"
|
||||
"types": "./dist/types/math/src/*.d.ts",
|
||||
"development": "./dist/dev/index.js",
|
||||
"production": "./dist/prod/index.js",
|
||||
"default": "./dist/prod/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@ -56,5 +59,8 @@
|
||||
"scripts": {
|
||||
"gen:types": "rimraf types && tsc",
|
||||
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/common": "0.18.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,71 +0,0 @@
|
||||
const { exec, execSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
|
||||
const core = require("@actions/core");
|
||||
|
||||
const excalidrawDir = `${__dirname}/../packages/excalidraw`;
|
||||
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||
const pkg = require(excalidrawPackage);
|
||||
const isPreview = process.argv.slice(2)[0] === "preview";
|
||||
|
||||
const getShortCommitHash = () => {
|
||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||
};
|
||||
|
||||
const publish = () => {
|
||||
const tag = isPreview ? "preview" : "next";
|
||||
|
||||
try {
|
||||
execSync(`yarn --frozen-lockfile`);
|
||||
execSync(`yarn run build:esm`, { cwd: excalidrawDir });
|
||||
execSync(`yarn --cwd ${excalidrawDir} publish --tag ${tag}`);
|
||||
console.info(`Published ${pkg.name}@${tag}🎉`);
|
||||
core.setOutput(
|
||||
"result",
|
||||
`**Preview version has been shipped** :rocket:
|
||||
You can use [@excalidraw/excalidraw@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw/v/${pkg.version}) for testing!`,
|
||||
);
|
||||
} catch (error) {
|
||||
core.setOutput("result", "package couldn't be published :warning:!");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
// get files changed between prev and head commit
|
||||
exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
|
||||
if (error || stderr) {
|
||||
console.error(error);
|
||||
core.setOutput("result", ":warning: Package couldn't be published!");
|
||||
process.exit(1);
|
||||
}
|
||||
const changedFiles = stdout.trim().split("\n");
|
||||
|
||||
const excalidrawPackageFiles = changedFiles.filter((file) => {
|
||||
return (
|
||||
file.indexOf("packages/excalidraw") >= 0 ||
|
||||
file.indexOf("buildPackage.js") > 0
|
||||
);
|
||||
});
|
||||
if (!excalidrawPackageFiles.length) {
|
||||
console.info("Skipping release as no valid diff found");
|
||||
core.setOutput("result", "Skipping release as no valid diff found");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// update package.json
|
||||
let version = `${pkg.version}-${getShortCommitHash()}`;
|
||||
|
||||
// update readme
|
||||
|
||||
if (isPreview) {
|
||||
// use pullNumber-commithash as the version for preview
|
||||
const pullRequestNumber = process.argv.slice(3)[0];
|
||||
version = `${pkg.version}-${pullRequestNumber}-${getShortCommitHash()}`;
|
||||
}
|
||||
pkg.version = version;
|
||||
|
||||
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
|
||||
|
||||
console.info("Publish in progress...");
|
||||
publish();
|
||||
});
|
@ -11,12 +11,9 @@ const getConfig = (outdir) => ({
|
||||
entryNames: "[name]",
|
||||
assetNames: "[dir]/[name]",
|
||||
alias: {
|
||||
"@excalidraw/common": path.resolve(__dirname, "../packages/common/src"),
|
||||
"@excalidraw/element": path.resolve(__dirname, "../packages/element/src"),
|
||||
"@excalidraw/excalidraw": path.resolve(__dirname, "../packages/excalidraw"),
|
||||
"@excalidraw/math": path.resolve(__dirname, "../packages/math/src"),
|
||||
"@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"),
|
||||
},
|
||||
external: ["@excalidraw/common", "@excalidraw/element", "@excalidraw/math"],
|
||||
});
|
||||
|
||||
function buildDev(config) {
|
||||
|
@ -28,12 +28,9 @@ const getConfig = (outdir) => ({
|
||||
assetNames: "[dir]/[name]",
|
||||
chunkNames: "[dir]/[name]-[hash]",
|
||||
alias: {
|
||||
"@excalidraw/common": path.resolve(__dirname, "../packages/common/src"),
|
||||
"@excalidraw/element": path.resolve(__dirname, "../packages/element/src"),
|
||||
"@excalidraw/excalidraw": path.resolve(__dirname, "../packages/excalidraw"),
|
||||
"@excalidraw/math": path.resolve(__dirname, "../packages/math/src"),
|
||||
"@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"),
|
||||
},
|
||||
external: ["@excalidraw/common", "@excalidraw/element", "@excalidraw/math"],
|
||||
loader: {
|
||||
".woff2": "file",
|
||||
},
|
||||
|
@ -1,38 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const util = require("util");
|
||||
|
||||
const exec = util.promisify(require("child_process").exec);
|
||||
const updateChangelog = require("./updateChangelog");
|
||||
|
||||
const excalidrawDir = `${__dirname}/../packages/excalidraw/`;
|
||||
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||
|
||||
const updatePackageVersion = (nextVersion) => {
|
||||
const pkg = require(excalidrawPackage);
|
||||
pkg.version = nextVersion;
|
||||
const content = `${JSON.stringify(pkg, null, 2)}\n`;
|
||||
fs.writeFileSync(excalidrawPackage, content, "utf-8");
|
||||
};
|
||||
|
||||
const prerelease = async (nextVersion) => {
|
||||
try {
|
||||
await updateChangelog(nextVersion);
|
||||
updatePackageVersion(nextVersion);
|
||||
await exec(`git add -u`);
|
||||
await exec(
|
||||
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
|
||||
);
|
||||
|
||||
console.info("Done!");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const nextVersion = process.argv.slice(2)[0];
|
||||
if (!nextVersion) {
|
||||
console.error("Pass the next version to release!");
|
||||
process.exit(1);
|
||||
}
|
||||
prerelease(nextVersion);
|
@ -1,28 +1,239 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
const excalidrawDir = `${__dirname}/../packages/excalidraw`;
|
||||
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||
const pkg = require(excalidrawPackage);
|
||||
const updateChangelog = require("./updateChangelog");
|
||||
|
||||
const publish = () => {
|
||||
try {
|
||||
console.info("Installing the dependencies in root folder...");
|
||||
execSync(`yarn --frozen-lockfile`);
|
||||
console.info("Installing the dependencies in excalidraw directory...");
|
||||
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
|
||||
console.info("Building ESM Package...");
|
||||
execSync(`yarn run build:esm`, { cwd: excalidrawDir });
|
||||
console.info("Publishing the package...");
|
||||
execSync(`yarn --cwd ${excalidrawDir} publish`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// skipping utils for now, as it has independent release process
|
||||
const PACKAGES = ["common", "math", "element", "excalidraw"];
|
||||
const PACKAGES_DIR = path.resolve(__dirname, "../packages");
|
||||
|
||||
/**
|
||||
* Returns the arguments for the release script.
|
||||
*
|
||||
* Usage examples:
|
||||
* - yarn release --help -> prints this help message
|
||||
* - yarn release -> publishes `@excalidraw` packages with "test" tag and "-[hash]" version suffix
|
||||
* - yarn release --tag=test -> same as above
|
||||
* - yarn release --tag=next -> publishes `@excalidraw` packages with "next" tag and version "-[hash]" suffix
|
||||
* - yarn release --tag=next --non-interactive -> skips interactive prompts (runs on CI/CD), otherwise same as above
|
||||
* - yarn release --tag=latest --version=0.19.0 -> publishes `@excalidraw` packages with "latest" tag and version "0.19.0" & prepares changelog for the release
|
||||
*
|
||||
* @returns [tag, version, nonInteractive]
|
||||
*/
|
||||
const getArguments = () => {
|
||||
let tag = "test";
|
||||
let version = "";
|
||||
let nonInteractive = false;
|
||||
|
||||
for (const argument of process.argv.slice(2)) {
|
||||
if (/--help/.test(argument)) {
|
||||
console.info(`Available arguments:
|
||||
--tag=<tag> -> (optional) "test" (default), "next" for auto release, "latest" for stable release
|
||||
--version=<version> -> (optional) for "next" and "test", (required) for "latest" i.e. "0.19.0"
|
||||
--non-interactive -> (optional) disables interactive prompts`);
|
||||
|
||||
console.info(`\nUsage examples:
|
||||
- yarn release -> publishes \`@excalidraw\` packages with "test" tag and "-[hash]" version suffix
|
||||
- yarn release --tag=test -> same as above
|
||||
- yarn release --tag=next -> publishes \`@excalidraw\` packages with "next" tag and version "-[hash]" suffix
|
||||
- yarn release --tag=next --non-interactive -> skips interactive prompts (runs on CI/CD), otherwise same as above
|
||||
- yarn release --tag=latest --version=0.19.0 -> publishes \`@excalidraw\` packages with "latest" tag and version "0.19.0" & prepares changelog for the release`);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (/--tag=/.test(argument)) {
|
||||
tag = argument.split("=")[1];
|
||||
}
|
||||
|
||||
if (/--version=/.test(argument)) {
|
||||
version = argument.split("=")[1];
|
||||
}
|
||||
|
||||
if (/--non-interactive/.test(argument)) {
|
||||
nonInteractive = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (tag !== "latest" && tag !== "next" && tag !== "test") {
|
||||
console.error(`Unsupported tag "${tag}", use "latest", "next" or "test".`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (tag === "latest" && !version) {
|
||||
console.error("Pass the version to make the latest stable release!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
// set the next version based on the excalidraw package version + commit hash
|
||||
const excalidrawPackageVersion = require(getPackageJsonPath(
|
||||
"excalidraw",
|
||||
)).version;
|
||||
|
||||
const hash = getShortCommitHash();
|
||||
|
||||
if (!excalidrawPackageVersion.includes(hash)) {
|
||||
version = `${excalidrawPackageVersion}-${hash}`;
|
||||
} else {
|
||||
// ensuring idempotency
|
||||
version = excalidrawPackageVersion;
|
||||
}
|
||||
}
|
||||
|
||||
console.info(`Running with tag "${tag}" and version "${version}"...`);
|
||||
|
||||
return [tag, version, nonInteractive];
|
||||
};
|
||||
|
||||
const validatePackageName = (packageName) => {
|
||||
if (!PACKAGES.includes(packageName)) {
|
||||
console.error(`Package "${packageName}" not found!`);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const release = () => {
|
||||
publish();
|
||||
console.info(`Published ${pkg.version}!`);
|
||||
const getPackageJsonPath = (packageName) => {
|
||||
validatePackageName(packageName);
|
||||
return path.resolve(PACKAGES_DIR, packageName, "package.json");
|
||||
};
|
||||
|
||||
release();
|
||||
const updatePackageJsons = (nextVersion) => {
|
||||
const packageJsons = new Map();
|
||||
|
||||
for (const packageName of PACKAGES) {
|
||||
const pkg = require(getPackageJsonPath(packageName));
|
||||
|
||||
pkg.version = nextVersion;
|
||||
|
||||
if (pkg.dependencies) {
|
||||
for (const dependencyName of PACKAGES) {
|
||||
if (!pkg.dependencies[`@excalidraw/${dependencyName}`]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pkg.dependencies[`@excalidraw/${dependencyName}`] = nextVersion;
|
||||
}
|
||||
}
|
||||
|
||||
packageJsons.set(packageName, `${JSON.stringify(pkg, null, 2)}\n`);
|
||||
}
|
||||
|
||||
// modify once, to avoid inconsistent state
|
||||
for (const packageName of PACKAGES) {
|
||||
const content = packageJsons.get(packageName);
|
||||
fs.writeFileSync(getPackageJsonPath(packageName), content, "utf-8");
|
||||
}
|
||||
};
|
||||
|
||||
const getShortCommitHash = () => {
|
||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||
};
|
||||
|
||||
const askToCommit = (tag, nextVersion) => {
|
||||
if (tag !== "latest") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const rl = require("readline").createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
rl.question(
|
||||
"Do you want to commit these changes to git? (Y/n): ",
|
||||
(answer) => {
|
||||
rl.close();
|
||||
|
||||
if (answer.toLowerCase() === "y") {
|
||||
execSync(`git add -u`);
|
||||
execSync(
|
||||
`git commit -m "chore: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
"Skipping commit. Don't forget to commit manually later!",
|
||||
);
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const buildPackages = () => {
|
||||
console.info("Running yarn install...");
|
||||
execSync(`yarn --frozen-lockfile`, { stdio: "inherit" });
|
||||
|
||||
console.info("Removing existing build artifacts...");
|
||||
execSync(`yarn rm:build`, { stdio: "inherit" });
|
||||
|
||||
for (const packageName of PACKAGES) {
|
||||
console.info(`Building "@excalidraw/${packageName}"...`);
|
||||
execSync(`yarn run build:esm`, {
|
||||
cwd: path.resolve(PACKAGES_DIR, packageName),
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const askToPublish = (tag, version) => {
|
||||
return new Promise((resolve) => {
|
||||
const rl = require("readline").createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
rl.question(
|
||||
"Do you want to publish these changes to npm? (Y/n): ",
|
||||
(answer) => {
|
||||
rl.close();
|
||||
|
||||
if (answer.toLowerCase() === "y") {
|
||||
publishPackages(tag, version);
|
||||
} else {
|
||||
console.info("Skipping publish.");
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const publishPackages = (tag, version) => {
|
||||
for (const packageName of PACKAGES) {
|
||||
execSync(`yarn publish --tag ${tag}`, {
|
||||
cwd: path.resolve(PACKAGES_DIR, packageName),
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
console.info(
|
||||
`Published "@excalidraw/${packageName}@${tag}" with version "${version}"! 🎉`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** main */
|
||||
(async () => {
|
||||
const [tag, version, nonInteractive] = getArguments();
|
||||
|
||||
buildPackages();
|
||||
|
||||
if (tag === "latest") {
|
||||
await updateChangelog(version);
|
||||
}
|
||||
|
||||
updatePackageJsons(version);
|
||||
|
||||
if (nonInteractive) {
|
||||
publishPackages(tag, version);
|
||||
} else {
|
||||
await askToCommit(tag, version);
|
||||
await askToPublish(tag, version);
|
||||
}
|
||||
})();
|
||||
|
@ -20,14 +20,16 @@ const headerForType = {
|
||||
perf: "Performance",
|
||||
build: "Build",
|
||||
};
|
||||
|
||||
const badCommits = [];
|
||||
const getCommitHashForLastVersion = async () => {
|
||||
try {
|
||||
const commitMessage = `"release @excalidraw/excalidraw@${lastVersion}"`;
|
||||
const commitMessage = `"release @excalidraw/excalidraw"`;
|
||||
const { stdout } = await exec(
|
||||
`git log --format=format:"%H" --grep=${commitMessage}`,
|
||||
);
|
||||
return stdout;
|
||||
// take commit hash from latest release
|
||||
return stdout.split(/\r?\n/)[0];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
Reference in New Issue
Block a user