fix: command palette tweaks and fixes (#7876)

This commit is contained in:
David Luzar 2024-04-11 11:39:19 +02:00 committed by GitHub
parent 4987cc53d0
commit f597bd3e01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 116 additions and 44 deletions

View File

@ -122,6 +122,7 @@ import {
usersIcon, usersIcon,
exportToPlus, exportToPlus,
share, share,
youtubeIcon,
} from "../packages/excalidraw/components/icons"; } from "../packages/excalidraw/components/icons";
import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme"; import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
@ -1053,6 +1054,20 @@ const ExcalidrawWrapper = () => {
); );
}, },
}, },
{
label: "YouTube",
icon: youtubeIcon,
category: DEFAULT_CATEGORIES.links,
predicate: true,
keywords: ["features", "tutorials", "howto", "help", "community"],
perform: () => {
window.open(
"https://youtube.com/@excalidraw",
"_blank",
"noopener noreferrer",
);
},
},
...(isExcalidrawPlusSignedUser ...(isExcalidrawPlusSignedUser
? [ ? [
{ {

View File

@ -1,7 +1,11 @@
import React from "react"; import React from "react";
import { PlusPromoIcon } from "../../packages/excalidraw/components/icons"; import {
arrowBarToLeftIcon,
ExcalLogo,
} from "../../packages/excalidraw/components/icons";
import { Theme } from "../../packages/excalidraw/element/types"; import { Theme } from "../../packages/excalidraw/element/types";
import { MainMenu } from "../../packages/excalidraw/index"; import { MainMenu } from "../../packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants";
import { LanguageList } from "./LanguageList"; import { LanguageList } from "./LanguageList";
export const AppMainMenu: React.FC<{ export const AppMainMenu: React.FC<{
@ -23,20 +27,29 @@ export const AppMainMenu: React.FC<{
onSelect={() => props.onCollabDialogOpen()} onSelect={() => props.onCollabDialogOpen()}
/> />
)} )}
<MainMenu.DefaultItems.CommandPalette /> <MainMenu.DefaultItems.CommandPalette className="highlighted" />
<MainMenu.DefaultItems.Help /> <MainMenu.DefaultItems.Help />
<MainMenu.DefaultItems.ClearCanvas /> <MainMenu.DefaultItems.ClearCanvas />
<MainMenu.Separator /> <MainMenu.Separator />
<MainMenu.ItemLink <MainMenu.ItemLink
icon={PlusPromoIcon} icon={ExcalLogo}
href={`${ href={`${
import.meta.env.VITE_APP_PLUS_LP import.meta.env.VITE_APP_PLUS_APP
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`} }/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`}
className="ExcalidrawPlus" className=""
> >
Excalidraw+ Excalidraw+
</MainMenu.ItemLink> </MainMenu.ItemLink>
<MainMenu.DefaultItems.Socials /> <MainMenu.DefaultItems.Socials />
<MainMenu.ItemLink
icon={arrowBarToLeftIcon}
href={`${import.meta.env.VITE_APP_PLUS_APP}${
isExcalidrawPlusSignedUser ? "" : "/sign-up"
}?utm_source=signin&utm_medium=app&utm_content=hamburger`}
className="highlighted"
>
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
</MainMenu.ItemLink>
<MainMenu.Separator /> <MainMenu.Separator />
<MainMenu.DefaultItems.ToggleTheme <MainMenu.DefaultItems.ToggleTheme
allowSystemTheme allowSystemTheme

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { PlusPromoIcon } from "../../packages/excalidraw/components/icons"; import { arrowBarToLeftIcon } from "../../packages/excalidraw/components/icons";
import { useI18n } from "../../packages/excalidraw/i18n"; import { useI18n } from "../../packages/excalidraw/i18n";
import { WelcomeScreen } from "../../packages/excalidraw/index"; import { WelcomeScreen } from "../../packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants"; import { isExcalidrawPlusSignedUser } from "../app_constants";
@ -61,9 +61,9 @@ export const AppWelcomeScreen: React.FC<{
import.meta.env.VITE_APP_PLUS_LP import.meta.env.VITE_APP_PLUS_LP
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`} }/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`}
shortcut={null} shortcut={null}
icon={PlusPromoIcon} icon={arrowBarToLeftIcon}
> >
Try Excalidraw Plus! Sign up
</WelcomeScreen.Center.MenuItemLink> </WelcomeScreen.Center.MenuItemLink>
)} )}
</WelcomeScreen.Center.Menu> </WelcomeScreen.Center.Menu>

View File

@ -38,7 +38,7 @@
background-color: #ecfdf5; background-color: #ecfdf5;
color: #064e3c; color: #064e3c;
} }
&.ExcalidrawPlus { &.highlighted {
color: var(--color-promo); color: var(--color-promo);
} }
} }

