mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-12 05:49:40 +08:00
Merge pull request #451 from TonyJiangWJ/feature/sql-snippets
# Conflicts: # frontend/package.json.md5
This commit is contained in:
@@ -1 +1 @@
|
||||
d0464f9da25e9356e61652e638c99ffe
|
||||
d0464f9da25e9356e61652e638c99ffe
|
||||
|
||||
@@ -6,6 +6,7 @@ import { BrowserOpenURL, Environment, EventsOn, Quit, WindowFullscreen, WindowGe
|
||||
import Sidebar from './components/Sidebar';
|
||||
import TabManager from './components/TabManager';
|
||||
import ConnectionModal from './components/ConnectionModal';
|
||||
import SnippetSettingsModal from './components/SnippetSettingsModal';
|
||||
import ConnectionPackagePasswordModal from './components/ConnectionPackagePasswordModal';
|
||||
import DataSyncModal from './components/DataSyncModal';
|
||||
import DriverManagerModal from './components/DriverManagerModal';
|
||||
@@ -1886,6 +1887,7 @@ function App() {
|
||||
const [themeModalSection, setThemeModalSection] = useState<'theme' | 'appearance'>('theme');
|
||||
const [isAppearanceModalOpen, setIsAppearanceModalOpen] = useState(false);
|
||||
const [isShortcutModalOpen, setIsShortcutModalOpen] = useState(false);
|
||||
const [isSnippetModalOpen, setIsSnippetModalOpen] = useState(false);
|
||||
const [capturingShortcutAction, setCapturingShortcutAction] = useState<ShortcutAction | null>(null);
|
||||
const [isProxyModalOpen, setIsProxyModalOpen] = useState(false);
|
||||
const [isDataRootModalOpen, setIsDataRootModalOpen] = useState(false);
|
||||
@@ -2425,6 +2427,16 @@ function App() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleOpenSnippetSettingsEvent = () => {
|
||||
setIsSnippetModalOpen(true);
|
||||
};
|
||||
window.addEventListener('gonavi:open-snippet-settings', handleOpenSnippetSettingsEvent as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener('gonavi:open-snippet-settings', handleOpenSnippetSettingsEvent as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMacRuntime || !useNativeMacWindowControls) {
|
||||
return;
|
||||
@@ -3606,6 +3618,12 @@ function App() {
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
<SnippetSettingsModal
|
||||
open={isSnippetModalOpen}
|
||||
onClose={() => setIsSnippetModalOpen(false)}
|
||||
darkMode={darkMode}
|
||||
overlayTheme={overlayTheme}
|
||||
/>
|
||||
<Modal
|
||||
title={renderUtilityModalTitle(<GlobalOutlined />, '全局代理设置', '统一配置更新检查、驱动管理与未单独指定代理的连接网络出口。')}
|
||||
open={isProxyModalOpen}
|
||||
|
||||
@@ -1372,6 +1372,41 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
},
|
||||
});
|
||||
|
||||
// SQL snippet completion provider
|
||||
monaco.languages.registerCompletionItemProvider('sql', {
|
||||
provideCompletionItems: (model: any, position: any) => {
|
||||
const word = model.getWordUntilPosition(position);
|
||||
const prefix = word.word.toLowerCase();
|
||||
if (!prefix) return { suggestions: [] };
|
||||
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
endColumn: word.endColumn,
|
||||
};
|
||||
|
||||
const allSnippets = useStore.getState().sqlSnippets;
|
||||
const matched = allSnippets.filter(s =>
|
||||
s.prefix.toLowerCase().startsWith(prefix) ||
|
||||
s.name.toLowerCase().includes(prefix)
|
||||
);
|
||||
|
||||
return {
|
||||
suggestions: matched.map(s => ({
|
||||
label: s.prefix,
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: s.body,
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
detail: s.name,
|
||||
documentation: s.description || s.body,
|
||||
range,
|
||||
sortText: '04' + s.prefix,
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
} // end sqlCompletionRegistered guard
|
||||
|
||||
// 每个编辑器实例都注册内容变化监听(检测斜杠命令标记)
|
||||
@@ -1462,6 +1497,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
onClick: () => setSqlFormatOptions({ keywordCase: 'lower' })
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: 'snippet-settings',
|
||||
label: '代码片段管理...',
|
||||
onClick: () => window.dispatchEvent(new CustomEvent('gonavi:open-snippet-settings')),
|
||||
},
|
||||
{
|
||||
key: 'shortcut-settings',
|
||||
label: '快捷键管理...',
|
||||
|
||||
422
frontend/src/components/SnippetSettingsModal.tsx
Normal file
422
frontend/src/components/SnippetSettingsModal.tsx
Normal file
@@ -0,0 +1,422 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { Modal, Button, Input, List, Tag, Popconfirm, message, Collapse, Typography } from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
UndoOutlined,
|
||||
SaveOutlined,
|
||||
CodeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { SqlSnippet } from '../types';
|
||||
import { useStore } from '../store';
|
||||
import { BUILTIN_SNIPPET_MAP } from '../utils/sqlSnippetDefaults';
|
||||
import type { OverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
|
||||
|
||||
interface SnippetSettingsModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
darkMode: boolean;
|
||||
overlayTheme: OverlayWorkbenchTheme;
|
||||
}
|
||||
|
||||
type DraftSnippet = Omit<SqlSnippet, 'createdAt'> & { createdAt?: number };
|
||||
|
||||
const emptyDraft = (): DraftSnippet => ({
|
||||
id: uuidv4(),
|
||||
prefix: '',
|
||||
name: '',
|
||||
description: '',
|
||||
body: '',
|
||||
isBuiltin: false,
|
||||
});
|
||||
|
||||
export default function SnippetSettingsModal({
|
||||
open,
|
||||
onClose,
|
||||
darkMode,
|
||||
overlayTheme,
|
||||
}: SnippetSettingsModalProps) {
|
||||
const sqlSnippets = useStore((s) => s.sqlSnippets);
|
||||
const saveSqlSnippet = useStore((s) => s.saveSqlSnippet);
|
||||
const deleteSqlSnippet = useStore((s) => s.deleteSqlSnippet);
|
||||
const resetBuiltinSqlSnippet = useStore((s) => s.resetBuiltinSqlSnippet);
|
||||
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState<DraftSnippet>(emptyDraft());
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
const shellStyle = useMemo(
|
||||
() => ({
|
||||
background: overlayTheme.shellBg,
|
||||
border: overlayTheme.shellBorder,
|
||||
boxShadow: overlayTheme.shellShadow,
|
||||
backdropFilter: overlayTheme.shellBackdropFilter,
|
||||
}),
|
||||
[overlayTheme],
|
||||
);
|
||||
|
||||
const panelStyle = useMemo(
|
||||
() => ({
|
||||
padding: 16,
|
||||
borderRadius: 14,
|
||||
border: overlayTheme.sectionBorder,
|
||||
background: overlayTheme.sectionBg,
|
||||
}),
|
||||
[overlayTheme],
|
||||
);
|
||||
|
||||
const textColor = darkMode ? 'rgba(255,255,255,0.85)' : 'rgba(16,24,40,0.9)';
|
||||
const mutedColor = darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(16,24,40,0.55)';
|
||||
const selectedBg = darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.04)';
|
||||
|
||||
const sortedSnippets = useMemo(
|
||||
() => [...sqlSnippets].sort((a, b) => a.prefix.localeCompare(b.prefix)),
|
||||
[sqlSnippets],
|
||||
);
|
||||
|
||||
const selectedSnippet = useMemo(
|
||||
() => sqlSnippets.find((s) => s.id === selectedId) ?? null,
|
||||
[sqlSnippets, selectedId],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(snippet: SqlSnippet) => {
|
||||
setIsCreating(false);
|
||||
setSelectedId(snippet.id);
|
||||
setDraft({ ...snippet });
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleNew = useCallback(() => {
|
||||
setIsCreating(true);
|
||||
setSelectedId(null);
|
||||
setDraft(emptyDraft());
|
||||
}, []);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const prefix = draft.prefix.toLowerCase().replace(/[^a-z0-9_]/g, '').slice(0, 20);
|
||||
if (!prefix) {
|
||||
void message.warning('前缀不能为空');
|
||||
return;
|
||||
}
|
||||
if (!draft.name.trim()) {
|
||||
void message.warning('名称不能为空');
|
||||
return;
|
||||
}
|
||||
if (!draft.body.trim()) {
|
||||
void message.warning('片段内容不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
const duplicate = sqlSnippets.find(
|
||||
(s) => s.prefix.toLowerCase() === prefix && s.id !== draft.id,
|
||||
);
|
||||
if (duplicate) {
|
||||
void message.warning(`前缀 "${prefix}" 已被其他片段使用`);
|
||||
return;
|
||||
}
|
||||
|
||||
const toSave: SqlSnippet = {
|
||||
id: draft.id,
|
||||
prefix,
|
||||
name: draft.name.trim(),
|
||||
description: draft.description?.trim() || undefined,
|
||||
body: draft.body,
|
||||
isBuiltin: draft.isBuiltin,
|
||||
createdAt: draft.createdAt ?? Date.now(),
|
||||
};
|
||||
|
||||
saveSqlSnippet(toSave);
|
||||
setSelectedId(toSave.id);
|
||||
setIsCreating(false);
|
||||
void message.success('片段已保存');
|
||||
}, [draft, sqlSnippets, saveSqlSnippet]);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(id: string) => {
|
||||
deleteSqlSnippet(id);
|
||||
if (selectedId === id) {
|
||||
setSelectedId(null);
|
||||
setDraft(emptyDraft());
|
||||
}
|
||||
void message.success('片段已删除');
|
||||
},
|
||||
[deleteSqlSnippet, selectedId],
|
||||
);
|
||||
|
||||
const handleReset = useCallback(
|
||||
(id: string) => {
|
||||
resetBuiltinSqlSnippet(id);
|
||||
const original = BUILTIN_SNIPPET_MAP[id];
|
||||
if (original && selectedId === id) {
|
||||
setDraft({ ...original });
|
||||
}
|
||||
void message.success('已重置为默认');
|
||||
},
|
||||
[resetBuiltinSqlSnippet, selectedId],
|
||||
);
|
||||
|
||||
const syntaxHelpItems = [
|
||||
{
|
||||
key: 'syntax',
|
||||
label: '片段语法说明',
|
||||
children: (
|
||||
<div style={{ fontSize: 12, lineHeight: 1.8, color: mutedColor, fontFamily: 'monospace' }}>
|
||||
<div>{'${1:占位符} 第一个 Tab 位,占位符为提示文字'}</div>
|
||||
<div>{'${2:默认值} 第二个 Tab 位,默认值可直接确认'}</div>
|
||||
<div>{'$0 最终光标位置'}</div>
|
||||
<div>{'${1:表名} 同一数字在多处出现时会同步编辑'}</div>
|
||||
<div style={{ marginTop: 6, fontWeight: 600, color: textColor }}>{'内置变量(展开时自动替换为实际值):'}</div>
|
||||
<div>{'${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} 当前日期'}</div>
|
||||
<div>{'${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND} 当前时间'}</div>
|
||||
<div>{'${CURRENT_SECONDS_UNIX} Unix 时间戳'}</div>
|
||||
<div>{'${UUID} 随机 UUID'}</div>
|
||||
<div>{'${RANDOM} 6 位随机数'}</div>
|
||||
<div style={{ marginTop: 8, fontFamily: 'inherit', color: textColor }}>
|
||||
{'示例:SELECT ${1:列名} FROM ${2:表名} WHERE date >= \'${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}\';$0'}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const showEditor = isCreating || selectedSnippet;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 12,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
background: overlayTheme.iconBg,
|
||||
color: overlayTheme.iconColor,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<CodeOutlined />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 16, fontWeight: 600, color: textColor }}>代码片段管理</div>
|
||||
<div style={{ fontSize: 12, color: mutedColor, lineHeight: 1.5 }}>
|
||||
管理 SQL 代码片段,输入前缀后按 Tab 展开
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
width={820}
|
||||
styles={{
|
||||
content: shellStyle,
|
||||
header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 },
|
||||
body: { paddingTop: 8 },
|
||||
footer: { background: 'transparent', borderTop: 'none', paddingTop: 40 },
|
||||
}}
|
||||
footer={[
|
||||
<Button key="close" type="primary" onClick={onClose}>
|
||||
关闭
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 16, minHeight: 420 }}>
|
||||
{/* Left: snippet list */}
|
||||
<div
|
||||
style={{
|
||||
width: 220,
|
||||
flexShrink: 0,
|
||||
borderRadius: 14,
|
||||
border: overlayTheme.sectionBorder,
|
||||
background: overlayTheme.sectionBg,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '8px 12px 4px', fontSize: 12, color: mutedColor, fontWeight: 600 }}>
|
||||
片段列表
|
||||
</div>
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={sortedSnippets}
|
||||
renderItem={(snippet) => (
|
||||
<List.Item
|
||||
onClick={() => handleSelect(snippet)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '6px 12px',
|
||||
background: selectedId === snippet.id ? selectedBg : 'transparent',
|
||||
borderLeft:
|
||||
selectedId === snippet.id
|
||||
? `3px solid ${overlayTheme.iconBg}`
|
||||
: '3px solid transparent',
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, overflow: 'hidden' }}>
|
||||
<Typography.Text
|
||||
code
|
||||
style={{ fontSize: 12, flexShrink: 0, color: textColor }}
|
||||
>
|
||||
{snippet.prefix}
|
||||
</Typography.Text>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: textColor,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{snippet.name}
|
||||
</span>
|
||||
{snippet.isBuiltin && (
|
||||
<Tag
|
||||
style={{
|
||||
fontSize: 10,
|
||||
lineHeight: '16px',
|
||||
padding: '0 4px',
|
||||
margin: 0,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
color="blue"
|
||||
>
|
||||
内置
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ padding: 8 }}>
|
||||
<Button type="dashed" icon={<PlusOutlined />} block size="small" onClick={handleNew}>
|
||||
新建片段
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: editor */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{showEditor ? (
|
||||
<div
|
||||
style={{
|
||||
...panelStyle,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<div style={{ flex: 0.4 }}>
|
||||
<div style={{ fontSize: 12, color: mutedColor, marginBottom: 4 }}>前缀</div>
|
||||
<Input
|
||||
value={draft.prefix}
|
||||
onChange={(e) =>
|
||||
setDraft((d) => ({ ...d, prefix: e.target.value.toLowerCase() }))
|
||||
}
|
||||
placeholder="如 sel, ins"
|
||||
maxLength={20}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 0.6 }}>
|
||||
<div style={{ fontSize: 12, color: mutedColor, marginBottom: 4 }}>名称</div>
|
||||
<Input
|
||||
value={draft.name}
|
||||
onChange={(e) => setDraft((d) => ({ ...d, name: e.target.value }))}
|
||||
placeholder="片段显示名称"
|
||||
maxLength={60}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: mutedColor, marginBottom: 4 }}>描述(可选)</div>
|
||||
<Input
|
||||
value={draft.description || ''}
|
||||
onChange={(e) => setDraft((d) => ({ ...d, description: e.target.value }))}
|
||||
placeholder="补全详情中的描述文字"
|
||||
maxLength={200}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
<div style={{ fontSize: 12, color: mutedColor, marginBottom: 4 }}>片段内容</div>
|
||||
<Input.TextArea
|
||||
value={draft.body}
|
||||
onChange={(e) => setDraft((d) => ({ ...d, body: e.target.value }))}
|
||||
placeholder={'SELECT ${1:columns} FROM ${2:table_name}$0;'}
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 120,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 13,
|
||||
resize: 'none',
|
||||
}}
|
||||
/>
|
||||
<Collapse
|
||||
size="small"
|
||||
items={syntaxHelpItems}
|
||||
style={{ marginTop: 8, background: 'transparent' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', paddingTop: 4 }}>
|
||||
{draft.isBuiltin && draft.createdAt && (
|
||||
<Popconfirm
|
||||
title="重置为默认"
|
||||
description="将恢复此内置片段的原始内容"
|
||||
onConfirm={() => handleReset(draft.id)}
|
||||
>
|
||||
<Button icon={<UndoOutlined />} size="small">
|
||||
重置为默认
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{!draft.isBuiltin && !isCreating && (
|
||||
<Popconfirm
|
||||
title="删除片段"
|
||||
description="确定要删除此片段吗?"
|
||||
onConfirm={() => handleDelete(draft.id)}
|
||||
>
|
||||
<Button danger icon={<DeleteOutlined />} size="small">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
<Button type="primary" icon={<SaveOutlined />} size="small" onClick={handleSave}>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
...panelStyle,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
height: '100%',
|
||||
color: mutedColor,
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
选择左侧片段编辑,或点击「新建片段」
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ExternalSQLDirectory,
|
||||
JVMDiagnosticCommandDraft,
|
||||
JVMDiagnosticEventChunk,
|
||||
SqlSnippet,
|
||||
} from "./types";
|
||||
import {
|
||||
ShortcutAction,
|
||||
@@ -23,6 +24,10 @@ import {
|
||||
sanitizeShortcutOptions,
|
||||
} from "./utils/shortcuts";
|
||||
import { buildExternalSQLDirectoryId } from "./utils/externalSqlTree";
|
||||
import {
|
||||
DEFAULT_SQL_SNIPPETS,
|
||||
BUILTIN_SNIPPET_MAP,
|
||||
} from "./utils/sqlSnippetDefaults";
|
||||
import { toPersistedGlobalProxy } from "./utils/globalProxyDraft";
|
||||
import {
|
||||
DEFAULT_DATA_GRID_DISPLAY_SETTINGS,
|
||||
@@ -60,7 +65,7 @@ const DEFAULT_TIMEOUT_SECONDS = 30;
|
||||
const MAX_TIMEOUT_SECONDS = 3600;
|
||||
const DEFAULT_DIAGNOSTIC_TIMEOUT_SECONDS = 15;
|
||||
const MAX_DIAGNOSTIC_TIMEOUT_SECONDS = 300;
|
||||
const PERSIST_VERSION = 8;
|
||||
const PERSIST_VERSION = 9;
|
||||
const PERSIST_STORAGE_KEY = "lite-db-storage";
|
||||
const DEFAULT_CONNECTION_TYPE = "mysql";
|
||||
const DEFAULT_JVM_PORT = 9010;
|
||||
@@ -788,6 +793,7 @@ interface AppState {
|
||||
sqlFormatOptions: { keywordCase: "upper" | "lower" };
|
||||
queryOptions: QueryOptions;
|
||||
shortcutOptions: ShortcutOptions;
|
||||
sqlSnippets: SqlSnippet[];
|
||||
sqlLogs: SqlLog[];
|
||||
tableAccessCount: Record<string, number>;
|
||||
tableSortPreference: Record<string, "name" | "frequency">;
|
||||
@@ -875,6 +881,9 @@ interface AppState {
|
||||
binding: Partial<ShortcutBinding>,
|
||||
) => void;
|
||||
resetShortcutOptions: () => void;
|
||||
saveSqlSnippet: (snippet: SqlSnippet) => void;
|
||||
deleteSqlSnippet: (id: string) => void;
|
||||
resetBuiltinSqlSnippet: (id: string) => void;
|
||||
|
||||
addSqlLog: (log: SqlLog) => void;
|
||||
clearSqlLogs: () => void;
|
||||
@@ -967,6 +976,37 @@ const sanitizeSavedQueries = (value: unknown): SavedQuery[] => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const sanitizeSqlSnippets = (value: unknown): SqlSnippet[] => {
|
||||
if (!Array.isArray(value)) return DEFAULT_SQL_SNIPPETS;
|
||||
const result: SqlSnippet[] = [];
|
||||
const seenIds = new Set<string>();
|
||||
value.forEach((entry, index) => {
|
||||
if (!entry || typeof entry !== "object") return;
|
||||
const raw = entry as Record<string, unknown>;
|
||||
const prefix = toTrimmedString(raw.prefix)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, "")
|
||||
.slice(0, 20);
|
||||
const body = toTrimmedString(raw.body);
|
||||
if (!prefix || !body) return;
|
||||
const id = toTrimmedString(raw.id, `snippet-${index + 1}`) || `snippet-${index + 1}`;
|
||||
if (seenIds.has(id)) return;
|
||||
seenIds.add(id);
|
||||
result.push({
|
||||
id,
|
||||
prefix,
|
||||
name: toTrimmedString(raw.name, `片段-${index + 1}`) || `片段-${index + 1}`,
|
||||
description: toTrimmedString(raw.description) || undefined,
|
||||
body,
|
||||
isBuiltin: raw.isBuiltin === true,
|
||||
createdAt: Number.isFinite(Number(raw.createdAt))
|
||||
? Number(raw.createdAt)
|
||||
: Date.now(),
|
||||
});
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const sanitizeExternalSQLDirectories = (
|
||||
value: unknown,
|
||||
): ExternalSQLDirectory[] => {
|
||||
@@ -1402,6 +1442,7 @@ export const useStore = create<AppState>()(
|
||||
showColumnType: true,
|
||||
},
|
||||
shortcutOptions: cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS),
|
||||
sqlSnippets: DEFAULT_SQL_SNIPPETS,
|
||||
sqlLogs: [],
|
||||
tableAccessCount: {},
|
||||
tableSortPreference: {},
|
||||
@@ -1805,6 +1846,33 @@ export const useStore = create<AppState>()(
|
||||
});
|
||||
},
|
||||
|
||||
saveSqlSnippet: (snippet) =>
|
||||
set((state) => {
|
||||
const existing = state.sqlSnippets.findIndex((s) => s.id === snippet.id);
|
||||
if (existing >= 0) {
|
||||
const updated = [...state.sqlSnippets];
|
||||
updated[existing] = snippet;
|
||||
return { sqlSnippets: updated };
|
||||
}
|
||||
return { sqlSnippets: [...state.sqlSnippets, snippet] };
|
||||
}),
|
||||
deleteSqlSnippet: (id) =>
|
||||
set((state) => ({
|
||||
sqlSnippets: state.sqlSnippets.filter(
|
||||
(s) => s.id !== id || s.isBuiltin,
|
||||
),
|
||||
})),
|
||||
resetBuiltinSqlSnippet: (id) =>
|
||||
set((state) => {
|
||||
const original = BUILTIN_SNIPPET_MAP[id];
|
||||
if (!original) return state;
|
||||
return {
|
||||
sqlSnippets: state.sqlSnippets.map((s) =>
|
||||
s.id === id ? { ...original } : s,
|
||||
),
|
||||
};
|
||||
}),
|
||||
|
||||
addSqlLog: (log) =>
|
||||
set((state) => ({ sqlLogs: [log, ...state.sqlLogs].slice(0, 1000) })), // Keep last 1000 logs
|
||||
clearSqlLogs: () => set({ sqlLogs: [] }),
|
||||
@@ -2140,6 +2208,15 @@ export const useStore = create<AppState>()(
|
||||
nextState.shortcutOptions = sanitizeShortcutOptions(
|
||||
state.shortcutOptions,
|
||||
);
|
||||
const existingSnippets = sanitizeSqlSnippets(state.sqlSnippets);
|
||||
const existingSnippetIds = new Set(existingSnippets.map((s) => s.id));
|
||||
const missingSnippets = DEFAULT_SQL_SNIPPETS.filter(
|
||||
(d) => !existingSnippetIds.has(d.id),
|
||||
);
|
||||
nextState.sqlSnippets =
|
||||
missingSnippets.length > 0
|
||||
? [...existingSnippets, ...missingSnippets]
|
||||
: existingSnippets;
|
||||
nextState.tableAccessCount = sanitizeTableAccessCount(
|
||||
state.tableAccessCount,
|
||||
);
|
||||
@@ -2204,6 +2281,7 @@ export const useStore = create<AppState>()(
|
||||
sqlFormatOptions: sanitizeSqlFormatOptions(state.sqlFormatOptions),
|
||||
queryOptions: sanitizeQueryOptions(state.queryOptions),
|
||||
shortcutOptions: sanitizeShortcutOptions(state.shortcutOptions),
|
||||
sqlSnippets: sanitizeSqlSnippets(state.sqlSnippets),
|
||||
tableAccessCount: sanitizeTableAccessCount(state.tableAccessCount),
|
||||
|
||||
// AI 会话数据不再从 localStorage 恢复,改为从后端文件加载
|
||||
@@ -2228,6 +2306,7 @@ export const useStore = create<AppState>()(
|
||||
sqlFormatOptions: state.sqlFormatOptions,
|
||||
queryOptions: state.queryOptions,
|
||||
shortcutOptions: resolveShortcutOptionsForPersistence(state.shortcutOptions),
|
||||
sqlSnippets: state.sqlSnippets,
|
||||
tableAccessCount: state.tableAccessCount,
|
||||
tableSortPreference: state.tableSortPreference,
|
||||
tableColumnOrders: state.tableColumnOrders,
|
||||
|
||||
@@ -454,6 +454,16 @@ export interface SavedQuery {
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface SqlSnippet {
|
||||
id: string;
|
||||
prefix: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
body: string;
|
||||
isBuiltin: boolean;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface ExternalSQLDirectory {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
73
frontend/src/utils/sqlSnippetDefaults.test.ts
Normal file
73
frontend/src/utils/sqlSnippetDefaults.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DEFAULT_SQL_SNIPPETS, BUILTIN_SNIPPET_MAP } from './sqlSnippetDefaults';
|
||||
import type { SqlSnippet } from '../types';
|
||||
|
||||
describe('sqlSnippetDefaults', () => {
|
||||
it('DEFAULT_SQL_SNIPPETS should be a non-empty array', () => {
|
||||
expect(Array.isArray(DEFAULT_SQL_SNIPPETS)).toBe(true);
|
||||
expect(DEFAULT_SQL_SNIPPETS.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('every default snippet should have required fields', () => {
|
||||
for (const s of DEFAULT_SQL_SNIPPETS) {
|
||||
expect(s.id).toBeTruthy();
|
||||
expect(s.prefix).toBeTruthy();
|
||||
expect(s.name).toBeTruthy();
|
||||
expect(s.body).toBeTruthy();
|
||||
expect(s.isBuiltin).toBe(true);
|
||||
expect(typeof s.createdAt).toBe('number');
|
||||
}
|
||||
});
|
||||
|
||||
it('every prefix should be lowercase alphanumeric/underscore', () => {
|
||||
for (const s of DEFAULT_SQL_SNIPPETS) {
|
||||
expect(s.prefix).toMatch(/^[a-z0-9_]+$/);
|
||||
}
|
||||
});
|
||||
|
||||
it('prefixes should be unique', () => {
|
||||
const prefixes = DEFAULT_SQL_SNIPPETS.map((s) => s.prefix);
|
||||
expect(new Set(prefixes).size).toBe(prefixes.length);
|
||||
});
|
||||
|
||||
it('ids should be unique', () => {
|
||||
const ids = DEFAULT_SQL_SNIPPETS.map((s) => s.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
});
|
||||
|
||||
it('all default snippets should have snippet syntax in body', () => {
|
||||
for (const s of DEFAULT_SQL_SNIPPETS) {
|
||||
const hasTabStopOrVariable = /\$\d|\$\{|CURRENT_/.test(s.body);
|
||||
expect(hasTabStopOrVariable).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('time-variable snippets should contain CURRENT_ markers', () => {
|
||||
const seld = DEFAULT_SQL_SNIPPETS.find((s) => s.prefix === 'seld');
|
||||
expect(seld).toBeDefined();
|
||||
expect(seld!.body).toContain('CURRENT_YEAR');
|
||||
expect(seld!.body).toContain('CURRENT_MONTH');
|
||||
expect(seld!.body).toContain('CURRENT_DATE');
|
||||
|
||||
const inst = DEFAULT_SQL_SNIPPETS.find((s) => s.prefix === 'inst');
|
||||
expect(inst).toBeDefined();
|
||||
expect(inst!.body).toContain('CURRENT_HOUR');
|
||||
expect(inst!.body).toContain('CURRENT_MINUTE');
|
||||
expect(inst!.body).toContain('CURRENT_SECOND');
|
||||
});
|
||||
|
||||
it('BUILTIN_SNIPPET_MAP should contain all default snippet ids', () => {
|
||||
for (const s of DEFAULT_SQL_SNIPPETS) {
|
||||
expect(BUILTIN_SNIPPET_MAP[s.id]).toBeDefined();
|
||||
expect(BUILTIN_SNIPPET_MAP[s.id].prefix).toBe(s.prefix);
|
||||
expect(BUILTIN_SNIPPET_MAP[s.id].body).toBe(s.body);
|
||||
}
|
||||
});
|
||||
|
||||
it('BUILTIN_SNIPPET_MAP entries should be independent copies', () => {
|
||||
for (const s of DEFAULT_SQL_SNIPPETS) {
|
||||
const mapped = BUILTIN_SNIPPET_MAP[s.id];
|
||||
expect(mapped).not.toBe(s);
|
||||
}
|
||||
});
|
||||
});
|
||||
154
frontend/src/utils/sqlSnippetDefaults.ts
Normal file
154
frontend/src/utils/sqlSnippetDefaults.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import type { SqlSnippet } from "../types";
|
||||
|
||||
const builtinSnippets: Omit<SqlSnippet, "createdAt">[] = [
|
||||
{
|
||||
id: "builtin-sel",
|
||||
prefix: "sel",
|
||||
name: "SELECT 基本查询",
|
||||
description: "基本 SELECT 查询模板",
|
||||
body: "SELECT ${1:column_list} FROM ${2:table_name}$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-selw",
|
||||
prefix: "selw",
|
||||
name: "SELECT WHERE",
|
||||
description: "带 WHERE 条件的 SELECT 查询",
|
||||
body: "SELECT ${1:columns} FROM ${2:table_name} WHERE ${3:condition}$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-selj",
|
||||
prefix: "selj",
|
||||
name: "SELECT JOIN",
|
||||
description: "带 INNER JOIN 的 SELECT 查询",
|
||||
body: "SELECT ${1:columns}\nFROM ${2:t1}\nINNER JOIN ${3:t2} ON ${4:t1.id} = ${5:t2.id}\nWHERE ${6:condition}$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-ins",
|
||||
prefix: "ins",
|
||||
name: "INSERT",
|
||||
description: "INSERT 插入数据模板",
|
||||
body: "INSERT INTO ${1:table_name} (${2:columns})\nVALUES (${3:values})$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-upd",
|
||||
prefix: "upd",
|
||||
name: "UPDATE",
|
||||
description: "UPDATE 更新数据模板",
|
||||
body: "UPDATE ${1:table_name}\nSET ${2:column} = ${3:value}\nWHERE ${4:condition}$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-del",
|
||||
prefix: "del",
|
||||
name: "DELETE",
|
||||
description: "DELETE 删除数据模板",
|
||||
body: "DELETE FROM ${1:table_name}\nWHERE ${2:condition}$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-ct",
|
||||
prefix: "ct",
|
||||
name: "CREATE TABLE",
|
||||
description: "CREATE TABLE 建表模板",
|
||||
body: "CREATE TABLE ${1:table_name} (\n ${2:id} INT PRIMARY KEY AUTO_INCREMENT,\n ${3:col} ${4:VARCHAR(255)} NOT NULL\n)$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-alt",
|
||||
prefix: "alt",
|
||||
name: "ALTER TABLE",
|
||||
description: "ALTER TABLE 添加列模板",
|
||||
body: "ALTER TABLE ${1:table_name}\nADD COLUMN ${2:col} ${3:VARCHAR(255)}$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-dro",
|
||||
prefix: "dro",
|
||||
name: "DROP TABLE",
|
||||
description: "DROP TABLE 删表模板",
|
||||
body: "DROP TABLE IF EXISTS ${1:table_name}$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-grp",
|
||||
prefix: "grp",
|
||||
name: "GROUP BY",
|
||||
description: "带 GROUP BY 的聚合查询模板",
|
||||
body: "SELECT ${1:col}, COUNT(*)\nFROM ${2:table_name}\nGROUP BY ${1:col}$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-ljo",
|
||||
prefix: "ljo",
|
||||
name: "LEFT JOIN",
|
||||
description: "LEFT JOIN 左连接模板",
|
||||
body: "LEFT JOIN ${1:t} ON ${2:left.col} = ${3:right.col}$0",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-sub",
|
||||
prefix: "sub",
|
||||
name: "子查询",
|
||||
description: "IN 子查询模板",
|
||||
body: "SELECT ${1:cols}\nFROM ${2:t1}\nWHERE ${3:col} IN (\n SELECT ${4:col} FROM ${5:t2} WHERE ${6:cond}\n)$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-lim",
|
||||
prefix: "lim",
|
||||
name: "LIMIT 查询",
|
||||
description: "带 LIMIT 的分页查询模板",
|
||||
body: "SELECT ${1:cols} FROM ${2:table_name} LIMIT ${3:10}$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-ord",
|
||||
prefix: "ord",
|
||||
name: "ORDER BY",
|
||||
description: "带排序的查询模板",
|
||||
body: "SELECT ${1:cols} FROM ${2:table_name} ORDER BY ${3:col} ${4|ASC,DESC|}$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-seld",
|
||||
prefix: "seld",
|
||||
name: "SELECT 按日期查询",
|
||||
description: "按日期条件过滤的 SELECT 查询,自动填入当天日期",
|
||||
body: "SELECT ${1:cols} FROM ${2:table_name}\nWHERE ${3:date_col} >= '${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}'$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-ctt",
|
||||
prefix: "ctt",
|
||||
name: "CREATE TABLE(含时间列)",
|
||||
description: "建表模板,含 created_at / updated_at 时间列",
|
||||
body: "CREATE TABLE ${1:table_name} (\n ${2:id} INT PRIMARY KEY AUTO_INCREMENT,\n ${3:col} ${4:VARCHAR(255)},\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n)$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
id: "builtin-inst",
|
||||
prefix: "inst",
|
||||
name: "INSERT(含时间戳)",
|
||||
description: "INSERT 模板,自动填入当前时间戳",
|
||||
body: "INSERT INTO ${1:table_name} (${2:columns}, created_at)\nVALUES (${3:values}, '${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} ${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND}')$0;",
|
||||
isBuiltin: true,
|
||||
},
|
||||
];
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
export const DEFAULT_SQL_SNIPPETS: SqlSnippet[] = builtinSnippets.map(
|
||||
(s, i) => ({
|
||||
...s,
|
||||
createdAt: now + i,
|
||||
})
|
||||
);
|
||||
|
||||
export const BUILTIN_SNIPPET_MAP: Record<string, SqlSnippet> = {};
|
||||
for (const s of DEFAULT_SQL_SNIPPETS) {
|
||||
BUILTIN_SNIPPET_MAP[s.id] = { ...s };
|
||||
}
|
||||
83
frontend/wailsjs/runtime/runtime.d.ts
vendored
83
frontend/wailsjs/runtime/runtime.d.ts
vendored
@@ -246,4 +246,85 @@ export function OnFileDropOff() :void
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
|
||||
// Notification types
|
||||
export interface NotificationOptions {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string; // macOS and Linux only
|
||||
body?: string;
|
||||
categoryId?: string;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
id?: string;
|
||||
title?: string;
|
||||
destructive?: boolean; // macOS-specific
|
||||
}
|
||||
|
||||
export interface NotificationCategory {
|
||||
id?: string;
|
||||
actions?: NotificationAction[];
|
||||
hasReplyField?: boolean;
|
||||
replyPlaceholder?: string;
|
||||
replyButtonTitle?: string;
|
||||
}
|
||||
|
||||
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||
// Initializes the notification service for the application.
|
||||
// This must be called before sending any notifications.
|
||||
export function InitializeNotifications(): Promise<void>;
|
||||
|
||||
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||
// Cleans up notification resources and releases any held connections.
|
||||
export function CleanupNotifications(): Promise<void>;
|
||||
|
||||
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||
// Checks if notifications are available on the current platform.
|
||||
export function IsNotificationAvailable(): Promise<boolean>;
|
||||
|
||||
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||
// Requests notification authorization from the user (macOS only).
|
||||
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||
// Checks the current notification authorization status (macOS only).
|
||||
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||
// Sends a basic notification with the given options.
|
||||
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||
// Sends a notification with action buttons. Requires a registered category.
|
||||
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||
|
||||
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||
// Removes a previously registered notification category.
|
||||
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||
|
||||
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||
// Removes all pending notifications from the notification center.
|
||||
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||
|
||||
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||
// Removes a specific pending notification by its identifier.
|
||||
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||
// Removes all delivered notifications from the notification center.
|
||||
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||
|
||||
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||
// Removes a specific delivered notification by its identifier.
|
||||
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||
// Removes a notification by its identifier (cross-platform convenience function).
|
||||
export function RemoveNotification(identifier: string): Promise<void>;
|
||||
|
||||
@@ -239,4 +239,60 @@ export function CanResolveFilePaths() {
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
}
|
||||
|
||||
export function InitializeNotifications() {
|
||||
return window.runtime.InitializeNotifications();
|
||||
}
|
||||
|
||||
export function CleanupNotifications() {
|
||||
return window.runtime.CleanupNotifications();
|
||||
}
|
||||
|
||||
export function IsNotificationAvailable() {
|
||||
return window.runtime.IsNotificationAvailable();
|
||||
}
|
||||
|
||||
export function RequestNotificationAuthorization() {
|
||||
return window.runtime.RequestNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function CheckNotificationAuthorization() {
|
||||
return window.runtime.CheckNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function SendNotification(options) {
|
||||
return window.runtime.SendNotification(options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options) {
|
||||
return window.runtime.SendNotificationWithActions(options);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category) {
|
||||
return window.runtime.RegisterNotificationCategory(category);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryId) {
|
||||
return window.runtime.RemoveNotificationCategory(categoryId);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications() {
|
||||
return window.runtime.RemoveAllPendingNotifications();
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier) {
|
||||
return window.runtime.RemovePendingNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications() {
|
||||
return window.runtime.RemoveAllDeliveredNotifications();
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier) {
|
||||
return window.runtime.RemoveDeliveredNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier) {
|
||||
return window.runtime.RemoveNotification(identifier);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user