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>
This commit is contained in:
Arun 2021-03-20 16:08:03 +05:30 committed by GitHub
parent de99484a1f
commit c3ecbcb3ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 75 additions and 3 deletions

View File

@ -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"}
/> />
), ),
}); });

View File

@ -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}
/> />
); );
} }

View File

@ -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;

View File

@ -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");

View File

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

View File

@ -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")}

View File

@ -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>
); );
} }
} }

View File

@ -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"]}

View File

@ -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.

View File

@ -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`

View File

@ -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>

View File

@ -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);
});
});
}); });

View File

@ -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 = {