mirror of https://github.com/usememos/memos
feat: implement shortcut components
parent
3a085f3639
commit
2db86f6644
@ -0,0 +1,135 @@
|
||||
import { Input, Textarea } from "@mui/joy";
|
||||
import { Button } from "@usememos/mui";
|
||||
import { XIcon } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { userServiceClient } from "@/grpcweb";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import useLoading from "@/hooks/useLoading";
|
||||
import { useUserStore } from "@/store/v1";
|
||||
import { Shortcut } from "@/types/proto/api/v1/user_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { generateUUID } from "@/utils/uuid";
|
||||
import { generateDialog } from "./Dialog";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
shortcut?: Shortcut;
|
||||
}
|
||||
|
||||
const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
const { destroy } = props;
|
||||
const t = useTranslate();
|
||||
const user = useCurrentUser();
|
||||
const userStore = useUserStore();
|
||||
const [shortcut, setShortcut] = useState(Shortcut.fromPartial({ ...props.shortcut }));
|
||||
const requestState = useLoading(false);
|
||||
const isCreating = !props.shortcut;
|
||||
|
||||
const onShortcutTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setShortcut({ ...shortcut, title: e.target.value });
|
||||
};
|
||||
|
||||
const onShortcutFilterChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setShortcut({ ...shortcut, filter: e.target.value });
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!shortcut.title || !shortcut.filter) {
|
||||
toast.error("Title and filter cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isCreating) {
|
||||
await userServiceClient.createShortcut({
|
||||
parent: user.name,
|
||||
shortcut: {
|
||||
...shortcut,
|
||||
id: generateUUID(),
|
||||
},
|
||||
});
|
||||
toast.success("Create shortcut successfully");
|
||||
} else {
|
||||
await userServiceClient.updateShortcut({ parent: user.name, shortcut, updateMask: ["title", "filter"] });
|
||||
toast.success("Update shortcut successfully");
|
||||
}
|
||||
// Refresh shortcuts.
|
||||
await userStore.fetchShortcuts();
|
||||
destroy();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.details);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">{`${isCreating ? "Create" : "Edit"} Shortcut`}</p>
|
||||
<Button size="sm" variant="plain" onClick={() => destroy()}>
|
||||
<XIcon className="w-5 h-auto" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="dialog-content-container max-w-md min-w-72">
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="text-sm whitespace-nowrap mb-1">Title</span>
|
||||
<Input className="w-full" type="text" placeholder="" value={shortcut.title} onChange={onShortcutTitleChange} />
|
||||
<span className="text-sm whitespace-nowrap mt-3 mb-1">Filter</span>
|
||||
<Textarea
|
||||
className="w-full"
|
||||
minRows={3}
|
||||
maxRows={5}
|
||||
size="sm"
|
||||
placeholder={"Shortcut filter"}
|
||||
value={shortcut.filter}
|
||||
onChange={onShortcutFilterChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full opacity-70">
|
||||
<p className="text-sm">{t("common.learn-more")}:</p>
|
||||
<ul className="list-disc list-inside text-sm pl-2 mt-1">
|
||||
<li>
|
||||
<a
|
||||
className="text-sm text-blue-600 hover:underline"
|
||||
href="https://www.usememos.com/docs/getting-started/shortcuts"
|
||||
target="_blank"
|
||||
>
|
||||
Docs - Shortcuts
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="text-sm text-blue-600 hover:underline"
|
||||
href="https://www.usememos.com/docs/getting-started/shortcuts#how-to-write-a-filter-in-a-shortcut"
|
||||
target="_blank"
|
||||
>
|
||||
How to Write a Filter in a Shortcut?
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-end items-center space-x-2 mt-2">
|
||||
<Button variant="plain" disabled={requestState.isLoading} onClick={destroy}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button color="primary" disabled={requestState.isLoading} onClick={handleConfirm}>
|
||||
{t("common.confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function showCreateShortcutDialog(props: Pick<Props, "shortcut">) {
|
||||
generateDialog(
|
||||
{
|
||||
className: "create-shortcut-dialog",
|
||||
dialogName: "create-shortcut-dialog",
|
||||
},
|
||||
CreateShortcutDialog,
|
||||
props,
|
||||
);
|
||||
}
|
||||
|
||||
export default showCreateShortcutDialog;
|
@ -0,0 +1,76 @@
|
||||
import { Dropdown, Menu, MenuButton, MenuItem, Tooltip } from "@mui/joy";
|
||||
import { Edit3Icon, MoreVerticalIcon, TrashIcon, PlusIcon } from "lucide-react";
|
||||
import { userServiceClient } from "@/grpcweb";
|
||||
import useAsyncEffect from "@/hooks/useAsyncEffect";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { useMemoFilterStore, useUserStore } from "@/store/v1";
|
||||
import { Shortcut } from "@/types/proto/api/v1/user_service";
|
||||
import { cn } from "@/utils";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import showCreateShortcutDialog from "../CreateShortcutDialog";
|
||||
|
||||
const ShortcutsSection = () => {
|
||||
const t = useTranslate();
|
||||
const user = useCurrentUser();
|
||||
const userStore = useUserStore();
|
||||
const memoFilterStore = useMemoFilterStore();
|
||||
const shortcuts = userStore.getState().shortcuts;
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
await userStore.fetchShortcuts();
|
||||
}, []);
|
||||
|
||||
const handleDeleteShortcut = async (shortcut: Shortcut) => {
|
||||
const confirmed = window.confirm("Are you sure you want to delete this shortcut?");
|
||||
if (confirmed) {
|
||||
await userServiceClient.deleteShortcut({ parent: user.name, id: shortcut.id });
|
||||
await userStore.fetchShortcuts();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-start items-start mt-3 px-1 h-auto shrink-0 flex-nowrap hide-scrollbar">
|
||||
<div className="flex flex-row justify-between items-center w-full gap-1 mb-1 text-sm leading-6 text-gray-400 select-none">
|
||||
<span>{t("common.shortcuts")}</span>
|
||||
<Tooltip title={t("common.create")} placement="top">
|
||||
<PlusIcon className="w-4 h-auto" onClick={() => showCreateShortcutDialog({})} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-start items-center relative flex-wrap gap-x-2 gap-y-1">
|
||||
{shortcuts.map((shortcut) => {
|
||||
const selected = memoFilterStore.shortcut === shortcut.id;
|
||||
return (
|
||||
<div
|
||||
key={shortcut.id}
|
||||
className="shrink-0 w-full text-sm rounded-md leading-6 flex flex-row justify-between items-center select-none gap-2 text-gray-600 dark:text-gray-400 dark:border-zinc-800"
|
||||
>
|
||||
<span
|
||||
className={cn("truncate cursor-pointer dark:opacity-80", selected && "font-medium underline")}
|
||||
onClick={() => (selected ? memoFilterStore.setShortcut(undefined) : memoFilterStore.setShortcut(shortcut.id))}
|
||||
>
|
||||
{shortcut.title}
|
||||
</span>
|
||||
<Dropdown>
|
||||
<MenuButton slots={{ root: "div" }}>
|
||||
<MoreVerticalIcon className="w-4 h-auto shrink-0 opacity-40" />
|
||||
</MenuButton>
|
||||
<Menu size="sm" placement="bottom-start">
|
||||
<MenuItem onClick={() => showCreateShortcutDialog({ shortcut })}>
|
||||
<Edit3Icon className="w-4 h-auto" />
|
||||
{t("common.edit")}
|
||||
</MenuItem>
|
||||
<MenuItem color="danger" onClick={() => handleDeleteShortcut(shortcut)}>
|
||||
<TrashIcon className="w-4 h-auto" />
|
||||
{t("common.delete")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShortcutsSection;
|
Loading…
Reference in New Issue