diff --git a/src/actions/actionExport.tsx b/src/actions/actionExport.tsx index 9ad5d7327..d49faf8c6 100644 --- a/src/actions/actionExport.tsx +++ b/src/actions/actionExport.tsx @@ -18,11 +18,12 @@ export const actionChangeProjectName = register({ trackEvent("change", "title"); return { appState: { ...appState, name: value }, commitToHistory: false }; }, - PanelComponent: ({ appState, updateData }) => ( + PanelComponent: ({ appState, updateData, appProps }) => ( updateData(name)} + isNameEditable={typeof appProps.name === "undefined"} /> ), }); diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index ef617776e..b242508b8 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -122,6 +122,7 @@ export class ActionManager implements ActionsManagerInterface { appState={this.getAppState()} updateData={updateData} id={id} + appProps={this.app.props} /> ); } diff --git a/src/actions/types.ts b/src/actions/types.ts index ae1baefec..26fa42449 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -1,6 +1,6 @@ import React from "react"; import { ExcalidrawElement } from "../element/types"; -import { AppState } from "../types"; +import { AppState, ExcalidrawProps } from "../types"; /** if false, the action should be prevented */ export type ActionResult = @@ -94,6 +94,7 @@ export interface Action { elements: readonly ExcalidrawElement[]; appState: AppState; updateData: (formData?: any) => void; + appProps: ExcalidrawProps; id?: string; }>; perform: ActionFn; diff --git a/src/components/App.tsx b/src/components/App.tsx index bc37b1399..12658cecb 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -303,6 +303,7 @@ class App extends React.Component { zenModeEnabled = false, gridModeEnabled = false, theme = defaultAppState.theme, + name = defaultAppState.name, } = props; this.state = { ...defaultAppState, @@ -314,6 +315,7 @@ class App extends React.Component { viewModeEnabled, zenModeEnabled, gridSize: gridModeEnabled ? GRID_SIZE : null, + name, }; if (excalidrawRef) { const readyPromise = @@ -523,6 +525,7 @@ class App extends React.Component { let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false; let gridSize = actionResult?.appState?.gridSize || null; let theme = actionResult?.appState?.theme || "light"; + let name = actionResult?.appState?.name || this.state.name; if (typeof this.props.viewModeEnabled !== "undefined") { viewModeEnabled = this.props.viewModeEnabled; @@ -540,6 +543,10 @@ class App extends React.Component { theme = this.props.theme; } + if (typeof this.props.name !== "undefined") { + name = this.props.name; + } + this.setState( (state) => { // using Object.assign instead of spread to fool TS 4.2.2+ into @@ -556,6 +563,7 @@ class App extends React.Component { zenModeEnabled, gridSize, theme, + name, }); }, () => { @@ -890,6 +898,13 @@ class App extends React.Component { gridSize: this.props.gridModeEnabled ? GRID_SIZE : null, }); } + + if (this.props.name && prevProps.name !== this.props.name) { + this.setState({ + name: this.props.name, + }); + } + document .querySelector(".excalidraw") ?.classList.toggle("theme--dark", this.state.theme === "dark"); diff --git a/src/components/ExportDialog.scss b/src/components/ExportDialog.scss index d313689f5..577cb12b9 100644 --- a/src/components/ExportDialog.scss +++ b/src/components/ExportDialog.scss @@ -34,6 +34,14 @@ .TextInput { height: calc(1rem - 3px); + + &--readonly { + background: none; + border: none; + &:hover { + background: none; + } + } } } diff --git a/src/components/ExportDialog.tsx b/src/components/ExportDialog.tsx index cc4219a51..bad31a910 100644 --- a/src/components/ExportDialog.tsx +++ b/src/components/ExportDialog.tsx @@ -257,6 +257,7 @@ export const ExportDialog = ({ onClick={() => { setModalIsShown(true); }} + data-testid="export-button" icon={exportFile} type="button" aria-label={t("buttons.export")} diff --git a/src/components/ProjectName.tsx b/src/components/ProjectName.tsx index e432e49e6..d474e7a30 100644 --- a/src/components/ProjectName.tsx +++ b/src/components/ProjectName.tsx @@ -7,6 +7,7 @@ type Props = { value: string; onChange: (value: string) => void; label: string; + isNameEditable: boolean; }; export class ProjectName extends Component { @@ -43,7 +44,7 @@ export class ProjectName extends Component { }; public render() { - return ( + return this.props.isNameEditable ? ( { > {this.props.value} + ) : ( + + {this.props.value} + ); } } diff --git a/src/components/ToolButton.tsx b/src/components/ToolButton.tsx index af90dde63..68db6dcfa 100644 --- a/src/components/ToolButton.tsx +++ b/src/components/ToolButton.tsx @@ -58,6 +58,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => { "ToolIcon--selected": props.selected, }, )} + data-testid={props["data-testid"]} hidden={props.hidden} title={props.title} aria-label={props["aria-label"]} diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 39a60823e..f5191b3fe 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -18,6 +18,7 @@ Please add the latest change on the top under the correct section. ### Features +- Add `name` prop to indicate the name of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over `intialData.appState.name`, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw [#3273](https://github.com/excalidraw/excalidraw/pull/3273). - Export API `setCanvasOffsets` via `ref` to set the offsets for Excalidraw[#3265](https://github.com/excalidraw/excalidraw/pull/3265). #### BREAKING CHANGE - `offsetLeft` and `offsetTop` props have been removed now so you have to use the `setCanvasOffsets` via `ref` to achieve the same. diff --git a/src/packages/excalidraw/README.md b/src/packages/excalidraw/README.md index f93a0d958..4ba6b7d9e 100644 --- a/src/packages/excalidraw/README.md +++ b/src/packages/excalidraw/README.md @@ -376,6 +376,7 @@ export default function IndexPage() { | [`gridModeEnabled`](#gridModeEnabled) | boolean | | This implies if the grid mode is enabled | | [`libraryReturnUrl`](#libraryReturnUrl) | string | | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to | | [`theme`](#theme) | `light` or `dark` | | The theme of the Excalidraw component | +| [`name`](#name) | string | | Name of the drawing | #### `width` @@ -534,6 +535,10 @@ If supplied, this URL will be used when user tries to install a library from [li This prop controls Excalidraw's theme. When supplied, the value takes precedence over `intialData.appState.theme`, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app. +### `name` + +This prop sets the name of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over `intialData.appState.name`, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw. + ### Extra API's #### `getSceneVersion` diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index 790ec2b4b..93cf7404d 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -29,6 +29,7 @@ const Excalidraw = (props: ExcalidrawProps) => { gridModeEnabled, libraryReturnUrl, theme, + name, } = props; useEffect(() => { @@ -69,6 +70,7 @@ const Excalidraw = (props: ExcalidrawProps) => { gridModeEnabled={gridModeEnabled} libraryReturnUrl={libraryReturnUrl} theme={theme} + name={name} /> diff --git a/src/tests/excalidrawPackage.test.tsx b/src/tests/excalidrawPackage.test.tsx index 9351a7f27..ac951f931 100644 --- a/src/tests/excalidrawPackage.test.tsx +++ b/src/tests/excalidrawPackage.test.tsx @@ -3,6 +3,7 @@ import { fireEvent, GlobalTestState, render } from "./test-utils"; import Excalidraw from "../packages/excalidraw/index"; import { queryByText, queryByTestId } from "@testing-library/react"; import { GRID_SIZE } from "../constants"; +import { t } from "../i18n"; const { h } = window; @@ -104,4 +105,30 @@ describe("", () => { expect(queryByTestId(container, "toggle-dark-mode")).toBe(null); }); }); + + describe("Test name prop", () => { + it('should allow editing name when the name prop is "undefined"', async () => { + const { container } = await render(); + + fireEvent.click(queryByTestId(container, "export-button")!); + const textInput = document.querySelector( + ".ExportDialog__name .TextInput", + ); + expect(textInput?.textContent).toContain(`${t("labels.untitled")}`); + expect(textInput?.hasAttribute("data-type")).toBe(true); + }); + + it('should set the name and not allow editing when the name prop is present"', async () => { + const name = "test"; + const { container } = await render(); + + await fireEvent.click(queryByTestId(container, "export-button")!); + const textInput = document.querySelector( + ".ExportDialog__name .TextInput--readonly", + ); + expect(textInput?.textContent).toEqual(name); + + expect(textInput?.hasAttribute("data-type")).toBe(false); + }); + }); }); diff --git a/src/types.ts b/src/types.ts index 4accbf87c..956f8f518 100644 --- a/src/types.ts +++ b/src/types.ts @@ -187,6 +187,7 @@ export interface ExcalidrawProps { gridModeEnabled?: boolean; libraryReturnUrl?: string; theme?: "dark" | "light"; + name?: string; } export type SceneData = {