Browse Source

feat: Allow host app to update title of drawing (#3273)

* Allow updating name on updateScene

* Revert "Allow updating name on updateScene"

This reverts commit 4e07a608d38a585e0f3c04e26b9f5e0e404824b1.

* Make requested changes

* Make requested changes

* Remove customName from state

* Remove redundant if statement

* Add tests, update changelog and minor fixes

* remove eempty lines

* minor fixes

* no border and on hover no background change

* Give preference to name prop when initialData.appState.name is present and update specs

* minor fix

* Fix name input style in dark mode

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
Arun 4 years ago
parent
commit
c3ecbcb3ab

+ 2 - 1
src/actions/actionExport.tsx

@@ -18,11 +18,12 @@ export const actionChangeProjectName = register({
     trackEvent("change", "title");
     trackEvent("change", "title");
     return { appState: { ...appState, name: value }, commitToHistory: false };
     return { appState: { ...appState, name: value }, commitToHistory: false };
   },
   },
-  PanelComponent: ({ appState, updateData }) => (
+  PanelComponent: ({ appState, updateData, appProps }) => (
     <ProjectName
     <ProjectName
       label={t("labels.fileTitle")}
       label={t("labels.fileTitle")}
       value={appState.name || "Unnamed"}
       value={appState.name || "Unnamed"}
       onChange={(name: string) => updateData(name)}
       onChange={(name: string) => updateData(name)}
+      isNameEditable={typeof appProps.name === "undefined"}
     />
     />
   ),
   ),
 });
 });

+ 1 - 0
src/actions/manager.tsx

@@ -122,6 +122,7 @@ export class ActionManager implements ActionsManagerInterface {
           appState={this.getAppState()}
           appState={this.getAppState()}
           updateData={updateData}
           updateData={updateData}
           id={id}
           id={id}
+          appProps={this.app.props}
         />
         />
       );
       );
     }
     }

+ 2 - 1
src/actions/types.ts

@@ -1,6 +1,6 @@
 import React from "react";
 import React from "react";
 import { ExcalidrawElement } from "../element/types";
 import { ExcalidrawElement } from "../element/types";
-import { AppState } from "../types";
+import { AppState, ExcalidrawProps } from "../types";
 
 
 /** if false, the action should be prevented */
 /** if false, the action should be prevented */
 export type ActionResult =
 export type ActionResult =
@@ -94,6 +94,7 @@ export interface Action {
     elements: readonly ExcalidrawElement[];
     elements: readonly ExcalidrawElement[];
     appState: AppState;
     appState: AppState;
     updateData: (formData?: any) => void;
     updateData: (formData?: any) => void;
+    appProps: ExcalidrawProps;
     id?: string;
     id?: string;
   }>;
   }>;
   perform: ActionFn;
   perform: ActionFn;

+ 15 - 0
src/components/App.tsx

@@ -303,6 +303,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       zenModeEnabled = false,
       zenModeEnabled = false,
       gridModeEnabled = false,
       gridModeEnabled = false,
       theme = defaultAppState.theme,
       theme = defaultAppState.theme,
+      name = defaultAppState.name,
     } = props;
     } = props;
     this.state = {
     this.state = {
       ...defaultAppState,
       ...defaultAppState,
@@ -314,6 +315,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       viewModeEnabled,
       viewModeEnabled,
       zenModeEnabled,
       zenModeEnabled,
       gridSize: gridModeEnabled ? GRID_SIZE : null,
       gridSize: gridModeEnabled ? GRID_SIZE : null,
+      name,
     };
     };
     if (excalidrawRef) {
     if (excalidrawRef) {
       const readyPromise =
       const readyPromise =
@@ -523,6 +525,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
         let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
         let gridSize = actionResult?.appState?.gridSize || null;
         let gridSize = actionResult?.appState?.gridSize || null;
         let theme = actionResult?.appState?.theme || "light";
         let theme = actionResult?.appState?.theme || "light";
+        let name = actionResult?.appState?.name || this.state.name;
 
 
         if (typeof this.props.viewModeEnabled !== "undefined") {
         if (typeof this.props.viewModeEnabled !== "undefined") {
           viewModeEnabled = this.props.viewModeEnabled;
           viewModeEnabled = this.props.viewModeEnabled;
@@ -540,6 +543,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
           theme = this.props.theme;
           theme = this.props.theme;
         }
         }
 
 
+        if (typeof this.props.name !== "undefined") {
+          name = this.props.name;
+        }
+
         this.setState(
         this.setState(
           (state) => {
           (state) => {
             // using Object.assign instead of spread to fool TS 4.2.2+ into
             // using Object.assign instead of spread to fool TS 4.2.2+ into
@@ -556,6 +563,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
               zenModeEnabled,
               zenModeEnabled,
               gridSize,
               gridSize,
               theme,
               theme,
+              name,
             });
             });
           },
           },
           () => {
           () => {
@@ -890,6 +898,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         gridSize: this.props.gridModeEnabled ? GRID_SIZE : null,
         gridSize: this.props.gridModeEnabled ? GRID_SIZE : null,
       });
       });
     }
     }
+
+    if (this.props.name && prevProps.name !== this.props.name) {
+      this.setState({
+        name: this.props.name,
+      });
+    }
+
     document
     document
       .querySelector(".excalidraw")
       .querySelector(".excalidraw")
       ?.classList.toggle("theme--dark", this.state.theme === "dark");
       ?.classList.toggle("theme--dark", this.state.theme === "dark");

+ 8 - 0
src/components/ExportDialog.scss

