feat: add Claude common config snippet functionality
- Create useCommonConfigSnippet hook to manage common config state - Create CommonConfigEditor component with modal for editing - Support merging/removing common config snippets from provider configs - Persist common config to localStorage for reuse across providers - Auto-detect if provider config contains common snippet - Replace JSON editor with CommonConfigEditor in ProviderForm
This commit is contained in:
145
src/components/providers/forms/CommonConfigEditor.tsx
Normal file
145
src/components/providers/forms/CommonConfigEditor.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import JsonEditor from "@/components/JsonEditor";
|
||||||
|
import { useTheme } from "@/components/theme-provider";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
interface CommonConfigEditorProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
useCommonConfig: boolean;
|
||||||
|
onCommonConfigToggle: (checked: boolean) => void;
|
||||||
|
commonConfigSnippet: string;
|
||||||
|
onCommonConfigSnippetChange: (value: string) => void;
|
||||||
|
commonConfigError: string;
|
||||||
|
onEditClick: () => void;
|
||||||
|
isModalOpen: boolean;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommonConfigEditor({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
useCommonConfig,
|
||||||
|
onCommonConfigToggle,
|
||||||
|
commonConfigSnippet,
|
||||||
|
onCommonConfigSnippetChange,
|
||||||
|
commonConfigError,
|
||||||
|
onEditClick,
|
||||||
|
isModalOpen,
|
||||||
|
onModalClose,
|
||||||
|
}: CommonConfigEditorProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
const isDarkMode = useMemo(() => {
|
||||||
|
if (theme === "dark") return true;
|
||||||
|
if (theme === "light") return false;
|
||||||
|
return typeof window !== "undefined"
|
||||||
|
? window.document.documentElement.classList.contains("dark")
|
||||||
|
: false;
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="settingsConfig">
|
||||||
|
{t("provider.configJson", { defaultValue: "配置 JSON" })}
|
||||||
|
</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="useCommonConfig"
|
||||||
|
checked={useCommonConfig}
|
||||||
|
onChange={(e) => onCommonConfigToggle(e.target.checked)}
|
||||||
|
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{t("claudeConfig.writeCommonConfig", {
|
||||||
|
defaultValue: "写入通用配置",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onEditClick}
|
||||||
|
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
||||||
|
>
|
||||||
|
{t("claudeConfig.editCommonConfig", {
|
||||||
|
defaultValue: "编辑通用配置",
|
||||||
|
})}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{commonConfigError && !isModalOpen && (
|
||||||
|
<p className="text-xs text-red-500 dark:text-red-400 text-right">
|
||||||
|
{commonConfigError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<JsonEditor
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={`{
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_BASE_URL": "https://your-api-endpoint.com",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN": "your-api-key-here"
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
darkMode={isDarkMode}
|
||||||
|
rows={14}
|
||||||
|
showValidation
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("claudeConfig.fullSettingsHint", {
|
||||||
|
defaultValue: "请填写完整的 Claude Code 配置",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={isModalOpen} onOpenChange={(open) => !open && onModalClose()}>
|
||||||
|
<DialogContent className="sm:max-w-[600px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t("claudeConfig.editCommonConfigTitle", {
|
||||||
|
defaultValue: "编辑通用配置片段",
|
||||||
|
})}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("claudeConfig.commonConfigHint", {
|
||||||
|
defaultValue:
|
||||||
|
"通用配置片段将合并到所有启用它的供应商配置中",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<JsonEditor
|
||||||
|
value={commonConfigSnippet}
|
||||||
|
onChange={onCommonConfigSnippetChange}
|
||||||
|
darkMode={isDarkMode}
|
||||||
|
rows={12}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{commonConfigError && (
|
||||||
|
<p className="text-sm text-red-500 dark:text-red-400">
|
||||||
|
{commonConfigError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,8 +12,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useTheme } from "@/components/theme-provider";
|
|
||||||
import JsonEditor from "@/components/JsonEditor";
|
|
||||||
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
|
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
|
||||||
import type { AppType } from "@/lib/api";
|
import type { AppType } from "@/lib/api";
|
||||||
import type { ProviderCategory, CustomEndpoint } from "@/types";
|
import type { ProviderCategory, CustomEndpoint } from "@/types";
|
||||||
@@ -27,6 +25,7 @@ import ApiKeyInput from "@/components/ProviderForm/ApiKeyInput";
|
|||||||
import EndpointSpeedTest from "@/components/ProviderForm/EndpointSpeedTest";
|
import EndpointSpeedTest from "@/components/ProviderForm/EndpointSpeedTest";
|
||||||
import CodexConfigEditor from "@/components/ProviderForm/CodexConfigEditor";
|
import CodexConfigEditor from "@/components/ProviderForm/CodexConfigEditor";
|
||||||
import KimiModelSelector from "@/components/ProviderForm/KimiModelSelector";
|
import KimiModelSelector from "@/components/ProviderForm/KimiModelSelector";
|
||||||
|
import { CommonConfigEditor } from "./CommonConfigEditor";
|
||||||
import { Zap } from "lucide-react";
|
import { Zap } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
useProviderCategory,
|
useProviderCategory,
|
||||||
@@ -38,6 +37,7 @@ import {
|
|||||||
useCustomEndpoints,
|
useCustomEndpoints,
|
||||||
useKimiModelSelector,
|
useKimiModelSelector,
|
||||||
useTemplateValues,
|
useTemplateValues,
|
||||||
|
useCommonConfigSnippet,
|
||||||
} from "./hooks";
|
} from "./hooks";
|
||||||
|
|
||||||
const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2);
|
const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2);
|
||||||
@@ -68,7 +68,6 @@ export function ProviderForm({
|
|||||||
initialData,
|
initialData,
|
||||||
}: ProviderFormProps) {
|
}: ProviderFormProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { theme } = useTheme();
|
|
||||||
const isEditMode = Boolean(initialData);
|
const isEditMode = Boolean(initialData);
|
||||||
|
|
||||||
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(
|
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(
|
||||||
@@ -172,14 +171,6 @@ export function ProviderForm({
|
|||||||
form.reset(defaultValues);
|
form.reset(defaultValues);
|
||||||
}, [defaultValues, form]);
|
}, [defaultValues, form]);
|
||||||
|
|
||||||
const isDarkMode = useMemo(() => {
|
|
||||||
if (theme === "dark") return true;
|
|
||||||
if (theme === "light") return false;
|
|
||||||
return typeof window !== "undefined"
|
|
||||||
? window.document.documentElement.classList.contains("dark")
|
|
||||||
: false;
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
const presetCategoryLabels: Record<string, string> = useMemo(
|
const presetCategoryLabels: Record<string, string> = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
official: t("providerPreset.categoryOfficial", {
|
official: t("providerPreset.categoryOfficial", {
|
||||||
@@ -243,6 +234,21 @@ export function ProviderForm({
|
|||||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 使用通用配置片段 hook (仅 Claude 模式)
|
||||||
|
const {
|
||||||
|
useCommonConfig,
|
||||||
|
commonConfigSnippet,
|
||||||
|
commonConfigError,
|
||||||
|
handleCommonConfigToggle,
|
||||||
|
handleCommonConfigSnippetChange,
|
||||||
|
} = useCommonConfigSnippet({
|
||||||
|
settingsConfig: form.watch("settingsConfig"),
|
||||||
|
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||||
|
initialData: appType === "claude" ? initialData : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = (values: ProviderFormData) => {
|
const handleSubmit = (values: ProviderFormData) => {
|
||||||
// 验证模板变量(仅 Claude 模式)
|
// 验证模板变量(仅 Claude 模式)
|
||||||
if (appType === "claude" && templateValueEntries.length > 0) {
|
if (appType === "claude" && templateValueEntries.length > 0) {
|
||||||
@@ -790,7 +796,7 @@ export function ProviderForm({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 配置编辑器:Claude 使用 JSON 编辑器,Codex 使用专用编辑器 */}
|
{/* 配置编辑器:Claude 使用通用配置编辑器,Codex 使用专用编辑器 */}
|
||||||
{appType === "codex" ? (
|
{appType === "codex" ? (
|
||||||
<CodexConfigEditor
|
<CodexConfigEditor
|
||||||
authValue={codexAuth}
|
authValue={codexAuth}
|
||||||
@@ -805,29 +811,17 @@ export function ProviderForm({
|
|||||||
authError={codexAuthError}
|
authError={codexAuthError}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FormField
|
<CommonConfigEditor
|
||||||
control={form.control}
|
value={form.watch("settingsConfig")}
|
||||||
name="settingsConfig"
|
onChange={(value) => form.setValue("settingsConfig", value)}
|
||||||
render={({ field }) => (
|
useCommonConfig={useCommonConfig}
|
||||||
<FormItem>
|
onCommonConfigToggle={handleCommonConfigToggle}
|
||||||
<FormLabel>
|
commonConfigSnippet={commonConfigSnippet}
|
||||||
{t("provider.configJson", { defaultValue: "配置 JSON" })}
|
onCommonConfigSnippetChange={handleCommonConfigSnippetChange}
|
||||||
</FormLabel>
|
commonConfigError={commonConfigError}
|
||||||
<FormControl>
|
onEditClick={() => setIsCommonConfigModalOpen(true)}
|
||||||
<div className="rounded-md border">
|
isModalOpen={isCommonConfigModalOpen}
|
||||||
<JsonEditor
|
onModalClose={() => setIsCommonConfigModalOpen(false)}
|
||||||
value={field.value}
|
|
||||||
onChange={field.onChange}
|
|
||||||
placeholder={CLAUDE_DEFAULT_CONFIG}
|
|
||||||
darkMode={isDarkMode}
|
|
||||||
rows={14}
|
|
||||||
showValidation
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export { useApiKeyLink } from "./useApiKeyLink";
|
|||||||
export { useCustomEndpoints } from "./useCustomEndpoints";
|
export { useCustomEndpoints } from "./useCustomEndpoints";
|
||||||
export { useKimiModelSelector } from "./useKimiModelSelector";
|
export { useKimiModelSelector } from "./useKimiModelSelector";
|
||||||
export { useTemplateValues } from "./useTemplateValues";
|
export { useTemplateValues } from "./useTemplateValues";
|
||||||
|
export { useCommonConfigSnippet } from "./useCommonConfigSnippet";
|
||||||
|
|||||||
198
src/components/providers/forms/hooks/useCommonConfigSnippet.ts
Normal file
198
src/components/providers/forms/hooks/useCommonConfigSnippet.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
import {
|
||||||
|
updateCommonConfigSnippet,
|
||||||
|
hasCommonConfigSnippet,
|
||||||
|
validateJsonConfig,
|
||||||
|
} from "@/utils/providerConfigUtils";
|
||||||
|
|
||||||
|
const COMMON_CONFIG_STORAGE_KEY = "cc-switch:common-config-snippet";
|
||||||
|
const DEFAULT_COMMON_CONFIG_SNIPPET = `{
|
||||||
|
"includeCoAuthoredBy": false
|
||||||
|
}`;
|
||||||
|
|
||||||
|
interface UseCommonConfigSnippetProps {
|
||||||
|
settingsConfig: string;
|
||||||
|
onConfigChange: (config: string) => void;
|
||||||
|
initialData?: {
|
||||||
|
settingsConfig?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理 Claude 通用配置片段
|
||||||
|
*/
|
||||||
|
export function useCommonConfigSnippet({
|
||||||
|
settingsConfig,
|
||||||
|
onConfigChange,
|
||||||
|
initialData,
|
||||||
|
}: UseCommonConfigSnippetProps) {
|
||||||
|
const [useCommonConfig, setUseCommonConfig] = useState(false);
|
||||||
|
const [commonConfigSnippet, setCommonConfigSnippetState] = useState<string>(
|
||||||
|
() => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return DEFAULT_COMMON_CONFIG_SNIPPET;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const stored = window.localStorage.getItem(COMMON_CONFIG_STORAGE_KEY);
|
||||||
|
if (stored && stored.trim()) {
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore localStorage 读取失败
|
||||||
|
}
|
||||||
|
return DEFAULT_COMMON_CONFIG_SNIPPET;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const [commonConfigError, setCommonConfigError] = useState("");
|
||||||
|
|
||||||
|
// 用于跟踪是否正在通过通用配置更新
|
||||||
|
const isUpdatingFromCommonConfig = useRef(false);
|
||||||
|
|
||||||
|
// 初始化时检查通用配置片段(编辑模式)
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData) {
|
||||||
|
const configString = JSON.stringify(
|
||||||
|
initialData.settingsConfig,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
const hasCommon = hasCommonConfigSnippet(
|
||||||
|
configString,
|
||||||
|
commonConfigSnippet,
|
||||||
|
);
|
||||||
|
setUseCommonConfig(hasCommon);
|
||||||
|
}
|
||||||
|
}, [initialData, commonConfigSnippet]);
|
||||||
|
|
||||||
|
// 同步本地存储的通用配置片段
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
if (commonConfigSnippet.trim()) {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
COMMON_CONFIG_STORAGE_KEY,
|
||||||
|
commonConfigSnippet,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
window.localStorage.removeItem(COMMON_CONFIG_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}, [commonConfigSnippet]);
|
||||||
|
|
||||||
|
// 处理通用配置开关
|
||||||
|
const handleCommonConfigToggle = useCallback(
|
||||||
|
(checked: boolean) => {
|
||||||
|
const { updatedConfig, error: snippetError } = updateCommonConfigSnippet(
|
||||||
|
settingsConfig,
|
||||||
|
commonConfigSnippet,
|
||||||
|
checked,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (snippetError) {
|
||||||
|
setCommonConfigError(snippetError);
|
||||||
|
setUseCommonConfig(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCommonConfigError("");
|
||||||
|
setUseCommonConfig(checked);
|
||||||
|
// 标记正在通过通用配置更新
|
||||||
|
isUpdatingFromCommonConfig.current = true;
|
||||||
|
onConfigChange(updatedConfig);
|
||||||
|
// 在下一个事件循环中重置标记
|
||||||
|
setTimeout(() => {
|
||||||
|
isUpdatingFromCommonConfig.current = false;
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
[settingsConfig, commonConfigSnippet, onConfigChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理通用配置片段变化
|
||||||
|
const handleCommonConfigSnippetChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const previousSnippet = commonConfigSnippet;
|
||||||
|
setCommonConfigSnippetState(value);
|
||||||
|
|
||||||
|
if (!value.trim()) {
|
||||||
|
setCommonConfigError("");
|
||||||
|
if (useCommonConfig) {
|
||||||
|
const { updatedConfig } = updateCommonConfigSnippet(
|
||||||
|
settingsConfig,
|
||||||
|
previousSnippet,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
onConfigChange(updatedConfig);
|
||||||
|
setUseCommonConfig(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证JSON格式
|
||||||
|
const validationError = validateJsonConfig(value, "通用配置片段");
|
||||||
|
if (validationError) {
|
||||||
|
setCommonConfigError(validationError);
|
||||||
|
} else {
|
||||||
|
setCommonConfigError("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若当前启用通用配置且格式正确,需要替换为最新片段
|
||||||
|
if (useCommonConfig && !validationError) {
|
||||||
|
const removeResult = updateCommonConfigSnippet(
|
||||||
|
settingsConfig,
|
||||||
|
previousSnippet,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (removeResult.error) {
|
||||||
|
setCommonConfigError(removeResult.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const addResult = updateCommonConfigSnippet(
|
||||||
|
removeResult.updatedConfig,
|
||||||
|
value,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (addResult.error) {
|
||||||
|
setCommonConfigError(addResult.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记正在通过通用配置更新,避免触发状态检查
|
||||||
|
isUpdatingFromCommonConfig.current = true;
|
||||||
|
onConfigChange(addResult.updatedConfig);
|
||||||
|
// 在下一个事件循环中重置标记
|
||||||
|
setTimeout(() => {
|
||||||
|
isUpdatingFromCommonConfig.current = false;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
commonConfigSnippet,
|
||||||
|
settingsConfig,
|
||||||
|
useCommonConfig,
|
||||||
|
onConfigChange,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isUpdatingFromCommonConfig.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hasCommon = hasCommonConfigSnippet(
|
||||||
|
settingsConfig,
|
||||||
|
commonConfigSnippet,
|
||||||
|
);
|
||||||
|
setUseCommonConfig(hasCommon);
|
||||||
|
}, [settingsConfig, commonConfigSnippet]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
useCommonConfig,
|
||||||
|
commonConfigSnippet,
|
||||||
|
commonConfigError,
|
||||||
|
handleCommonConfigToggle,
|
||||||
|
handleCommonConfigSnippetChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user