mirror of https://github.com/usememos/memos
refactor: migrate eslint
parent
d649d326ef
commit
b770042a8a
@ -1,50 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react", "@typescript-eslint", "prettier"],
|
||||
"ignorePatterns": ["node_modules", "dist", "public", "src/assets"],
|
||||
"rules": {
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
],
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-explicit-any": ["off"],
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/jsx-no-target-blank": "off",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "VariableDeclarator[init.callee.name='useTranslation'] > ObjectPattern > Property[key.name='t']:not([parent.declarations.0.init.callee.object.name='i18n'])",
|
||||
"message": "Destructuring 't' from useTranslation is not allowed. Please use the 'useTranslate' hook from '@/utils/i18n'."
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["src/utils/i18n.ts"],
|
||||
"rules": {
|
||||
"no-restricted-syntax": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import eslint from "@eslint/js";
|
||||
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
...tseslint.config(eslint.configs.recommended, tseslint.configs.recommended),
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
ignores: ["**/dist/**", "**/node_modules/**", "**/proto/**"],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-explicit-any": ["off"],
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/jsx-no-target-blank": "off",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
selector:
|
||||
"VariableDeclarator[init.callee.name='useTranslation'] > ObjectPattern > Property[key.name='t']:not([parent.declarations.0.init.callee.object.name='i18n'])",
|
||||
message: "Destructuring 't' from useTranslation is not allowed. Please use the 'useTranslate' hook from '@/utils/i18n'.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/utils/i18n.ts"],
|
||||
rules: {
|
||||
"no-restricted-syntax": "off",
|
||||
},
|
||||
},
|
||||
];
|
File diff suppressed because it is too large
Load Diff
@ -1,147 +1,147 @@
|
||||
import { Radio, RadioGroup } from "@mui/joy";
|
||||
import { Button, Input } 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 { useTranslate } from "@/utils/i18n";
|
||||
import { generateDialog } from "./Dialog";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
description: string;
|
||||
expiration: number;
|
||||
}
|
||||
|
||||
const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => {
|
||||
const { destroy, onConfirm } = props;
|
||||
const t = useTranslate();
|
||||
const currentUser = useCurrentUser();
|
||||
const [state, setState] = useState({
|
||||
description: "",
|
||||
expiration: 3600 * 8,
|
||||
});
|
||||
const requestState = useLoading(false);
|
||||
|
||||
const expirationOptions = [
|
||||
{
|
||||
label: t("setting.access-token-section.create-dialog.duration-8h"),
|
||||
value: 3600 * 8,
|
||||
},
|
||||
{
|
||||
label: t("setting.access-token-section.create-dialog.duration-1m"),
|
||||
value: 3600 * 24 * 30,
|
||||
},
|
||||
{
|
||||
label: t("setting.access-token-section.create-dialog.duration-never"),
|
||||
value: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const setPartialState = (partialState: Partial<State>) => {
|
||||
setState({
|
||||
...state,
|
||||
...partialState,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDescriptionInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
description: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRoleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
expiration: Number(e.target.value),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
if (!state.description) {
|
||||
toast.error(t("message.description-is-required"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await userServiceClient.createUserAccessToken({
|
||||
name: currentUser.name,
|
||||
description: state.description,
|
||||
expiresAt: state.expiration ? new Date(Date.now() + state.expiration * 1000) : undefined,
|
||||
});
|
||||
|
||||
onConfirm();
|
||||
destroy();
|
||||
} catch (error: any) {
|
||||
toast.error(error.details);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg">
|
||||
<div className="flex flex-row justify-between items-center w-full mb-4 gap-2">
|
||||
<p>{t("setting.access-token-section.create-dialog.create-access-token")}</p>
|
||||
<Button size="sm" variant="plain" onClick={() => destroy()}>
|
||||
<XIcon className="w-5 h-auto" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start items-start !w-80">
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
{t("setting.access-token-section.create-dialog.description")} <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder={t("setting.access-token-section.create-dialog.some-description")}
|
||||
value={state.description}
|
||||
onChange={handleDescriptionInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
{t("setting.access-token-section.create-dialog.expiration")} <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="w-full flex flex-row justify-start items-center text-base">
|
||||
<RadioGroup orientation="horizontal" value={state.expiration} onChange={handleRoleInputChange}>
|
||||
{expirationOptions.map((option) => (
|
||||
<Radio key={option.value} value={option.value} checked={state.expiration === option.value} label={option.label} />
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
|
||||
<Button variant="plain" disabled={requestState.isLoading} onClick={destroy}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button color="primary" disabled={requestState.isLoading} onClick={handleSaveBtnClick}>
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function showCreateAccessTokenDialog(onConfirm: () => void) {
|
||||
generateDialog(
|
||||
{
|
||||
className: "create-access-token-dialog",
|
||||
dialogName: "create-access-token-dialog",
|
||||
},
|
||||
CreateAccessTokenDialog,
|
||||
{
|
||||
onConfirm,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export default showCreateAccessTokenDialog;
|
||||
import { Radio, RadioGroup } from "@mui/joy";
|
||||
import { Button, Input } 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 { useTranslate } from "@/utils/i18n";
|
||||
import { generateDialog } from "./Dialog";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
description: string;
|
||||
expiration: number;
|
||||
}
|
||||
|
||||
const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => {
|
||||
const { destroy, onConfirm } = props;
|
||||
const t = useTranslate();
|
||||
const currentUser = useCurrentUser();
|
||||
const [state, setState] = useState({
|
||||
description: "",
|
||||
expiration: 3600 * 8,
|
||||
});
|
||||
const requestState = useLoading(false);
|
||||
|
||||
const expirationOptions = [
|
||||
{
|
||||
label: t("setting.access-token-section.create-dialog.duration-8h"),
|
||||
value: 3600 * 8,
|
||||
},
|
||||
{
|
||||
label: t("setting.access-token-section.create-dialog.duration-1m"),
|
||||
value: 3600 * 24 * 30,
|
||||
},
|
||||
{
|
||||
label: t("setting.access-token-section.create-dialog.duration-never"),
|
||||
value: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const setPartialState = (partialState: Partial<State>) => {
|
||||
setState({
|
||||
...state,
|
||||
...partialState,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDescriptionInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
description: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRoleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
expiration: Number(e.target.value),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
if (!state.description) {
|
||||
toast.error(t("message.description-is-required"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await userServiceClient.createUserAccessToken({
|
||||
name: currentUser.name,
|
||||
description: state.description,
|
||||
expiresAt: state.expiration ? new Date(Date.now() + state.expiration * 1000) : undefined,
|
||||
});
|
||||
|
||||
onConfirm();
|
||||
destroy();
|
||||
} catch (error: any) {
|
||||
toast.error(error.details);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg">
|
||||
<div className="flex flex-row justify-between items-center w-full mb-4 gap-2">
|
||||
<p>{t("setting.access-token-section.create-dialog.create-access-token")}</p>
|
||||
<Button size="sm" variant="plain" onClick={() => destroy()}>
|
||||
<XIcon className="w-5 h-auto" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start items-start !w-80">
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
{t("setting.access-token-section.create-dialog.description")} <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder={t("setting.access-token-section.create-dialog.some-description")}
|
||||
value={state.description}
|
||||
onChange={handleDescriptionInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
{t("setting.access-token-section.create-dialog.expiration")} <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="w-full flex flex-row justify-start items-center text-base">
|
||||
<RadioGroup orientation="horizontal" value={state.expiration} onChange={handleRoleInputChange}>
|
||||
{expirationOptions.map((option) => (
|
||||
<Radio key={option.value} value={option.value} checked={state.expiration === option.value} label={option.label} />
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
|
||||
<Button variant="plain" disabled={requestState.isLoading} onClick={destroy}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button color="primary" disabled={requestState.isLoading} onClick={handleSaveBtnClick}>
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function showCreateAccessTokenDialog(onConfirm: () => void) {
|
||||
generateDialog(
|
||||
{
|
||||
className: "create-access-token-dialog",
|
||||
dialogName: "create-access-token-dialog",
|
||||
},
|
||||
CreateAccessTokenDialog,
|
||||
{
|
||||
onConfirm,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export default showCreateAccessTokenDialog;
|
||||
|
@ -1,160 +1,160 @@
|
||||
import { Button, Input } from "@usememos/mui";
|
||||
import { XIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { webhookServiceClient } from "@/grpcweb";
|
||||
import useLoading from "@/hooks/useLoading";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { generateDialog } from "./Dialog";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
webhookId?: number;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
|
||||
const { webhookId, destroy, onConfirm } = props;
|
||||
const t = useTranslate();
|
||||
const [state, setState] = useState({
|
||||
name: "",
|
||||
url: "",
|
||||
});
|
||||
const requestState = useLoading(false);
|
||||
const isCreating = webhookId === undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (webhookId) {
|
||||
webhookServiceClient
|
||||
.getWebhook({
|
||||
id: webhookId,
|
||||
})
|
||||
.then((webhook) => {
|
||||
setState({
|
||||
name: webhook.name,
|
||||
url: webhook.url,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setPartialState = (partialState: Partial<State>) => {
|
||||
setState({
|
||||
...state,
|
||||
...partialState,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
name: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUrlInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
url: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
if (!state.name || !state.url) {
|
||||
toast.error(t("message.fill-all-required-fields"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isCreating) {
|
||||
await webhookServiceClient.createWebhook({
|
||||
name: state.name,
|
||||
url: state.url,
|
||||
});
|
||||
} else {
|
||||
await webhookServiceClient.updateWebhook({
|
||||
webhook: {
|
||||
id: webhookId,
|
||||
name: state.name,
|
||||
url: state.url,
|
||||
},
|
||||
updateMask: ["name", "url"],
|
||||
});
|
||||
}
|
||||
|
||||
onConfirm();
|
||||
destroy();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.details);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg">
|
||||
<div className="flex flex-row justify-between items-center mb-4 gap-2 w-full">
|
||||
<p className="title-text">
|
||||
{isCreating ? t("setting.webhook-section.create-dialog.create-webhook") : t("setting.webhook-section.create-dialog.edit-webhook")}
|
||||
</p>
|
||||
<Button size="sm" variant="plain" onClick={() => destroy()}>
|
||||
<XIcon className="w-5 h-auto" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start items-start !w-80">
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
{t("setting.webhook-section.create-dialog.title")} <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder={t("setting.webhook-section.create-dialog.an-easy-to-remember-name")}
|
||||
value={state.name}
|
||||
onChange={handleTitleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
{t("setting.webhook-section.create-dialog.payload-url")} <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder={t("setting.webhook-section.create-dialog.url-example-post-receive")}
|
||||
value={state.url}
|
||||
onChange={handleUrlInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-end items-center mt-2 space-x-2">
|
||||
<Button variant="plain" disabled={requestState.isLoading} onClick={destroy}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button color="primary" disabled={requestState.isLoading} onClick={handleSaveBtnClick}>
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function showCreateWebhookDialog(onConfirm: () => void) {
|
||||
generateDialog(
|
||||
{
|
||||
className: "create-webhook-dialog",
|
||||
dialogName: "create-webhook-dialog",
|
||||
},
|
||||
CreateWebhookDialog,
|
||||
{
|
||||
onConfirm,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export default showCreateWebhookDialog;
|
||||
import { Button, Input } from "@usememos/mui";
|
||||
import { XIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { webhookServiceClient } from "@/grpcweb";
|
||||
import useLoading from "@/hooks/useLoading";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { generateDialog } from "./Dialog";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
webhookId?: number;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
|
||||
const { webhookId, destroy, onConfirm } = props;
|
||||
const t = useTranslate();
|
||||
const [state, setState] = useState({
|
||||
name: "",
|
||||
url: "",
|
||||
});
|
||||
const requestState = useLoading(false);
|
||||
const isCreating = webhookId === undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (webhookId) {
|
||||
webhookServiceClient
|
||||
.getWebhook({
|
||||
id: webhookId,
|
||||
})
|
||||
.then((webhook) => {
|
||||
setState({
|
||||
name: webhook.name,
|
||||
url: webhook.url,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setPartialState = (partialState: Partial<State>) => {
|
||||
setState({
|
||||
...state,
|
||||
...partialState,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
name: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUrlInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
url: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
if (!state.name || !state.url) {
|
||||
toast.error(t("message.fill-all-required-fields"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isCreating) {
|
||||
await webhookServiceClient.createWebhook({
|
||||
name: state.name,
|
||||
url: state.url,
|
||||
});
|
||||
} else {
|
||||
await webhookServiceClient.updateWebhook({
|
||||
webhook: {
|
||||
id: webhookId,
|
||||
name: state.name,
|
||||
url: state.url,
|
||||
},
|
||||
updateMask: ["name", "url"],
|
||||
});
|
||||
}
|
||||
|
||||
onConfirm();
|
||||
destroy();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.details);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg">
|
||||
<div className="flex flex-row justify-between items-center mb-4 gap-2 w-full">
|
||||
<p className="title-text">
|
||||
{isCreating ? t("setting.webhook-section.create-dialog.create-webhook") : t("setting.webhook-section.create-dialog.edit-webhook")}
|
||||
</p>
|
||||
<Button size="sm" variant="plain" onClick={() => destroy()}>
|
||||
<XIcon className="w-5 h-auto" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start items-start !w-80">
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
{t("setting.webhook-section.create-dialog.title")} <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder={t("setting.webhook-section.create-dialog.an-easy-to-remember-name")}
|
||||
value={state.name}
|
||||
onChange={handleTitleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
{t("setting.webhook-section.create-dialog.payload-url")} <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder={t("setting.webhook-section.create-dialog.url-example-post-receive")}
|
||||
value={state.url}
|
||||
onChange={handleUrlInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-end items-center mt-2 space-x-2">
|
||||
<Button variant="plain" disabled={requestState.isLoading} onClick={destroy}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button color="primary" disabled={requestState.isLoading} onClick={handleSaveBtnClick}>
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function showCreateWebhookDialog(onConfirm: () => void) {
|
||||
generateDialog(
|
||||
{
|
||||
className: "create-webhook-dialog",
|
||||
dialogName: "create-webhook-dialog",
|
||||
},
|
||||
CreateWebhookDialog,
|
||||
{
|
||||
onConfirm,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export default showCreateWebhookDialog;
|
||||
|
@ -1,187 +1,187 @@
|
||||
import { Autocomplete, AutocompleteOption, Chip } from "@mui/joy";
|
||||
import { Button, Checkbox } from "@usememos/mui";
|
||||
import { uniqBy } from "lodash-es";
|
||||
import { LinkIcon } from "lucide-react";
|
||||
import React, { useContext, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import useDebounce from "react-use/lib/useDebounce";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/Popover";
|
||||
import { memoServiceClient } from "@/grpcweb";
|
||||
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { extractMemoIdFromName } from "@/store/v1";
|
||||
import { Memo, MemoRelation_Memo, MemoRelation_Type } from "@/types/proto/api/v1/memo_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { EditorRefActions } from "../Editor";
|
||||
import { MemoEditorContext } from "../types";
|
||||
|
||||
interface Props {
|
||||
editorRef: React.RefObject<EditorRefActions>;
|
||||
}
|
||||
|
||||
const AddMemoRelationPopover = (props: Props) => {
|
||||
const { editorRef } = props;
|
||||
const t = useTranslate();
|
||||
const context = useContext(MemoEditorContext);
|
||||
const user = useCurrentUser();
|
||||
const [searchText, setSearchText] = useState<string>("");
|
||||
const [isFetching, setIsFetching] = useState<boolean>(true);
|
||||
const [fetchedMemos, setFetchedMemos] = useState<Memo[]>([]);
|
||||
const [selectedMemos, setSelectedMemos] = useState<Memo[]>([]);
|
||||
const [embedded, setEmbedded] = useState<boolean>(false);
|
||||
const [popoverOpen, setPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const filteredMemos = fetchedMemos.filter(
|
||||
(memo) =>
|
||||
!selectedMemos.includes(memo) &&
|
||||
memo.name !== context.memoName &&
|
||||
!context.relationList.some((relation) => relation.relatedMemo?.name === memo.name),
|
||||
);
|
||||
|
||||
useDebounce(
|
||||
async () => {
|
||||
if (!popoverOpen) return;
|
||||
|
||||
setIsFetching(true);
|
||||
try {
|
||||
const conditions = [];
|
||||
if (searchText) {
|
||||
conditions.push(`content_search == [${JSON.stringify(searchText)}]`);
|
||||
}
|
||||
const { memos } = await memoServiceClient.listMemos({
|
||||
parent: user.name,
|
||||
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
|
||||
oldFilter: conditions.length > 0 ? conditions.join(" && ") : undefined,
|
||||
});
|
||||
setFetchedMemos(memos);
|
||||
} catch (error: any) {
|
||||
toast.error(error.details);
|
||||
console.error(error);
|
||||
}
|
||||
setIsFetching(false);
|
||||
},
|
||||
300,
|
||||
[popoverOpen, searchText],
|
||||
);
|
||||
|
||||
const getHighlightedContent = (content: string) => {
|
||||
const index = content.toLowerCase().indexOf(searchText.toLowerCase());
|
||||
if (index === -1) {
|
||||
return content;
|
||||
}
|
||||
let before = content.slice(0, index);
|
||||
if (before.length > 20) {
|
||||
before = "..." + before.slice(before.length - 20);
|
||||
}
|
||||
const highlighted = content.slice(index, index + searchText.length);
|
||||
let after = content.slice(index + searchText.length);
|
||||
if (after.length > 20) {
|
||||
after = after.slice(0, 20) + "...";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{before}
|
||||
<mark className="font-medium">{highlighted}</mark>
|
||||
{after}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const addMemoRelations = async () => {
|
||||
// If embedded mode is enabled, embed the memo instead of creating a relation.
|
||||
if (embedded) {
|
||||
if (!editorRef.current) {
|
||||
toast.error(t("message.failed-to-embed-memo"));
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPosition = editorRef.current.getCursorPosition();
|
||||
const prevValue = editorRef.current.getContent().slice(0, cursorPosition);
|
||||
if (prevValue !== "" && !prevValue.endsWith("\n")) {
|
||||
editorRef.current.insertText("\n");
|
||||
}
|
||||
for (const memo of selectedMemos) {
|
||||
editorRef.current.insertText(`![[memos/${extractMemoIdFromName(memo.name)}]]\n`);
|
||||
}
|
||||
setTimeout(() => {
|
||||
editorRef.current?.scrollToCursor();
|
||||
editorRef.current?.focus();
|
||||
});
|
||||
} else {
|
||||
context.setRelationList(
|
||||
uniqBy(
|
||||
[
|
||||
...selectedMemos.map((memo) => ({
|
||||
memo: MemoRelation_Memo.fromPartial({ name: memo.name }),
|
||||
relatedMemo: MemoRelation_Memo.fromPartial({ name: memo.name }),
|
||||
type: MemoRelation_Type.REFERENCE,
|
||||
})),
|
||||
...context.relationList,
|
||||
].filter((relation) => relation.relatedMemo !== context.memoName),
|
||||
"relatedMemo",
|
||||
),
|
||||
);
|
||||
}
|
||||
setSelectedMemos([]);
|
||||
setPopoverOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||
<PopoverTrigger className="w-9 relative">
|
||||
<Button className="flex items-center justify-center" size="sm" variant="plain" asChild>
|
||||
<LinkIcon className="w-5 h-5 mx-auto p-0" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="center">
|
||||
<div className="w-[16rem] flex flex-col justify-start items-start">
|
||||
<Autocomplete
|
||||
className="w-full"
|
||||
size="md"
|
||||
clearOnBlur
|
||||
disableClearable
|
||||
placeholder={t("reference.search-placeholder")}
|
||||
noOptionsText={t("reference.no-memos-found")}
|
||||
options={filteredMemos}
|
||||
loading={isFetching}
|
||||
inputValue={searchText}
|
||||
value={selectedMemos}
|
||||
multiple
|
||||
onInputChange={(_, value) => setSearchText(value.trim())}
|
||||
getOptionKey={(memo) => memo.name}
|
||||
getOptionLabel={(memo) => memo.content}
|
||||
isOptionEqualToValue={(memo, value) => memo.name === value.name}
|
||||
renderOption={(props, memo) => (
|
||||
<AutocompleteOption {...props} key={memo.name}>
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<p className="text-xs text-gray-400 select-none">{memo.displayTime?.toLocaleString()}</p>
|
||||
<p className="mt-0.5 text-sm leading-5 line-clamp-2">{searchText ? getHighlightedContent(memo.content) : memo.snippet}</p>
|
||||
</div>
|
||||
</AutocompleteOption>
|
||||
)}
|
||||
renderTags={(memos) =>
|
||||
memos.map((memo) => (
|
||||
<Chip key={memo.name} className="!max-w-full !rounded" variant="outlined" color="neutral">
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<p className="text-xs text-gray-400 select-none">{memo.displayTime?.toLocaleString()}</p>
|
||||
<span className="w-full text-sm leading-5 truncate">{memo.content}</span>
|
||||
</div>
|
||||
</Chip>
|
||||
))
|
||||
}
|
||||
onChange={(_, value) => setSelectedMemos(value)}
|
||||
/>
|
||||
<div className="mt-2 w-full flex flex-row justify-end items-center gap-2">
|
||||
<Checkbox size="sm" label={"Embed"} checked={embedded} onChange={(e) => setEmbedded(e.target.checked)} />
|
||||
<Button size="sm" color="primary" onClick={addMemoRelations} disabled={selectedMemos.length === 0}>
|
||||
{t("common.add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddMemoRelationPopover;
|
||||
import { Autocomplete, AutocompleteOption, Chip } from "@mui/joy";
|
||||
import { Button, Checkbox } from "@usememos/mui";
|
||||
import { uniqBy } from "lodash-es";
|
||||
import { LinkIcon } from "lucide-react";
|
||||
import React, { useContext, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import useDebounce from "react-use/lib/useDebounce";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/Popover";
|
||||
import { memoServiceClient } from "@/grpcweb";
|
||||
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { extractMemoIdFromName } from "@/store/v1";
|
||||
import { Memo, MemoRelation_Memo, MemoRelation_Type } from "@/types/proto/api/v1/memo_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { EditorRefActions } from "../Editor";
|
||||
import { MemoEditorContext } from "../types";
|
||||
|
||||
interface Props {
|
||||
editorRef: React.RefObject<EditorRefActions>;
|
||||
}
|
||||
|
||||
const AddMemoRelationPopover = (props: Props) => {
|
||||
const { editorRef } = props;
|
||||
const t = useTranslate();
|
||||
const context = useContext(MemoEditorContext);
|
||||
const user = useCurrentUser();
|
||||
const [searchText, setSearchText] = useState<string>("");
|
||||
const [isFetching, setIsFetching] = useState<boolean>(true);
|
||||
const [fetchedMemos, setFetchedMemos] = useState<Memo[]>([]);
|
||||
const [selectedMemos, setSelectedMemos] = useState<Memo[]>([]);
|
||||
const [embedded, setEmbedded] = useState<boolean>(false);
|
||||
const [popoverOpen, setPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const filteredMemos = fetchedMemos.filter(
|
||||
(memo) =>
|
||||
!selectedMemos.includes(memo) &&
|
||||
memo.name !== context.memoName &&
|
||||
!context.relationList.some((relation) => relation.relatedMemo?.name === memo.name),
|
||||
);
|
||||
|
||||
useDebounce(
|
||||
async () => {
|
||||
if (!popoverOpen) return;
|
||||
|
||||
setIsFetching(true);
|
||||
try {
|
||||
const conditions = [];
|
||||
if (searchText) {
|
||||
conditions.push(`content_search == [${JSON.stringify(searchText)}]`);
|
||||
}
|
||||
const { memos } = await memoServiceClient.listMemos({
|
||||
parent: user.name,
|
||||
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
|
||||
oldFilter: conditions.length > 0 ? conditions.join(" && ") : undefined,
|
||||
});
|
||||
setFetchedMemos(memos);
|
||||
} catch (error: any) {
|
||||
toast.error(error.details);
|
||||
console.error(error);
|
||||
}
|
||||
setIsFetching(false);
|
||||
},
|
||||
300,
|
||||
[popoverOpen, searchText],
|
||||
);
|
||||
|
||||
const getHighlightedContent = (content: string) => {
|
||||
const index = content.toLowerCase().indexOf(searchText.toLowerCase());
|
||||
if (index === -1) {
|
||||
return content;
|
||||
}
|
||||
let before = content.slice(0, index);
|
||||
if (before.length > 20) {
|
||||
before = "..." + before.slice(before.length - 20);
|
||||
}
|
||||
const highlighted = content.slice(index, index + searchText.length);
|
||||
let after = content.slice(index + searchText.length);
|
||||
if (after.length > 20) {
|
||||
after = after.slice(0, 20) + "...";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{before}
|
||||
<mark className="font-medium">{highlighted}</mark>
|
||||
{after}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const addMemoRelations = async () => {
|
||||
// If embedded mode is enabled, embed the memo instead of creating a relation.
|
||||
if (embedded) {
|
||||
if (!editorRef.current) {
|
||||
toast.error(t("message.failed-to-embed-memo"));
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPosition = editorRef.current.getCursorPosition();
|
||||
const prevValue = editorRef.current.getContent().slice(0, cursorPosition);
|
||||
if (prevValue !== "" && !prevValue.endsWith("\n")) {
|
||||
editorRef.current.insertText("\n");
|
||||
}
|
||||
for (const memo of selectedMemos) {
|
||||
editorRef.current.insertText(`![[memos/${extractMemoIdFromName(memo.name)}]]\n`);
|
||||
}
|
||||
setTimeout(() => {
|
||||
editorRef.current?.scrollToCursor();
|
||||
editorRef.current?.focus();
|
||||
});
|
||||
} else {
|
||||
context.setRelationList(
|
||||
uniqBy(
|
||||
[
|
||||
...selectedMemos.map((memo) => ({
|
||||
memo: MemoRelation_Memo.fromPartial({ name: memo.name }),
|
||||
relatedMemo: MemoRelation_Memo.fromPartial({ name: memo.name }),
|
||||
type: MemoRelation_Type.REFERENCE,
|
||||
})),
|
||||
...context.relationList,
|
||||
].filter((relation) => relation.relatedMemo !== context.memoName),
|
||||
"relatedMemo",
|
||||
),
|
||||
);
|
||||
}
|
||||
setSelectedMemos([]);
|
||||
setPopoverOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||
<PopoverTrigger className="w-9 relative">
|
||||
<Button className="flex items-center justify-center" size="sm" variant="plain" asChild>
|
||||
<LinkIcon className="w-5 h-5 mx-auto p-0" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="center">
|
||||
<div className="w-[16rem] flex flex-col justify-start items-start">
|
||||
<Autocomplete
|
||||
className="w-full"
|
||||
size="md"
|
||||
clearOnBlur
|
||||
disableClearable
|
||||
placeholder={t("reference.search-placeholder")}
|
||||
noOptionsText={t("reference.no-memos-found")}
|
||||
options={filteredMemos}
|
||||
loading={isFetching}
|
||||
inputValue={searchText}
|
||||
value={selectedMemos}
|
||||
multiple
|
||||
onInputChange={(_, value) => setSearchText(value.trim())}
|
||||
getOptionKey={(memo) => memo.name}
|
||||
getOptionLabel={(memo) => memo.content}
|
||||
isOptionEqualToValue={(memo, value) => memo.name === value.name}
|
||||
renderOption={(props, memo) => (
|
||||
<AutocompleteOption {...props} key={memo.name}>
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<p className="text-xs text-gray-400 select-none">{memo.displayTime?.toLocaleString()}</p>
|
||||
<p className="mt-0.5 text-sm leading-5 line-clamp-2">{searchText ? getHighlightedContent(memo.content) : memo.snippet}</p>
|
||||
</div>
|
||||
</AutocompleteOption>
|
||||
)}
|
||||
renderTags={(memos) =>
|
||||
memos.map((memo) => (
|
||||
<Chip key={memo.name} className="!max-w-full !rounded" variant="outlined" color="neutral">
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<p className="text-xs text-gray-400 select-none">{memo.displayTime?.toLocaleString()}</p>
|
||||
<span className="w-full text-sm leading-5 truncate">{memo.content}</span>
|
||||
</div>
|
||||
</Chip>
|
||||
))
|
||||
}
|
||||
onChange={(_, value) => setSelectedMemos(value)}
|
||||
/>
|
||||
<div className="mt-2 w-full flex flex-row justify-end items-center gap-2">
|
||||
<Checkbox size="sm" label={"Embed"} checked={embedded} onChange={(e) => setEmbedded(e.target.checked)} />
|
||||
<Button size="sm" color="primary" onClick={addMemoRelations} disabled={selectedMemos.length === 0}>
|
||||
{t("common.add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddMemoRelationPopover;
|
||||
|
Loading…
Reference in New Issue