diff --git a/src/actions/actionCanvas.tsx b/src/actions/actionCanvas.tsx index aff1a7f664..96c153872a 100644 --- a/src/actions/actionCanvas.tsx +++ b/src/actions/actionCanvas.tsx @@ -15,7 +15,7 @@ import { getNormalizedZoom, getSelectedElements } from "../scene"; import { centerScrollOn } from "../scene/scroll"; import { getNewZoom } from "../scene/zoom"; import { AppState, NormalizedZoomValue } from "../types"; -import { getShortcutKey } from "../utils"; +import { getNewSceneName, getShortcutKey } from "../utils"; import { register } from "./register"; export const actionChangeViewBackgroundColor = register({ @@ -59,6 +59,7 @@ export const actionClearCanvas = register({ ), appState: { ...getDefaultAppState(), + name: getNewSceneName(), appearance: appState.appearance, elementLocked: appState.elementLocked, exportBackground: appState.exportBackground, diff --git a/src/actions/actionExport.tsx b/src/actions/actionExport.tsx index 2748ad3a4d..4641dfad79 100644 --- a/src/actions/actionExport.tsx +++ b/src/actions/actionExport.tsx @@ -12,6 +12,7 @@ import { KEYS } from "../keys"; import { muteFSAbortError } from "../utils"; import { register } from "./register"; import "../components/ToolIcon.scss"; +import { SCENE_NAME_FALLBACK } from "../constants"; export const actionChangeProjectName = register({ name: "changeProjectName", @@ -22,7 +23,7 @@ export const actionChangeProjectName = register({ PanelComponent: ({ appState, updateData }) => ( updateData(name)} /> ), diff --git a/src/appState.ts b/src/appState.ts index 02036a43e0..1939c2e0c5 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -2,16 +2,20 @@ import oc from "open-color"; import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, + SCENE_NAME_FALLBACK, DEFAULT_TEXT_ALIGN, } from "./constants"; -import { t } from "./i18n"; import { AppState, FlooredNumber, NormalizedZoomValue } from "./types"; -import { getDateTime } from "./utils"; -export const getDefaultAppState = (): Omit< - AppState, - "offsetTop" | "offsetLeft" -> => { +type DefaultAppState = Omit & { + /** + * You should override this with current appState.name, or whatever is + * applicable at a given place where you get default appState. + */ + name: undefined; +}; + +export const getDefaultAppState = (): DefaultAppState => { return { appearance: "light", collaborators: new Map(), @@ -50,7 +54,11 @@ export const getDefaultAppState = (): Omit< isRotating: false, lastPointerDownWith: "mouse", multiElement: null, - name: `${t("labels.untitled")}-${getDateTime()}`, + // for safety (because TS mostly doesn't distinguish optional types and + // undefined values), we set `name` to the fallback name, but we cast it to + // `undefined` so that TS forces us to explicitly specify it wherever + // possible + name: (SCENE_NAME_FALLBACK as unknown) as undefined, openMenu: null, pasteDialog: { shown: false, data: null }, previousSelectedElementIds: {}, diff --git a/src/components/App.tsx b/src/components/App.tsx index 30c069f421..39d7fb1224 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -148,6 +148,7 @@ import { import { debounce, distance, + getNewSceneName, isInputLike, isToolIcon, isWritableElement, @@ -281,6 +282,7 @@ class App extends React.Component { } = props; this.state = { ...defaultAppState, + name: getNewSceneName(), isLoading: true, width, height, @@ -528,6 +530,7 @@ class App extends React.Component { this.scene.replaceAllElements([]); this.setState((state) => ({ ...getDefaultAppState(), + name: getNewSceneName(), isLoading: opts?.resetLoadingState ? false : state.isLoading, appearance: this.state.appearance, })); diff --git a/src/constants.ts b/src/constants.ts index 0a6a793cdb..eab2407ce7 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -88,3 +88,5 @@ export const STORAGE_KEYS = { export const TAP_TWICE_TIMEOUT = 300; export const TOUCH_CTX_MENU_TIMEOUT = 500; export const TITLE_TIMEOUT = 10000; + +export const SCENE_NAME_FALLBACK = "Untitled"; diff --git a/src/data/restore.ts b/src/data/restore.ts index 675b080d9b..c378492c92 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -15,6 +15,7 @@ import { DEFAULT_VERTICAL_ALIGN, } from "../constants"; import { getDefaultAppState } from "../appState"; +import { getNewSceneName } from "../utils"; const getFontFamilyByName = (fontFamilyName: string): FontFamily => { for (const [id, fontFamilyString] of Object.entries(FONT_FAMILY)) { @@ -166,6 +167,7 @@ const restoreAppState = ( return { ...nextAppState, + name: appState.name ?? localAppState?.name ?? getNewSceneName(), offsetLeft: appState.offsetLeft || 0, offsetTop: appState.offsetTop || 0, // Migrates from previous version where appState.zoom was a number diff --git a/src/excalidraw-app/data/localStorage.ts b/src/excalidraw-app/data/localStorage.ts index 2920b47bfc..cc3d2f6735 100644 --- a/src/excalidraw-app/data/localStorage.ts +++ b/src/excalidraw-app/data/localStorage.ts @@ -6,6 +6,7 @@ import { } from "../../appState"; import { clearElementsForLocalStorage } from "../../element"; import { STORAGE_KEYS as APP_STORAGE_KEYS } from "../../constants"; +import { ImportedDataState } from "../../data/types"; export const STORAGE_KEYS = { LOCAL_STORAGE_ELEMENTS: "excalidraw", @@ -81,7 +82,7 @@ export const importFromLocalStorage = () => { } } - let appState = null; + let appState: ImportedDataState["appState"] = null; if (savedState) { try { appState = { diff --git a/src/index-node.ts b/src/index-node.ts index 94858075f1..5f9967f36e 100644 --- a/src/index-node.ts +++ b/src/index-node.ts @@ -1,5 +1,6 @@ import { exportToCanvas } from "./scene/export"; import { getDefaultAppState } from "./appState"; +import { SCENE_NAME_FALLBACK } from "./constants"; const { registerFont, createCanvas } = require("canvas"); @@ -61,6 +62,7 @@ const canvas = exportToCanvas( elements as any, { ...getDefaultAppState(), + name: SCENE_NAME_FALLBACK, offsetTop: 0, offsetLeft: 0, }, diff --git a/src/packages/utils.ts b/src/packages/utils.ts index 442d6db10f..f8b3824a49 100644 --- a/src/packages/utils.ts +++ b/src/packages/utils.ts @@ -6,6 +6,7 @@ import { getDefaultAppState } from "../appState"; import { AppState } from "../types"; import { ExcalidrawElement } from "../element/types"; import { getNonDeletedElements } from "../element"; +import { SCENE_NAME_FALLBACK } from "../constants"; type ExportOpts = { elements: readonly ExcalidrawElement[]; @@ -18,7 +19,7 @@ type ExportOpts = { export const exportToCanvas = ({ elements, - appState = getDefaultAppState(), + appState = { ...getDefaultAppState(), name: SCENE_NAME_FALLBACK }, getDimensions = (width, height) => ({ width, height, scale: 1 }), }: ExportOpts) => { return _exportToCanvas( @@ -74,7 +75,7 @@ export const exportToBlob = ( export const exportToSvg = ({ elements, - appState = getDefaultAppState(), + appState = { ...getDefaultAppState(), name: SCENE_NAME_FALLBACK }, exportPadding, metadata, }: ExportOpts & { diff --git a/src/utils.ts b/src/utils.ts index e1c276d45c..b8216a4147 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,6 +8,7 @@ import { FontFamily, FontString } from "./element/types"; import { Zoom } from "./types"; import { unstable_batchedUpdates } from "react-dom"; import { isDarwin } from "./keys"; +import { t } from "./i18n"; export const SVG_NS = "http://www.w3.org/2000/svg"; @@ -32,6 +33,10 @@ export const getDateTime = () => { return `${year}-${month}-${day}-${hr}${min}`; }; +export const getNewSceneName = () => { + return `${t("labels.untitled")}-${getDateTime()}`; +}; + export const capitalizeString = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);