mirror of https://github.com/usememos/memos
chore: refactor Settings UI structure (#5912)
Co-authored-by: memoclaw <265580040+memoclaw@users.noreply.github.com>pull/5915/head
parent
14480bfc46
commit
267f90a3ff
@ -0,0 +1,95 @@
|
||||
import { type ReactNode } from "react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SettingListProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SettingList = ({ children, className }: SettingListProps) => {
|
||||
return (
|
||||
<div className={cn("overflow-hidden rounded-lg border border-border bg-background divide-y divide-border", className)}>{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SettingListItemProps {
|
||||
label: ReactNode;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
controlClassName?: string;
|
||||
vertical?: boolean;
|
||||
}
|
||||
|
||||
export const SettingListItem = ({
|
||||
label,
|
||||
description,
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
contentClassName,
|
||||
controlClassName,
|
||||
vertical = false,
|
||||
}: SettingListItemProps) => {
|
||||
return (
|
||||
<div className={cn("flex min-w-0 flex-col gap-3 px-3 py-3", !vertical && "sm:flex-row sm:items-center sm:justify-between", className)}>
|
||||
<div className={cn("flex min-w-0 gap-2", contentClassName)}>
|
||||
{icon && <div className="mt-0.5 shrink-0 text-muted-foreground">{icon}</div>}
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-foreground">{label}</div>
|
||||
{description && <div className="mt-1 text-xs leading-5 text-muted-foreground">{description}</div>}
|
||||
</div>
|
||||
</div>
|
||||
{children && <div className={cn("flex min-w-0 items-center", !vertical && "sm:shrink-0", controlClassName)}>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SettingPanelProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
header?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
}
|
||||
|
||||
export const SettingPanel = ({ children, className, header, footer }: SettingPanelProps) => {
|
||||
return (
|
||||
<div className={cn("overflow-hidden rounded-lg border border-border bg-background", className)}>
|
||||
{header && <div className="border-b border-border px-3 py-2">{header}</div>}
|
||||
{children}
|
||||
{footer && <div className="border-t border-border bg-muted/20 px-3 py-2">{footer}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SettingCodeEditorProps {
|
||||
label: string;
|
||||
description: string;
|
||||
value: string;
|
||||
placeholder: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const SettingCodeEditor = ({ label, description, value, placeholder, onChange }: SettingCodeEditorProps) => {
|
||||
return (
|
||||
<SettingPanel
|
||||
header={
|
||||
<>
|
||||
<div className="text-sm font-medium text-foreground">{label}</div>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">{description}</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Textarea
|
||||
className="min-h-24 rounded-none border-0 font-mono shadow-none focus-visible:ring-0"
|
||||
rows={4}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
/>
|
||||
</SettingPanel>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,120 @@
|
||||
import {
|
||||
CogIcon,
|
||||
DatabaseIcon,
|
||||
HeartHandshakeIcon,
|
||||
KeyIcon,
|
||||
LibraryIcon,
|
||||
type LucideIcon,
|
||||
Settings2Icon,
|
||||
TagsIcon,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
WebhookIcon,
|
||||
} from "lucide-react";
|
||||
import { type ComponentType } from "react";
|
||||
import AISection from "@/components/Settings/AISection";
|
||||
import InstanceSection from "@/components/Settings/InstanceSection";
|
||||
import MemberSection from "@/components/Settings/MemberSection";
|
||||
import MemoRelatedSettings from "@/components/Settings/MemoRelatedSettings";
|
||||
import MyAccountSection from "@/components/Settings/MyAccountSection";
|
||||
import PreferencesSection from "@/components/Settings/PreferencesSection";
|
||||
import SSOSection from "@/components/Settings/SSOSection";
|
||||
import StorageSection from "@/components/Settings/StorageSection";
|
||||
import TagsSection from "@/components/Settings/TagsSection";
|
||||
import WebhookSection from "@/components/Settings/WebhookSection";
|
||||
import { InstanceSetting_Key } from "@/types/proto/api/v1/instance_service_pb";
|
||||
|
||||
export type SettingSectionKey = "my-account" | "preference" | "webhook" | "member" | "system" | "memo" | "storage" | "sso" | "tags" | "ai";
|
||||
|
||||
type SettingSectionScope = "basic" | "admin";
|
||||
|
||||
export interface SettingSectionDefinition {
|
||||
key: SettingSectionKey;
|
||||
scope: SettingSectionScope;
|
||||
labelKey: `setting.${SettingSectionKey}.label`;
|
||||
icon: LucideIcon;
|
||||
component: ComponentType;
|
||||
preloadSettingKeys?: InstanceSetting_Key[];
|
||||
}
|
||||
|
||||
export const SETTINGS_SECTIONS: SettingSectionDefinition[] = [
|
||||
{
|
||||
key: "my-account",
|
||||
scope: "basic",
|
||||
labelKey: "setting.my-account.label",
|
||||
icon: UserIcon,
|
||||
component: MyAccountSection,
|
||||
},
|
||||
{
|
||||
key: "preference",
|
||||
scope: "basic",
|
||||
labelKey: "setting.preference.label",
|
||||
icon: CogIcon,
|
||||
component: PreferencesSection,
|
||||
},
|
||||
{
|
||||
key: "webhook",
|
||||
scope: "basic",
|
||||
labelKey: "setting.webhook.label",
|
||||
icon: WebhookIcon,
|
||||
component: WebhookSection,
|
||||
},
|
||||
{
|
||||
key: "member",
|
||||
scope: "admin",
|
||||
labelKey: "setting.member.label",
|
||||
icon: UsersIcon,
|
||||
component: MemberSection,
|
||||
},
|
||||
{
|
||||
key: "system",
|
||||
scope: "admin",
|
||||
labelKey: "setting.system.label",
|
||||
icon: Settings2Icon,
|
||||
component: InstanceSection,
|
||||
},
|
||||
{
|
||||
key: "memo",
|
||||
scope: "admin",
|
||||
labelKey: "setting.memo.label",
|
||||
icon: LibraryIcon,
|
||||
component: MemoRelatedSettings,
|
||||
},
|
||||
{
|
||||
key: "tags",
|
||||
scope: "admin",
|
||||
labelKey: "setting.tags.label",
|
||||
icon: TagsIcon,
|
||||
component: TagsSection,
|
||||
preloadSettingKeys: [InstanceSetting_Key.TAGS],
|
||||
},
|
||||
{
|
||||
key: "storage",
|
||||
scope: "admin",
|
||||
labelKey: "setting.storage.label",
|
||||
icon: DatabaseIcon,
|
||||
component: StorageSection,
|
||||
preloadSettingKeys: [InstanceSetting_Key.STORAGE],
|
||||
},
|
||||
{
|
||||
key: "sso",
|
||||
scope: "admin",
|
||||
labelKey: "setting.sso.label",
|
||||
icon: KeyIcon,
|
||||
component: SSOSection,
|
||||
},
|
||||
{
|
||||
key: "ai",
|
||||
scope: "admin",
|
||||
labelKey: "setting.ai.label",
|
||||
icon: HeartHandshakeIcon,
|
||||
component: AISection,
|
||||
preloadSettingKeys: [InstanceSetting_Key.AI],
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_SETTING_SECTION: SettingSectionKey = "my-account";
|
||||
|
||||
export const isSettingSectionKey = (value: string): value is SettingSectionKey => {
|
||||
return SETTINGS_SECTIONS.some((section) => section.key === value);
|
||||
};
|
||||
@ -0,0 +1,36 @@
|
||||
import { useCallback } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useInstance } from "@/contexts/InstanceContext";
|
||||
import { handleError } from "@/lib/error";
|
||||
import { InstanceSetting, InstanceSetting_Key } from "@/types/proto/api/v1/instance_service_pb";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
|
||||
interface SaveInstanceSettingOptions {
|
||||
key: InstanceSetting_Key;
|
||||
setting: InstanceSetting;
|
||||
errorContext: string;
|
||||
}
|
||||
|
||||
export const buildInstanceSettingName = (key: InstanceSetting_Key) => `instance/settings/${InstanceSetting_Key[key]}`;
|
||||
|
||||
const useInstanceSettingUpdater = () => {
|
||||
const t = useTranslate();
|
||||
const { updateSetting, fetchSetting } = useInstance();
|
||||
|
||||
return useCallback(
|
||||
async ({ key, setting, errorContext }: SaveInstanceSettingOptions) => {
|
||||
try {
|
||||
await updateSetting(setting);
|
||||
await fetchSetting(key);
|
||||
toast.success(t("message.update-succeed"));
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
await handleError(error, toast.error, { context: errorContext });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[fetchSetting, t, updateSetting],
|
||||
);
|
||||
};
|
||||
|
||||
export default useInstanceSettingUpdater;
|
||||
Loading…
Reference in New Issue