[Feature] Add save interface. (#7)

* [Feature] Add save interface.

* [Fix] Revise ts.
diff --git a/studio/components/files/index.tsx b/studio/components/files/index.tsx
index c666390..c4a0eb0 100644
--- a/studio/components/files/index.tsx
+++ b/studio/components/files/index.tsx
@@ -52,8 +52,8 @@
     expose({ refresh })
 
     const renderLabel = (info: { option: TreeOption }): VNodeChild => {
-      const { isCreate, label, type } = info.option as IFileRecord
-      return !isCreate
+      const { isEditing, label, type } = info.option as IFileRecord
+      return !isEditing
         ? `${label}${type ? '.' + FILE_TYPES_SUFFIX[type] : ''}`
         : h(
             NInput,
@@ -63,7 +63,9 @@
               onBlur: onBlur,
               ref: props.inputRef
             },
-            { suffix: () => (type ? '.' + FILE_TYPES_SUFFIX[type] : '') }
+            {
+              suffix: () => (type ? '.' + FILE_TYPES_SUFFIX[type] : '')
+            }
           )
     }
     return () => (
@@ -77,6 +79,8 @@
         key={keyRef.value}
         defaultExpandAll
         expand-on-click
+        labelField='name'
+        keyField='id'
       ></NTree>
     )
   }
diff --git a/studio/components/studio-sider/helper.ts b/studio/components/studio-sider/helper.ts
new file mode 100644
index 0000000..443f6fc
--- /dev/null
+++ b/studio/components/studio-sider/helper.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import type { IFileRecord } from './types'
+
+export const sameNameValidator = (
+  name: string,
+  list: IFileRecord[]
+): boolean => {
+  return list.some((record) => record.name === name)
+}
diff --git a/studio/components/studio-sider/use-file.ts b/studio/components/studio-sider/use-file.ts
index a4acb07..ee44e2b 100644
--- a/studio/components/studio-sider/use-file.ts
+++ b/studio/components/studio-sider/use-file.ts
@@ -15,15 +15,22 @@
  * limitations under the License.
  */
 import { reactive, Ref, nextTick } from 'vue'
