Skip to content

Commit

Permalink
feat: implement shortcut components
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnyjoygh committed Feb 3, 2025
1 parent 3a085f3 commit 2db86f6
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 5 deletions.
135 changes: 135 additions & 0 deletions web/src/components/CreateShortcutDialog.tsx
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions web/src/components/HomeSidebar/HomeSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import StatisticsView from "@/components/StatisticsView";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useMemoList, useUserStatsStore } from "@/store/v1";
import { cn } from "@/utils";
import ShortcutsSection from "./ShortcutsSection";
import TagsSection from "./TagsSection";

interface Props {
Expand Down Expand Up @@ -32,6 +33,7 @@ const HomeSidebar = (props: Props) => {
>
<SearchBar />
<StatisticsView />
<ShortcutsSection />
<TagsSection />
</aside>
);
Expand Down
76 changes: 76 additions & 0 deletions web/src/components/HomeSidebar/ShortcutsSection.tsx
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 3 additions & 1 deletion web/src/components/PagedMemoList/PagedMemoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface Props {
owner?: string;
state?: State;
direction?: Direction;
filter?: string;
oldFilter?: string;
pageSize?: number;
}
Expand All @@ -42,6 +43,7 @@ const PagedMemoList = (props: Props) => {
parent: props.owner || "",
state: props.state || State.NORMAL,
direction: props.direction || Direction.DESC,
filter: props.filter || "",
oldFilter: props.oldFilter || "",
pageSize: props.pageSize || DEFAULT_LIST_MEMOS_PAGE_SIZE,
pageToken: nextPageToken,
Expand All @@ -60,7 +62,7 @@ const PagedMemoList = (props: Props) => {

useEffect(() => {
refreshList();
}, [props.owner, props.state, props.direction, props.oldFilter, props.pageSize]);
}, [props.owner, props.state, props.direction, props.filter, props.oldFilter, props.pageSize]);

const children = (
<div className="flex flex-col justify-start items-start w-full max-w-full">
Expand Down
6 changes: 4 additions & 2 deletions web/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
"username": "Username",
"version": "Version",
"visibility": "Visibility",
"yourself": "Yourself"
"yourself": "Yourself",
"shortcuts": "Shortcuts"
},
"days": {
"fri": "Fri",
Expand Down Expand Up @@ -371,6 +372,7 @@
},
"version": "Version"
},
"shortcut": {},
"tag": {
"all-tags": "All Tags",
"create-tag": "Create Tag",
Expand All @@ -391,4 +393,4 @@
"blogs": "Blogs",
"documents": "Documents"
}
}
}
5 changes: 4 additions & 1 deletion web/src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import MobileHeader from "@/components/MobileHeader";
import PagedMemoList from "@/components/PagedMemoList";
import useCurrentUser from "@/hooks/useCurrentUser";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import { useMemoFilterStore } from "@/store/v1";
import { useMemoFilterStore, useUserStore } from "@/store/v1";
import { Direction, State } from "@/types/proto/api/v1/common";
import { Memo } from "@/types/proto/api/v1/memo_service";
import { cn } from "@/utils";

const Home = () => {
const { md } = useResponsiveWidth();
const user = useCurrentUser();
const userStore = useUserStore();
const memoFilterStore = useMemoFilterStore();
const selectedShortcut = userStore.shortcuts.find((shortcut) => shortcut.id === memoFilterStore.shortcut);

const memoListFilter = useMemo(() => {
const conditions = [];
Expand Down Expand Up @@ -79,6 +81,7 @@ const Home = () => {
}
owner={user.name}
direction={memoFilterStore.orderByTimeAsc ? Direction.ASC : Direction.DESC}
filter={selectedShortcut?.filter || ""}
oldFilter={memoListFilter}
/>
</div>
Expand Down
3 changes: 3 additions & 0 deletions web/src/store/v1/memoFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export const stringifyFilters = (filters: MemoFilter[]): string => {
interface State {
filters: MemoFilter[];
orderByTimeAsc: boolean;
// The id of selected shortcut.
shortcut?: string;
}

const getInitialState = (): State => {
Expand All @@ -59,5 +61,6 @@ export const useMemoFilterStore = create(
addFilter: (filter: MemoFilter) => set((state) => ({ filters: uniqBy([...state.filters, filter], getMemoFilterKey) })),
removeFilter: (filterFn: (f: MemoFilter) => boolean) => set((state) => ({ filters: state.filters.filter((f) => !filterFn(f)) })),
setOrderByTimeAsc: (orderByTimeAsc: boolean) => set({ orderByTimeAsc }),
setShortcut: (shortcut?: string) => set({ shortcut }),
})),
);
12 changes: 11 additions & 1 deletion web/src/store/v1/user.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { create } from "zustand";
import { combine } from "zustand/middleware";
import { authServiceClient, userServiceClient } from "@/grpcweb";
import { User, UserSetting, User_Role } from "@/types/proto/api/v1/user_service";
import { Shortcut, User, UserSetting, User_Role } from "@/types/proto/api/v1/user_service";

interface State {
userMapByName: Record<string, User>;
// The name of current user. Format: `users/${uid}`
currentUser?: string;
userSetting?: UserSetting;
shortcuts: Shortcut[];
}

const getDefaultState = (): State => ({
userMapByName: {},
currentUser: undefined,
userSetting: undefined,
shortcuts: [],
});

const getDefaultUserSetting = () => {
Expand Down Expand Up @@ -129,6 +131,14 @@ export const useUserStore = create(
set({ userSetting: updatedUserSetting });
return updatedUserSetting;
},
fetchShortcuts: async () => {
const { currentUser } = get();
if (!currentUser) {
return;
}
const { shortcuts } = await userServiceClient.listShortcuts({ parent: currentUser });
set({ shortcuts });
},
})),
);

Expand Down

0 comments on commit 2db86f6

Please sign in to comment.