View File

@ -216,32 +216,23 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
stroke-width="2" stroke-width="2"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<g <g>
stroke-width="1.5"
>
<path <path
d="M0 0h24v24H0z" d="M0 0h24v24H0z"
fill="none" fill="none"
stroke="none" stroke="none"
/> />
<rect <path
height="4" d="M10 12l10 0"
rx="1"
width="18"
x="3"
y="8"
/>
<line
x1="12"
x2="12"
y1="8"
y2="21"
/> />
<path <path
d="M19 12v7a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-7" d="M10 12l4 4"
/> />
<path <path
d="M7.5 8a2.5 2.5 0 0 1 0 -5a4.8 8 0 0 1 4.5 5a4.8 8 0 0 1 4.5 -5a2.5 2.5 0 0 1 0 5" d="M10 12l4 -4"
/>
<path
d="M4 4l0 16"
/> />
</g> </g>
</svg> </svg>
@ -249,7 +240,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
<div <div
class="welcome-screen-menu-item__text" class="welcome-screen-menu-item__text"
> >
Try Excalidraw Plus! Sign up
</div> </div>
</a> </a>
</div> </div>

View File

@ -1,6 +1,6 @@
// place here categories that you want to track. We want to track just a // place here categories that you want to track. We want to track just a
// small subset of categories at a given time. // small subset of categories at a given time.
const ALLOWED_CATEGORIES_TO_TRACK = ["ai"] as string[]; const ALLOWED_CATEGORIES_TO_TRACK = ["ai", "command_palette"] as string[];
export const trackEvent = ( export const trackEvent = (
category: string, category: string,

View File

@ -49,6 +49,8 @@ import { jotaiStore } from "../../jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog"; import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
import { CommandPaletteItem } from "./types"; import { CommandPaletteItem } from "./types";
import * as defaultItems from "./defaultCommandPaletteItems"; import * as defaultItems from "./defaultCommandPaletteItems";
import { trackEvent } from "../../analytics";
import { useStable } from "../../hooks/useStable";
import "./CommandPalette.scss"; import "./CommandPalette.scss";
@ -130,12 +132,20 @@ export const CommandPalette = Object.assign(
if (isCommandPaletteToggleShortcut(event)) { if (isCommandPaletteToggleShortcut(event)) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
setAppState((appState) => ({ setAppState((appState) => {
openDialog: const nextState =
appState.openDialog?.name === "commandPalette" appState.openDialog?.name === "commandPalette"
? null ? null
: { name: "commandPalette" }, : ({ name: "commandPalette" } as const);
}));
if (nextState) {
trackEvent("command_palette", "open", "shortcut");
}
return {
openDialog: nextState,
};
});
} }
}; };
window.addEventListener(EVENT.KEYDOWN, commandPaletteShortcut, { window.addEventListener(EVENT.KEYDOWN, commandPaletteShortcut, {
@ -174,10 +184,20 @@ function CommandPaletteInner({
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const stableDeps = useStable({
uiAppState,
customCommandPaletteItems,
appProps,
});
useEffect(() => { useEffect(() => {
if (!uiAppState || !app.scene || !actionManager) { // these props change often and we don't want them to re-run the effect
return; // which would renew `allCommands`, cascading down and resetting state.
} //
// This means that the commands won't update on appState/appProps changes
// while the command palette is open
const { uiAppState, customCommandPaletteItems, appProps } = stableDeps;
const getActionLabel = (action: Action) => { const getActionLabel = (action: Action) => {
let label = ""; let label = "";
if (action.label) { if (action.label) {
@ -533,15 +553,13 @@ function CommandPaletteInner({
); );
} }
}, [ }, [
stableDeps,
app, app,
appProps,
uiAppState,
actionManager, actionManager,
setAllCommands, setAllCommands,
lastUsed?.label, lastUsed?.label,
setLastUsed, setLastUsed,
setAppState, setAppState,
customCommandPaletteItems,
]); ]);
const [commandSearch, setCommandSearch] = useState(""); const [commandSearch, setCommandSearch] = useState("");

View File

@ -4,7 +4,7 @@ import { KEYS } from "../keys";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import "./HelpDialog.scss"; import "./HelpDialog.scss";
import { ExternalLinkIcon } from "./icons"; import { ExternalLinkIcon, GithubIcon, youtubeIcon } from "./icons";
import { probablySupportsClipboardBlob } from "../clipboard"; import { probablySupportsClipboardBlob } from "../clipboard";
import { isDarwin, isFirefox, isWindows } from "../constants"; import { isDarwin, isFirefox, isWindows } from "../constants";
import { getShortcutFromShortcutName } from "../actions/shortcuts"; import { getShortcutFromShortcutName } from "../actions/shortcuts";
@ -17,8 +17,8 @@ const Header = () => (
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{t("helpDialog.documentation")}
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div> <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
{t("helpDialog.documentation")}
</a> </a>
<a <a
className="HelpDialog__btn" className="HelpDialog__btn"
@ -26,8 +26,8 @@ const Header = () => (
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{t("helpDialog.blog")}
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div> <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
{t("helpDialog.blog")}
</a> </a>
<a <a
className="HelpDialog__btn" className="HelpDialog__btn"
@ -35,8 +35,17 @@ const Header = () => (
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<div className="HelpDialog__link-icon">{GithubIcon}</div>
{t("helpDialog.github")} {t("helpDialog.github")}
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div> </a>
<a
className="HelpDialog__btn"
href="https://youtube.com/@excalidraw"
target="_blank"
rel="noopener noreferrer"
>
<div className="HelpDialog__link-icon">{youtubeIcon}</div>
YouTube
</a> </a>
</div> </div>
); );

View File

@ -2095,3 +2095,24 @@ export const DeviceDesktopIcon = createIcon(
</g>, </g>,
{ ...tablerIconProps, strokeWidth: 1.5 }, { ...tablerIconProps, strokeWidth: 1.5 },
); );
// arrow-bar-to-left
export const arrowBarToLeftIcon = createIcon(
<g>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12l10 0" />
<path d="M10 12l4 4" />
<path d="M10 12l4 -4" />
<path d="M4 4l0 16" />
</g>,
tablerIconProps,
);
export const youtubeIcon = createIcon(
<g>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M2 8a4 4 0 0 1 4 -4h12a4 4 0 0 1 4 4v8a4 4 0 0 1 -4 4h-12a4 4 0 0 1 -4 -4v-8z" />
<path d="M10 9l5 3l-5 3z" />
</g>,
tablerIconProps,
);

View File

@ -39,6 +39,7 @@ import Trans from "../Trans";
import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemContentRadio"; import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemContentRadio";
import { THEME } from "../../constants"; import { THEME } from "../../constants";
import type { Theme } from "../../element/types"; import type { Theme } from "../../element/types";
import { trackEvent } from "../../analytics";
import "./DefaultItems.scss"; import "./DefaultItems.scss";
@ -122,7 +123,7 @@ export const SaveAsImage = () => {
}; };
SaveAsImage.displayName = "SaveAsImage"; SaveAsImage.displayName = "SaveAsImage";
export const CommandPalette = () => { export const CommandPalette = (opts?: { className?: string }) => {
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
const { t } = useI18n(); const { t } = useI18n();
@ -130,9 +131,13 @@ export const CommandPalette = () => {
<DropdownMenuItem <DropdownMenuItem
icon={boltIcon} icon={boltIcon}
data-testid="command-palette-button" data-testid="command-palette-button"
onSelect={() => setAppState({ openDialog: { name: "commandPalette" } })} onSelect={() => {
trackEvent("command_palette", "open", "menu");
setAppState({ openDialog: { name: "commandPalette" } });
}}
shortcut={getShortcutFromShortcutName("commandPalette")} shortcut={getShortcutFromShortcutName("commandPalette")}
aria-label={t("commandPalette.title")} aria-label={t("commandPalette.title")}
className={opts?.className}
> >
{t("commandPalette.title")} {t("commandPalette.title")}
</DropdownMenuItem> </DropdownMenuItem>