feat: 新增 Tools 页面并重构 Skill 页面
- 添加 Tools.vue 工具管理页面 - 重构 Skill.vue 代码 - 更新路由和侧边栏导航 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -54,7 +54,7 @@ const group2 = computed(() => [
|
||||
// 第3组: Skills, Tools, Script, Plan, Memory
|
||||
const group3 = computed(() => [
|
||||
{ name: 'Skills', icon: 'fa-wand-magic-sparkles', badge: 21, path: '/mcp' },
|
||||
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/model-apis' },
|
||||
{ name: 'Tools', icon: 'fa-tools', badge: 13, path: '/tools' },
|
||||
{ name: 'Script', icon: 'fa-code', path: '/script' },
|
||||
{ name: 'Plan', icon: 'fa-clock', path: '/plan' },
|
||||
{ name: 'Memory', icon: 'fa-brain', path: '/memory' },
|
||||
|
||||
@@ -5,7 +5,7 @@ import Chat from '@/views/Chat.vue'
|
||||
import Agents from '@/views/Agents.vue'
|
||||
import Team from '@/views/Team.vue'
|
||||
import Skill from '@/views/Skill.vue'
|
||||
import ModelAPIs from '@/views/ModelAPIs.vue'
|
||||
import Tools from '@/views/Tools.vue'
|
||||
import Database from '@/views/Database.vue'
|
||||
import Script from '@/views/Script.vue'
|
||||
import Plan from '@/views/Plan.vue'
|
||||
@@ -49,9 +49,9 @@ const router = createRouter({
|
||||
component: Skill
|
||||
},
|
||||
{
|
||||
path: '/model-apis',
|
||||
name: 'model-apis',
|
||||
component: ModelAPIs
|
||||
path: '/tools',
|
||||
name: 'tools',
|
||||
component: Tools
|
||||
},
|
||||
{
|
||||
path: '/database',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
265
web/src/views/Tools.vue
Normal file
265
web/src/views/Tools.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import '@/views/database/database.css'
|
||||
|
||||
interface Tool {
|
||||
id: number
|
||||
name: string
|
||||
type: 'built-in' | 'custom' | 'mcp'
|
||||
description: string
|
||||
status: 'active' | 'inactive'
|
||||
icon?: string
|
||||
provider?: string
|
||||
config?: object
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// Mock data for tools
|
||||
const builtInTools = ref<Tool[]>([
|
||||
{ id: 1, name: 'File Reader', type: 'built-in', description: 'Read files from the filesystem', status: 'active', icon: 'fa-file-lines', createdAt: '2025-01-15' },
|
||||
{ id: 2, name: 'Web Search', type: 'built-in', description: 'Search the web for information', status: 'active', icon: 'fa-globe', createdAt: '2025-01-15' },
|
||||
{ id: 3, name: 'Calculator', type: 'built-in', description: 'Perform mathematical calculations', status: 'active', icon: 'fa-calculator', createdAt: '2025-01-15' },
|
||||
{ id: 4, name: 'Code Executor', type: 'built-in', description: 'Execute code in a sandbox', status: 'active', icon: 'fa-code', createdAt: '2025-01-15' },
|
||||
{ id: 5, name: 'JSON Parser', type: 'built-in', description: 'Parse and validate JSON data', status: 'active', icon: 'fa-brackets-curly', createdAt: '2025-01-15' },
|
||||
])
|
||||
|
||||
const customTools = ref<Tool[]>([
|
||||
{ id: 101, name: 'GitHub API', type: 'custom', description: 'Interact with GitHub REST API', status: 'active', icon: 'fa-github', provider: 'Custom', createdAt: '2025-03-01' },
|
||||
{ id: 102, name: 'Slack Notifier', type: 'custom', description: 'Send notifications to Slack', status: 'inactive', icon: 'fa-slack', provider: 'Custom', createdAt: '2025-03-05' },
|
||||
{ id: 103, name: 'Email Sender', type: 'custom', description: 'Send emails via SMTP', status: 'active', icon: 'fa-envelope', provider: 'Custom', createdAt: '2025-03-08' },
|
||||
])
|
||||
|
||||
const mcpTools = ref<Tool[]>([
|
||||
{ id: 201, name: 'Puppeteer', type: 'mcp', description: 'Browser automation via Puppeteer', status: 'active', icon: 'fa-browser', provider: 'MCP Server', createdAt: '2025-02-10' },
|
||||
{ id: 202, name: 'SQL Database', type: 'mcp', description: 'Execute SQL queries on databases', status: 'active', icon: 'fa-database', provider: 'MCP Server', createdAt: '2025-02-12' },
|
||||
{ id: 203, name: 'Filesystem', type: 'mcp', description: 'File operations via MCP', status: 'active', icon: 'fa-folder', provider: 'MCP Server', createdAt: '2025-02-15' },
|
||||
{ id: 204, name: 'Git Operations', type: 'mcp', description: 'Git repository operations', status: 'inactive', icon: 'fa-git-alt', provider: 'MCP Server', createdAt: '2025-02-18' },
|
||||
])
|
||||
|
||||
const activeTab = ref<'built-in' | 'custom' | 'mcp'>('built-in')
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref('all')
|
||||
const editingTool = ref<Tool | null>(null)
|
||||
const isEditing = ref(false)
|
||||
|
||||
const editForm = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
provider: '',
|
||||
})
|
||||
|
||||
// Statistics
|
||||
const stats = computed(() => ({
|
||||
total: builtInTools.value.length + customTools.value.length + mcpTools.value.length,
|
||||
active: [...builtInTools.value, ...customTools.value, ...mcpTools.value].filter(t => t.status === 'active').length,
|
||||
builtIn: builtInTools.value.length,
|
||||
custom: customTools.value.length,
|
||||
mcp: mcpTools.value.length
|
||||
}))
|
||||
|
||||
const currentTools = computed(() => {
|
||||
let tools: Tool[] = []
|
||||
switch (activeTab.value) {
|
||||
case 'built-in': tools = builtInTools.value; break
|
||||
case 'custom': tools = customTools.value; break
|
||||
case 'mcp': tools = mcpTools.value; break
|
||||
}
|
||||
return tools.filter(tool => {
|
||||
const matchSearch = tool.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
tool.description.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
const matchStatus = filterStatus.value === 'all' || tool.status === filterStatus.value
|
||||
return matchSearch && matchStatus
|
||||
})
|
||||
})
|
||||
|
||||
const tabCounts = computed(() => ({
|
||||
'built-in': builtInTools.value.length,
|
||||
'custom': customTools.value.length,
|
||||
'mcp': mcpTools.value.length,
|
||||
}))
|
||||
|
||||
const openEdit = (tool: Tool) => {
|
||||
editingTool.value = tool
|
||||
editForm.value = { name: tool.name, description: tool.description, provider: tool.provider || '' }
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
const saveEdit = () => {
|
||||
if (editingTool.value) {
|
||||
const targetArray = editingTool.value.type === 'built-in' ? builtInTools.value :
|
||||
editingTool.value.type === 'custom' ? customTools.value : mcpTools.value
|
||||
const index = targetArray.findIndex(t => t.id === editingTool.value!.id)
|
||||
if (index !== -1) targetArray[index] = { ...targetArray[index], ...editForm.value }
|
||||
}
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const cancelEdit = () => { isEditing.value = false; editingTool.value = null }
|
||||
|
||||
const toggleStatus = (tool: Tool) => {
|
||||
const targetArray = tool.type === 'built-in' ? builtInTools.value :
|
||||
tool.type === 'custom' ? customTools.value : mcpTools.value
|
||||
const found = targetArray.find(t => t.id === tool.id)
|
||||
if (found) found.status = found.status === 'active' ? 'inactive' : 'active'
|
||||
}
|
||||
|
||||
const deleteTool = (id: number) => {
|
||||
switch (activeTab.value) {
|
||||
case 'built-in': builtInTools.value = builtInTools.value.filter(t => t.id !== id); break
|
||||
case 'custom': customTools.value = customTools.value.filter(t => t.id !== id); break
|
||||
case 'mcp': mcpTools.value = mcpTools.value.filter(t => t.id !== id); break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 min-h-screen">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-tools text-orange-500"></i>
|
||||
<span class="font-medium">Tools</span>
|
||||
</div>
|
||||
<button class="btn-primary">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
New Tool
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 导航 -->
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<button
|
||||
v-for="(label, tab) in { 'built-in': 'Built-in', 'custom': 'Custom', 'mcp': 'MCP Servers' }"
|
||||
:key="tab"
|
||||
@click="activeTab = tab as 'built-in' | 'custom' | 'mcp'"
|
||||
class="px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all"
|
||||
:class="activeTab === tab
|
||||
? 'bg-orange-500 text-white'
|
||||
: 'bg-dark-700 text-gray-400 hover:bg-dark-600 hover:text-white'"
|
||||
>
|
||||
<span>{{ label }}</span>
|
||||
<span class="px-2 py-0.5 rounded-full text-xs" :class="activeTab === tab ? 'bg-white/20' : 'bg-dark-600'">
|
||||
{{ tabCounts[tab as keyof typeof tabCounts] }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="flex gap-4 mb-6">
|
||||
<div class="flex-1 relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search tools by name or description..."
|
||||
class="search-input w-full"
|
||||
>
|
||||
</div>
|
||||
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
|
||||
<el-option label="All Status" value="all" />
|
||||
<el-option label="Active" value="active" />
|
||||
<el-option label="Inactive" value="inactive" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- Tools 列表 -->
|
||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||||
<table v-if="currentTools.length > 0" class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
<tr>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Tool Name</th>
|
||||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Provider</th>
|
||||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
||||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Created</th>
|
||||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="tool in currentTools" :key="tool.id" class="table-row">
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg flex items-center justify-center" :class="tool.type === 'built-in' ? 'bg-orange-500/20' : tool.type === 'custom' ? 'bg-blue-500/20' : 'bg-emerald-500/20'">
|
||||
<i :class="['fa-solid', tool.icon || 'fa-cube', 'text-lg', tool.type === 'built-in' ? 'text-orange-400' : tool.type === 'custom' ? 'text-blue-400' : 'text-emerald-400']"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium">{{ tool.name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ tool.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center">
|
||||
<span class="text-gray-400 text-sm">{{ tool.provider || (tool.type === 'built-in' ? 'System' : 'Custom') }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center">
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs" :class="tool.status === 'active' ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'">
|
||||
<span class="w-1.5 h-1.5 rounded-full" :class="tool.status === 'active' ? 'bg-green-500' : 'bg-gray-400'"></span>
|
||||
<span class="capitalize">{{ tool.status }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ tool.createdAt }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button @click="toggleStatus(tool)" class="btn-icon" :title="tool.status === 'active' ? 'Deactivate' : 'Activate'">
|
||||
<i :class="['fa-solid', tool.status === 'active' ? 'fa-pause' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
|
||||
</button>
|
||||
<button @click="openEdit(tool)" class="btn-icon" title="Edit">
|
||||
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||||
</button>
|
||||
<button @click="deleteTool(tool.id)" class="btn-icon" title="Delete">
|
||||
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-box">
|
||||
<div class="empty-icon">
|
||||
<i class="fa-solid fa-tools"></i>
|
||||
</div>
|
||||
<p class="empty-text">No tools found</p>
|
||||
<p class="empty-tip">Click "New Tool" to add a tool</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50 modal-overlay">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl modal-content">
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Edit Tool</h3>
|
||||
<button @click="cancelEdit" class="btn-icon">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Tool Name</label>
|
||||
<input v-model="editForm.name" type="text" class="input-field">
|
||||
</div>
|
||||
<div v-if="activeTab !== 'built-in'">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Provider</label>
|
||||
<input v-model="editForm.provider" type="text" class="input-field">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea v-model="editForm.description" rows="3" class="input-field resize-none"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button @click="cancelEdit" class="btn-secondary">Cancel</button>
|
||||
<button @click="saveEdit" class="btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 使用全局样式 */
|
||||
</style>
|
||||
191
web/src/views/skill/useSkills.ts
Normal file
191
web/src/views/skill/useSkills.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
export interface Skill {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
category: string
|
||||
status: 'running' | 'stopped' | 'error'
|
||||
port: number
|
||||
createdAt: string
|
||||
tools: number
|
||||
}
|
||||
|
||||
export function useSkills() {
|
||||
// Skill 数据
|
||||
const skills = ref<Skill[]>([
|
||||
{ id: 1, name: 'Linear', description: 'Linear API integration for project management', type: 'API', category: 'api', status: 'running', port: 3001, createdAt: '2025-04-10', tools: 12 },
|
||||
{ id: 2, name: 'Google Maps', description: 'Google Maps API for location services', type: 'Google Maps', category: 'api', status: 'running', port: 3002, createdAt: '2025-04-08', tools: 8 },
|
||||
{ id: 3, name: 'File Explorer', description: 'File system explorer and editor', type: 'File System', category: 'filesystem', status: 'error', port: 3003, createdAt: '2025-04-05', tools: 15 },
|
||||
{ id: 4, name: 'PostgreSQL', description: 'PostgreSQL database operations', type: 'Database', category: 'database', status: 'running', port: 3004, createdAt: '2025-04-12', tools: 10 },
|
||||
{ id: 5, name: 'GitHub', description: 'GitHub API integration', type: 'GitHub', category: 'communication', status: 'stopped', port: 3005, createdAt: '2025-04-11', tools: 20 },
|
||||
{ id: 6, name: 'Slack', description: 'Slack messaging integration', type: 'Communication', category: 'communication', status: 'running', port: 3006, createdAt: '2025-04-09', tools: 6 },
|
||||
{ id: 7, name: 'OpenAI', description: 'OpenAI GPT models for AI conversations', type: 'AI/ML', category: 'ai', status: 'running', port: 3007, createdAt: '2025-04-07', tools: 5 },
|
||||
{ id: 8, name: 'MySQL', description: 'MySQL database operations', type: 'Database', category: 'database', status: 'stopped', port: 3008, createdAt: '2025-04-06', tools: 10 },
|
||||
])
|
||||
|
||||
// 搜索和筛选
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref('all')
|
||||
|
||||
// 编辑状态
|
||||
const isEditing = ref(false)
|
||||
const isCreating = ref(false)
|
||||
const editingSkill = ref<Skill | null>(null)
|
||||
|
||||
// 表单
|
||||
const editForm = ref({
|
||||
name: '',
|
||||
type: '',
|
||||
category: '',
|
||||
port: 3000,
|
||||
description: '',
|
||||
})
|
||||
|
||||
const newSkillForm = ref({
|
||||
name: '',
|
||||
type: 'API',
|
||||
category: 'api',
|
||||
port: 3000,
|
||||
description: '',
|
||||
})
|
||||
|
||||
// 分类选项
|
||||
const categories = [
|
||||
{ value: 'api', label: 'API' },
|
||||
{ value: 'database', label: 'Database' },
|
||||
{ value: 'filesystem', label: 'File System' },
|
||||
{ value: 'communication', label: 'Communication' },
|
||||
{ value: 'ai', label: 'AI/ML' },
|
||||
]
|
||||
|
||||
const types = ['API', 'Database', 'File System', 'Communication', 'AI/ML']
|
||||
|
||||
// 筛选
|
||||
const filteredSkills = computed(() => {
|
||||
return skills.value.filter(skill => {
|
||||
const matchSearch = skill.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
skill.description.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
const matchStatus = filterStatus.value === 'all' || skill.status === filterStatus.value
|
||||
return matchSearch && matchStatus
|
||||
})
|
||||
})
|
||||
|
||||
// 状态样式
|
||||
const statusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running': return 'bg-green-500'
|
||||
case 'stopped': return 'bg-gray-500'
|
||||
case 'error': return 'bg-red-500'
|
||||
default: return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
// 打开创建弹窗
|
||||
const openCreate = () => {
|
||||
newSkillForm.value = { name: '', type: 'API', category: 'api', port: 3000, description: '' }
|
||||
isCreating.value = true
|
||||
}
|
||||
|
||||
// 关闭创建弹窗
|
||||
const closeCreate = () => {
|
||||
isCreating.value = false
|
||||
}
|
||||
|
||||
// 保存新技能
|
||||
const saveNewSkill = () => {
|
||||
const newId = Math.max(...skills.value.map(s => s.id)) + 1
|
||||
skills.value.push({
|
||||
id: newId,
|
||||
name: newSkillForm.value.name,
|
||||
description: newSkillForm.value.description,
|
||||
type: newSkillForm.value.type,
|
||||
category: newSkillForm.value.category,
|
||||
status: 'stopped',
|
||||
port: newSkillForm.value.port,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
tools: 0,
|
||||
})
|
||||
isCreating.value = false
|
||||
}
|
||||
|
||||
// 打开编辑弹窗
|
||||
const openEdit = (skill: Skill) => {
|
||||
editingSkill.value = skill
|
||||
editForm.value = {
|
||||
name: skill.name,
|
||||
type: skill.type,
|
||||
category: skill.category,
|
||||
port: skill.port,
|
||||
description: skill.description,
|
||||
}
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
// 关闭编辑弹窗
|
||||
const closeEdit = () => {
|
||||
isEditing.value = false
|
||||
editingSkill.value = null
|
||||
}
|
||||
|
||||
// 保存编辑
|
||||
const saveEdit = () => {
|
||||
const index = skills.value.findIndex(s => s.id === editingSkill.value!.id)
|
||||
if (index !== -1) {
|
||||
skills.value[index] = {
|
||||
...skills.value[index],
|
||||
name: editForm.value.name,
|
||||
type: editForm.value.type,
|
||||
category: editForm.value.category,
|
||||
port: editForm.value.port,
|
||||
description: editForm.value.description,
|
||||
}
|
||||
}
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
const toggleStatus = (skill: Skill) => {
|
||||
if (skill.status === 'running') skill.status = 'stopped'
|
||||
else if (skill.status === 'stopped') skill.status = 'running'
|
||||
}
|
||||
|
||||
// 删除技能
|
||||
const deleteSkill = (id: number) => {
|
||||
ElMessageBox.confirm('Are you sure you want to delete this skill?', 'Confirm Delete', {
|
||||
confirmButtonText: 'Delete',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
skills.value = skills.value.filter(s => s.id !== id)
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
skills,
|
||||
searchQuery,
|
||||
filterStatus,
|
||||
isEditing,
|
||||
isCreating,
|
||||
editingSkill,
|
||||
editForm,
|
||||
newSkillForm,
|
||||
categories,
|
||||
types,
|
||||
// Computed
|
||||
filteredSkills,
|
||||
// Methods
|
||||
statusClass,
|
||||
openCreate,
|
||||
closeCreate,
|
||||
saveNewSkill,
|
||||
openEdit,
|
||||
closeEdit,
|
||||
saveEdit,
|
||||
toggleStatus,
|
||||
deleteSkill,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user