feat: d2c tweaks (#7336)
This commit is contained in:
parent
c7ee46e7f8
commit
3d1631f375
@ -56,13 +56,18 @@ export const actionShortcuts = register({
|
|||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "menu", action: "toggleHelpDialog" },
|
trackEvent: { category: "menu", action: "toggleHelpDialog" },
|
||||||
perform: (_elements, appState, _, { focusContainer }) => {
|
perform: (_elements, appState, _, { focusContainer }) => {
|
||||||
if (appState.openDialog === "help") {
|
if (appState.openDialog?.name === "help") {
|
||||||
focusContainer();
|
focusContainer();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
openDialog: appState.openDialog === "help" ? null : "help",
|
openDialog:
|
||||||
|
appState.openDialog?.name === "help"
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
name: "help",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
// place here categories that you want to track. We want to track just a
|
||||||
|
// small subset of categories at a given time.
|
||||||
|
const ALLOWED_CATEGORIES_TO_TRACK = ["ai"] as string[];
|
||||||
|
|
||||||
export const trackEvent = (
|
export const trackEvent = (
|
||||||
category: string,
|
category: string,
|
||||||
action: string,
|
action: string,
|
||||||
@ -5,13 +9,13 @@ export const trackEvent = (
|
|||||||
value?: number,
|
value?: number,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// place here categories that you want to track as events
|
// prettier-ignore
|
||||||
// KEEP IN MIND THE PRICING
|
if (
|
||||||
const ALLOWED_CATEGORIES_TO_TRACK = [] as string[];
|
typeof window === "undefined"
|
||||||
// Uncomment the next line to track locally
|
|| import.meta.env.VITE_WORKER_ID
|
||||||
// console.log("Track Event", { category, action, label, value });
|
// comment out to debug locally
|
||||||
|
|| import.meta.env.PROD
|
||||||
if (typeof window === "undefined" || import.meta.env.VITE_WORKER_ID) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,6 +23,10 @@ export const trackEvent = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!import.meta.env.PROD) {
|
||||||
|
console.info("trackEvent", { category, action, label, value });
|
||||||
|
}
|
||||||
|
|
||||||
if (window.sa_event) {
|
if (window.sa_event) {
|
||||||
window.sa_event(action, {
|
window.sa_event(action, {
|
||||||
category,
|
category,
|
||||||
|
@ -339,7 +339,7 @@ export const ShapesSwitcher = ({
|
|||||||
Generate
|
Generate
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => app.setOpenDialog("mermaid")}
|
onSelect={() => app.setOpenDialog({ name: "mermaid" })}
|
||||||
icon={mermaidLogoIcon}
|
icon={mermaidLogoIcon}
|
||||||
data-testid="toolbar-embeddable"
|
data-testid="toolbar-embeddable"
|
||||||
>
|
>
|
||||||
@ -349,14 +349,20 @@ export const ShapesSwitcher = ({
|
|||||||
{app.props.aiEnabled !== false && (
|
{app.props.aiEnabled !== false && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => app.onMagicButtonSelect()}
|
onSelect={() => app.onMagicframeToolSelect()}
|
||||||
icon={MagicIcon}
|
icon={MagicIcon}
|
||||||
data-testid="toolbar-magicframe"
|
data-testid="toolbar-magicframe"
|
||||||
>
|
>
|
||||||
{t("toolBar.magicframe")}
|
{t("toolBar.magicframe")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => app.setOpenDialog("magicSettings")}
|
onSelect={() => {
|
||||||
|
trackEvent("ai", "d2c-settings", "settings");
|
||||||
|
app.setOpenDialog({
|
||||||
|
name: "magicSettings",
|
||||||
|
source: "settings",
|
||||||
|
});
|
||||||
|
}}
|
||||||
icon={OpenAIIcon}
|
icon={OpenAIIcon}
|
||||||
data-testid="toolbar-magicSettings"
|
data-testid="toolbar-magicSettings"
|
||||||
>
|
>
|
||||||
|
@ -1435,7 +1435,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
onMagicSettingsConfirm={this.onMagicSettingsConfirm}
|
onMagicSettingsConfirm={this.onMagicSettingsConfirm}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
{this.state.openDialog === "mermaid" && (
|
{this.state.openDialog?.name === "mermaid" && (
|
||||||
<MermaidToExcalidraw />
|
<MermaidToExcalidraw />
|
||||||
)}
|
)}
|
||||||
</LayerUI>
|
</LayerUI>
|
||||||
@ -1467,6 +1467,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
onChange={() =>
|
onChange={() =>
|
||||||
this.onMagicFrameGenerate(
|
this.onMagicFrameGenerate(
|
||||||
firstSelectedElement,
|
firstSelectedElement,
|
||||||
|
"button",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -1697,11 +1698,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onMagicFrameGenerate(magicFrame: ExcalidrawMagicFrameElement) {
|
private async onMagicFrameGenerate(
|
||||||
|
magicFrame: ExcalidrawMagicFrameElement,
|
||||||
|
source: "button" | "upstream",
|
||||||
|
) {
|
||||||
if (!this.OPENAI_KEY) {
|
if (!this.OPENAI_KEY) {
|
||||||
this.setState({
|
this.setState({
|
||||||
openDialog: "magicSettings",
|
openDialog: { name: "magicSettings", source: "generation" },
|
||||||
});
|
});
|
||||||
|
trackEvent("ai", "d2c-generate", "missing-key");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1712,7 +1717,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}).filter((el) => !isMagicFrameElement(el));
|
}).filter((el) => !isMagicFrameElement(el));
|
||||||
|
|
||||||
if (!magicFrameChildren.length) {
|
if (!magicFrameChildren.length) {
|
||||||
this.setState({ errorMessage: "Cannot generate from an empty frame" });
|
if (source === "button") {
|
||||||
|
this.setState({ errorMessage: "Cannot generate from an empty frame" });
|
||||||
|
trackEvent("ai", "d2c-generate", "no-children");
|
||||||
|
} else {
|
||||||
|
this.setActiveTool({ type: "magicframe" });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1751,6 +1761,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const textFromFrameChildren = this.getTextFromElements(magicFrameChildren);
|
const textFromFrameChildren = this.getTextFromElements(magicFrameChildren);
|
||||||
|
|
||||||
|
trackEvent("ai", "d2c-generate", "generating");
|
||||||
|
|
||||||
const result = await diagramToHTML({
|
const result = await diagramToHTML({
|
||||||
image: dataURL,
|
image: dataURL,
|
||||||
apiKey: this.OPENAI_KEY,
|
apiKey: this.OPENAI_KEY,
|
||||||
@ -1759,6 +1771,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
|
trackEvent("ai", "d2c-generate", "generating-failed");
|
||||||
console.error(result.error);
|
console.error(result.error);
|
||||||
this.updateMagicGeneration({
|
this.updateMagicGeneration({
|
||||||
frameElement,
|
frameElement,
|
||||||
@ -1770,6 +1783,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
trackEvent("ai", "d2c-generate", "generating-done");
|
||||||
|
|
||||||
if (result.choices[0].message.content == null) {
|
if (result.choices[0].message.content == null) {
|
||||||
this.updateMagicGeneration({
|
this.updateMagicGeneration({
|
||||||
@ -1813,7 +1827,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
private OPENAI_KEY_IS_PERSISTED: boolean =
|
private OPENAI_KEY_IS_PERSISTED: boolean =
|
||||||
EditorLocalStorage.has(EDITOR_LS_KEYS.OAI_API_KEY) || false;
|
EditorLocalStorage.has(EDITOR_LS_KEYS.OAI_API_KEY) || false;
|
||||||
|
|
||||||
private onOpenAIKeyChange = (openAIKey: string, shouldPersist: boolean) => {
|
private onOpenAIKeyChange = (
|
||||||
|
openAIKey: string | null,
|
||||||
|
shouldPersist: boolean,
|
||||||
|
) => {
|
||||||
this.OPENAI_KEY = openAIKey || null;
|
this.OPENAI_KEY = openAIKey || null;
|
||||||
if (shouldPersist) {
|
if (shouldPersist) {
|
||||||
const didPersist = EditorLocalStorage.set(
|
const didPersist = EditorLocalStorage.set(
|
||||||
@ -1826,26 +1843,41 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMagicSettingsConfirm = (apiKey: string, shouldPersist: boolean) => {
|
private onMagicSettingsConfirm = (
|
||||||
this.onOpenAIKeyChange(apiKey, shouldPersist);
|
apiKey: string,
|
||||||
|
shouldPersist: boolean,
|
||||||
|
source: "tool" | "generation" | "settings",
|
||||||
|
) => {
|
||||||
|
this.OPENAI_KEY = apiKey || null;
|
||||||
|
this.onOpenAIKeyChange(this.OPENAI_KEY, shouldPersist);
|
||||||
|
|
||||||
|
if (source === "settings") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedElements = this.scene.getSelectedElements({
|
||||||
|
selectedElementIds: this.state.selectedElementIds,
|
||||||
|
});
|
||||||
|
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
const selectedElements = this.scene.getSelectedElements({
|
|
||||||
selectedElementIds: this.state.selectedElementIds,
|
|
||||||
});
|
|
||||||
if (selectedElements.length) {
|
if (selectedElements.length) {
|
||||||
this.onMagicButtonSelect();
|
this.onMagicframeToolSelect();
|
||||||
|
} else {
|
||||||
|
this.setActiveTool({ type: "magicframe" });
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!isMagicFrameElement(selectedElements[0])) {
|
||||||
this.OPENAI_KEY = null;
|
// even if user didn't end up setting api key, let's pick the tool
|
||||||
|
// so they can draw up a frame and move forward
|
||||||
|
this.setActiveTool({ type: "magicframe" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public onMagicButtonSelect = () => {
|
public onMagicframeToolSelect = () => {
|
||||||
if (!this.OPENAI_KEY) {
|
if (!this.OPENAI_KEY) {
|
||||||
this.setState({
|
this.setState({
|
||||||
openDialog: "magicSettings",
|
openDialog: { name: "magicSettings", source: "tool" },
|
||||||
});
|
});
|
||||||
|
trackEvent("ai", "d2c-tool", "missing-key");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1855,19 +1887,33 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (selectedElements.length === 0) {
|
if (selectedElements.length === 0) {
|
||||||
this.setActiveTool({ type: TOOL_TYPE.magicframe });
|
this.setActiveTool({ type: TOOL_TYPE.magicframe });
|
||||||
|
trackEvent("ai", "d2c-tool", "empty-selection");
|
||||||
} else {
|
} else {
|
||||||
if (selectedElements.some((el) => isFrameLikeElement(el))) {
|
const selectedMagicFrame: ExcalidrawMagicFrameElement | false =
|
||||||
|
selectedElements.length === 1 &&
|
||||||
|
isMagicFrameElement(selectedElements[0]) &&
|
||||||
|
selectedElements[0];
|
||||||
|
|
||||||
|
// case: user selected elements containing frame-like(s) or are frame
|
||||||
|
// members, we don't want to wrap into another magicframe
|
||||||
|
// (unless the only selected element is a magic frame which we reuse)
|
||||||
|
if (
|
||||||
|
!selectedMagicFrame &&
|
||||||
|
selectedElements.some((el) => isFrameLikeElement(el) || el.frameId)
|
||||||
|
) {
|
||||||
this.setActiveTool({ type: TOOL_TYPE.magicframe });
|
this.setActiveTool({ type: TOOL_TYPE.magicframe });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let frame: ExcalidrawMagicFrameElement | null = null;
|
trackEvent("ai", "d2c-tool", "existing-selection");
|
||||||
if (
|
|
||||||
selectedElements.length === 1 &&
|
let frame: ExcalidrawMagicFrameElement;
|
||||||
isMagicFrameElement(selectedElements[0])
|
if (selectedMagicFrame) {
|
||||||
) {
|
// a single magicframe already selected -> use it
|
||||||
frame = selectedElements[0];
|
frame = selectedMagicFrame;
|
||||||
} else {
|
} else {
|
||||||
|
// selected elements aren't wrapped in magic frame yet -> wrap now
|
||||||
|
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
|
||||||
const padding = 50;
|
const padding = 50;
|
||||||
|
|
||||||
@ -1880,19 +1926,19 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
opacity: 100,
|
opacity: 100,
|
||||||
locked: false,
|
locked: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.scene.addNewElement(frame);
|
||||||
|
|
||||||
|
for (const child of selectedElements) {
|
||||||
|
mutateElement(child, { frameId: frame.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedElementIds: { [frame.id]: true },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scene.addNewElement(frame);
|
this.onMagicFrameGenerate(frame, "upstream");
|
||||||
|
|
||||||
for (const child of selectedElements) {
|
|
||||||
mutateElement(child, { frameId: frame.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
selectedElementIds: { [frame.id]: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
this.onMagicFrameGenerate(frame);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -3551,7 +3597,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (event.key === KEYS.QUESTION_MARK) {
|
if (event.key === KEYS.QUESTION_MARK) {
|
||||||
this.setState({
|
this.setState({
|
||||||
openDialog: "help",
|
openDialog: { name: "help" },
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
@ -3560,7 +3606,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event[KEYS.CTRL_OR_CMD]
|
event[KEYS.CTRL_OR_CMD]
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.setState({ openDialog: "imageExport" });
|
this.setState({ openDialog: { name: "imageExport" } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ export const JSONExportDialog = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{appState.openDialog === "jsonExport" && (
|
{appState.openDialog?.name === "jsonExport" && (
|
||||||
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
||||||
<JSONExportModal
|
<JSONExportModal
|
||||||
elements={elements}
|
elements={elements}
|
||||||
|
@ -86,7 +86,11 @@ interface LayerUIProps {
|
|||||||
openAIKey: string | null;
|
openAIKey: string | null;
|
||||||
isOpenAIKeyPersisted: boolean;
|
isOpenAIKeyPersisted: boolean;
|
||||||
onOpenAIAPIKeyChange: (apiKey: string, shouldPersist: boolean) => void;
|
onOpenAIAPIKeyChange: (apiKey: string, shouldPersist: boolean) => void;
|
||||||
onMagicSettingsConfirm: (apiKey: string, shouldPersist: boolean) => void;
|
onMagicSettingsConfirm: (
|
||||||
|
apiKey: string,
|
||||||
|
shouldPersist: boolean,
|
||||||
|
source: "tool" | "generation" | "settings",
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultMainMenu: React.FC<{
|
const DefaultMainMenu: React.FC<{
|
||||||
@ -177,7 +181,7 @@ const LayerUI = ({
|
|||||||
const renderImageExportDialog = () => {
|
const renderImageExportDialog = () => {
|
||||||
if (
|
if (
|
||||||
!UIOptions.canvasActions.saveAsImage ||
|
!UIOptions.canvasActions.saveAsImage ||
|
||||||
appState.openDialog !== "imageExport"
|
appState.openDialog?.name !== "imageExport"
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -448,21 +452,26 @@ const LayerUI = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{appState.openDialog === "help" && (
|
{appState.openDialog?.name === "help" && (
|
||||||
<HelpDialog
|
<HelpDialog
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setAppState({ openDialog: null });
|
setAppState({ openDialog: null });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{appState.openDialog === "magicSettings" && (
|
{appState.openDialog?.name === "magicSettings" && (
|
||||||
<MagicSettings
|
<MagicSettings
|
||||||
openAIKey={openAIKey}
|
openAIKey={openAIKey}
|
||||||
isPersisted={isOpenAIKeyPersisted}
|
isPersisted={isOpenAIKeyPersisted}
|
||||||
onChange={onOpenAIAPIKeyChange}
|
onChange={onOpenAIAPIKeyChange}
|
||||||
onConfirm={(apiKey, shouldPersist) => {
|
onConfirm={(apiKey, shouldPersist) => {
|
||||||
setAppState({ openDialog: null });
|
const source =
|
||||||
onMagicSettingsConfirm(apiKey, shouldPersist);
|
appState.openDialog?.name === "magicSettings"
|
||||||
|
? appState.openDialog?.source
|
||||||
|
: "settings";
|
||||||
|
setAppState({ openDialog: null }, () => {
|
||||||
|
onMagicSettingsConfirm(apiKey, shouldPersist, source);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setAppState({ openDialog: null });
|
setAppState({ openDialog: null });
|
||||||
|
@ -106,7 +106,7 @@ export const MagicSettings = (props: {
|
|||||||
own limit in your OpenAI account dashboard if needed.
|
own limit in your OpenAI account dashboard if needed.
|
||||||
</p>
|
</p>
|
||||||
<TextField
|
<TextField
|
||||||
isPassword
|
isRedacted
|
||||||
value={keyInputValue}
|
value={keyInputValue}
|
||||||
placeholder="Paste your API key here"
|
placeholder="Paste your API key here"
|
||||||
label="OpenAI API key"
|
label="OpenAI API key"
|
||||||
|
@ -47,7 +47,7 @@ export const ExportToImage = () => {
|
|||||||
actionLabel={t("overwriteConfirm.action.exportToImage.button")}
|
actionLabel={t("overwriteConfirm.action.exportToImage.button")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
actionManager.executeAction(actionChangeExportEmbedScene, "ui", true);
|
actionManager.executeAction(actionChangeExportEmbedScene, "ui", true);
|
||||||
setAppState({ openDialog: "imageExport" });
|
setAppState({ openDialog: { name: "imageExport" } });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("overwriteConfirm.action.exportToImage.description")}
|
{t("overwriteConfirm.action.exportToImage.description")}
|
||||||
|
@ -25,7 +25,7 @@ type TextFieldProps = {
|
|||||||
|
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isPassword?: boolean;
|
isRedacted?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||||
@ -39,7 +39,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
readonly,
|
readonly,
|
||||||
selectOnRender,
|
selectOnRender,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
isPassword = false,
|
isRedacted = false,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
@ -53,7 +53,8 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
}
|
}
|
||||||
}, [selectOnRender]);
|
}, [selectOnRender]);
|
||||||
|
|
||||||
const [isVisible, setIsVisible] = useState<boolean>(true);
|
const [isTemporarilyUnredacted, setIsTemporarilyUnredacted] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -71,7 +72,9 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type={isPassword && isVisible ? "password" : undefined}
|
className={clsx({
|
||||||
|
"is-redacted": value && isRedacted && !isTemporarilyUnredacted,
|
||||||
|
})}
|
||||||
readOnly={readonly}
|
readOnly={readonly}
|
||||||
value={value}
|
value={value}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@ -79,12 +82,14 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
onChange={(event) => onChange?.(event.target.value)}
|
onChange={(event) => onChange?.(event.target.value)}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
{isPassword && (
|
{isRedacted && (
|
||||||
<Button
|
<Button
|
||||||
onSelect={() => setIsVisible(!isVisible)}
|
onSelect={() =>
|
||||||
style={{ border: 0 }}
|
setIsTemporarilyUnredacted(!isTemporarilyUnredacted)
|
||||||
|
}
|
||||||
|
style={{ border: 0, userSelect: "none" }}
|
||||||
>
|
>
|
||||||
{isVisible ? eyeIcon : eyeClosedIcon}
|
{isTemporarilyUnredacted ? eyeClosedIcon : eyeIcon}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,7 +107,7 @@ export const SaveAsImage = () => {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
icon={ExportImageIcon}
|
icon={ExportImageIcon}
|
||||||
data-testid="image-export-button"
|
data-testid="image-export-button"
|
||||||
onSelect={() => setAppState({ openDialog: "imageExport" })}
|
onSelect={() => setAppState({ openDialog: { name: "imageExport" } })}
|
||||||
shortcut={getShortcutFromShortcutName("imageExport")}
|
shortcut={getShortcutFromShortcutName("imageExport")}
|
||||||
aria-label={t("buttons.exportImage")}
|
aria-label={t("buttons.exportImage")}
|
||||||
>
|
>
|
||||||
@ -230,7 +230,7 @@ export const Export = () => {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
icon={ExportIcon}
|
icon={ExportIcon}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setAppState({ openDialog: "jsonExport" });
|
setAppState({ openDialog: { name: "jsonExport" } });
|
||||||
}}
|
}}
|
||||||
data-testid="json-export-button"
|
data-testid="json-export-button"
|
||||||
aria-label={t("buttons.export")}
|
aria-label={t("buttons.export")}
|
||||||
|
@ -533,6 +533,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.is-redacted {
|
||||||
|
// we don't use type=password because browsers (chrome?) prompt
|
||||||
|
// you to save it which is annoying
|
||||||
|
-webkit-text-security: disc;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
textarea:not(.excalidraw-wysiwyg) {
|
textarea:not(.excalidraw-wysiwyg) {
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
|
@ -11,6 +11,12 @@ The change should be grouped under one of the below section and must contain PR
|
|||||||
Please add the latest change on the top under the correct section.
|
Please add the latest change on the top under the correct section.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- `appState.openDialog` type was changed from `null | string` to `null | { name: string }`. [#7336](https://github.com/excalidraw/excalidraw/pull/7336)
|
||||||
|
|
||||||
## 0.17.0 (2023-11-14)
|
## 0.17.0 (2023-11-14)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
@ -102,7 +102,7 @@ describe("Test <MermaidToExcalidraw/>", () => {
|
|||||||
<Excalidraw
|
<Excalidraw
|
||||||
initialData={{
|
initialData={{
|
||||||
appState: {
|
appState: {
|
||||||
openDialog: "mermaid",
|
openDialog: { name: "mermaid" },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
|
17
src/types.ts
17
src/types.ts
@ -245,12 +245,15 @@ export interface AppState {
|
|||||||
openPopup: "canvasBackground" | "elementBackground" | "elementStroke" | null;
|
openPopup: "canvasBackground" | "elementBackground" | "elementStroke" | null;
|
||||||
openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
|
openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
|
||||||
openDialog:
|
openDialog:
|
||||||
| "imageExport"
|
| null
|
||||||
| "help"
|
| { name: "imageExport" | "help" | "jsonExport" | "mermaid" }
|
||||||
| "jsonExport"
|
| {
|
||||||
| "mermaid"
|
name: "magicSettings";
|
||||||
| "magicSettings"
|
source:
|
||||||
| null;
|
| "tool" // when magicframe tool is selected
|
||||||
|
| "generation" // when magicframe generate button is clicked
|
||||||
|
| "settings"; // when AI settings dialog is explicitly invoked
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Reflects user preference for whether the default sidebar should be docked.
|
* Reflects user preference for whether the default sidebar should be docked.
|
||||||
*
|
*
|
||||||
@ -549,7 +552,7 @@ export type AppClassProperties = {
|
|||||||
setActiveTool: App["setActiveTool"];
|
setActiveTool: App["setActiveTool"];
|
||||||
setOpenDialog: App["setOpenDialog"];
|
setOpenDialog: App["setOpenDialog"];
|
||||||
insertEmbeddableElement: App["insertEmbeddableElement"];
|
insertEmbeddableElement: App["insertEmbeddableElement"];
|
||||||
onMagicButtonSelect: App["onMagicButtonSelect"];
|
onMagicframeToolSelect: App["onMagicframeToolSelect"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user