🐛 fix(editor): Add Go template editor component and update related schemas
This commit is contained in:
parent
e7844d077b
commit
4a907257c6
@ -46,6 +46,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@workspace/ui/components/tooltip';
|
||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||
import { GoTemplateEditor } from '@workspace/ui/custom-components/editor';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
|
||||
@ -63,7 +64,7 @@ const createClientFormSchema = (t: any) =>
|
||||
description: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
user_agent: z.string().min(1, `User-Agent ${t('form.validation.userAgentRequiredSuffix')}`),
|
||||
proxy_template: z.string().default(''),
|
||||
schema: z.string().default(''),
|
||||
template: z.string().default(''),
|
||||
output_format: z.string().default(''),
|
||||
download_link: z.object({
|
||||
@ -94,7 +95,7 @@ export function ProtocolForm() {
|
||||
description: '',
|
||||
icon: '',
|
||||
user_agent: '',
|
||||
proxy_template: '',
|
||||
schema: '',
|
||||
template: '',
|
||||
output_format: '',
|
||||
download_link: {
|
||||
@ -230,7 +231,7 @@ export function ProtocolForm() {
|
||||
description: '',
|
||||
icon: '',
|
||||
user_agent: '',
|
||||
proxy_template: '',
|
||||
schema: '',
|
||||
template: '',
|
||||
output_format: '',
|
||||
download_link: {
|
||||
@ -252,7 +253,7 @@ export function ProtocolForm() {
|
||||
description: client.description || '',
|
||||
icon: client.icon || '',
|
||||
user_agent: client.user_agent,
|
||||
proxy_template: client.proxy_template || '',
|
||||
schema: client.proxy_template || '',
|
||||
template: client.template || '',
|
||||
output_format: client.output_format || '',
|
||||
download_link: {
|
||||
@ -301,6 +302,7 @@ export function ProtocolForm() {
|
||||
if (editingClient) {
|
||||
await updateSubscribeApplication({
|
||||
...data,
|
||||
proxy_template: data.schema || '',
|
||||
is_default: editingClient.is_default,
|
||||
id: editingClient.id,
|
||||
});
|
||||
@ -308,6 +310,7 @@ export function ProtocolForm() {
|
||||
} else {
|
||||
await createSubscribeApplication({
|
||||
...data,
|
||||
proxy_template: data.schema || '',
|
||||
is_default: false,
|
||||
});
|
||||
toast.success(t('actions.createSuccess'));
|
||||
@ -491,20 +494,17 @@ export function ProtocolForm() {
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='proxy_template'
|
||||
name='schema'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.fields.proxyTemplate')}</FormLabel>
|
||||
<FormLabel>{t('form.fields.schema')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder='clash://install-config?url={url}&name={name}'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('form.descriptions.proxyTemplate')}
|
||||
{`{url} {name}`}
|
||||
</FormDescription>
|
||||
<FormDescription>{t('form.descriptions.schema')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@ -517,10 +517,10 @@ export function ProtocolForm() {
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.fields.template')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder='proxies:\n - name: {name}\n type: {type}\n server: {server}\n port: {port}'
|
||||
className='min-h-[120px] font-mono text-sm'
|
||||
{...field}
|
||||
<GoTemplateEditor
|
||||
enableSprig
|
||||
value={field.value || ''}
|
||||
onChange={(value) => field.onChange(value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('form.descriptions.template')}</FormDescription>
|
||||
|
||||
@ -52,8 +52,8 @@
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"outputFormat": "Output Format",
|
||||
"proxyTemplate": "Subscription Link",
|
||||
"template": "Config Template"
|
||||
"schema": "URL Schema",
|
||||
"template": "Subscription File Template"
|
||||
},
|
||||
"descriptions": {
|
||||
"icon": "Icon URL or base64 encoding",
|
||||
@ -61,8 +61,8 @@
|
||||
"userAgentPrefix": "Client identifier for distinguishing different clients",
|
||||
"description": "Detailed client description",
|
||||
"outputFormat": "Subscription configuration file format",
|
||||
"proxyTemplate": "Subscription link template, supports variables: ",
|
||||
"template": "Subscription configuration template, supports various variables",
|
||||
"schema": "URL Schema template, supports variables: '{url}' subscription URL, '{name}' site name",
|
||||
"template": "Subscription file template, supports various variables",
|
||||
"downloadLink": "platform download URL"
|
||||
},
|
||||
"validation": {
|
||||
|
||||
@ -47,8 +47,8 @@
|
||||
"icon": "图标 URL 或 base64 编码",
|
||||
"name": "客户端显示名称",
|
||||
"outputFormat": "订阅配置文件格式",
|
||||
"proxyTemplate": "订阅链接模板,支持变量:",
|
||||
"template": "订阅配置模板,支持各种变量",
|
||||
"schema": "URL Schema 模板,支持变量:'{url}' 订阅地址,'{name}' 站点名称",
|
||||
"template": "订阅文件模板,支持各种变量",
|
||||
"userAgentPrefix": "用于区分不同客户端的客户端标识符"
|
||||
},
|
||||
"editTitle": "编辑客户端",
|
||||
@ -57,8 +57,8 @@
|
||||
"icon": "图标",
|
||||
"name": "名称",
|
||||
"outputFormat": "输出格式",
|
||||
"proxyTemplate": "订阅链接",
|
||||
"template": "配置模板"
|
||||
"schema": "URL Schema",
|
||||
"template": "订阅文件模板"
|
||||
},
|
||||
"tabs": {
|
||||
"basic": "基本信息",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// @ts-ignore
|
||||
|
||||
|
||||
// API 更新时间:
|
||||
// API 唯一标识:
|
||||
import * as ads from './ads';
|
||||
|
||||
3
apps/admin/services/admin/typings.d.ts
vendored
3
apps/admin/services/admin/typings.d.ts
vendored
@ -339,6 +339,7 @@ declare namespace API {
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
scheme?: string;
|
||||
user_agent: string;
|
||||
is_default: boolean;
|
||||
proxy_template: string;
|
||||
@ -1501,6 +1502,7 @@ declare namespace API {
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
scheme?: string;
|
||||
user_agent: string;
|
||||
is_default: boolean;
|
||||
proxy_template: string;
|
||||
@ -1779,6 +1781,7 @@ declare namespace API {
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
scheme?: string;
|
||||
user_agent: string;
|
||||
is_default: boolean;
|
||||
proxy_template: string;
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.473.0",
|
||||
"mathjs": "^14.0.1",
|
||||
"monaco-themes": "^0.4.6",
|
||||
"motion": "^11.18.1",
|
||||
"next-themes": "^0.4.4",
|
||||
"react-day-picker": "8.10.1",
|
||||
|
||||
402
packages/ui/src/custom-components/editor/go-template.tsx
Normal file
402
packages/ui/src/custom-components/editor/go-template.tsx
Normal file
@ -0,0 +1,402 @@
|
||||
'use client';
|
||||
|
||||
import { type Monaco } from '@monaco-editor/react';
|
||||
import {
|
||||
MonacoEditor,
|
||||
MonacoEditorProps,
|
||||
} from '@workspace/ui/custom-components/editor/monaco-editor';
|
||||
import { useMemo } from 'react';
|
||||
// Import Dracula theme
|
||||
import DraculaTheme from 'monaco-themes/themes/Dracula.json' with { type: 'json' };
|
||||
|
||||
export interface GoTemplateEditorProps extends Omit<MonacoEditorProps, 'language'> {
|
||||
schema?: Record<string, unknown>;
|
||||
enableSprig?: boolean;
|
||||
}
|
||||
|
||||
// Go template syntax keywords
|
||||
const GO_TEMPLATE_KEYWORDS = [
|
||||
'if',
|
||||
'else',
|
||||
'end',
|
||||
'with',
|
||||
'range',
|
||||
'template',
|
||||
'define',
|
||||
'block',
|
||||
'and',
|
||||
'or',
|
||||
'not',
|
||||
'eq',
|
||||
'ne',
|
||||
'lt',
|
||||
'le',
|
||||
'gt',
|
||||
'ge',
|
||||
'print',
|
||||
'printf',
|
||||
'println',
|
||||
];
|
||||
|
||||
// Sprig function list (commonly used)
|
||||
const SPRIG_FUNCTIONS = [
|
||||
// String functions
|
||||
'trim',
|
||||
'trimAll',
|
||||
'trimSuffix',
|
||||
'trimPrefix',
|
||||
'upper',
|
||||
'lower',
|
||||
'title',
|
||||
'untitle',
|
||||
'repeat',
|
||||
'substr',
|
||||
'nospace',
|
||||
'trunc',
|
||||
'abbrev',
|
||||
'abbrevboth',
|
||||
'initials',
|
||||
'randAlphaNum',
|
||||
'randAlpha',
|
||||
'randNumeric',
|
||||
'wrap',
|
||||
'wrapWith',
|
||||
'contains',
|
||||
'hasPrefix',
|
||||
'hasSuffix',
|
||||
'quote',
|
||||
'squote',
|
||||
'cat',
|
||||
'indent',
|
||||
'nindent',
|
||||
'replace',
|
||||
'plural',
|
||||
'sha1sum',
|
||||
'sha256sum',
|
||||
'adler32sum',
|
||||
'htmlEscape',
|
||||
'htmlUnescape',
|
||||
'urlEscape',
|
||||
'urlUnescape',
|
||||
|
||||
// Math functions
|
||||
'add',
|
||||
'sub',
|
||||
'mul',
|
||||
'div',
|
||||
'mod',
|
||||
'max',
|
||||
'min',
|
||||
'ceil',
|
||||
'floor',
|
||||
'round',
|
||||
|
||||
// Date functions
|
||||
'now',
|
||||
'date',
|
||||
'dateInZone',
|
||||
'duration',
|
||||
'durationRound',
|
||||
'unixEpoch',
|
||||
'dateModify',
|
||||
'mustDateModify',
|
||||
'htmlDate',
|
||||
'htmlDateInZone',
|
||||
'toDate',
|
||||
'mustToDate',
|
||||
|
||||
// Lists and Dict functions
|
||||
'list',
|
||||
'first',
|
||||
'rest',
|
||||
'last',
|
||||
'initial',
|
||||
'reverse',
|
||||
'uniq',
|
||||
'compact',
|
||||
'slice',
|
||||
'has',
|
||||
'set',
|
||||
'unset',
|
||||
'hasKey',
|
||||
'pluck',
|
||||
'keys',
|
||||
'pick',
|
||||
'omit',
|
||||
'merge',
|
||||
'mergeOverwrite',
|
||||
'values',
|
||||
'append',
|
||||
'prepend',
|
||||
'concat',
|
||||
'dict',
|
||||
'get',
|
||||
'index',
|
||||
'dig',
|
||||
|
||||
// Type functions
|
||||
'typeOf',
|
||||
'typeIs',
|
||||
'typeIsLike',
|
||||
'kindOf',
|
||||
'kindIs',
|
||||
'deepEqual',
|
||||
|
||||
// Default functions
|
||||
'default',
|
||||
'empty',
|
||||
'coalesce',
|
||||
'fromJson',
|
||||
'toJson',
|
||||
'toPrettyJson',
|
||||
'toRawJson',
|
||||
'mustFromJson',
|
||||
'mustToJson',
|
||||
'mustToPrettyJson',
|
||||
|
||||
// Encoding functions
|
||||
'b64enc',
|
||||
'b64dec',
|
||||
'b32enc',
|
||||
'b32dec',
|
||||
|
||||
// Flow control
|
||||
'fail',
|
||||
'required',
|
||||
'tpl',
|
||||
|
||||
// UUID functions
|
||||
'uuidv4',
|
||||
|
||||
// OS functions
|
||||
'env',
|
||||
'expandenv',
|
||||
];
|
||||
|
||||
export function GoTemplateEditor({ schema, enableSprig = true, ...props }: GoTemplateEditorProps) {
|
||||
const completionItems = useMemo(() => {
|
||||
const items = [];
|
||||
|
||||
// Add Go template keywords
|
||||
items.push(
|
||||
...GO_TEMPLATE_KEYWORDS.map((keyword) => ({
|
||||
label: keyword,
|
||||
kind: 14,
|
||||
insertText: keyword,
|
||||
documentation: `Go template keyword: ${keyword}`,
|
||||
})),
|
||||
);
|
||||
|
||||
// Add Sprig functions if enabled
|
||||
if (enableSprig) {
|
||||
items.push(
|
||||
...SPRIG_FUNCTIONS.map((func) => ({
|
||||
label: func,
|
||||
kind: 3, // Function
|
||||
insertText: `${func} `,
|
||||
documentation: `Sprig function: ${func}`,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
// Add schema field completion
|
||||
if (schema && typeof schema === 'object') {
|
||||
const addSchemaFields = (schemaObj: Record<string, unknown>, prefix = '.') => {
|
||||
// Handle JSON Schema properties
|
||||
if (schemaObj.properties && typeof schemaObj.properties === 'object') {
|
||||
const properties = schemaObj.properties as Record<string, unknown>;
|
||||
Object.keys(properties).forEach((key) => {
|
||||
const property = properties[key] as Record<string, unknown>;
|
||||
const fullPath = prefix === '.' ? `.${key}` : `${prefix}.${key}`;
|
||||
const type = property.type || 'unknown';
|
||||
|
||||
items.push({
|
||||
label: fullPath,
|
||||
kind: 10, // Property
|
||||
insertText: fullPath,
|
||||
documentation: `Schema field: ${fullPath} (${type})${property.description ? ` - ${property.description}` : ''}`,
|
||||
});
|
||||
|
||||
// Recursively add nested object properties
|
||||
if (property.type === 'object' && property.properties) {
|
||||
addSchemaFields(property, fullPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Handle direct object structure (non-JSON Schema format)
|
||||
else {
|
||||
Object.keys(schemaObj).forEach((key) => {
|
||||
if (key === 'properties' || key === 'type' || key === 'description') return;
|
||||
|
||||
const value = schemaObj[key];
|
||||
const fullPath = prefix === '.' ? `.${key}` : `${prefix}.${key}`;
|
||||
|
||||
items.push({
|
||||
label: fullPath,
|
||||
kind: 10, // Property
|
||||
insertText: fullPath,
|
||||
documentation: `Schema field: ${fullPath} (${typeof value})`,
|
||||
});
|
||||
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
addSchemaFields(value as Record<string, unknown>, fullPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
addSchemaFields(schema);
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [schema, enableSprig]);
|
||||
|
||||
const handleEditorMount = (editor: unknown, monaco: Monaco) => {
|
||||
// Register custom Go template language
|
||||
monaco.languages.register({ id: 'gotemplate' });
|
||||
|
||||
// Set syntax highlighting
|
||||
monaco.languages.setMonarchTokensProvider('gotemplate', {
|
||||
tokenizer: {
|
||||
root: [
|
||||
// Comments - match {{/*...*/}} first
|
||||
[/\{\{\/\*[\s\S]*?\*\/\}\}/, 'comment'],
|
||||
// Template tags - enter template state
|
||||
[/\{\{-?/, { token: 'template-tag', next: '@template' }],
|
||||
// Regular text
|
||||
[/[^{]+/, ''],
|
||||
[/[{]/, ''],
|
||||
],
|
||||
template: [
|
||||
// Exit template
|
||||
[/-?\}\}/, { token: 'template-tag', next: '@pop' }],
|
||||
// Strings in template
|
||||
[/"([^"\\]|\\.)*"/, 'string'],
|
||||
[/'([^'\\]|\\.)*'/, 'string'],
|
||||
// Go template keywords - exact word match
|
||||
[new RegExp(`\\b(${GO_TEMPLATE_KEYWORDS.join('|')})\\b`), 'keyword'],
|
||||
// Sprig functions - exact word match
|
||||
[new RegExp(`\\b(${SPRIG_FUNCTIONS.join('|')})\\b`), 'function'],
|
||||
// Variables starting with $
|
||||
[/\$\w+/, 'variable'],
|
||||
// Properties starting with .
|
||||
[/\.\w+/, 'property'],
|
||||
// Numbers
|
||||
[/\d+(\.\d+)?/, 'number'],
|
||||
// Operators and other tokens
|
||||
[/[|:]/, 'operator'],
|
||||
// Whitespace
|
||||
[/\s+/, ''],
|
||||
// Any other characters in template
|
||||
[/./, ''],
|
||||
],
|
||||
string: [
|
||||
[/[^\\"]+/, 'string'],
|
||||
[/\\./, 'string.escape'],
|
||||
[/"/, 'string', '@pop'],
|
||||
],
|
||||
string_single: [
|
||||
[/[^\\']+/, 'string'],
|
||||
[/\\./, 'string.escape'],
|
||||
[/'/, 'string', '@pop'],
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Override the Dracula theme with Go template colors
|
||||
monaco.editor.defineTheme('transparentTheme', {
|
||||
base: DraculaTheme.base as 'vs' | 'vs-dark' | 'hc-black',
|
||||
inherit: DraculaTheme.inherit,
|
||||
rules: [
|
||||
...DraculaTheme.rules,
|
||||
{ token: 'template-tag', foreground: 'FFB86C', fontStyle: 'bold' }, // Dracula orange for template tags
|
||||
{ token: 'keyword', foreground: 'FF79C6' }, // Dracula pink for keywords
|
||||
{ token: 'function', foreground: '50FA7B' }, // Dracula green for functions
|
||||
{ token: 'variable', foreground: 'F1FA8C' }, // Dracula yellow for variables
|
||||
{ token: 'property', foreground: '8BE9FD' }, // Dracula cyan for properties
|
||||
{ token: 'operator', foreground: 'FF79C6' }, // Dracula pink for operators
|
||||
],
|
||||
colors: {
|
||||
'editor.background': '#00000000',
|
||||
},
|
||||
});
|
||||
|
||||
// Force theme refresh
|
||||
const editorInstance = editor as { updateOptions?: (options: unknown) => void };
|
||||
if (editorInstance && editorInstance.updateOptions) {
|
||||
editorInstance.updateOptions({ theme: 'transparentTheme' });
|
||||
}
|
||||
|
||||
// Register auto-completion
|
||||
monaco.languages.registerCompletionItemProvider('gotemplate', {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
provideCompletionItems: (model: any, position: any) => {
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: position.column,
|
||||
endColumn: position.column,
|
||||
};
|
||||
|
||||
return {
|
||||
suggestions: completionItems.map((item) => ({
|
||||
...item,
|
||||
range,
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Register hover provider
|
||||
monaco.languages.registerHoverProvider('gotemplate', {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
provideHover: (model: any, position: any) => {
|
||||
const word = model.getWordAtPosition(position);
|
||||
if (word) {
|
||||
const wordText = word.word;
|
||||
|
||||
if (GO_TEMPLATE_KEYWORDS.includes(wordText)) {
|
||||
return {
|
||||
range: new monaco.Range(
|
||||
position.lineNumber,
|
||||
word.startColumn,
|
||||
position.lineNumber,
|
||||
word.endColumn,
|
||||
),
|
||||
contents: [{ value: `**Go Template Keyword**: ${wordText}` }],
|
||||
};
|
||||
}
|
||||
|
||||
if (SPRIG_FUNCTIONS.includes(wordText)) {
|
||||
return {
|
||||
range: new monaco.Range(
|
||||
position.lineNumber,
|
||||
word.startColumn,
|
||||
position.lineNumber,
|
||||
word.endColumn,
|
||||
),
|
||||
contents: [{ value: `**Sprig Function**: ${wordText}` }],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
if (props.onMount) {
|
||||
props.onMount(editor as Parameters<NonNullable<typeof props.onMount>>[0], monaco);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MonacoEditor
|
||||
title='Go Template Editor'
|
||||
description={`Go text/template syntax${enableSprig ? ' with Sprig functions' : ''}`}
|
||||
{...props}
|
||||
language='gotemplate'
|
||||
placeholder='Enter your Go template here...'
|
||||
onMount={handleEditorMount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
export { GoTemplateEditor } from '@workspace/ui/custom-components/editor/go-template';
|
||||
export { HTMLEditor } from '@workspace/ui/custom-components/editor/html';
|
||||
export { JSONEditor } from '@workspace/ui/custom-components/editor/json';
|
||||
export { MarkdownEditor } from '@workspace/ui/custom-components/editor/markdown';
|
||||
|
||||
@ -5,6 +5,7 @@ import { Button } from '@workspace/ui/components/button';
|
||||
import { cn } from '@workspace/ui/lib/utils';
|
||||
import { useSize } from 'ahooks';
|
||||
import { EyeIcon, EyeOff, FullscreenIcon, MinimizeIcon } from 'lucide-react';
|
||||
import DraculaTheme from 'monaco-themes/themes/Dracula.json' with { type: 'json' };
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export interface MonacoEditorProps {
|
||||
@ -145,10 +146,11 @@ export function MonacoEditor({
|
||||
theme='transparentTheme'
|
||||
beforeMount={(monaco: Monaco) => {
|
||||
monaco.editor.defineTheme('transparentTheme', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
base: DraculaTheme.base as 'vs' | 'vs-dark' | 'hc-black',
|
||||
inherit: DraculaTheme.inherit,
|
||||
rules: DraculaTheme.rules,
|
||||
colors: {
|
||||
...DraculaTheme.colors,
|
||||
'editor.background': '#00000000',
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user