+import { useMessage } from 'naive-ui'
+import { addFile } from '@/service/modules/file'
+import { useLocale } from '@/hooks'
+import { sameNameValidator } from './helper'
 import type { IFileState, FileType, IFileRecord } from './types'
 
 export const useFile = (inputRef: Ref, fileRef: Ref) => {
   const state = reactive({
     currentKey: 0,
-    files: [],
+    files: [{ type: '', id: 1, name: '123', pid: 0 }],
     isCreating: false
   } as IFileState)
 
+  const message = useMessage()
+  const { t } = useLocale()
+
   const filesCached = {} as { [key: number]: IFileRecord }
 
   const freshFiles = () => {
@@ -33,52 +40,68 @@
   const getCurrentFolderKey = (): number => {
     if (state.currentKey === 0) return 0
     const currentRecord = filesCached[state.currentKey]
-    return currentRecord.type ? currentRecord.pid : currentRecord.key
+    return currentRecord.type ? currentRecord.pid : currentRecord.id
   }
 
-  const create = async (isFile: boolean, type?: FileType) => {
+  const create = async (isFile: boolean, type: FileType | '') => {
     if (state.isCreating) return
     state.isCreating = true
     const currentFolderKey = getCurrentFolderKey()
     const record = {
-      isCreate: true,
-      key: Date.now(),
-      label: '',
+      isEditing: true,
+      id: Date.now(),
+      name: '',
       pid: currentFolderKey
     } as IFileRecord
 
-    isFile ? (record.type = type) : (record.children = [])
+    record.type = type
+    !isFile && (record.children = [])
 
-    filesCached[record.key] = record
+    filesCached[record.id] = record
 
     if (currentFolderKey === 0) {
       state.files.unshift(record)
     } else {
-      filesCached[currentFolderKey].children.unshift(record)
+      filesCached[currentFolderKey].children?.unshift(record)
     }
 
-    state.currentKey = record.key
+    state.currentKey = record.id
 
     freshFiles()
     await nextTick()
     inputRef.value?.focus()
   }
 
+  const save = async () => {
+    const currentRecord = filesCached[state.currentKey]
+    try {
+      const { id } = await addFile(currentRecord.pid, {
+        type: currentRecord.type || '',
+        name: currentRecord.name
+      })
+      message.success(t('saved_successfully'))
+      currentRecord.id = id
+      return true
+    } catch (err) {
+      return false
+    }
+  }
+
   const onCreateFile = (type: FileType) => void create(true, type)
 
-  const onCreateFolder = () => void create(false)
+  const onCreateFolder = () => void create(false, '')
 
   const onSelectFile = (key: number) => {
     state.currentKey = key
   }
 
-  const onInputBlur = (value: string) => {
+  const onInputBlur = async (value: string) => {
     state.isCreating = false
     if (!value) {
       const currentFolderKey = getCurrentFolderKey()
 
       currentFolderKey
-        ? filesCached[currentFolderKey].children.shift()
+        ? filesCached[currentFolderKey].children?.shift()
         : state.files.shift()
 
       delete filesCached[state.currentKey]
@@ -88,9 +111,23 @@
       freshFiles()
       return
     }
-    filesCached[state.currentKey].isCreate = false
-    filesCached[state.currentKey].label = value
-    freshFiles()
+
+    const pid = filesCached[state.currentKey].pid
+    const isSame = sameNameValidator(
+      value,
+      pid ? filesCached[pid].children || [] : state.files
+    )
+    if (isSame) {
+      message.error(t('same_name_tips'))
+      return
+    }
+
+    const result = await save()
+    if (result) {
+      filesCached[state.currentKey].isEditing = false
+      filesCached[state.currentKey].name = value
+      freshFiles()
+    }
   }
 
   return { state, onCreateFile, onCreateFolder, onSelectFile, onInputBlur }
diff --git a/studio/locales/en_US.ts b/studio/locales/en_US.ts
index 6d2a562..2900b2b 100644
--- a/studio/locales/en_US.ts
+++ b/studio/locales/en_US.ts
@@ -16,7 +16,9 @@
  */
 
 export const enUS = {
-  test: 'Test'
+  success: 'Success',
+  saved_successfully: 'Saved successfully',
+  same_name_tips: 'Same name exits for files at the same level.'
 }
 
 export type Locale = typeof enUS
diff --git a/studio/locales/zh_CN.ts b/studio/locales/zh_CN.ts
index e451bcb..1822818 100644
--- a/studio/locales/zh_CN.ts
+++ b/studio/locales/zh_CN.ts
@@ -16,5 +16,7 @@
  */
 
 export const zhCN = {
-  test: '测试'
+  success: '成功',
+  saved_successfully: '保存成功',
+  same_name_tips: '同级文件存在相同名字'
 }
diff --git a/studio/service/modules/file/index.ts b/studio/service/modules/file/index.ts
index 6c9f44e..3065801 100644
--- a/studio/service/modules/file/index.ts
+++ b/studio/service/modules/file/index.ts
@@ -16,7 +16,7 @@
  */
 
 import { axios } from '@/service/service'
-import type { IFileContent } from './types'
+import type { IFileContent, IFileRecord, FileType } from './types'
 
 export const getFileContent = (id: number): Promise<IFileContent> => {
   return axios.get(`files/${id}`)
@@ -29,3 +29,12 @@
 export const runFile = (id: number) => {
   return axios.post(`files/${id}/run`)
 }
+
+export const getFiles = (): Promise<IFileRecord[]> => axios.get('/files')
+
+export const addFile = (
+  pid: number,
+  data: { type: FileType | ''; name: string }
+): Promise<{ id: number }> => axios.put(`files/${pid}/add`, data)
+
+export const deleteFile = (id: number) => axios.delete(`/files/${id}`)
diff --git a/studio/service/modules/file/types.ts b/studio/service/modules/file/types.ts
index d4d66a0..755c6b3 100644
--- a/studio/service/modules/file/types.ts
+++ b/studio/service/modules/file/types.ts
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+export type { IFileRecord, FileType } from '@/types/file'
 
 export interface IFileContent {
   content: string
diff --git a/studio/types/file.ts b/studio/types/file.ts
index c8864d7..c46438f 100644
--- a/studio/types/file.ts
+++ b/studio/types/file.ts
@@ -29,11 +29,12 @@
 }
 
 export interface IFileRecord extends TreeOption {
-  isCreate?: boolean
-  type?: FileType
-  key: number
-  children: IFileRecord[]
+  isEditing?: boolean
+  type: FileType | ''
+  id: number
+  children?: IFileRecord[]
   pid: number
+  name: string
 }
 
 export { TreeOption }