refactor(admin): 钉钉集成配置缓存迁入 zustand 页面级 store,移除模块级单例
This commit is contained in:
@@ -30,12 +30,7 @@ import {
|
||||
isAppSecretRequired,
|
||||
type DingTalkConfigFormValues,
|
||||
} from './integration-config-form';
|
||||
import {
|
||||
cacheDingTalkDraft,
|
||||
cacheDingTalkServerSnapshot,
|
||||
commitDingTalkConfig,
|
||||
readDingTalkConfigCache,
|
||||
} from './integration-config-cache';
|
||||
import { useIntegrationConfigStore } from './integrationConfigStore';
|
||||
import { IntegrationOrgSyncPanel } from './IntegrationOrgSyncPanel';
|
||||
|
||||
interface DingTalkConfig {
|
||||
@@ -44,7 +39,10 @@ interface DingTalkConfig {
|
||||
}
|
||||
|
||||
const IntegrationConfigPage: React.FC = () => {
|
||||
const initialCache = useMemo(() => readDingTalkConfigCache(), []);
|
||||
const initialCache = useMemo(() => {
|
||||
const { loaded, config, verified, formValues, dirty } = useIntegrationConfigStore.getState();
|
||||
return { loaded, config, verified, formValues, dirty };
|
||||
}, []);
|
||||
const { hasPermission, hasAllPermissions } = usePermission();
|
||||
const canCreateClass = hasPermission('class:create');
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -93,8 +91,8 @@ const IntegrationConfigPage: React.FC = () => {
|
||||
|
||||
// 服务端配置同步进 localStorage 缓存,并回填表单
|
||||
useEffect(() => {
|
||||
cacheDingTalkServerSnapshot(config, verified);
|
||||
if (config) form.setFieldsValue(readDingTalkConfigCache().formValues);
|
||||
useIntegrationConfigStore.getState().cacheServerSnapshot(config, verified);
|
||||
if (config) form.setFieldsValue(useIntegrationConfigStore.getState().formValues);
|
||||
}, [config, verified, form]);
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -104,7 +102,7 @@ const IntegrationConfigPage: React.FC = () => {
|
||||
try {
|
||||
await saveMutation.mutateAsync(payload);
|
||||
message.success('配置已保存');
|
||||
commitDingTalkConfig({ corpId: payload.corpId, agentId: payload.agentId });
|
||||
useIntegrationConfigStore.getState().commitConfig({ corpId: payload.corpId, agentId: payload.agentId });
|
||||
form.setFieldValue('appSecret', undefined);
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
@@ -180,7 +178,7 @@ const IntegrationConfigPage: React.FC = () => {
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={initialCache.formValues}
|
||||
onValuesChange={(_changed, values) => cacheDingTalkDraft(values)}
|
||||
onValuesChange={(_changed, values) => useIntegrationConfigStore.getState().cacheDraft(values)}
|
||||
style={{ maxWidth: 520 }}
|
||||
>
|
||||
<Form.Item
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
cacheDingTalkDraft,
|
||||
cacheDingTalkServerSnapshot,
|
||||
commitDingTalkConfig,
|
||||
readDingTalkConfigCache,
|
||||
resetDingTalkConfigCache,
|
||||
} from './integration-config-cache';
|
||||
|
||||
describe('DingTalk integration config page cache', () => {
|
||||
beforeEach(resetDingTalkConfigCache);
|
||||
|
||||
it('keeps an unsaved secret when a background refresh returns', () => {
|
||||
cacheDingTalkDraft({ corpId: 'draft-corp', agentId: 'draft-key', appSecret: 'draft-secret' });
|
||||
cacheDingTalkServerSnapshot({ corpId: 'saved-corp', agentId: 'saved-key' }, true);
|
||||
|
||||
expect(readDingTalkConfigCache()).toMatchObject({
|
||||
loaded: true,
|
||||
dirty: true,
|
||||
config: { corpId: 'saved-corp', agentId: 'saved-key' },
|
||||
formValues: {
|
||||
corpId: 'draft-corp',
|
||||
agentId: 'draft-key',
|
||||
appSecret: 'draft-secret',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('clears the secret after a successful save', () => {
|
||||
cacheDingTalkDraft({ corpId: 'corp', agentId: 'key', appSecret: 'secret' });
|
||||
commitDingTalkConfig({ corpId: 'corp', agentId: 'key' });
|
||||
|
||||
expect(readDingTalkConfigCache()).toMatchObject({
|
||||
loaded: true,
|
||||
dirty: false,
|
||||
formValues: { corpId: 'corp', agentId: 'key', appSecret: undefined },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
import type { DingTalkConfigFormValues } from './integration-config-form';
|
||||
|
||||
export interface DingTalkSavedConfig {
|
||||
agentId: string;
|
||||
corpId: string;
|
||||
}
|
||||
|
||||
interface DingTalkConfigCache {
|
||||
loaded: boolean;
|
||||
config: DingTalkSavedConfig | null;
|
||||
verified: boolean | null;
|
||||
formValues: Partial<DingTalkConfigFormValues>;
|
||||
dirty: boolean;
|
||||
}
|
||||
|
||||
const cache: DingTalkConfigCache = {
|
||||
loaded: false,
|
||||
config: null,
|
||||
verified: null,
|
||||
formValues: {},
|
||||
dirty: false,
|
||||
};
|
||||
|
||||
export function readDingTalkConfigCache(): DingTalkConfigCache {
|
||||
return {
|
||||
...cache,
|
||||
config: cache.config ? { ...cache.config } : null,
|
||||
formValues: { ...cache.formValues },
|
||||
};
|
||||
}
|
||||
|
||||
export function cacheDingTalkDraft(values: Partial<DingTalkConfigFormValues>): void {
|
||||
cache.formValues = { ...values };
|
||||
cache.dirty = true;
|
||||
}
|
||||
|
||||
export function cacheDingTalkServerSnapshot(
|
||||
config: DingTalkSavedConfig | null,
|
||||
verified: boolean | null,
|
||||
): void {
|
||||
cache.loaded = true;
|
||||
cache.config = config ? { ...config } : null;
|
||||
cache.verified = verified;
|
||||
if (!cache.dirty) {
|
||||
cache.formValues = config ? { ...config, appSecret: undefined } : {};
|
||||
}
|
||||
}
|
||||
|
||||
export function commitDingTalkConfig(config: DingTalkSavedConfig): void {
|
||||
cache.loaded = true;
|
||||
cache.config = { ...config };
|
||||
cache.formValues = { ...config, appSecret: undefined };
|
||||
cache.dirty = false;
|
||||
}
|
||||
|
||||
export function resetDingTalkConfigCache(): void {
|
||||
cache.loaded = false;
|
||||
cache.config = null;
|
||||
cache.verified = null;
|
||||
cache.formValues = {};
|
||||
cache.dirty = false;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { useIntegrationConfigStore } from './integrationConfigStore';
|
||||
|
||||
const { getState } = useIntegrationConfigStore;
|
||||
|
||||
const readCache = () => {
|
||||
const { loaded, config, verified, formValues, dirty } = getState();
|
||||
return { loaded, config, verified, formValues, dirty };
|
||||
};
|
||||
|
||||
describe('DingTalk integration config page cache', () => {
|
||||
beforeEach(() => getState().reset());
|
||||
|
||||
it('keeps an unsaved secret when a background refresh returns', () => {
|
||||
getState().cacheDraft({ corpId: 'draft-corp', agentId: 'draft-key', appSecret: 'draft-secret' });
|
||||
getState().cacheServerSnapshot({ corpId: 'saved-corp', agentId: 'saved-key' }, true);
|
||||
|
||||
expect(readCache()).toMatchObject({
|
||||
loaded: true,
|
||||
dirty: true,
|
||||
config: { corpId: 'saved-corp', agentId: 'saved-key' },
|
||||
formValues: {
|
||||
corpId: 'draft-corp',
|
||||
agentId: 'draft-key',
|
||||
appSecret: 'draft-secret',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('clears the secret after a successful save', () => {
|
||||
getState().cacheDraft({ corpId: 'corp', agentId: 'key', appSecret: 'secret' });
|
||||
getState().commitConfig({ corpId: 'corp', agentId: 'key' });
|
||||
|
||||
expect(readCache()).toMatchObject({
|
||||
loaded: true,
|
||||
dirty: false,
|
||||
formValues: { corpId: 'corp', agentId: 'key', appSecret: undefined },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import type { DingTalkConfigFormValues } from './integration-config-form';
|
||||
|
||||
export interface DingTalkSavedConfig {
|
||||
agentId: string;
|
||||
corpId: string;
|
||||
}
|
||||
|
||||
interface DingTalkConfigState {
|
||||
/** 服务端配置是否已加载过(区分「未加载」与「确实为空」) */
|
||||
loaded: boolean;
|
||||
config: DingTalkSavedConfig | null;
|
||||
verified: boolean | null;
|
||||
/** 表单草稿:保留未保存的 appSecret 等敏感字段 */
|
||||
formValues: Partial<DingTalkConfigFormValues>;
|
||||
/** 是否存在未保存的草稿 */
|
||||
dirty: boolean;
|
||||
}
|
||||
|
||||
interface DingTalkConfigActions {
|
||||
/** 表单变化时缓存草稿 */
|
||||
cacheDraft: (values: Partial<DingTalkConfigFormValues>) => void;
|
||||
/** 后台拉取服务端配置成功时缓存快照(不覆盖已有草稿) */
|
||||
cacheServerSnapshot: (config: DingTalkSavedConfig | null, verified: boolean | null) => void;
|
||||
/** 保存成功后提交配置并清空草稿 */
|
||||
commitConfig: (config: DingTalkSavedConfig) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export type DingTalkConfigStore = DingTalkConfigState & DingTalkConfigActions;
|
||||
|
||||
const initialDingTalkConfig: DingTalkConfigState = {
|
||||
loaded: false,
|
||||
config: null,
|
||||
verified: null,
|
||||
formValues: {},
|
||||
dirty: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* 钉钉集成页面的表单/配置会话缓存(页面级 store)。
|
||||
*
|
||||
* 原实现为模块级单例(integration-config-cache.ts):在 RouteKeeper 淘汰
|
||||
* 页面或重新进入页面时仍保留草稿。迁入 zustand 后语义一致、可重置可测试。
|
||||
*/
|
||||
export const useIntegrationConfigStore = create<DingTalkConfigStore>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
...initialDingTalkConfig,
|
||||
cacheDraft: (values) =>
|
||||
set(
|
||||
{ formValues: { ...values }, dirty: true },
|
||||
false,
|
||||
'integrationConfig/cacheDraft',
|
||||
),
|
||||
cacheServerSnapshot: (config, verified) => {
|
||||
const { dirty, formValues } = get();
|
||||
set(
|
||||
{
|
||||
loaded: true,
|
||||
config: config ? { ...config } : null,
|
||||
verified,
|
||||
formValues: dirty ? formValues : config ? { ...config, appSecret: undefined } : {},
|
||||
},
|
||||
false,
|
||||
'integrationConfig/cacheServerSnapshot',
|
||||
);
|
||||
},
|
||||
commitConfig: (config) =>
|
||||
set(
|
||||
{
|
||||
loaded: true,
|
||||
config: { ...config },
|
||||
formValues: { ...config, appSecret: undefined },
|
||||
dirty: false,
|
||||
},
|
||||
false,
|
||||
'integrationConfig/commitConfig',
|
||||
),
|
||||
reset: () =>
|
||||
set({ ...initialDingTalkConfig }, false, 'integrationConfig/reset'),
|
||||
}),
|
||||
{ name: 'integration-config-store', enabled: import.meta.env.DEV },
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user