Compare commits

..

1 Commits

Author SHA1 Message Date
Mark Tolmacs
b4078b1589 Add automatic issue staleness tracking 2025-07-28 13:07:59 +02:00
5 changed files with 43 additions and 62 deletions

23
.github/workflows/stale-issues.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
steps:
- uses: actions/stale@v9
with:
days-before-issue-stale: 90
days-before-issue-close: 180
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 90 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 180 days since being marked as stale."
exempt-issue-assignees: "dwelle,ryan-di,Mrazator,ad1992,zsviczian,mtolmacs"
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -4,13 +4,7 @@ import {
supported as nativeFileSystemSupported, supported as nativeFileSystemSupported,
} from "browser-fs-access"; } from "browser-fs-access";
import { import { EVENT, MIME_TYPES, debounce } from "@excalidraw/common";
EVENT,
MIME_TYPES,
debounce,
isIOS,
isAndroid,
} from "@excalidraw/common";
import { AbortError } from "../errors"; import { AbortError } from "../errors";
@@ -19,8 +13,6 @@ import type { FileSystemHandle } from "browser-fs-access";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">; type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
const INPUT_CHANGE_INTERVAL_MS = 500; const INPUT_CHANGE_INTERVAL_MS = 500;
// increase timeout for mobile devices to give more time for file selection
const MOBILE_INPUT_CHANGE_INTERVAL_MS = 2000;
export const fileOpen = <M extends boolean | undefined = false>(opts: { export const fileOpen = <M extends boolean | undefined = false>(opts: {
extensions?: FILE_EXTENSION[]; extensions?: FILE_EXTENSION[];
@@ -49,22 +41,13 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
mimeTypes, mimeTypes,
multiple: opts.multiple ?? false, multiple: opts.multiple ?? false,
legacySetup: (resolve, reject, input) => { legacySetup: (resolve, reject, input) => {
const isMobile = isIOS || isAndroid; const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
const intervalMs = isMobile
? MOBILE_INPUT_CHANGE_INTERVAL_MS
: INPUT_CHANGE_INTERVAL_MS;
const scheduleRejection = debounce(reject, intervalMs);
const focusHandler = () => { const focusHandler = () => {
checkForFile(); checkForFile();
// on mobile, be less aggressive with rejection
if (!isMobile) {
document.addEventListener(EVENT.KEYUP, scheduleRejection); document.addEventListener(EVENT.KEYUP, scheduleRejection);
document.addEventListener(EVENT.POINTER_UP, scheduleRejection); document.addEventListener(EVENT.POINTER_UP, scheduleRejection);
scheduleRejection(); scheduleRejection();
}
}; };
const checkForFile = () => { const checkForFile = () => {
// this hack might not work when expecting multiple files // this hack might not work when expecting multiple files
if (input.files?.length) { if (input.files?.length) {
@@ -72,15 +55,12 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
resolve(ret as RetType); resolve(ret as RetType);
} }
}; };
requestAnimationFrame(() => { requestAnimationFrame(() => {
window.addEventListener(EVENT.FOCUS, focusHandler); window.addEventListener(EVENT.FOCUS, focusHandler);
}); });
const interval = window.setInterval(() => { const interval = window.setInterval(() => {
checkForFile(); checkForFile();
}, intervalMs); }, INPUT_CHANGE_INTERVAL_MS);
return (rejectPromise) => { return (rejectPromise) => {
clearInterval(interval); clearInterval(interval);
scheduleRejection.cancel(); scheduleRejection.cancel();
@@ -89,9 +69,7 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
document.removeEventListener(EVENT.POINTER_UP, scheduleRejection); document.removeEventListener(EVENT.POINTER_UP, scheduleRejection);
if (rejectPromise) { if (rejectPromise) {
// so that something is shown in console if we need to debug this // so that something is shown in console if we need to debug this
console.warn( console.warn("Opening the file was canceled (legacy-fs).");
"Opening the file was canceled (legacy-fs). This may happen on mobile devices.",
);
rejectPromise(new AbortError()); rejectPromise(new AbortError());
} }
}; };

View File

@@ -1,16 +1,9 @@
import { throttleRAF } from "@excalidraw/common"; import { throttleRAF } from "@excalidraw/common";
import { import { isInvisiblySmallElement, renderElement } from "@excalidraw/element";
getTargetFrame,
isInvisiblySmallElement,
renderElement,
shouldApplyFrameClip,
} from "@excalidraw/element";
import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers"; import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
import { frameClip } from "./staticScene";
import type { NewElementSceneRenderConfig } from "../scene/types"; import type { NewElementSceneRenderConfig } from "../scene/types";
const _renderNewElementScene = ({ const _renderNewElementScene = ({
@@ -36,9 +29,8 @@ const _renderNewElementScene = ({
normalizedHeight, normalizedHeight,
}); });
context.save();
// Apply zoom // Apply zoom
context.save();
context.scale(appState.zoom.value, appState.zoom.value); context.scale(appState.zoom.value, appState.zoom.value);
if (newElement && newElement.type !== "selection") { if (newElement && newElement.type !== "selection") {
@@ -50,23 +42,6 @@ const _renderNewElementScene = ({
return; return;
} }
const frameId = newElement.frameId || appState.frameToHighlight?.id;
if (
frameId &&
appState.frameRendering.enabled &&
appState.frameRendering.clip
) {
const frame = getTargetFrame(newElement, elementsMap, appState);
if (
frame &&
shouldApplyFrameClip(newElement, frame, appState, elementsMap)
) {
frameClip(frame, context, renderConfig, appState);
}
}
renderElement( renderElement(
newElement, newElement,
elementsMap, elementsMap,
@@ -79,8 +54,6 @@ const _renderNewElementScene = ({
} else { } else {
context.clearRect(0, 0, normalizedWidth, normalizedHeight); context.clearRect(0, 0, normalizedWidth, normalizedHeight);
} }
context.restore();
} }
}; };

View File

@@ -113,7 +113,7 @@ const strokeGrid = (
context.restore(); context.restore();
}; };
export const frameClip = ( const frameClip = (
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
renderConfig: StaticCanvasRenderConfig, renderConfig: StaticCanvasRenderConfig,

View File

@@ -13,7 +13,7 @@ import {
getDraggedElementsBounds, getDraggedElementsBounds,
getElementAbsoluteCoords, getElementAbsoluteCoords,
} from "@excalidraw/element"; } from "@excalidraw/element";
import { isBoundToContainer } from "@excalidraw/element"; import { isBoundToContainer, isFrameLikeElement } from "@excalidraw/element";
import { getMaximumGroups } from "@excalidraw/element"; import { getMaximumGroups } from "@excalidraw/element";
@@ -311,13 +311,20 @@ const getReferenceElements = (
selectedElements: NonDeletedExcalidrawElement[], selectedElements: NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
elementsMap: ElementsMap, elementsMap: ElementsMap,
) => ) => {
getVisibleAndNonSelectedElements( const selectedFrames = selectedElements
.filter((element) => isFrameLikeElement(element))
.map((frame) => frame.id);
return getVisibleAndNonSelectedElements(
elements, elements,
selectedElements, selectedElements,
appState, appState,
elementsMap, elementsMap,
).filter(
(element) => !(element.frameId && selectedFrames.includes(element.frameId)),
); );
};
export const getVisibleGaps = ( export const getVisibleGaps = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],