chore: use shortcut v2 api

This commit is contained in:
Steven 2023-11-21 23:33:34 +08:00
parent c449669793
commit 61b167ef66
32 changed files with 515 additions and 724 deletions

View File

@ -13,11 +13,11 @@
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@mui/joy": "5.0.0-beta.14", "@mui/joy": "5.0.0-beta.14",
"@reduxjs/toolkit": "^1.9.7", "@reduxjs/toolkit": "^1.9.7",
"axios": "^1.6.0", "axios": "^1.6.2",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"i18next": "^23.6.0", "i18next": "^23.7.6",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lucide-react": "^0.292.0", "lucide-react": "^0.292.0",
"nice-grpc-web": "^3.3.2", "nice-grpc-web": "^3.3.2",
@ -25,24 +25,23 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-i18next": "^13.3.1", "react-i18next": "^13.5.0",
"react-redux": "^8.1.3", "react-router-dom": "^6.19.0",
"react-router-dom": "^6.18.0",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"tailwindcss": "^3.3.5", "tailwindcss": "^3.3.5",
"zustand": "^4.4.6" "zustand": "^4.4.6"
}, },
"devDependencies": { "devDependencies": {
"@bufbuild/buf": "^1.27.2", "@bufbuild/buf": "^1.28.1",
"@trivago/prettier-plugin-sort-imports": "^4.2.1", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/lodash-es": "^4.17.11", "@types/lodash-es": "^4.17.11",
"@types/react": "^18.2.37", "@types/react": "^18.2.38",
"@types/react-dom": "^18.2.15", "@types/react-dom": "^18.2.16",
"@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.10.0", "@typescript-eslint/parser": "^6.12.0",
"@vitejs/plugin-react-swc": "^3.4.1", "@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"eslint": "^8.53.0", "eslint": "^8.54.0",
"eslint-config-prettier": "^8.10.0", "eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
@ -50,7 +49,7 @@
"postcss": "^8.4.31", "postcss": "^8.4.31",
"prettier": "2.6.2", "prettier": "2.6.2",
"protobufjs": "^7.2.5", "protobufjs": "^7.2.5",
"typescript": "^5.2.2", "typescript": "^5.3.2",
"vite": "^4.5.0" "vite": "^4.5.0"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ import * as api from "../helpers/api";
import Icon from "./Icon"; import Icon from "./Icon";
interface Props { interface Props {
shortcutId: ShortcutId; shortcutId: number;
className?: string; className?: string;
} }

View File

@ -7,10 +7,11 @@ import { Link } from "react-router-dom";
import { absolutifyLink } from "@/helpers/utils"; import { absolutifyLink } from "@/helpers/utils";
import useNavigateTo from "@/hooks/useNavigateTo"; import useNavigateTo from "@/hooks/useNavigateTo";
import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import { useAppSelector } from "@/stores";
import useCollectionStore from "@/stores/v1/collection"; import useCollectionStore from "@/stores/v1/collection";
import useShortcutStore from "@/stores/v1/shortcut";
import useUserStore from "@/stores/v1/user"; import useUserStore from "@/stores/v1/user";
import { Collection } from "@/types/proto/api/v2/collection_service"; import { Collection } from "@/types/proto/api/v2/collection_service";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
import { showCommonDialog } from "./Alert"; import { showCommonDialog } from "./Alert";
import CreateCollectionDialog from "./CreateCollectionDrawer"; import CreateCollectionDialog from "./CreateCollectionDrawer";
import Icon from "./Icon"; import Icon from "./Icon";
@ -28,7 +29,7 @@ const CollectionView = (props: Props) => {
const navigateTo = useNavigateTo(); const navigateTo = useNavigateTo();
const currentUser = useUserStore().getCurrentUser(); const currentUser = useUserStore().getCurrentUser();
const collectionStore = useCollectionStore(); const collectionStore = useCollectionStore();
const { shortcutList } = useAppSelector((state) => state.shortcut); const shortcutList = useShortcutStore().getShortcutList();
const [showEditDialog, setShowEditDialog] = useState<boolean>(false); const [showEditDialog, setShowEditDialog] = useState<boolean>(false);
const shortcuts = collection.shortcutIds const shortcuts = collection.shortcutIds
.map((shortcutId) => shortcutList.find((shortcut) => shortcut?.id === shortcutId)) .map((shortcutId) => shortcutList.find((shortcut) => shortcut?.id === shortcutId))

View File

@ -3,10 +3,11 @@ import { isUndefined } from "lodash-es";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAppSelector } from "@/stores";
import useCollectionStore from "@/stores/v1/collection"; import useCollectionStore from "@/stores/v1/collection";
import useShortcutStore from "@/stores/v1/shortcut";
import { Collection } from "@/types/proto/api/v2/collection_service"; import { Collection } from "@/types/proto/api/v2/collection_service";
import { Visibility } from "@/types/proto/api/v2/common"; import { Visibility } from "@/types/proto/api/v2/common";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
import { convertVisibilityFromPb } from "@/utils/visibility"; import { convertVisibilityFromPb } from "@/utils/visibility";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
import Icon from "./Icon"; import Icon from "./Icon";
@ -27,7 +28,7 @@ const CreateCollectionDrawer: React.FC<Props> = (props: Props) => {
const { onClose, onConfirm, collectionId } = props; const { onClose, onConfirm, collectionId } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const collectionStore = useCollectionStore(); const collectionStore = useCollectionStore();
const { shortcutList } = useAppSelector((state) => state.shortcut); const shortcutList = useShortcutStore().getShortcutList();
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
collectionCreate: Collection.fromPartial({ collectionCreate: Collection.fromPartial({
visibility: Visibility.PRIVATE, visibility: Visibility.PRIVATE,
@ -40,9 +41,9 @@ const CreateCollectionDrawer: React.FC<Props> = (props: Props) => {
const unselectedShortcuts = shortcutList const unselectedShortcuts = shortcutList
.filter((shortcut) => { .filter((shortcut) => {
if (state.collectionCreate.visibility === Visibility.PUBLIC) { if (state.collectionCreate.visibility === Visibility.PUBLIC) {
return shortcut.visibility === "PUBLIC"; return shortcut.visibility === Visibility.PUBLIC;
} else if (state.collectionCreate.visibility === Visibility.WORKSPACE) { } else if (state.collectionCreate.visibility === Visibility.WORKSPACE) {
return shortcut.visibility === "PUBLIC" || shortcut.visibility === "WORKSPACE"; return shortcut.visibility === Visibility.PUBLIC || shortcut.visibility === Visibility.WORKSPACE;
} else { } else {
return true; return true;
} }

View File

@ -16,46 +16,42 @@ import { isUndefined, uniq } from "lodash-es";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAppSelector } from "@/stores"; import useShortcutStore from "@/stores/v1/shortcut";
import { Visibility } from "@/types/proto/api/v2/common";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
import { convertVisibilityFromPb } from "@/utils/visibility";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
import { shortcutService } from "../services";
import Icon from "./Icon"; import Icon from "./Icon";
import ResourceNameInput from "./ResourceNameInput"; import ResourceNameInput from "./ResourceNameInput";
interface Props { interface Props {
shortcutId?: ShortcutId; shortcutId?: number;
initialShortcut?: Partial<Shortcut>; initialShortcut?: Partial<Shortcut>;
onClose: () => void; onClose: () => void;
onConfirm?: () => void; onConfirm?: () => void;
} }
interface State { interface State {
shortcutCreate: ShortcutCreate; shortcutCreate: Shortcut;
} }
const visibilities: Visibility[] = ["PRIVATE", "WORKSPACE", "PUBLIC"];
const CreateShortcutDrawer: React.FC<Props> = (props: Props) => { const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
const { onClose, onConfirm, shortcutId, initialShortcut } = props; const { onClose, onConfirm, shortcutId, initialShortcut } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const { shortcutList } = useAppSelector((state) => state.shortcut);
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
shortcutCreate: { shortcutCreate: Shortcut.fromPartial({
name: "", visibility: Visibility.PRIVATE,
link: "", ogMetadata: {
title: "",
description: "",
visibility: "PRIVATE",
tags: [],
openGraphMetadata: {
title: "", title: "",
description: "", description: "",
image: "", image: "",
}, },
...initialShortcut, ...initialShortcut,
}, }),
}); });
const shortcutStore = useShortcutStore();
const [showOpenGraphMetadata, setShowOpenGraphMetadata] = useState<boolean>(false); const [showOpenGraphMetadata, setShowOpenGraphMetadata] = useState<boolean>(false);
const shortcutList = shortcutStore.getShortcutList();
const [tag, setTag] = useState<string>(""); const [tag, setTag] = useState<string>("");
const tagSuggestions = uniq(shortcutList.map((shortcut) => shortcut.tags).flat()); const tagSuggestions = uniq(shortcutList.map((shortcut) => shortcut.tags).flat());
const isCreating = isUndefined(shortcutId); const isCreating = isUndefined(shortcutId);
@ -64,7 +60,7 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
useEffect(() => { useEffect(() => {
if (shortcutId) { if (shortcutId) {
const shortcut = shortcutService.getShortcutById(shortcutId); const shortcut = shortcutStore.getShortcutById(shortcutId);
if (shortcut) { if (shortcut) {
setState({ setState({
...state, ...state,
@ -74,7 +70,7 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
title: shortcut.title, title: shortcut.title,
description: shortcut.description, description: shortcut.description,
visibility: shortcut.visibility, visibility: shortcut.visibility,
openGraphMetadata: shortcut.openGraphMetadata, ogMetadata: shortcut.ogMetadata,
}), }),
}); });
setTag(shortcut.tags.join(" ")); setTag(shortcut.tags.join(" "));
@ -121,7 +117,7 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
const handleVisibilityInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleVisibilityInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPartialState({ setPartialState({
shortcutCreate: Object.assign(state.shortcutCreate, { shortcutCreate: Object.assign(state.shortcutCreate, {
visibility: e.target.value, visibility: Number(e.target.value),
}), }),
}); });
}; };
@ -142,8 +138,8 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
const handleOpenGraphMetadataImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleOpenGraphMetadataImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPartialState({ setPartialState({
shortcutCreate: Object.assign(state.shortcutCreate, { shortcutCreate: Object.assign(state.shortcutCreate, {
openGraphMetadata: { ogMetadata: {
...state.shortcutCreate.openGraphMetadata, ...state.shortcutCreate.ogMetadata,
image: e.target.value, image: e.target.value,
}, },
}), }),
@ -153,8 +149,8 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
const handleOpenGraphMetadataTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleOpenGraphMetadataTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPartialState({ setPartialState({
shortcutCreate: Object.assign(state.shortcutCreate, { shortcutCreate: Object.assign(state.shortcutCreate, {
openGraphMetadata: { ogMetadata: {
...state.shortcutCreate.openGraphMetadata, ...state.shortcutCreate.ogMetadata,
title: e.target.value, title: e.target.value,
}, },
}), }),
@ -164,8 +160,8 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
const handleOpenGraphMetadataDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleOpenGraphMetadataDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setPartialState({ setPartialState({
shortcutCreate: Object.assign(state.shortcutCreate, { shortcutCreate: Object.assign(state.shortcutCreate, {
openGraphMetadata: { ogMetadata: {
...state.shortcutCreate.openGraphMetadata, ...state.shortcutCreate.ogMetadata,
description: e.target.value, description: e.target.value,
}, },
}), }),
@ -188,18 +184,13 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
try { try {
if (shortcutId) { if (shortcutId) {
await shortcutService.patchShortcut({ await shortcutStore.updateShortcut({
...state.shortcutCreate,
id: shortcutId, id: shortcutId,
name: state.shortcutCreate.name,
link: state.shortcutCreate.link,
title: state.shortcutCreate.title,
description: state.shortcutCreate.description,
visibility: state.shortcutCreate.visibility,
tags: tag.split(" ").filter(Boolean), tags: tag.split(" ").filter(Boolean),
openGraphMetadata: state.shortcutCreate.openGraphMetadata,
}); });
} else { } else {
await shortcutService.createShortcut({ await shortcutStore.createShortcut({
...state.shortcutCreate, ...state.shortcutCreate,
tags: tag.split(" ").filter(Boolean), tags: tag.split(" ").filter(Boolean),
}); });
@ -281,13 +272,13 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
<span className="mb-2">Visibility</span> <span className="mb-2">Visibility</span>
<div className="w-full flex flex-row justify-start items-center text-base"> <div className="w-full flex flex-row justify-start items-center text-base">
<RadioGroup orientation="horizontal" value={state.shortcutCreate.visibility} onChange={handleVisibilityInputChange}> <RadioGroup orientation="horizontal" value={state.shortcutCreate.visibility} onChange={handleVisibilityInputChange}>
{visibilities.map((visibility) => ( <Radio value={Visibility.PRIVATE} label={t(`shortcut.visibility.private.self`)} />
<Radio key={visibility} value={visibility} label={t(`shortcut.visibility.${visibility.toLowerCase()}.self`)} /> <Radio value={Visibility.WORKSPACE} label={t(`shortcut.visibility.workspace.self`)} />
))} <Radio value={Visibility.PUBLIC} label={t(`shortcut.visibility.public.self`)} />
</RadioGroup> </RadioGroup>
</div> </div>
<p className="mt-3 text-sm text-gray-500 w-full bg-gray-100 border border-gray-200 dark:bg-zinc-800 dark:border-zinc-700 dark:text-gray-400 px-2 py-1 rounded-md"> <p className="mt-3 text-sm text-gray-500 w-full bg-gray-100 border border-gray-200 dark:bg-zinc-800 dark:border-zinc-700 dark:text-gray-400 px-2 py-1 rounded-md">
{t(`shortcut.visibility.${state.shortcutCreate.visibility.toLowerCase()}.description`)} {t(`shortcut.visibility.${convertVisibilityFromPb(state.shortcutCreate.visibility).toLowerCase()}.description`)}
</p> </p>
</div> </div>
<Divider className="text-gray-500">More</Divider> <Divider className="text-gray-500">More</Divider>
@ -316,7 +307,7 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
type="text" type="text"
placeholder="https://the.link.to/the/image.png" placeholder="https://the.link.to/the/image.png"
size="sm" size="sm"
value={state.shortcutCreate.openGraphMetadata.image} value={state.shortcutCreate.ogMetadata?.image}
onChange={handleOpenGraphMetadataImageChange} onChange={handleOpenGraphMetadataImageChange}
/> />
</div> </div>
@ -327,7 +318,7 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
type="text" type="text"
placeholder="Slash - An open source, self-hosted bookmarks and link sharing platform" placeholder="Slash - An open source, self-hosted bookmarks and link sharing platform"
size="sm" size="sm"
value={state.shortcutCreate.openGraphMetadata.title} value={state.shortcutCreate.ogMetadata?.title}
onChange={handleOpenGraphMetadataTitleChange} onChange={handleOpenGraphMetadataTitleChange}
/> />
</div> </div>
@ -338,7 +329,7 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
placeholder="An open source, self-hosted bookmarks and link sharing platform." placeholder="An open source, self-hosted bookmarks and link sharing platform."
size="sm" size="sm"
maxRows={3} maxRows={3}
value={state.shortcutCreate.openGraphMetadata.description} value={state.shortcutCreate.ogMetadata?.description}
onChange={handleOpenGraphMetadataDescriptionChange} onChange={handleOpenGraphMetadataDescriptionChange}
/> />
</div> </div>

View File

@ -1,4 +1,5 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { convertVisibilityFromPb } from "@/utils/visibility";
import useViewStore from "../stores/v1/view"; import useViewStore from "../stores/v1/view";
import Icon from "./Icon"; import Icon from "./Icon";
import VisibilityIcon from "./VisibilityIcon"; import VisibilityIcon from "./VisibilityIcon";
@ -32,7 +33,7 @@ const FilterView = () => {
onClick={() => viewStore.setFilter({ visibility: undefined })} onClick={() => viewStore.setFilter({ visibility: undefined })}
> >
<VisibilityIcon className="w-4 h-auto mr-1" visibility={filter.visibility} /> <VisibilityIcon className="w-4 h-auto mr-1" visibility={filter.visibility} />
{t(`shortcut.visibility.${filter.visibility.toLowerCase()}.self`)} {t(`shortcut.visibility.${convertVisibilityFromPb(filter.visibility).toLowerCase()}.self`)}
<Icon.X className="w-4 h-auto ml-1" /> <Icon.X className="w-4 h-auto ml-1" />
</button> </button>
)} )}

View File

@ -3,6 +3,7 @@ import { QRCodeCanvas } from "qrcode.react";
import { useRef } from "react"; import { useRef } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
import { absolutifyLink } from "../helpers/utils"; import { absolutifyLink } from "../helpers/utils";
import Icon from "./Icon"; import Icon from "./Icon";

View File

@ -1,8 +1,9 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useNavigateTo from "@/hooks/useNavigateTo"; import useNavigateTo from "@/hooks/useNavigateTo";
import useShortcutStore from "@/stores/v1/shortcut";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
import { Role } from "@/types/proto/api/v2/user_service"; import { Role } from "@/types/proto/api/v2/user_service";
import { shortcutService } from "../services";
import useUserStore from "../stores/v1/user"; import useUserStore from "../stores/v1/user";
import { showCommonDialog } from "./Alert"; import { showCommonDialog } from "./Alert";
import CreateShortcutDrawer from "./CreateShortcutDrawer"; import CreateShortcutDrawer from "./CreateShortcutDrawer";
@ -18,6 +19,7 @@ const ShortcutActionsDropdown = (props: Props) => {
const { shortcut } = props; const { shortcut } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const navigateTo = useNavigateTo(); const navigateTo = useNavigateTo();
const shortcutStore = useShortcutStore();
const currentUser = useUserStore().getCurrentUser(); const currentUser = useUserStore().getCurrentUser();
const [showEditDrawer, setShowEditDrawer] = useState<boolean>(false); const [showEditDrawer, setShowEditDrawer] = useState<boolean>(false);
const [showQRCodeDialog, setShowQRCodeDialog] = useState<boolean>(false); const [showQRCodeDialog, setShowQRCodeDialog] = useState<boolean>(false);
@ -29,7 +31,7 @@ const ShortcutActionsDropdown = (props: Props) => {
content: `Are you sure to delete shortcut \`${shortcut.name}\`? You cannot undo this action.`, content: `Are you sure to delete shortcut \`${shortcut.name}\`? You cannot undo this action.`,
style: "danger", style: "danger",
onConfirm: async () => { onConfirm: async () => {
await shortcutService.deleteShortcutById(shortcut.id); await shortcutStore.deleteShortcut(shortcut.id);
}, },
}); });
}; };

View File

@ -4,6 +4,8 @@ import copy from "copy-to-clipboard";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
import { convertVisibilityFromPb } from "@/utils/visibility";
import { absolutifyLink, getFaviconWithGoogleS2 } from "../helpers/utils"; import { absolutifyLink, getFaviconWithGoogleS2 } from "../helpers/utils";
import useViewStore from "../stores/v1/view"; import useViewStore from "../stores/v1/view";
import Icon from "./Icon"; import Icon from "./Icon";
@ -102,13 +104,18 @@ const ShortcutCard = (props: Props) => {
{shortcut.tags.length === 0 && <span className="text-gray-400 text-sm leading-4 italic">No tags</span>} {shortcut.tags.length === 0 && <span className="text-gray-400 text-sm leading-4 italic">No tags</span>}
</div> </div>
<div className="w-full flex mt-2 gap-2 overflow-x-auto"> <div className="w-full flex mt-2 gap-2 overflow-x-auto">
<Tooltip title={t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.description`)} variant="solid" placement="top" arrow> <Tooltip
title={t(`shortcut.visibility.${convertVisibilityFromPb(shortcut.visibility).toLowerCase()}.description`)}
variant="solid"
placement="top"
arrow
>
<div <div
className="w-auto px-2 leading-6 flex flex-row justify-start items-center flex-nowrap whitespace-nowrap border rounded-full cursor-pointer text-gray-500 dark:text-gray-400 text-sm dark:border-zinc-700" className="w-auto px-2 leading-6 flex flex-row justify-start items-center flex-nowrap whitespace-nowrap border rounded-full cursor-pointer text-gray-500 dark:text-gray-400 text-sm dark:border-zinc-700"
onClick={() => viewStore.setFilter({ visibility: shortcut.visibility })} onClick={() => viewStore.setFilter({ visibility: shortcut.visibility })}
> >
<VisibilityIcon className="w-4 h-auto mr-1 opacity-60" visibility={shortcut.visibility} /> <VisibilityIcon className="w-4 h-auto mr-1 opacity-60" visibility={shortcut.visibility} />
{t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.self`)} {t(`shortcut.visibility.${convertVisibilityFromPb(shortcut.visibility).toLowerCase()}.self`)}
</div> </div>
</Tooltip> </Tooltip>
<Tooltip title="View count" variant="solid" placement="top" arrow> <Tooltip title="View count" variant="solid" placement="top" arrow>
@ -117,7 +124,7 @@ const ShortcutCard = (props: Props) => {
className="w-auto px-2 leading-6 flex flex-row justify-start items-center flex-nowrap whitespace-nowrap border rounded-full cursor-pointer text-gray-500 dark:text-gray-400 text-sm dark:border-zinc-700" className="w-auto px-2 leading-6 flex flex-row justify-start items-center flex-nowrap whitespace-nowrap border rounded-full cursor-pointer text-gray-500 dark:text-gray-400 text-sm dark:border-zinc-700"
> >
<Icon.BarChart2 className="w-4 h-auto mr-1 opacity-80" /> <Icon.BarChart2 className="w-4 h-auto mr-1 opacity-80" />
{t("shortcut.visits", { count: shortcut.view })} {t("shortcut.visits", { count: shortcut.viewCount })}
</Link> </Link>
</Tooltip> </Tooltip>
</div> </div>

View File

@ -1,5 +1,6 @@
import classNames from "classnames"; import classNames from "classnames";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
import { getFaviconWithGoogleS2 } from "../helpers/utils"; import { getFaviconWithGoogleS2 } from "../helpers/utils";
import Icon from "./Icon"; import Icon from "./Icon";
import ShortcutActionsDropdown from "./ShortcutActionsDropdown"; import ShortcutActionsDropdown from "./ShortcutActionsDropdown";

View File

@ -1,5 +1,6 @@
import classNames from "classnames"; import classNames from "classnames";
import useNavigateTo from "@/hooks/useNavigateTo"; import useNavigateTo from "@/hooks/useNavigateTo";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
import useViewStore from "../stores/v1/view"; import useViewStore from "../stores/v1/view";
import ShortcutCard from "./ShortcutCard"; import ShortcutCard from "./ShortcutCard";
import ShortcutView from "./ShortcutView"; import ShortcutView from "./ShortcutView";

View File

@ -1,13 +1,13 @@
import classNames from "classnames"; import classNames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAppSelector } from "../stores"; import useShortcutStore from "@/stores/v1/shortcut";
import useViewStore from "../stores/v1/view"; import useViewStore from "../stores/v1/view";
import Icon from "./Icon"; import Icon from "./Icon";
const ShortcutsNavigator = () => { const ShortcutsNavigator = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const viewStore = useViewStore(); const viewStore = useViewStore();
const { shortcutList } = useAppSelector((state) => state.shortcut); const shortcutList = useShortcutStore().getShortcutList();
const tags = shortcutList.map((shortcut) => shortcut.tags).flat(); const tags = shortcutList.map((shortcut) => shortcut.tags).flat();
const currentTab = viewStore.filter.tab || `tab:all`; const currentTab = viewStore.filter.tab || `tab:all`;
const sortedTagMap = sortTags(tags); const sortedTagMap = sortTags(tags);

View File

@ -1,3 +1,4 @@
import { Visibility } from "@/types/proto/api/v2/common";
import Icon from "./Icon"; import Icon from "./Icon";
interface Props { interface Props {
@ -7,11 +8,11 @@ interface Props {
const VisibilityIcon = (props: Props) => { const VisibilityIcon = (props: Props) => {
const { visibility, className } = props; const { visibility, className } = props;
if (visibility === "PRIVATE") { if (visibility === Visibility.PRIVATE) {
return <Icon.Lock className={className || ""} />; return <Icon.Lock className={className || ""} />;
} else if (visibility === "WORKSPACE") { } else if (visibility === Visibility.WORKSPACE) {
return <Icon.Building2 className={className || ""} />; return <Icon.Building2 className={className || ""} />;
} else if (visibility === "PUBLIC") { } else if (visibility === Visibility.PUBLIC) {
return <Icon.Globe2 className={className || ""} />; return <Icon.Globe2 className={className || ""} />;
} }
return null; return null;

View File

@ -19,30 +19,6 @@ export function signout() {
return axios.post("/api/v1/auth/logout"); return axios.post("/api/v1/auth/logout");
} }
export function getShortcutList(shortcutFind?: ShortcutFind) { export function getShortcutAnalytics(shortcutId: number) {
const queryList = [];
if (shortcutFind?.tag) {
queryList.push(`tag=${shortcutFind.tag}`);
}
return axios.get<Shortcut[]>(`/api/v1/shortcut?${queryList.join("&")}`);
}
export function getShortcutById(id: number) {
return axios.get<Shortcut>(`/api/v1/shortcut/${id}`);
}
export function createShortcut(shortcutCreate: ShortcutCreate) {
return axios.post<Shortcut>("/api/v1/shortcut", shortcutCreate);
}
export function getShortcutAnalytics(shortcutId: ShortcutId) {
return axios.get<AnalysisData>(`/api/v1/shortcut/${shortcutId}/analytics`); return axios.get<AnalysisData>(`/api/v1/shortcut/${shortcutId}/analytics`);
} }
export function patchShortcut(shortcutPatch: ShortcutPatch) {
return axios.patch<Shortcut>(`/api/v1/shortcut/${shortcutPatch.id}`, shortcutPatch);
}
export function deleteShortcutById(shortcutId: ShortcutId) {
return axios.delete(`/api/v1/shortcut/${shortcutId}`);
}

View File

@ -1,22 +1,18 @@
import { CssVarsProvider } from "@mui/joy"; import { CssVarsProvider } from "@mui/joy";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { Provider } from "react-redux";
import { RouterProvider } from "react-router-dom"; import { RouterProvider } from "react-router-dom";
import "./css/index.css"; import "./css/index.css";
import "./css/joy-ui.css"; import "./css/joy-ui.css";
import "./i18n"; import "./i18n";
import router from "./routers"; import router from "./routers";
import store from "./stores";
const container = document.getElementById("root"); const container = document.getElementById("root");
const root = createRoot(container as HTMLElement); const root = createRoot(container as HTMLElement);
root.render( root.render(
<Provider store={store}> <CssVarsProvider>
<CssVarsProvider> <RouterProvider router={router} />
<RouterProvider router={router} /> <Toaster position="top-center" />
<Toaster position="top-center" /> </CssVarsProvider>
</CssVarsProvider>
</Provider>
); );

View File

@ -3,8 +3,8 @@ import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import CollectionView from "@/components/CollectionView"; import CollectionView from "@/components/CollectionView";
import CreateCollectionDrawer from "@/components/CreateCollectionDrawer"; import CreateCollectionDrawer from "@/components/CreateCollectionDrawer";
import { shortcutService } from "@/services";
import useCollectionStore from "@/stores/v1/collection"; import useCollectionStore from "@/stores/v1/collection";
import useShortcutStore from "@/stores/v1/shortcut";
import FilterView from "../components/FilterView"; import FilterView from "../components/FilterView";
import Icon from "../components/Icon"; import Icon from "../components/Icon";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
@ -16,6 +16,7 @@ interface State {
const CollectionDashboard: React.FC = () => { const CollectionDashboard: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const loadingState = useLoading(); const loadingState = useLoading();
const shortcutStore = useShortcutStore();
const collectionStore = useCollectionStore(); const collectionStore = useCollectionStore();
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
showCreateCollectionDrawer: false, showCreateCollectionDrawer: false,
@ -30,7 +31,7 @@ const CollectionDashboard: React.FC = () => {
}); });
useEffect(() => { useEffect(() => {
Promise.all([shortcutService.getMyAllShortcuts(), collectionStore.fetchCollectionList()]).finally(() => { Promise.all([shortcutStore.fetchShortcutList(), collectionStore.fetchCollectionList()]).finally(() => {
loadingState.setFinish(); loadingState.setFinish();
}); });
}, []); }, []);

View File

@ -12,7 +12,6 @@ import useShortcutStore from "@/stores/v1/shortcut";
import useUserStore from "@/stores/v1/user"; import useUserStore from "@/stores/v1/user";
import { Collection } from "@/types/proto/api/v2/collection_service"; import { Collection } from "@/types/proto/api/v2/collection_service";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service"; import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
import { convertShortcutFromPb } from "@/utils/shortcut";
const CollectionSpace = () => { const CollectionSpace = () => {
const { collectionName } = useParams(); const { collectionName } = useParams();
@ -90,7 +89,7 @@ const CollectionSpace = () => {
: "sm:border-transparent dark:sm:border-transparent" : "sm:border-transparent dark:sm:border-transparent"
)} )}
key={shortcut.name} key={shortcut.name}
shortcut={convertShortcutFromPb(shortcut)} shortcut={shortcut}
alwaysShowLink={!sm} alwaysShowLink={!sm}
onClick={() => handleShortcutClick(shortcut)} onClick={() => handleShortcutClick(shortcut)}
/> />

View File

@ -1,6 +1,7 @@
import { Button, Input } from "@mui/joy"; import { Button, Input } from "@mui/joy";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useShortcutStore from "@/stores/v1/shortcut";
import CreateShortcutDrawer from "../components/CreateShortcutDrawer"; import CreateShortcutDrawer from "../components/CreateShortcutDrawer";
import FilterView from "../components/FilterView"; import FilterView from "../components/FilterView";
import Icon from "../components/Icon"; import Icon from "../components/Icon";
@ -8,8 +9,6 @@ import ShortcutsContainer from "../components/ShortcutsContainer";
import ShortcutsNavigator from "../components/ShortcutsNavigator"; import ShortcutsNavigator from "../components/ShortcutsNavigator";
import ViewSetting from "../components/ViewSetting"; import ViewSetting from "../components/ViewSetting";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
import { shortcutService } from "../services";
import { useAppSelector } from "../stores";
import useUserStore from "../stores/v1/user"; import useUserStore from "../stores/v1/user";
import useViewStore, { getFilteredShortcutList, getOrderedShortcutList } from "../stores/v1/view"; import useViewStore, { getFilteredShortcutList, getOrderedShortcutList } from "../stores/v1/view";
@ -21,8 +20,9 @@ const Home: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const loadingState = useLoading(); const loadingState = useLoading();
const currentUser = useUserStore().getCurrentUser(); const currentUser = useUserStore().getCurrentUser();
const shortcutStore = useShortcutStore();
const viewStore = useViewStore(); const viewStore = useViewStore();
const { shortcutList } = useAppSelector((state) => state.shortcut); const shortcutList = shortcutStore.getShortcutList();
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
showCreateShortcutDrawer: false, showCreateShortcutDrawer: false,
}); });
@ -31,7 +31,7 @@ const Home: React.FC = () => {
const orderedShortcutList = getOrderedShortcutList(filteredShortcutList, viewStore.order); const orderedShortcutList = getOrderedShortcutList(filteredShortcutList, viewStore.order);
useEffect(() => { useEffect(() => {
Promise.all([shortcutService.getMyAllShortcuts()]).finally(() => { Promise.all([shortcutStore.fetchShortcutList()]).finally(() => {
loadingState.setFinish(); loadingState.setFinish();
}); });
}, []); }, []);

View File

@ -1,12 +1,16 @@
import { Tooltip } from "@mui/joy"; import { Tooltip } from "@mui/joy";
import classNames from "classnames"; import classNames from "classnames";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import { useState } from "react"; import { useEffect, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLoaderData } from "react-router-dom"; import { useParams } from "react-router-dom";
import useLoading from "@/hooks/useLoading";
import useNavigateTo from "@/hooks/useNavigateTo"; import useNavigateTo from "@/hooks/useNavigateTo";
import useShortcutStore from "@/stores/v1/shortcut";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
import { Role } from "@/types/proto/api/v2/user_service"; import { Role } from "@/types/proto/api/v2/user_service";
import { convertVisibilityFromPb } from "@/utils/visibility";
import { showCommonDialog } from "../components/Alert"; import { showCommonDialog } from "../components/Alert";
import AnalyticsView from "../components/AnalyticsView"; import AnalyticsView from "../components/AnalyticsView";
import CreateShortcutDrawer from "../components/CreateShortcutDrawer"; import CreateShortcutDrawer from "../components/CreateShortcutDrawer";
@ -15,7 +19,6 @@ import Icon from "../components/Icon";
import VisibilityIcon from "../components/VisibilityIcon"; import VisibilityIcon from "../components/VisibilityIcon";
import Dropdown from "../components/common/Dropdown"; import Dropdown from "../components/common/Dropdown";
import { absolutifyLink, getFaviconWithGoogleS2 } from "../helpers/utils"; import { absolutifyLink, getFaviconWithGoogleS2 } from "../helpers/utils";
import { shortcutService } from "../services";
import useUserStore from "../stores/v1/user"; import useUserStore from "../stores/v1/user";
interface State { interface State {
@ -24,18 +27,35 @@ interface State {
const ShortcutDetail = () => { const ShortcutDetail = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const params = useParams();
const navigateTo = useNavigateTo(); const navigateTo = useNavigateTo();
const shortcutId = (useLoaderData() as Shortcut).id; const shortcutId = Number(params.shortcutId);
const shortcut = shortcutService.getShortcutById(shortcutId) as Shortcut; const shortcutStore = useShortcutStore();
const userStore = useUserStore();
const shortcut = shortcutStore.getShortcutById(shortcutId);
const currentUser = useUserStore().getCurrentUser(); const currentUser = useUserStore().getCurrentUser();
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
showEditDrawer: false, showEditDrawer: false,
}); });
const [showQRCodeDialog, setShowQRCodeDialog] = useState<boolean>(false); const [showQRCodeDialog, setShowQRCodeDialog] = useState<boolean>(false);
const loadingState = useLoading(true);
const creator = userStore.getUserById(shortcut.creatorId);
const havePermission = currentUser.role === Role.ADMIN || shortcut.creatorId === currentUser.id; const havePermission = currentUser.role === Role.ADMIN || shortcut.creatorId === currentUser.id;
const shortcutLink = absolutifyLink(`/s/${shortcut.name}`); const shortcutLink = absolutifyLink(`/s/${shortcut.name}`);
const favicon = getFaviconWithGoogleS2(shortcut.link); const favicon = getFaviconWithGoogleS2(shortcut.link);
useEffect(() => {
(async () => {
const shortcut = await shortcutStore.getOrFetchShortcutById(shortcutId);
await userStore.getOrFetchUserById(shortcut.creatorId);
loadingState.setFinish();
})();
}, [shortcutId]);
if (loadingState.isLoading) {
return null;
}
const handleCopyButtonClick = () => { const handleCopyButtonClick = () => {
copy(shortcutLink); copy(shortcutLink);
toast.success("Shortcut link copied to clipboard."); toast.success("Shortcut link copied to clipboard.");
@ -47,7 +67,7 @@ const ShortcutDetail = () => {
content: `Are you sure to delete shortcut \`${shortcut.name}\`? You cannot undo this action.`, content: `Are you sure to delete shortcut \`${shortcut.name}\`? You cannot undo this action.`,
style: "danger", style: "danger",
onConfirm: async () => { onConfirm: async () => {
await shortcutService.deleteShortcutById(shortcut.id); await shortcutStore.deleteShortcut(shortcut.id);
navigateTo("/", { navigateTo("/", {
replace: true, replace: true,
}); });
@ -151,19 +171,24 @@ const ShortcutDetail = () => {
<Tooltip title="Creator" variant="solid" placement="top" arrow> <Tooltip title="Creator" variant="solid" placement="top" arrow>
<div className="w-auto px-2 leading-6 flex flex-row justify-start items-center border rounded-full text-gray-500 text-sm dark:border-zinc-800"> <div className="w-auto px-2 leading-6 flex flex-row justify-start items-center border rounded-full text-gray-500 text-sm dark:border-zinc-800">
<Icon.User className="w-4 h-auto mr-1" /> <Icon.User className="w-4 h-auto mr-1" />
<span className="max-w-[4rem] sm:max-w-[6rem] truncate">{shortcut.creator.nickname}</span> <span className="max-w-[4rem] sm:max-w-[6rem] truncate">{creator.nickname}</span>
</div> </div>
</Tooltip> </Tooltip>
<Tooltip title={t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.description`)} variant="solid" placement="top" arrow> <Tooltip
title={t(`shortcut.visibility.${convertVisibilityFromPb(shortcut.visibility).toLowerCase()}.description`)}
variant="solid"
placement="top"
arrow
>
<div className="w-auto px-2 leading-6 flex flex-row justify-start items-center border rounded-full text-gray-500 text-sm dark:border-zinc-800"> <div className="w-auto px-2 leading-6 flex flex-row justify-start items-center border rounded-full text-gray-500 text-sm dark:border-zinc-800">
<VisibilityIcon className="w-4 h-auto mr-1" visibility={shortcut.visibility} /> <VisibilityIcon className="w-4 h-auto mr-1" visibility={shortcut.visibility} />
{t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.self`)} {t(`shortcut.visibility.${convertVisibilityFromPb(shortcut.visibility).toLowerCase()}.self`)}
</div> </div>
</Tooltip> </Tooltip>
<Tooltip title="View count" variant="solid" placement="top" arrow> <Tooltip title="View count" variant="solid" placement="top" arrow>
<div className="w-auto px-2 leading-6 flex flex-row justify-start items-center border rounded-full text-gray-500 text-sm dark:border-zinc-800"> <div className="w-auto px-2 leading-6 flex flex-row justify-start items-center border rounded-full text-gray-500 text-sm dark:border-zinc-800">
<Icon.BarChart2 className="w-4 h-auto mr-1" /> <Icon.BarChart2 className="w-4 h-auto mr-1" />
{shortcut.view} visits {shortcut.viewCount} visits
</div> </div>
</Tooltip> </Tooltip>
</div> </div>

View File

@ -11,7 +11,6 @@ import App from "../App";
import Root from "../layouts/Root"; import Root from "../layouts/Root";
import Home from "../pages/Home"; import Home from "../pages/Home";
import ShortcutDetail from "../pages/ShortcutDetail"; import ShortcutDetail from "../pages/ShortcutDetail";
import { shortcutService } from "../services";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -41,10 +40,6 @@ const router = createBrowserRouter([
{ {
path: "/shortcut/:shortcutId", path: "/shortcut/:shortcutId",
element: <ShortcutDetail />, element: <ShortcutDetail />,
loader: async ({ params }) => {
const shortcut = await shortcutService.getOrFetchShortcutById(Number(params.shortcutId));
return shortcut;
},
}, },
{ {
path: "/setting/general", path: "/setting/general",

View File

@ -1,3 +0,0 @@
import shortcutService from "./shortcutService";
export { shortcutService };

View File

@ -1,64 +0,0 @@
import * as api from "../helpers/api";
import store from "../stores";
import { createShortcut, deleteShortcut, patchShortcut, setShortcuts } from "../stores/modules/shortcut";
const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => {
return {
...shortcut,
createdTs: shortcut.createdTs * 1000,
updatedTs: shortcut.updatedTs * 1000,
};
};
const shortcutService = {
getState: () => {
return store.getState().shortcut;
},
getMyAllShortcuts: async () => {
const data = (await api.getShortcutList()).data;
const shortcuts = data.map((s) => convertResponseModelShortcut(s));
store.dispatch(setShortcuts(shortcuts));
},
getShortcutById: (id: ShortcutId) => {
for (const shortcut of shortcutService.getState().shortcutList) {
if (shortcut.id === id) {
return shortcut;
}
}
return null;
},
getOrFetchShortcutById: async (id: ShortcutId) => {
for (const shortcut of shortcutService.getState().shortcutList) {
if (shortcut.id === id) {
return shortcut;
}
}
const data = (await api.getShortcutById(id)).data;
const shortcut = convertResponseModelShortcut(data);
store.dispatch(createShortcut(shortcut));
return shortcut;
},
createShortcut: async (shortcutCreate: ShortcutCreate) => {
const data = (await api.createShortcut(shortcutCreate)).data;
const shortcut = convertResponseModelShortcut(data);
store.dispatch(createShortcut(shortcut));
},
patchShortcut: async (shortcutPatch: ShortcutPatch) => {
const data = (await api.patchShortcut(shortcutPatch)).data;
const shortcut = convertResponseModelShortcut(data);
store.dispatch(patchShortcut(shortcut));
},
deleteShortcutById: async (shortcutId: ShortcutId) => {
await api.deleteShortcutById(shortcutId);
store.dispatch(deleteShortcut(shortcutId));
},
};
export default shortcutService;

View File

@ -1,15 +0,0 @@
import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useSelector } from "react-redux";
import shortcutReducer from "./modules/shortcut";
const store = configureStore({
reducer: {
shortcut: shortcutReducer,
},
});
type AppState = ReturnType<typeof store.getState>;
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector;
export default store;

View File

@ -1,51 +0,0 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface State {
shortcutList: Shortcut[];
}
const shortcutSlice = createSlice({
name: "shortcut",
initialState: {
shortcutList: [],
} as State,
reducers: {
setShortcuts: (state, action: PayloadAction<Shortcut[]>) => {
return {
...state,
shortcutList: action.payload,
};
},
createShortcut: (state, action: PayloadAction<Shortcut>) => {
return {
...state,
shortcutList: state.shortcutList.concat(action.payload).sort((a, b) => b.createdTs - a.createdTs),
};
},
patchShortcut: (state, action: PayloadAction<Partial<Shortcut>>) => {
return {
...state,
shortcutList: state.shortcutList.map((s) => {
if (s.id === action.payload.id) {
return {
...s,
...action.payload,
};
} else {
return s;
}
}),
};
},
deleteShortcut: (state, action: PayloadAction<ShortcutId>) => {
return {
...state,
shortcutList: [...state.shortcutList].filter((shortcut) => shortcut.id !== action.payload),
};
},
},
});
export const { setShortcuts, createShortcut, patchShortcut, deleteShortcut } = shortcutSlice.actions;
export default shortcutSlice.reducer;

View File

@ -84,8 +84,6 @@ const useCollectionStore = create<CollectionState>()((set, get) => ({
throw new Error("Collection not found"); throw new Error("Collection not found");
} }
console.log("updatedCollection", updatedCollection);
const collectionMap = get().collectionMapById; const collectionMap = get().collectionMapById;
collectionMap[updatedCollection.id] = updatedCollection; collectionMap[updatedCollection.id] = updatedCollection;
set(collectionMap); set(collectionMap);

View File

@ -3,11 +3,14 @@ import { shortcutServiceClient } from "@/grpcweb";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service"; import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
interface ShortcutState { interface ShortcutState {
shortcutMapById: Record<ShortcutId, Shortcut>; shortcutMapById: Record<number, Shortcut>;
fetchShortcutList: () => Promise<Shortcut[]>; fetchShortcutList: () => Promise<Shortcut[]>;
getOrFetchShortcutById: (id: ShortcutId) => Promise<Shortcut>; getOrFetchShortcutById: (id: number) => Promise<Shortcut>;
getShortcutById: (id: ShortcutId) => Shortcut; getShortcutById: (id: number) => Shortcut;
getShortcutList: () => Shortcut[]; getShortcutList: () => Shortcut[];
createShortcut: (shortcut: Shortcut) => Promise<Shortcut>;
updateShortcut: (shortcut: Partial<Shortcut>) => Promise<Shortcut>;
deleteShortcut: (id: number) => Promise<void>;
} }
const useShortcutStore = create<ShortcutState>()((set, get) => ({ const useShortcutStore = create<ShortcutState>()((set, get) => ({
@ -21,7 +24,7 @@ const useShortcutStore = create<ShortcutState>()((set, get) => ({
set(shortcutMap); set(shortcutMap);
return shortcuts; return shortcuts;
}, },
getOrFetchShortcutById: async (id: ShortcutId) => { getOrFetchShortcutById: async (id: number) => {
const shortcutMap = get().shortcutMapById; const shortcutMap = get().shortcutMapById;
if (shortcutMap[id]) { if (shortcutMap[id]) {
return shortcutMap[id] as Shortcut; return shortcutMap[id] as Shortcut;
@ -38,13 +41,50 @@ const useShortcutStore = create<ShortcutState>()((set, get) => ({
set(shortcutMap); set(shortcutMap);
return shortcut; return shortcut;
}, },
getShortcutById: (id: ShortcutId) => { getShortcutById: (id: number) => {
const shortcutMap = get().shortcutMapById; const shortcutMap = get().shortcutMapById;
return shortcutMap[id] as Shortcut; return shortcutMap[id] || unknownShortcut;
}, },
getShortcutList: () => { getShortcutList: () => {
return Object.values(get().shortcutMapById); return Object.values(get().shortcutMapById);
}, },
createShortcut: async (shortcut: Shortcut) => {
const { shortcut: createdShortcut } = await shortcutServiceClient.createShortcut({
shortcut: shortcut,
});
if (!createdShortcut) {
throw new Error(`Failed to create shortcut`);
}
const shortcutMap = get().shortcutMapById;
shortcutMap[createdShortcut.id] = createdShortcut;
set(shortcutMap);
return createdShortcut;
},
updateShortcut: async (shortcut: Partial<Shortcut>) => {
const { shortcut: updatedShortcut } = await shortcutServiceClient.updateShortcut({
shortcut: shortcut,
});
if (!updatedShortcut) {
throw new Error(`Failed to update shortcut`);
}
const shortcutMap = get().shortcutMapById;
shortcutMap[updatedShortcut.id] = updatedShortcut;
set(shortcutMap);
return updatedShortcut;
},
deleteShortcut: async (id: number) => {
await shortcutServiceClient.deleteShortcut({
id: id,
});
const shortcutMap = get().shortcutMapById;
delete shortcutMap[id];
set(shortcutMap);
},
})); }));
const unknownShortcut: Shortcut = Shortcut.fromPartial({
id: -1,
name: "Unknown",
});
export default useShortcutStore; export default useShortcutStore;

View File

@ -103,7 +103,7 @@ const useUserStore = create<UserState>()((set, get) => ({
}, },
getUserById: (id: number) => { getUserById: (id: number) => {
const userMap = get().userMapById; const userMap = get().userMapById;
return userMap[id] as User; return userMap[id] || unknownUser;
}, },
getCurrentUser: () => { getCurrentUser: () => {
const userMap = get().userMapById; const userMap = get().userMapById;
@ -148,4 +148,10 @@ const useUserStore = create<UserState>()((set, get) => ({
}, },
})); }));
const unknownUser: User = User.fromPartial({
id: -1,
email: "Unknown",
nickname: "Unknown",
});
export default useUserStore; export default useUserStore;

View File

@ -1,5 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { Visibility } from "@/types/proto/api/v2/common";
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
import { User } from "@/types/proto/api/v2/user_service"; import { User } from "@/types/proto/api/v2/user_service";
export interface Filter { export interface Filter {
@ -102,11 +104,15 @@ export const getOrderedShortcutList = (shortcutList: Shortcut[], order: Order) =
if (field === "name") { if (field === "name") {
return direction === "asc" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name); return direction === "asc" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
} else if (field === "createdTs") { } else if (field === "createdTs") {
return direction === "asc" ? a.createdTs - b.createdTs : b.createdTs - a.createdTs; return direction === "asc"
? getDateTimestamp(a.createdTime) - getDateTimestamp(b.createdTime)
: getDateTimestamp(b.createdTime) - getDateTimestamp(a.createdTime);
} else if (field === "updatedTs") { } else if (field === "updatedTs") {
return direction === "asc" ? a.updatedTs - b.updatedTs : b.updatedTs - a.updatedTs; return direction === "asc"
? getDateTimestamp(a.updatedTime) - getDateTimestamp(b.updatedTime)
: getDateTimestamp(b.updatedTime) - getDateTimestamp(a.updatedTime);
} else if (field === "view") { } else if (field === "view") {
return direction === "asc" ? a.view - b.view : b.view - a.view; return direction === "asc" ? a.viewCount - b.viewCount : b.viewCount - a.viewCount;
} else { } else {
return 0; return 0;
} }
@ -114,4 +120,8 @@ export const getOrderedShortcutList = (shortcutList: Shortcut[], order: Order) =
return orderedShortcutList; return orderedShortcutList;
}; };
const getDateTimestamp = (date: Date = new Date()) => {
return new Date(date).getTime();
};
export default useViewStore; export default useViewStore;

View File

@ -1 +0,0 @@
type RowStatus = "NORMAL" | "ARCHIVED";

View File

@ -1,54 +0,0 @@
type ShortcutId = number;
type Visibility = "PRIVATE" | "WORKSPACE" | "PUBLIC";
interface OpenGraphMetadata {
title: string;
description: string;
image: string;
}
interface Shortcut {
id: ShortcutId;
creatorId: number;
creator: User;
createdTs: TimeStamp;
updatedTs: TimeStamp;
rowStatus: RowStatus;
name: string;
link: string;
title: string;
description: string;
visibility: Visibility;
tags: string[];
openGraphMetadata: OpenGraphMetadata;
view: number;
}
interface ShortcutCreate {
name: string;
link: string;
title: string;
description: string;
visibility: Visibility;
tags: string[];
openGraphMetadata: OpenGraphMetadata;
}
interface ShortcutPatch {
id: ShortcutId;
name?: string;
link?: string;
title?: string;
description?: string;
visibility?: Visibility;
tags?: string[];
openGraphMetadata?: OpenGraphMetadata;
}
interface ShortcutFind {
tag?: string;
}

View File

@ -1,15 +0,0 @@
import { Shortcut as ShortcutPb } from "@/types/proto/api/v2/shortcut_service";
export const convertShortcutFromPb = (shortcutMessage: ShortcutPb): Shortcut => {
return {
id: shortcutMessage.id,
creatorId: shortcutMessage.creatorId,
createdTs: shortcutMessage.createdTime?.getTime() || 0,
updatedTs: shortcutMessage.updatedTime?.getTime() || 0,
name: shortcutMessage.name,
link: shortcutMessage.link,
title: shortcutMessage.title,
description: shortcutMessage.description,
tags: shortcutMessage.tags,
} as Shortcut;
};