Compare commits

...

3 Commits

Author SHA1 Message Date
dwelle
ba951b9a03 [debug] disable image tool to test 2023-03-26 12:52:33 +02:00
dwelle
6fb5c2acda Merge branch 'master' into dwelle/support-disabling-image-tool
# Conflicts:
#	src/components/App.tsx
#	src/locales/en.json
2023-03-26 12:51:46 +02:00
dwelle
df9af0052d feat: support disabling image tool 2023-03-05 22:59:01 +01:00
11 changed files with 95 additions and 17 deletions

View File

@ -14,7 +14,7 @@ import {
hasText,
} from "../scene";
import { SHAPES } from "../shapes";
import { AppState, Zoom } from "../types";
import { AppProps, AppState, CanvasActions, Zoom } from "../types";
import {
capitalizeString,
isTransparent,
@ -213,15 +213,25 @@ export const ShapesSwitcher = ({
setAppState,
onImageAction,
appState,
UIOptions,
}: {
canvas: HTMLCanvasElement | null;
activeTool: AppState["activeTool"];
setAppState: React.Component<any, AppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void;
appState: AppState;
UIOptions: AppProps["UIOptions"];
}) => (
<>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
if (
UIOptions.canvasActions.tools[
value as Extract<typeof value, keyof CanvasActions["tools"]>
] === false
) {
return null;
}
const label = t(`toolBar.${value}`);
const letter =
key && capitalizeString(typeof key === "string" ? key : key[0]);

View File

@ -205,6 +205,7 @@ import {
PointerDownState,
SceneData,
Device,
CanvasActions,
} from "../types";
import {
debounce,
@ -292,6 +293,7 @@ import {
import { jotaiStore } from "../jotai";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { actionCreateContainerFromText } from "../actions/actionBoundText";
import { ImageSceneDataError } from "../errors";
import BraveMeasureTextError from "./BraveMeasureTextError";
const deviceContextInitialValue = {
@ -1547,6 +1549,10 @@ class App extends React.Component<AppProps, AppState> {
// prefer spreadsheet data over image file (MS Office/Libre Office)
if (isSupportedImageFile(file) && !data.spreadsheet) {
if (!this.isToolSupported("image")) {
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
return;
}
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
{ clientX: cursorX, clientY: cursorY },
this.state,
@ -2343,11 +2349,32 @@ class App extends React.Component<AppProps, AppState> {
}
});
// we pruposefuly widen tyhe `tool` type so this helper can be called with
// any tool without having to type check it
private isToolSupported = <
T extends typeof SHAPES[number]["value"] | "eraser" | "hand" | "custom",
>(
tool: T,
) => {
return (
this.props.UIOptions.canvasActions.tools[
tool as Extract<T, keyof CanvasActions["tools"]>
] !== false
);
};
private setActiveTool = (
tool:
| { type: typeof SHAPES[number]["value"] | "eraser" | "hand" }
| { type: "custom"; customType: string },
) => {
if (!this.isToolSupported(tool.type)) {
console.warn(
`"${tool.type}" tool is disabled via "UIOptions.canvasActions.tools.${tool.type}"`,
);
return;
}
const nextActiveTool = updateActiveTool(this.state, tool);
if (nextActiveTool.type === "hand") {
setCursor(this.canvas, CURSOR_TYPE.GRAB);
@ -5994,7 +6021,10 @@ class App extends React.Component<AppProps, AppState> {
const { file, fileHandle } = await getFileFromEvent(event);
try {
if (isSupportedImageFile(file)) {
// if image tool not supported, don't show an error here and let it fall
// through so we still support importing scene data from images. If no
// scene data encoded, we'll show an error then
if (isSupportedImageFile(file) && this.isToolSupported("image")) {
// first attempt to decode scene from the image if it's embedded
// ---------------------------------------------------------------------
@ -6103,6 +6133,17 @@ class App extends React.Component<AppProps, AppState> {
});
}
} catch (error: any) {
if (
error instanceof ImageSceneDataError &&
error.code === "IMAGE_NOT_CONTAINS_SCENE_DATA" &&
!this.isToolSupported("image")
) {
this.setState({
isLoading: false,
errorMessage: t("errors.imageToolNotSupported"),
});
return;
}
this.setState({ isLoading: false, errorMessage: error.message });
}
};

View File

@ -305,6 +305,7 @@ const LayerUI = ({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
UIOptions={UIOptions}
/>
</Stack.Row>
</Island>
@ -408,6 +409,7 @@ const LayerUI = ({
renderSidebars={renderSidebars}
device={device}
renderWelcomeScreen={renderWelcomeScreen}
UIOptions={UIOptions}
/>
)}

View File

@ -1,5 +1,5 @@
import React from "react";
import { AppState, Device, ExcalidrawProps } from "../types";
import { AppProps, AppState, Device, ExcalidrawProps } from "../types";
import { ActionManager } from "../actions/manager";
import { t } from "../i18n";
import Stack from "./Stack";
@ -42,6 +42,7 @@ type MobileMenuProps = {
renderSidebars: () => JSX.Element | null;
device: Device;
renderWelcomeScreen: boolean;
UIOptions: AppProps["UIOptions"];
};
export const MobileMenu = ({
@ -59,6 +60,7 @@ export const MobileMenu = ({
renderSidebars,
device,
renderWelcomeScreen,
UIOptions,
}: MobileMenuProps) => {
const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels();
const renderToolbar = () => {
@ -82,6 +84,7 @@ export const MobileMenu = ({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
UIOptions={UIOptions}
/>
</Stack.Row>
</Island>

View File

@ -164,6 +164,9 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
saveToActiveFile: true,
toggleTheme: null,
saveAsImage: true,
tools: {
image: true,
},
},
};

View File

@ -3,7 +3,7 @@ import { cleanAppStateForExport } from "../appState";
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { ExcalidrawElement, FileId } from "../element/types";
import { CanvasError } from "../errors";
import { CanvasError, ImageSceneDataError } from "../errors";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { AppState, DataURL, LibraryItem } from "../types";
@ -24,15 +24,12 @@ const parseFileContents = async (blob: Blob | File) => {
).decodePngMetadata(blob);
} catch (error: any) {
if (error.message === "INVALID") {
throw new DOMException(
throw new ImageSceneDataError(
t("alerts.imageDoesNotContainScene"),
"EncodingError",
"IMAGE_NOT_CONTAINS_SCENE_DATA",
);
} else {
throw new DOMException(
t("alerts.cannotRestoreFromImage"),
"EncodingError",
);
throw new ImageSceneDataError(t("alerts.cannotRestoreFromImage"));
}
}
} else {
@ -58,15 +55,12 @@ const parseFileContents = async (blob: Blob | File) => {
});
} catch (error: any) {
if (error.message === "INVALID") {
throw new DOMException(
throw new ImageSceneDataError(
t("alerts.imageDoesNotContainScene"),
"EncodingError",
"IMAGE_NOT_CONTAINS_SCENE_DATA",
);
} else {
throw new DOMException(
t("alerts.cannotRestoreFromImage"),
"EncodingError",
);
throw new ImageSceneDataError(t("alerts.cannotRestoreFromImage"));
}
}
}

View File

@ -16,3 +16,19 @@ export class AbortError extends DOMException {
super(message, "AbortError");
}
}
type ImageSceneDataErrorCode =
| "IMAGE_NOT_CONTAINS_SCENE_DATA"
| "IMAGE_SCENE_DATA_ERROR";
export class ImageSceneDataError extends Error {
public code;
constructor(
message = "Image Scene Data Error",
code: ImageSceneDataErrorCode = "IMAGE_SCENE_DATA_ERROR",
) {
super(message);
this.name = "EncodingError";
this.code = code;
}
}

View File

@ -618,6 +618,7 @@ const ExcalidrawWrapper = () => {
onPointerUpdate={collabAPI?.onPointerUpdate}
UIOptions={{
canvasActions: {
tools: { image: false },
toggleTheme: true,
export: {
onExportToBackend,

View File

@ -206,6 +206,7 @@
"importLibraryError": "Couldn't load library",
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
"imageToolNotSupported": "Images are disabled.",
"brave_measure_text_error": {
"start": "Looks like you are using Brave browser with the",
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting",

View File

@ -52,6 +52,10 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
canvasActions: {
...DEFAULT_UI_OPTIONS.canvasActions,
...canvasActions,
tools: {
...DEFAULT_UI_OPTIONS.canvasActions.tools,
...props.UIOptions?.canvasActions?.tools,
},
},
};

View File

@ -375,7 +375,7 @@ export type ExportOpts = {
// truthiness value will determine whether the action is rendered or not
// (see manager renderAction). We also override canvasAction values in
// excalidraw package index.tsx.
type CanvasActions = Partial<{
export type CanvasActions = Partial<{
changeViewBackgroundColor: boolean;
clearCanvas: boolean;
export: false | ExportOpts;
@ -383,6 +383,9 @@ type CanvasActions = Partial<{
saveToActiveFile: boolean;
toggleTheme: boolean | null;
saveAsImage: boolean;
tools: {
image: boolean;
};
}>;
type UIOptions = Partial<{