From ba489743510ebae554906034191db7af1faba735 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Sat, 29 May 2021 02:56:25 +0530 Subject: [PATCH] feat: customise export dialog with UIOptions.canvasActions.export prop (#3658) * refactor: update UIOptions.canvasActions.export to be a an object * fix * fix * dnt show export icon when false * fix * inline * memoize UIOptions * update docs * fix * tweak readme Co-authored-by: David Luzar --- src/actions/actionExport.tsx | 4 +- src/actions/index.ts | 2 +- src/actions/types.ts | 2 +- src/components/App.tsx | 2 - src/components/JSONExportDialog.tsx | 51 ++++++++++++--------- src/components/LayerUI.tsx | 23 ++++------ src/constants.ts | 3 +- src/excalidraw-app/index.tsx | 6 ++- src/packages/excalidraw/CHANGELOG.md | 11 +++++ src/packages/excalidraw/README_NEXT.md | 17 ++++--- src/packages/excalidraw/index.tsx | 62 +++++++++++++++++++++++--- src/tests/excalidrawPackage.test.tsx | 6 ++- src/types.ts | 19 ++++---- 13 files changed, 140 insertions(+), 68 deletions(-) diff --git a/src/actions/actionExport.tsx b/src/actions/actionExport.tsx index a583bcb6d..01868f688 100644 --- a/src/actions/actionExport.tsx +++ b/src/actions/actionExport.tsx @@ -113,8 +113,8 @@ export const actionSaveToActiveFile = register({ ), }); -export const actionSaveAsScene = register({ - name: "saveAsScene", +export const actionSaveFileToDisk = register({ + name: "saveFileToDisk", perform: async (elements, appState, value) => { try { const { fileHandle } = await saveAsJSON(elements, { diff --git a/src/actions/index.ts b/src/actions/index.ts index 3c699da34..f37c78422 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -35,7 +35,7 @@ export { actionChangeProjectName, actionChangeExportBackground, actionSaveToActiveFile, - actionSaveAsScene, + actionSaveFileToDisk, actionLoadScene, } from "./actionExport"; diff --git a/src/actions/types.ts b/src/actions/types.ts index eff523f99..7a98d96e7 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -67,7 +67,7 @@ export type ActionName = | "changeExportBackground" | "changeExportEmbedScene" | "saveToActiveFile" - | "saveAsScene" + | "saveFileToDisk" | "loadScene" | "duplicateSelection" | "deleteSelectedElements" diff --git a/src/components/App.tsx b/src/components/App.tsx index b2bd9345d..c5c0d1fc7 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -452,7 +452,6 @@ class App extends React.Component { const { onCollabButtonClick, - onExportToBackend, renderTopRightUI, renderFooter, renderCustomStats, @@ -493,7 +492,6 @@ class App extends React.Component { toggleZenMode={this.toggleZenMode} langCode={getLanguage().code} isCollaborating={this.props.isCollaborating || false} - onExportToBackend={onExportToBackend} renderTopRightUI={renderTopRightUI} renderCustomFooter={renderFooter} viewModeEnabled={viewModeEnabled} diff --git a/src/components/JSONExportDialog.tsx b/src/components/JSONExportDialog.tsx index c878c8e7a..7efea4be3 100644 --- a/src/components/JSONExportDialog.tsx +++ b/src/components/JSONExportDialog.tsx @@ -3,11 +3,11 @@ import { ActionsManagerInterface } from "../actions/types"; import { NonDeletedExcalidrawElement } from "../element/types"; import { t } from "../i18n"; import { useIsMobile } from "./App"; -import { AppState } from "../types"; +import { AppState, ExportOpts } from "../types"; import { Dialog } from "./Dialog"; import { exportFile, exportToFileIcon, link } from "./icons"; import { ToolButton } from "./ToolButton"; -import { actionSaveAsScene } from "../actions/actionExport"; +import { actionSaveFileToDisk } from "../actions/actionExport"; import { Card } from "./Card"; import "./ExportDialog.scss"; @@ -23,35 +23,39 @@ const JSONExportModal = ({ appState, actionManager, onExportToBackend, + exportOpts, }: { appState: AppState; elements: readonly NonDeletedExcalidrawElement[]; actionManager: ActionsManagerInterface; onExportToBackend?: ExportCB; onCloseRequest: () => void; + exportOpts: ExportOpts; }) => { return (
- -
{exportToFileIcon}
-

{t("exportDialog.disk_title")}

-
- {t("exportDialog.disk_details")} - {!fsSupported && actionManager.renderAction("changeProjectName")} -
- { - actionManager.executeAction(actionSaveAsScene); - }} - /> -
- {onExportToBackend && ( + {exportOpts.saveFileToDisk && ( + +
{exportToFileIcon}
+

{t("exportDialog.disk_title")}

+
+ {t("exportDialog.disk_details")} + {!fsSupported && actionManager.renderAction("changeProjectName")} +
+ { + actionManager.executeAction(actionSaveFileToDisk); + }} + /> +
+ )} + {exportOpts.onExportToBackend && (
{link}

{t("exportDialog.link_title")}

@@ -62,7 +66,7 @@ const JSONExportModal = ({ title={t("exportDialog.link_button")} aria-label={t("exportDialog.link_button")} showAriaLabel={true} - onClick={() => onExportToBackend(elements)} + onClick={() => onExportToBackend!(elements)} />
)} @@ -76,11 +80,13 @@ export const JSONExportDialog = ({ appState, actionManager, onExportToBackend, + exportOpts, }: { appState: AppState; elements: readonly NonDeletedExcalidrawElement[]; actionManager: ActionsManagerInterface; onExportToBackend?: ExportCB; + exportOpts: ExportOpts; }) => { const [modalIsShown, setModalIsShown] = useState(false); @@ -109,6 +115,7 @@ export const JSONExportDialog = ({ actionManager={actionManager} onExportToBackend={onExportToBackend} onCloseRequest={handleClose} + exportOpts={exportOpts} /> )} diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 7c73c3681..d914779e8 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -63,11 +63,6 @@ interface LayerUIProps { toggleZenMode: () => void; langCode: Language["code"]; isCollaborating: boolean; - onExportToBackend?: ( - exportedElements: readonly NonDeletedExcalidrawElement[], - appState: AppState, - canvas: HTMLCanvasElement | null, - ) => void; renderTopRightUI?: (isMobile: boolean, appState: AppState) => JSX.Element; renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element; viewModeEnabled: boolean; @@ -371,7 +366,6 @@ const LayerUI = ({ showThemeBtn, toggleZenMode, isCollaborating, - onExportToBackend, renderTopRightUI, renderCustomFooter, viewModeEnabled, @@ -393,14 +387,15 @@ const LayerUI = ({ elements={elements} appState={appState} actionManager={actionManager} - onExportToBackend={ - onExportToBackend - ? (elements) => { - onExportToBackend && - onExportToBackend(elements, appState, canvas); - } - : undefined - } + onExportToBackend={(elements) => { + UIOptions.canvasActions.export.onExportToBackend && + UIOptions.canvasActions.export.onExportToBackend( + elements, + appState, + canvas, + ); + }} + exportOpts={UIOptions.canvasActions.export} /> ); }; diff --git a/src/constants.ts b/src/constants.ts index 1575a6278..9ac619b1c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -131,9 +131,8 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = { canvasActions: { changeViewBackgroundColor: true, clearCanvas: true, - export: true, + export: { saveFileToDisk: true }, loadScene: true, - saveAsScene: true, saveToActiveFile: true, theme: true, }, diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 299339514..741b0d55b 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -424,7 +424,11 @@ const ExcalidrawWrapper = () => { onCollabButtonClick={collabAPI?.onCollabButtonClick} isCollaborating={collabAPI?.isCollaborating()} onPointerUpdate={collabAPI?.onPointerUpdate} - onExportToBackend={onExportToBackend} + UIOptions={{ + canvasActions: { + export: { onExportToBackend }, + }, + }} renderTopRightUI={renderTopRightUI} renderFooter={renderFooter} langCode={langCode} diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 80056e073..9e3bc4634 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -15,6 +15,17 @@ Please add the latest change on the top under the correct section. ## Excalidraw API +### Features + +- Export dialog can be customised with [`UiOptions.canvasActions.export`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportOpts) [#3658](https://github.com/excalidraw/excalidraw/pull/3658). + + Also, [`UIOptions`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#UIOptions) is now memoized to avoid unnecessary rerenders. + +#### BREAKING CHANGE + +- `UIOptions.canvasActions.saveAsScene` is now renamed to `UiOptions.canvasActions.export.saveFileToDisk`. Defaults to `true` hence the **save file to disk** button is rendered inside the export dialog. +- `exportToBackend` is now renamed to `UIOptions.canvasActions.export.exportToBackend`. If this prop is not passed, the **shareable-link** button will not be rendered, same as before. + ### Refactor - #### BREAKING CHANGE diff --git a/src/packages/excalidraw/README_NEXT.md b/src/packages/excalidraw/README_NEXT.md index f1e5a6129..ec7eadb24 100644 --- a/src/packages/excalidraw/README_NEXT.md +++ b/src/packages/excalidraw/README_NEXT.md @@ -363,7 +363,6 @@ To view the full example visit :point_down: | [`onCollabButtonClick`](#onCollabButtonClick) | Function | | Callback to be triggered when the collab button is clicked | | [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode | | [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. | -| [`onExportToBackend`](#onExportToBackend) | Function | | Callback triggered when link button is clicked on export dialog | | [`langCode`](#langCode) | string | `en` | Language code string | | [`renderTopRightUI`](#renderTopRightUI) | Function | | Function that renders custom UI in top right corner | | [`renderFooter `](#renderFooter) | Function | | Function that renders custom UI footer | @@ -488,10 +487,6 @@ This callback is triggered when mouse pointer is updated. 3.`pointersMap`: [`pointers map`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L131) of the scene -#### `onExportToBackend` - -This callback is triggered when the shareable-link button is clicked in the export dialog. The link button will only be shown if this callback is passed. - ```js (exportedElements, appState, canvas) => void ``` @@ -571,12 +566,20 @@ This prop can be used to customise UI of Excalidraw. Currently we support custom | --- | --- | --- | --- | | `changeViewBackgroundColor` | boolean | true | Implies whether to show `Background color picker` | | `clearCanvas` | boolean | true | Implies whether to show `Clear canvas button` | -| `export` | boolean | true | Implies whether to show `Export button` | +| `export` | false | [exportOpts](#exportOpts) |
{ saveFileToDisk: true }
| This prop allows to customize the UI inside the export dialog. By default it shows the "saveFileToDisk". If this prop is `false` the export button will not be rendered. For more details visit [`exportOpts`](#exportOpts). | | `loadScene` | boolean | true | Implies whether to show `Load button` | -| `saveAsScene` | boolean | true | Implies whether to show `Save as button` | | `saveToActiveFile` | boolean | true | Implies whether to show `Save button` to save to current file | | `theme` | boolean | true | Implies whether to show `Theme toggle` | +#### `exportOpts` + +The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog. If `UIOptions.canvasActions.export` is `false` the export button will not be rendered. + +| Attribute | Type | Default | Description | +| --- | --- | --- | --- | +| `saveFileToDisk` | boolean | true | Implies if save file to disk button should be shown | +| `exportToBackend` |
 (exportedElements: readonly NonDeletedExcalidrawElement[],appState: AppState,canvas: HTMLCanvasElement | null) => void 
| | This callback is triggered when the shareable-link button is clicked in the export dialog. The link button will only be shown if this callback is passed. | + #### `onPaste` This callback is triggered if passed when something is pasted into the scene. You can use this callback in case you want to do something additional when the paste event occurs. diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index 69af56954..b7bc53ad2 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -7,7 +7,7 @@ import App from "../../components/App"; import "../../css/app.scss"; import "../../css/styles.scss"; -import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types"; +import { AppProps, ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types"; import { defaultLang } from "../../i18n"; import { DEFAULT_UI_OPTIONS } from "../../constants"; @@ -19,7 +19,6 @@ const Excalidraw = (props: ExcalidrawProps) => { onCollabButtonClick, isCollaborating, onPointerUpdate, - onExportToBackend, renderTopRightUI, renderFooter, langCode = defaultLang.code, @@ -38,13 +37,19 @@ const Excalidraw = (props: ExcalidrawProps) => { const canvasActions = props.UIOptions?.canvasActions; - const UIOptions = { + const UIOptions: AppProps["UIOptions"] = { canvasActions: { ...DEFAULT_UI_OPTIONS.canvasActions, ...canvasActions, }, }; + if (canvasActions?.export) { + UIOptions.canvasActions.export.saveFileToDisk = + canvasActions.export?.saveFileToDisk || + DEFAULT_UI_OPTIONS.canvasActions.export.saveFileToDisk; + } + useEffect(() => { // Block pinch-zooming on iOS outside of the content area const handleTouchMove = (event: TouchEvent) => { @@ -72,7 +77,6 @@ const Excalidraw = (props: ExcalidrawProps) => { onCollabButtonClick={onCollabButtonClick} isCollaborating={isCollaborating} onPointerUpdate={onPointerUpdate} - onExportToBackend={onExportToBackend} renderTopRightUI={renderTopRightUI} renderFooter={renderFooter} langCode={langCode} @@ -99,12 +103,58 @@ const areEqual = ( prevProps: PublicExcalidrawProps, nextProps: PublicExcalidrawProps, ) => { - const { initialData: prevInitialData, ...prev } = prevProps; - const { initialData: nextInitialData, ...next } = nextProps; + const { + initialData: prevInitialData, + UIOptions: prevUIOptions = {}, + ...prev + } = prevProps; + const { + initialData: nextInitialData, + UIOptions: nextUIOptions = {}, + ...next + } = nextProps; + + // comparing UIOptions + const prevUIOptionsKeys = Object.keys(prevUIOptions) as (keyof Partial< + typeof DEFAULT_UI_OPTIONS + >)[]; + const nextUIOptionsKeys = Object.keys(nextUIOptions) as (keyof Partial< + typeof DEFAULT_UI_OPTIONS + >)[]; + + if (prevUIOptionsKeys.length !== nextUIOptionsKeys.length) { + return false; + } + + const isUIOptionsSame = prevUIOptionsKeys.every((key) => { + if (key === "canvasActions") { + const canvasOptionKeys = Object.keys( + prevUIOptions.canvasActions!, + ) as (keyof Partial)[]; + canvasOptionKeys.every((key) => { + if ( + key === "export" && + prevUIOptions?.canvasActions?.export && + nextUIOptions?.canvasActions?.export + ) { + return ( + prevUIOptions.canvasActions.export.saveFileToDisk === + nextUIOptions.canvasActions.export.saveFileToDisk + ); + } + return ( + prevUIOptions?.canvasActions?.[key] === + nextUIOptions?.canvasActions?.[key] + ); + }); + } + return true; + }); const prevKeys = Object.keys(prevProps) as (keyof typeof prev)[]; const nextKeys = Object.keys(nextProps) as (keyof typeof next)[]; return ( + isUIOptionsSame && prevKeys.length === nextKeys.length && prevKeys.every((key) => prev[key] === next[key]) ); diff --git a/src/tests/excalidrawPackage.test.tsx b/src/tests/excalidrawPackage.test.tsx index f2149b126..628c27a47 100644 --- a/src/tests/excalidrawPackage.test.tsx +++ b/src/tests/excalidrawPackage.test.tsx @@ -178,9 +178,11 @@ describe("", () => { expect(queryByTestId(container, "load-button")).toBeNull(); }); - it("should hide save as button when saveAsScene is false", async () => { + it("should hide save as button when saveFileToDisk is false", async () => { const { container } = await render( - , + , ); expect(queryByTestId(container, "save-as-button")).toBeNull(); diff --git a/src/types.ts b/src/types.ts index 210d06903..10f99dcea 100644 --- a/src/types.ts +++ b/src/types.ts @@ -178,11 +178,6 @@ export interface ExcalidrawProps { button: "down" | "up"; pointersMap: Gesture["pointers"]; }) => void; - onExportToBackend?: ( - exportedElements: readonly NonDeletedExcalidrawElement[], - appState: AppState, - canvas: HTMLCanvasElement | null, - ) => void; onPaste?: ( data: ClipboardData, event: ClipboardEvent | null, @@ -219,12 +214,20 @@ export enum UserIdleState { IDLE = "idle", } +export type ExportOpts = { + saveFileToDisk?: boolean; + onExportToBackend?: ( + exportedElements: readonly NonDeletedExcalidrawElement[], + appState: AppState, + canvas: HTMLCanvasElement | null, + ) => void; +}; + type CanvasActions = { changeViewBackgroundColor?: boolean; clearCanvas?: boolean; - export?: boolean; + export?: false | ExportOpts; loadScene?: boolean; - saveAsScene?: boolean; saveToActiveFile?: boolean; theme?: boolean; }; @@ -235,7 +238,7 @@ export type UIOptions = { export type AppProps = ExcalidrawProps & { UIOptions: { - canvasActions: Required; + canvasActions: Required & { export: ExportOpts }; }; detectScroll: boolean; handleKeyboardGlobally: boolean;