feat: support disabling image tool (#6320)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
This commit is contained in:
parent
9d1d45a8ea
commit
9c425224c7
@ -13,7 +13,7 @@ import {
|
||||
hasStrokeWidth,
|
||||
} from "../scene";
|
||||
import { SHAPES } from "../shapes";
|
||||
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
||||
import { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
|
||||
import { capitalizeString, isTransparent } from "../utils";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
@ -218,10 +218,12 @@ export const ShapesSwitcher = ({
|
||||
activeTool,
|
||||
appState,
|
||||
app,
|
||||
UIOptions,
|
||||
}: {
|
||||
activeTool: UIAppState["activeTool"];
|
||||
appState: UIAppState;
|
||||
app: AppClassProperties;
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
}) => {
|
||||
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||
|
||||
@ -232,6 +234,14 @@ export const ShapesSwitcher = ({
|
||||
return (
|
||||
<>
|
||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||
if (
|
||||
UIOptions.tools?.[
|
||||
value as Extract<typeof value, keyof AppProps["UIOptions"]["tools"]>
|
||||
] === false
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = t(`toolBar.${value}`);
|
||||
const letter =
|
||||
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
||||
|
@ -341,6 +341,7 @@ import {
|
||||
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
|
||||
import { jotaiStore } from "../jotai";
|
||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||
import { ImageSceneDataError } from "../errors";
|
||||
import {
|
||||
getSnapLinesAtPointer,
|
||||
snapDraggedElements,
|
||||
@ -2272,6 +2273,11 @@ 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 imageElement = this.createImageElement({ sceneX, sceneY });
|
||||
this.insertImageElement(imageElement, file);
|
||||
this.initializeImageDimensions(imageElement);
|
||||
@ -2477,7 +2483,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
if (
|
||||
!isPlainPaste &&
|
||||
mixedContent.some((node) => node.type === "imageUrl")
|
||||
mixedContent.some((node) => node.type === "imageUrl") &&
|
||||
this.isToolSupported("image")
|
||||
) {
|
||||
const imageURLs = mixedContent
|
||||
.filter((node) => node.type === "imageUrl")
|
||||
@ -3284,6 +3291,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
});
|
||||
|
||||
// We purposely widen the `tool` type so this helper can be called with
|
||||
// any tool without having to type check it
|
||||
private isToolSupported = <T extends ToolType | "custom">(tool: T) => {
|
||||
return (
|
||||
this.props.UIOptions.tools?.[
|
||||
tool as Extract<T, keyof AppProps["UIOptions"]["tools"]>
|
||||
] !== false
|
||||
);
|
||||
};
|
||||
|
||||
setActiveTool = (
|
||||
tool: (
|
||||
| (
|
||||
@ -3296,6 +3313,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
| { type: "custom"; customType: string }
|
||||
) & { locked?: boolean },
|
||||
) => {
|
||||
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.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||
@ -7479,6 +7503,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
imageFile: File,
|
||||
showCursorImagePreview?: boolean,
|
||||
) => {
|
||||
// we should be handling all cases upstream, but in case we forget to handle
|
||||
// a future case, let's throw here
|
||||
if (!this.isToolSupported("image")) {
|
||||
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.addNewElement(imageElement);
|
||||
|
||||
try {
|
||||
@ -7863,7 +7894,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
|
||||
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
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
@ -7991,6 +8025,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 });
|
||||
}
|
||||
};
|
||||
|
@ -280,6 +280,7 @@ const LayerUI = ({
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
activeTool={appState.activeTool}
|
||||
UIOptions={UIOptions}
|
||||
app={app}
|
||||
/>
|
||||
</Stack.Row>
|
||||
@ -470,6 +471,7 @@ const LayerUI = ({
|
||||
renderSidebars={renderSidebars}
|
||||
device={device}
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
UIOptions={UIOptions}
|
||||
/>
|
||||
)}
|
||||
{!device.editor.isMobile && (
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AppClassProperties,
|
||||
AppProps,
|
||||
AppState,
|
||||
Device,
|
||||
ExcalidrawProps,
|
||||
@ -45,6 +46,7 @@ type MobileMenuProps = {
|
||||
renderSidebars: () => JSX.Element | null;
|
||||
device: Device;
|
||||
renderWelcomeScreen: boolean;
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
app: AppClassProperties;
|
||||
};
|
||||
|
||||
@ -62,6 +64,7 @@ export const MobileMenu = ({
|
||||
renderSidebars,
|
||||
device,
|
||||
renderWelcomeScreen,
|
||||
UIOptions,
|
||||
app,
|
||||
}: MobileMenuProps) => {
|
||||
const {
|
||||
@ -83,6 +86,7 @@ export const MobileMenu = ({
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
activeTool={appState.activeTool}
|
||||
UIOptions={UIOptions}
|
||||
app={app}
|
||||
/>
|
||||
</Stack.Row>
|
||||
|
@ -222,6 +222,9 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
||||
toggleTheme: null,
|
||||
saveAsImage: true,
|
||||
},
|
||||
tools: {
|
||||
image: true,
|
||||
},
|
||||
};
|
||||
|
||||
// breakpoints
|
||||
|
@ -3,7 +3,7 @@ import { cleanAppStateForExport } from "../appState";
|
||||
import { 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -131,8 +125,19 @@ export const loadSceneOrLibraryFromBlob = async (
|
||||
fileHandle?: FileSystemHandle | null,
|
||||
) => {
|
||||
const contents = await parseFileContents(blob);
|
||||
let data;
|
||||
try {
|
||||
const data = JSON.parse(contents);
|
||||
try {
|
||||
data = JSON.parse(contents);
|
||||
} catch (error: any) {
|
||||
if (isSupportedImageFile(blob)) {
|
||||
throw new ImageSceneDataError(
|
||||
t("alerts.imageDoesNotContainScene"),
|
||||
"IMAGE_NOT_CONTAINS_SCENE_DATA",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (isValidExcalidrawData(data)) {
|
||||
return {
|
||||
type: MIME_TYPES.excalidraw,
|
||||
@ -162,7 +167,9 @@ export const loadSceneOrLibraryFromBlob = async (
|
||||
}
|
||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
if (error instanceof ImageSceneDataError) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -209,6 +209,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": {
|
||||
"line1": "Looks like you are using Brave browser with the <bold>Aggressively Block Fingerprinting</bold> setting enabled.",
|
||||
"line2": "This could result in breaking the <bold>Text Elements</bold> in your drawings.",
|
||||
|
@ -15,6 +15,16 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for disabling `image` tool (also disabling image insertion in general, though keeps support for importing from `.excalidraw` files) [#6320](https://github.com/excalidraw/excalidraw/pull/6320).
|
||||
|
||||
For disabling `image` you need to set 👇
|
||||
|
||||
```
|
||||
UIOptions.tools = {
|
||||
image: false
|
||||
}
|
||||
```
|
||||
|
||||
- Support `excalidrawAPI` prop for accessing the Excalidraw API [#7251](https://github.com/excalidraw/excalidraw/pull/7251).
|
||||
|
||||
- Export [`getCommonBounds`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#getcommonbounds) helper from the package [#7247](https://github.com/excalidraw/excalidraw/pull/7247).
|
||||
|
@ -98,6 +98,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
||||
const [theme, setTheme] = useState<Theme>("light");
|
||||
const [disableImageTool, setDisableImageTool] = useState(false);
|
||||
const [isCollaborating, setIsCollaborating] = useState(false);
|
||||
const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
|
||||
{},
|
||||
@ -606,6 +607,16 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
/>
|
||||
Switch to Dark Theme
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={disableImageTool === true}
|
||||
onChange={() => {
|
||||
setDisableImageTool(!disableImageTool);
|
||||
}}
|
||||
/>
|
||||
Disable Image Tool
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -686,6 +697,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
},
|
||||
tools: { image: !disableImageTool },
|
||||
}}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
onLinkOpen={onLinkOpen}
|
||||
|
@ -56,6 +56,9 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
...DEFAULT_UI_OPTIONS.canvasActions,
|
||||
...canvasActions,
|
||||
},
|
||||
tools: {
|
||||
image: props.UIOptions?.tools?.image ?? true,
|
||||
},
|
||||
};
|
||||
|
||||
if (canvasActions?.export) {
|
||||
|
@ -471,7 +471,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;
|
||||
@ -481,9 +481,12 @@ type CanvasActions = Partial<{
|
||||
saveAsImage: boolean;
|
||||
}>;
|
||||
|
||||
type UIOptions = Partial<{
|
||||
export type UIOptions = Partial<{
|
||||
dockedSidebarBreakpoint: number;
|
||||
canvasActions: CanvasActions;
|
||||
tools: {
|
||||
image: boolean;
|
||||
};
|
||||
/** @deprecated does nothing. Will be removed in 0.15 */
|
||||
welcomeScreen?: boolean;
|
||||
}>;
|
||||
|
Loading…
x
Reference in New Issue
Block a user