Forráskód Böngészése

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 éve
szülő
commit
c3ecbcb3ab

+ 2 - 1
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 }) => (
     <ProjectName
       label={t("labels.fileTitle")}
       value={appState.name || "Unnamed"}
       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()}
           updateData={updateData}
           id={id}
+          appProps={this.app.props}
         />
       );
     }

+ 2 - 1
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;

+ 15 - 0
src/components/App.tsx

@@ -303,6 +303,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       zenModeEnabled = false,
       gridModeEnabled = false,
       theme = defaultAppState.theme,
+      name = defaultAppState.name,
     } = props;
     this.state = {
       ...defaultAppState,
@@ -314,6 +315,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       viewModeEnabled,
       zenModeEnabled,
       gridSize: gridModeEnabled ? GRID_SIZE : null,
+      name,
     };
     if (excalidrawRef) {
       const readyPromise =
@@ -523,6 +525,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         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<ExcalidrawProps, AppState> {
           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<ExcalidrawProps, AppState> {
               zenModeEnabled,
               gridSize,
               theme,
+              name,
             });
           },
           () => {
@@ -890,6 +898,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         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");

+ 8 - 0
src/components/ExportDialog.scss

@@ -34,6 +34,14 @@
 
     .TextInput {
       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={() => {
           setModalIsShown(true);
         }}
+        data-testid="export-button"
         icon={exportFile}
         type="button"
         aria-label={t("buttons.export")}

+ 9 - 1
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<Props> {
@@ -43,7 +44,7 @@ export class ProjectName extends Component<Props> {
   };
 
   public render() {
-    return (
+    return this.props.isNameEditable ? (
       <span
         suppressContentEditableWarning
         ref={this.makeEditable}
@@ -57,6 +58,13 @@ export class ProjectName extends Component<Props> {
       >
         {this.props.value}
       </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,
           },
         )}
+        data-testid={props["data-testid"]}
         hidden={props.hidden}
         title={props.title}
         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
 
+- 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.

+ 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 |
 | [`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`

+ 2 - 0
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}
         />
       </IsMobileProvider>
     </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 { queryByText, queryByTestId } from "@testing-library/react";
 import { GRID_SIZE } from "../constants";
+import { t } from "../i18n";
 
 const { h } = window;
 
@@ -104,4 +105,30 @@ describe("<Excalidraw/>", () => {
       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;
   libraryReturnUrl?: string;
   theme?: "dark" | "light";
+  name?: string;
 }
 
 export type SceneData = {