refactor(ui): refactor emails state using zustand store

This commit is contained in:
Fu Diwei 2024-12-11 16:42:23 +08:00
parent 83ba3d4450
commit b744363736
21 changed files with 141 additions and 166 deletions

View File

@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
@ -10,10 +10,8 @@ import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PbErrorData } from "@/domain/base";
import { EmailsSetting } from "@/domain/settings";
import { update } from "@/repository/settings";
import { useConfigContext } from "@/providers/config";
import { type PbErrorData } from "@/domain/base";
import { useContactStore } from "@/stores/contact";
type EmailsEditProps = {
className?: string;
@ -21,10 +19,7 @@ type EmailsEditProps = {
};
const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
const {
config: { emails },
setEmails,
} = useConfigContext();
const { emails, setEmails, fetchEmails } = useContactStore();
const [open, setOpen] = useState(false);
const { t } = useTranslation();
@ -40,30 +35,21 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
},
});
useEffect(() => {
fetchEmails();
}, []);
const onSubmit = async (data: z.infer<typeof formSchema>) => {
if ((emails.content as EmailsSetting).emails.includes(data.email)) {
if (emails.includes(data.email)) {
form.setError("email", {
message: "common.errmsg.email_duplicate",
});
return;
}
// 保存到 config
const newEmails = [...(emails.content as EmailsSetting).emails, data.email];
try {
const resp = await update({
...emails,
name: "emails",
content: {
emails: newEmails,
},
});
await setEmails([...emails, data.email]);
// 更新本地状态
setEmails(resp);
// 关闭弹窗
form.reset();
form.clearErrors();

View File

@ -8,7 +8,7 @@ import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { NotifyChannels, NotifyChannelBark } from "@/domain/settings";
import { update } from "@/repository/settings";
import { save } from "@/repository/settings";
import { useNotifyContext } from "@/providers/notify";
import { notifyTest } from "@/api/notify";
import Show from "@/components/Show";
@ -96,7 +96,7 @@ const Bark = () => {
const handleSaveClick = async () => {
try {
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {
@ -160,7 +160,7 @@ const Bark = () => {
setBark(newData);
try {
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {

View File

@ -9,7 +9,7 @@ import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { NotifyChannelDingTalk, NotifyChannels } from "@/domain/settings";
import { useNotifyContext } from "@/providers/notify";
import { update } from "@/repository/settings";
import { save } from "@/repository/settings";
import Show from "@/components/Show";
import { notifyTest } from "@/api/notify";
@ -96,7 +96,7 @@ const DingTalk = () => {
const handleSaveClick = async () => {
try {
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {
@ -160,7 +160,7 @@ const DingTalk = () => {
setDingtalk(newData);
try {
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {

View File

@ -9,7 +9,7 @@ import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { NotifyChannelEmail, NotifyChannels } from "@/domain/settings";
import { useNotifyContext } from "@/providers/notify";
import { update } from "@/repository/settings";
import { save } from "@/repository/settings";
import Show from "@/components/Show";
import { notifyTest } from "@/api/notify";
@ -119,7 +119,7 @@ const Mail = () => {
const handleSaveClick = async () => {
try {
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {
@ -183,7 +183,7 @@ const Mail = () => {
setMail(newData);
try {
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {

View File

@ -5,7 +5,7 @@ import { Label } from "@/components/ui/label";
import { useNotifyContext } from "@/providers/notify";
import { NotifyChannelLark, NotifyChannels } from "@/domain/settings";
import { useEffect, useState } from "react";
import { update } from "@/repository/settings";
import { save } from "@/repository/settings";
import { getErrMsg } from "@/utils/error";
import { useToast } from "@/components/ui/use-toast";
import { useTranslation } from "react-i18next";
@ -92,7 +92,7 @@ const Lark = () => {
const handleSaveClick = async () => {
try {
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {
@ -156,7 +156,7 @@ const Lark = () => {
setLark(newData);
try {
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {

View File

@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/components/ui/use-toast";
import { defaultNotifyTemplate, NotifyTemplates, NotifyTemplate as NotifyTemplateT } from "@/domain/settings";
import { getSetting, update } from "@/repository/settings";
import { get, save } from "@/repository/settings";
const NotifyTemplate = () => {
const [id, setId] = useState("");
@ -17,7 +17,7 @@ const NotifyTemplate = () => {
useEffect(() => {
const featchData = async () => {
const resp = await getSetting("templates");
const resp = await get("templates");
if (resp.content) {
setTemplates((resp.content as NotifyTemplates).notifyTemplates);
@ -50,7 +50,7 @@ const NotifyTemplate = () => {
};
const handleSaveClick = async () => {
const resp = await update({
const resp = await save({
id: id,
content: {
notifyTemplates: templates,

View File

@ -9,7 +9,7 @@ import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { isValidURL } from "@/utils/url";
import { NotifyChannels, NotifyChannelServerChan } from "@/domain/settings";
import { update } from "@/repository/settings";
import { save } from "@/repository/settings";
import { useNotifyContext } from "@/providers/notify";
import { notifyTest } from "@/api/notify";
import Show from "@/components/Show";
@ -103,7 +103,7 @@ const ServerChan = () => {
return;
}
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {
@ -167,7 +167,7 @@ const ServerChan = () => {
setServerChan(newData);
try {
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {

View File

@ -8,7 +8,7 @@ import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { NotifyChannels, NotifyChannelTelegram } from "@/domain/settings";
import { update } from "@/repository/settings";
import { save } from "@/repository/settings";
import { useNotifyContext } from "@/providers/notify";
import { notifyTest } from "@/api/notify";
import Show from "@/components/Show";
@ -96,7 +96,7 @@ const Telegram = () => {
const handleSaveClick = async () => {
try {
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {
@ -160,7 +160,7 @@ const Telegram = () => {
setTelegram(newData);
try {
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {

View File

@ -9,7 +9,7 @@ import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { isValidURL } from "@/utils/url";
import { NotifyChannels, NotifyChannelWebhook } from "@/domain/settings";
import { update } from "@/repository/settings";
import { save } from "@/repository/settings";
import { useNotifyContext } from "@/providers/notify";
import { notifyTest } from "@/api/notify";
import Show from "@/components/Show";
@ -103,7 +103,7 @@ const Webhook = () => {
return;
}
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {
@ -167,7 +167,7 @@ const Webhook = () => {
setWebhook(newData);
try {
const resp = await update({
const resp = await save({
...config,
name: "notifyChannels",
content: {

View File

@ -1,5 +1,4 @@
import { memo } from "react";
import { memo, useEffect } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import z from "zod";
@ -16,8 +15,7 @@ import EmailsEdit from "@/components/certimate/EmailsEdit";
import StringList from "@/components/certimate/StringList";
import { accessProvidersMap } from "@/domain/access";
import { EmailsSetting } from "@/domain/settings";
import { useContactStore } from "@/stores/contact";
import { useConfigContext } from "@/providers/config";
import { Switch } from "@/components/ui/switch";
import { TooltipFast } from "@/components/ui/tooltip";
@ -36,8 +34,13 @@ const ApplyForm = ({ data }: ApplyFormProps) => {
const { updateNode } = useWorkflowStore(useShallow(selectState));
const {
config: { accesses, emails },
config: { accesses },
} = useConfigContext();
const { emails, fetchEmails } = useContactStore();
useEffect(() => {
fetchEmails();
}, []);
const { t } = useTranslation();
@ -141,7 +144,7 @@ const ApplyForm = ({ data }: ApplyFormProps) => {
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.application.form.email.list")}</SelectLabel>
{(emails.content as EmailsSetting).emails.map((item) => (
{emails.map((item) => (
<SelectItem key={item} value={item}>
<div>{item}</div>
</SelectItem>

View File

@ -1,10 +1,11 @@
export type Setting<T> = {
id?: string;
name?: string;
content?: T;
};
import { type BaseModel } from "pocketbase";
export type EmailsSetting = {
export interface Settings<T> extends BaseModel {
name: string;
content: T;
}
export type EmailsSettingsContent = {
emails: string[];
};

View File

@ -1,7 +1,7 @@
{
"access.page.title": "授权管理",
"access.nodata": "暂无授权信息,请先新建",
"access.nodata": "暂无授权信息,请先新建授权",
"access.action.add": "新建授权",
"access.action.edit": "编辑授权",

View File

@ -1,7 +1,7 @@
{
"workflow.page.title": "工作流",
"workflow.nodata": "暂无工作流,请先新建",
"workflow.nodata": "暂无工作流,请先新建工作流",
"workflow.action.create": "新建工作流",
"workflow.action.edit": "编辑工作流",

View File

@ -7,7 +7,7 @@ import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
import AccessEditDialog from "@/components/certimate/AccessEditDialog";
import { Access as AccessType, accessProvidersMap } from "@/domain/access";
import { accessProvidersMap, type Access as AccessType } from "@/domain/access";
import { remove as removeAccess } from "@/repository/access";
import { useConfigContext } from "@/providers/config";
@ -120,10 +120,10 @@ const AccessList = () => {
try {
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const items = configContext.config.accesses.slice(startIndex, endIndex);
const items = configContext.config?.accesses?.slice(startIndex, endIndex) ?? [];
setTableData(items);
setTableTotal(configContext.config.accesses.length);
setTableTotal(configContext.config?.accesses?.length ?? 0);
} catch (err) {
if (err instanceof ClientResponseError && err.isAbort) {
return;

View File

@ -12,14 +12,14 @@ import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { SSLProvider as SSLProviderType, SSLProviderSetting, Setting } from "@/domain/settings";
import { getSetting, update } from "@/repository/settings";
import { SSLProvider as SSLProviderType, SSLProviderSetting, Settings } from "@/domain/settings";
import { get, save } from "@/repository/settings";
import { produce } from "immer";
type SSLProviderContext = {
setting: Setting<SSLProviderSetting>;
onSubmit: (data: Setting<SSLProviderSetting>) => void;
setConfig: (config: Setting<SSLProviderSetting>) => void;
setting: Settings<SSLProviderSetting>;
onSubmit: (data: Settings<SSLProviderSetting>) => void;
setConfig: (config: Settings<SSLProviderSetting>) => void;
};
const Context = createContext({} as SSLProviderContext);
@ -31,19 +31,18 @@ export const useSSLProviderContext = () => {
const SSLProvider = () => {
const { t } = useTranslation();
const [config, setConfig] = useState<Setting<SSLProviderSetting>>({
id: "",
const [config, setConfig] = useState<Settings<SSLProviderSetting>>({
content: {
provider: "letsencrypt",
config: {},
},
});
} as Settings<SSLProviderSetting>);
const { toast } = useToast();
useEffect(() => {
const fetchData = async () => {
const setting = await getSetting<SSLProviderSetting>("ssl-provider");
const setting = await get<SSLProviderSetting>("ssl-provider");
if (setting) {
setConfig(setting);
@ -74,10 +73,10 @@ const SSLProvider = () => {
return "";
};
const onSubmit = async (data: Setting<SSLProviderSetting>) => {
const onSubmit = async (data: Settings<SSLProviderSetting>) => {
try {
console.log(data);
const resp = await update({ ...data });
const resp = await save({ ...data });
setConfig(resp);
toast({
title: t("common.text.operation_succeeded"),

View File

@ -1,22 +1,17 @@
import { createContext, ReactNode, useCallback, useContext, useEffect, useReducer } from "react";
import { createContext, useCallback, useContext, useEffect, useReducer, type ReactNode } from "react";
import { Access } from "@/domain/access";
import { EmailsSetting, Setting } from "@/domain/settings";
import { list } from "@/repository/access";
import { getEmails } from "@/repository/settings";
import { type Access as AccessType } from "@/domain/access";
import { list as listAccess } from "@/repository/access";
import { configReducer } from "./reducer";
export type ConfigData = {
accesses: Access[];
emails: Setting<EmailsSetting>;
accesses: AccessType[];
};
export type ConfigContext = {
config: ConfigData;
setEmails: (email: Setting<EmailsSetting>) => void;
addAccess: (access: Access) => void;
updateAccess: (access: Access) => void;
addAccess: (access: AccessType) => void;
updateAccess: (access: AccessType) => void;
deleteAccess: (id: string) => void;
};
@ -24,45 +19,28 @@ const Context = createContext({} as ConfigContext);
export const useConfigContext = () => useContext(Context);
interface ConfigProviderProps {
children: ReactNode;
}
export const ConfigProvider = ({ children }: ConfigProviderProps) => {
export const ConfigProvider = ({ children }: { children: ReactNode }) => {
const [config, dispatchConfig] = useReducer(configReducer, {
accesses: [],
emails: { content: { emails: [] } },
});
useEffect(() => {
const featchData = async () => {
const data = await list();
const data = await listAccess();
dispatchConfig({ type: "SET_ACCESSES", payload: data });
};
featchData();
}, []);
useEffect(() => {
const featchEmails = async () => {
const emails = await getEmails();
dispatchConfig({ type: "SET_EMAILS", payload: emails });
};
featchEmails();
}, []);
const setEmails = useCallback((emails: Setting<EmailsSetting>) => {
dispatchConfig({ type: "SET_EMAILS", payload: emails });
}, []);
const deleteAccess = useCallback((id: string) => {
dispatchConfig({ type: "DELETE_ACCESS", payload: id });
}, []);
const addAccess = useCallback((access: Access) => {
const addAccess = useCallback((access: AccessType) => {
dispatchConfig({ type: "ADD_ACCESS", payload: access });
}, []);
const updateAccess = useCallback((access: Access) => {
const updateAccess = useCallback((access: AccessType) => {
dispatchConfig({ type: "UPDATE_ACCESS", payload: access });
}, []);
@ -71,15 +49,13 @@ export const ConfigProvider = ({ children }: ConfigProviderProps) => {
value={{
config: {
accesses: config.accesses,
emails: config.emails,
},
setEmails,
addAccess,
updateAccess,
deleteAccess,
}}
>
{children && children}
{children}
</Context.Provider>
);
};

View File

@ -1,14 +1,11 @@
import { Access } from "@/domain/access";
import { EmailsSetting, Setting } from "@/domain/settings";
import { ConfigData } from "./";
type Action =
| { type: "ADD_ACCESS"; payload: Access }
| { type: "DELETE_ACCESS"; payload: string }
| { type: "UPDATE_ACCESS"; payload: Access }
| { type: "SET_ACCESSES"; payload: Access[] }
| { type: "SET_EMAILS"; payload: Setting<EmailsSetting> }
| { type: "ADD_EMAIL"; payload: string };
| { type: "SET_ACCESSES"; payload: Access[] };
export const configReducer = (state: ConfigData, action: Action): ConfigData => {
switch (action.type) {
@ -36,23 +33,6 @@ export const configReducer = (state: ConfigData, action: Action): ConfigData =>
accesses: state.accesses.map((access) => (access.id === action.payload.id ? action.payload : access)),
};
}
case "SET_EMAILS": {
return {
...state,
emails: action.payload,
};
}
case "ADD_EMAIL": {
return {
...state,
emails: {
...state.emails,
content: {
emails: [...(state.emails.content as EmailsSetting).emails, action.payload],
},
},
};
}
default:
return state;
}

View File

@ -1,13 +1,13 @@
import { ReactNode, useContext, createContext, useEffect, useReducer, useCallback } from "react";
import { NotifyChannel, NotifyChannels, Setting } from "@/domain/settings";
import { getSetting } from "@/repository/settings";
import { NotifyChannel, NotifyChannels, Settings } from "@/domain/settings";
import { get } from "@/repository/settings";
import { notifyReducer } from "./reducer";
export type NotifyContext = {
config: Setting<NotifyChannels>;
config: Settings<NotifyChannels>;
setChannel: (data: { channel: string; data: NotifyChannel }) => void;
setChannels: (data: Setting<NotifyChannels>) => void;
setChannels: (data: Settings<NotifyChannels>) => void;
initChannels: () => void;
};
@ -27,7 +27,7 @@ export const NotifyProvider = ({ children }: NotifyProviderProps) => {
}, []);
const featchData = async () => {
const chanels = await getSetting<NotifyChannels>("notifyChannels");
const chanels = await get<NotifyChannels>("notifyChannels");
dispatchNotify({
type: "SET_CHANNELS",
payload: chanels,
@ -45,7 +45,7 @@ export const NotifyProvider = ({ children }: NotifyProviderProps) => {
});
}, []);
const setChannels = useCallback((setting: Setting<NotifyChannels>) => {
const setChannels = useCallback((setting: Settings<NotifyChannels>) => {
dispatchNotify({
type: "SET_CHANNELS",
payload: setting,

View File

@ -1,4 +1,4 @@
import { NotifyChannel, NotifyChannels, Setting } from "@/domain/settings";
import { NotifyChannel, NotifyChannels, Settings } from "@/domain/settings";
type Action =
| {
@ -10,10 +10,10 @@ type Action =
}
| {
type: "SET_CHANNELS";
payload: Setting<NotifyChannels>;
payload: Settings<NotifyChannels>;
};
export const notifyReducer = (state: Setting<NotifyChannels>, action: Action) => {
export const notifyReducer = (state: Settings<NotifyChannels>, action: Action) => {
switch (action.type) {
case "SET_CHANNEL": {
const channel = action.payload.channel;

View File

@ -1,36 +1,22 @@
import { EmailsSetting, Setting } from "@/domain/settings";
import { Settings } from "@/domain/settings";
import { getPocketBase } from "./pocketbase";
export const getEmails = async () => {
export const get = async <T>(name: string) => {
try {
const resp = await getPocketBase().collection("settings").getFirstListItem<Setting<EmailsSetting>>("name='emails'");
const resp = await getPocketBase().collection("settings").getFirstListItem<Settings<T>>(`name='${name}'`);
return resp;
} catch (e) {
} catch {
return {
content: { emails: [] },
};
}
};
export const getSetting = async <T>(name: string) => {
try {
const resp = await getPocketBase().collection("settings").getFirstListItem<Setting<T>>(`name='${name}'`);
return resp;
} catch (e) {
const rs: Setting<T> = {
name: name,
};
return rs;
content: {} as T,
} as Settings<T>;
}
};
export const update = async <T>(setting: Setting<T>) => {
const pb = getPocketBase();
let resp: Setting<T>;
if (setting.id) {
resp = await pb.collection("settings").update(setting.id, setting);
} else {
resp = await pb.collection("settings").create(setting);
export const save = async <T>(record: Settings<T>) => {
if (record.id) {
return await getPocketBase().collection("settings").update<Settings<T>>(record.id, record);
}
return resp;
return await getPocketBase().collection("settings").create<Settings<T>>(record);
};

View File

@ -0,0 +1,44 @@
import { create } from "zustand";
import { produce } from "immer";
import { type EmailsSettingsContent, type Settings } from "@/domain/settings";
import { get as getSettings, save as saveSettings } from "@/repository/settings";
export interface ContactState {
emails: string[];
setEmails: (emails: string[]) => void;
fetchEmails: () => Promise<void>;
}
export const useContactStore = create<ContactState>((set) => {
let settings: Settings<EmailsSettingsContent>;
return {
emails: [],
setEmails: async (emails: string[]) => {
settings ??= await getSettings<EmailsSettingsContent>("emails");
settings = await saveSettings<EmailsSettingsContent>({
...settings,
content: {
...settings.content,
emails: emails,
},
});
set(
produce((state: ContactState) => {
state.emails = settings.content.emails;
})
);
},
fetchEmails: async () => {
settings = await getSettings<EmailsSettingsContent>("emails");
set({
emails: settings.content.emails?.sort() ?? [],
});
},
};
});