@@ -34,6 +34,14 @@
 
 
     .TextInput {
     .TextInput {
       height: calc(1rem - 3px);
       height: calc(1rem - 3px);
+
+      &--readonly {
+        background: none;
+        border: none;
+        &:hover {
+          background: none;
+        }
+      }
     }
     }
   }
   }
 
 

+ 1 - 0
src/components/ExportDialog.tsx

@@ -257,6 +257,7 @@ export const ExportDialog = ({
         onClick={() => {
         onClick={() => {
           setModalIsShown(true);
           setModalIsShown(true);
         }}
         }}
+        data-testid="export-button"
         icon={exportFile}
         icon={exportFile}
         type="button"
         type="button"
         aria-label={t("buttons.export")}
         aria-label={t("buttons.export")}

+ 9 - 1
src/components/ProjectName.tsx

@@ -7,6 +7,7 @@ type Props = {
   value: string;
   value: string;
   onChange: (value: string) => void;
   onChange: (value: string) => void;
   label: string;
   label: string;
+  isNameEditable: boolean;
 };
 };
 
 
 export class ProjectName extends Component<Props> {
 export class ProjectName extends Component<Props> {
@@ -43,7 +44,7 @@ export class ProjectName extends Component<Props> {
   };
   };
 
 
   public render() {
   public render() {
-    return (
+    return this.props.isNameEditable ? (
       <span
       <span
         suppressContentEditableWarning
         suppressContentEditableWarning
         ref={this.makeEditable}
         ref={this.makeEditable}
@@ -57,6 +58,13 @@ export class ProjectName extends Component<Props> {
       >
       >
         {this.props.value}
         {this.props.value}
       </span>
       </span>
+    ) : (
+      <span
+        className="TextInput TextInput--readonly"
+        aria-label={this.props.label}
+      >
+        {this.props.value}
+      </span>
     );
     );
   }
   }
 }
 }

+ 1 - 0
src/components/ToolButton.tsx

@@ -58,6 +58,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
             "ToolIcon--selected": props.selected,
             "ToolIcon--selected": props.selected,
           },
           },
         )}
         )}
+        data-testid={props["data-testid"]}
         hidden={props.hidden}
         hidden={props.hidden}
         title={props.title}
         title={props.title}
         aria-label={props["aria-label"]}
         aria-label={props["aria-label"]}

+ 1 - 0
src/packages/excalidraw/CHANGELOG.md

@@ -18,6 +18,7 @@ Please add the latest change on the top under the correct section.
 
 
 ### Features
 ### 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).
 - Export API `setCanvasOffsets` via `ref` to set the offsets for Excalidraw[#3265](https://github.com/excalidraw/excalidraw/pull/3265).
   #### BREAKING CHANGE
   #### BREAKING CHANGE
   - `offsetLeft` and `offsetTop` props have been removed now so you have to use the `setCanvasOffsets` via `ref` to achieve the same.
   - `offsetLeft` and `offsetTop` props have been removed now so you have to use the `setCanvasOffsets` via `ref` to achieve the same.

+ 5 - 0
src/packages/excalidraw/README.md

@@ -376,6 +376,7 @@ export default function IndexPage() {
 | [`gridModeEnabled`](#gridModeEnabled) | boolean |  | This implies if the grid mode is enabled |
 | [`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 |
 | [`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 |
 | [`theme`](#theme) | `light` or `dark` |  | The theme of the Excalidraw component |
+| [`name`](#name) | string |  | Name of the drawing |
 
 
 #### `width`
 #### `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.
 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
 ### Extra API's
 
 
 #### `getSceneVersion`
 #### `getSceneVersion`

+ 2 - 0
src/packages/excalidraw/index.tsx

@@ -29,6 +29,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
     gridModeEnabled,
     gridModeEnabled,
     libraryReturnUrl,
     libraryReturnUrl,
     theme,
     theme,
+    name,
   } = props;
   } = props;
 
 
   useEffect(() => {
   useEffect(() => {
@@ -69,6 +70,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
           gridModeEnabled={gridModeEnabled}
           gridModeEnabled={gridModeEnabled}
           libraryReturnUrl={libraryReturnUrl}
           libraryReturnUrl={libraryReturnUrl}
           theme={theme}
           theme={theme}
+          name={name}
         />
         />
       </IsMobileProvider>
       </IsMobileProvider>
     </InitializeApp>
     </InitializeApp>

+ 27 - 0
src/tests/excalidrawPackage.test.tsx

@@ -3,6 +3,7 @@ import { fireEvent, GlobalTestState, render } from "./test-utils";
 import Excalidraw from "../packages/excalidraw/index";
 import Excalidraw from "../packages/excalidraw/index";
 import { queryByText, queryByTestId } from "@testing-library/react";
 import { queryByText, queryByTestId } from "@testing-library/react";
 import { GRID_SIZE } from "../constants";
 import { GRID_SIZE } from "../constants";
+import { t } from "../i18n";
 
 
 const { h } = window;
 const { h } = window;
 
 
@@ -104,4 +105,30 @@ describe("<Excalidraw/>", () => {
       expect(queryByTestId(container, "toggle-dark-mode")).toBe(null);
       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(<Excalidraw />);
+
+      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(<Excalidraw name={name} />);
+
+      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);
+    });
+  });
 });
 });

+ 1 - 0
src/types.ts

@@ -187,6 +187,7 @@ export interface ExcalidrawProps {
   gridModeEnabled?: boolean;
   gridModeEnabled?: boolean;
   libraryReturnUrl?: string;
   libraryReturnUrl?: string;
   theme?: "dark" | "light";
   theme?: "dark" | "light";
+  name?: string;
 }
 }
 
 
 export type SceneData = {
 export type SceneData = {