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:
parent
de99484a1f
commit
c3ecbcb3ab
@ -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"}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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");
|
||||||
|
@ -34,6 +34,14 @@
|
|||||||
|
|
||||||
.TextInput {
|
.TextInput {
|
||||||
height: calc(1rem - 3px);
|
height: calc(1rem - 3px);
|
||||||
|
|
||||||
|
&--readonly {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
&:hover {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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")}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"]}
|
||||||
|
@ -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.
|
||||||
|
@ -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`
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user