Browse Source

refactor/gui-v2-store-added

pull/2798/head
Muhammed Mustafa 2 years ago
parent
commit
164fad3bc3
  1. 4
      packages/nc-gui-v2/components.d.ts
  2. 240
      packages/nc-gui-v2/components/dashboard/settings/AppStore/AppInstall.vue
  3. 177
      packages/nc-gui-v2/components/dashboard/settings/AppStore/index.vue

4
packages/nc-gui-v2/components.d.ts vendored

@ -8,6 +8,7 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
AAvatar: typeof import('ant-design-vue/es')['Avatar']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
@ -39,13 +40,16 @@ declare module '@vue/runtime-core' {
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
ATableColumn: typeof import('ant-design-vue/es')['TableColumn']
ATableColumnGroup: typeof import('ant-design-vue/es')['TableColumnGroup']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

240
packages/nc-gui-v2/components/dashboard/settings/AppStore/AppInstall.vue

@ -0,0 +1,240 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useToast } from 'vue-toastification'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
import MdiPlusIcon from '~icons/mdi/plus'
import { extractSdkResponseErrorMsg } from '~~/utils/errorUtils'
interface Props {
id: string
}
const { id } = defineProps<Props>()
const emits = defineEmits(['saved'])
const toast = useToast()
const { $api } = useNuxtApp()
const plugin = ref<any>(null)
const pluginFormData = ref<any>({})
const formRef = ref()
const isLoading = ref(true)
const loadingAction = ref<null | string>(null)
const saveSettings = async () => {
loadingAction.value = 'save'
try {
await formRef.value?.validateFields()
await $api.plugin.update(id, {
input: JSON.stringify(pluginFormData.value),
active: true,
})
emits('saved')
toast.success(plugin.value.formDetails.msgOnInstall || 'Plugin settings saved successfully')
} catch (_e: any) {
const e = await extractSdkResponseErrorMsg(_e)
toast.error(e.message)
} finally {
loadingAction.value = null
}
}
const addSetting = () => {
pluginFormData.value.push({})
}
const testSettings = async () => {
loadingAction.value = 'test'
try {
const res = await $api.plugin.test({
input: pluginFormData.value,
id: plugin.value?.id,
category: plugin.value?.category,
title: plugin.value?.title,
})
if (res) {
toast.success('Successfully tested plugin settings')
} else {
toast.info('Invalid credentials')
}
} catch (_e: any) {
const e = await extractSdkResponseErrorMsg(_e)
toast.error(e.message)
} finally {
loadingAction.value = null
}
}
const doAction = async (action: { key: string }) => {
switch (action.key) {
case 'save':
await saveSettings()
break
case 'test':
await testSettings()
break
default:
break
}
}
const readPluginDetails = async () => {
try {
isLoading.value = true
const res = await $api.plugin.read(id)
const formDetails = JSON.parse(res.input_schema ?? '{}')
const emptyParsedInput = formDetails.array ? [{}] : {}
const parsedInput = res.input ? JSON.parse(res.input) : emptyParsedInput
plugin.value = { ...res, formDetails, parsedInput }
pluginFormData.value = plugin.value.parsedInput
} catch (e) {
console.log(e)
} finally {
isLoading.value = false
}
}
const deleteRow = (index: number) => {
pluginFormData.value.splice(index, 1)
}
onMounted(async () => {
if (plugin.value === null) {
await readPluginDetails()
}
})
</script>
<template>
<div>
<div v-if="isLoading" class="flex flex-row w-full justify-center">
<a-spin size="large" />
</div>
<template v-else>
<div class="flex flex-col">
<div class="flex flex-row justify-center pb-4 mb-2 border-b-1 w-full space-x-1">
<div
v-if="plugin.logo"
:style="{ background: plugin.title === 'SES' ? '#242f3e' : '' }"
class="mr-1 d-flex align-center justify-center"
:class="{ 'pa-2': plugin.title === 'SES' }"
>
<img :src="`/${plugin.logo}`" class="h-6" />
</div>
<span class="font-semibold text-lg">{{ plugin.formDetails.title }}</span>
</div>
<a-form ref="formRef" :model="pluginFormData" class="mx-auto mt-2">
<!-- Form with multiple entry -->
<table v-if="plugin.formDetails.array" class="form-table">
<thead>
<tr>
<th v-for="(columnData, columnIndex) in plugin.formDetails.items" :key="columnIndex">
<div class="text-center font-normal">
{{ columnData.label }} <span v-if="columnData.required" class="text-red-600">*</span>
</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(itemRow, itemIndex) in plugin.parsedInput" :key="itemIndex">
<td v-for="(columnData, columnIndex) in plugin.formDetails.items" :key="columnIndex" class="px-2">
<a-form-item
:name="[`${itemIndex}`, columnData.key]"
:rules="[{ required: columnData.required, message: `${columnData.label} is required` }]"
>
<a-input-password
v-if="columnData.type === 'Password'"
v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-textarea
v-else-if="columnData.type === 'LongText'"
v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-switch
v-else-if="columnData.type === 'Checkbox'"
v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-input v-else v-model:value="itemRow[columnData.key]" :placeholder="columnData.placeholder" />
</a-form-item>
</td>
<td v-if="itemIndex !== 0" class="pb-4">
<MdiDeleteOutlineIcon class="hover:text-red-400 cursor-pointer" @click="deleteRow(itemIndex)" />
</td>
</tr>
</tbody>
<tr>
<td :colspan="plugin.formDetails.items.length" class="text-center">
<a-button type="default" class="!bg-gray-100 rounded-md border-0" @click="addSetting">
<template #icon>
<MdiPlusIcon class="flex mx-auto" />
</template>
</a-button>
</td>
</tr>
</table>
<!-- Form with only one entry -->
<div v-else>
<a-form-item
v-for="(columnData, i) in plugin.formDetails.items"
:key="i"
:label="columnData.label"
:name="columnData.key"
:rules="[{ required: columnData.required, message: `${columnData.label} is required` }]"
>
<a-input-password
v-if="columnData.type === 'Password'"
v-model:value="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-textarea
v-else-if="columnData.type === 'LongText'"
v-model:value="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-switch
v-else-if="columnData.type === 'Checkbox'"
v-model:checked="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-input v-else v-model:value="pluginFormData[columnData.key]" :placeholder="columnData.placeholder" />
</a-form-item>
</div>
<div class="flex flex-row space-x-4 justify-center mt-4">
<a-button
v-for="(action, i) in plugin.formDetails.actions"
:key="i"
:loading="loadingAction === action.key"
:type="action.key === 'save' ? 'primary' : 'default'"
:disabled="!!loadingAction"
@click="doAction(action)"
>
{{ action.label }}
</a-button>
</div>
</a-form>
</div>
</template>
</div>
</template>
<style scoped lang="scss">
.form-table {
border: none;
min-width: 400px;
}
tbody tr:nth-of-type(odd) {
background-color: transparent;
}
</style>

177
packages/nc-gui-v2/components/dashboard/settings/AppStore/index.vue

@ -0,0 +1,177 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useToast } from 'vue-toastification'
import AppInstall from './AppInstall.vue'
import MdiEditIcon from '~icons/ic/round-edit'
import MdiCloseCircleIcon from '~icons/mdi/close-circle-outline'
import MdiPlusIcon from '~icons/mdi/plus'
const { $api, $e } = useNuxtApp()
const toast = useToast()
const apps = ref<null | Array<any>>(null)
const showPluginUninstallModal = ref(false)
const showPluginInstallModal = ref(false)
const pluginApp = ref<any>(null)
const loadPluginList = async () => {
try {
const plugins = (await $api.plugin.list()).list ?? []
apps.value = plugins.map((p) => ({
...p,
tags: p.tags ? p.tags.split(',') : [],
parsedInput: p.input && JSON.parse(p.input),
}))
} catch (e) {
console.error(e)
}
}
const confirmResetPlugin = async () => {
try {
await $api.plugin.update(pluginApp.value.id, {
input: undefined,
active: false,
})
toast.success('Plugin uninstalled successfully')
showPluginUninstallModal.value = false
await loadPluginList()
} catch (e: any) {
console.log(e)
toast.error(e.message)
}
$e('a:appstore:reset', { app: pluginApp.value.title })
}
const saved = async () => {
showPluginInstallModal.value = false
await loadPluginList()
$e('a:appstore:install', { app: pluginApp.value.title })
}
const installApp = async (app: any) => {
showPluginInstallModal.value = true
pluginApp.value = app
$e('c:appstore:install', { app: app.title })
}
const resetApp = async (app: any) => {
showPluginUninstallModal.value = true
pluginApp.value = app
}
onMounted(async () => {
if (apps.value === null) {
loadPluginList()
}
})
</script>
<template>
<a-modal v-model:visible="showPluginInstallModal" min-width="400px" max-width="700px" min-height="300" :footer="null">
<AppInstall
v-if="pluginApp && showPluginInstallModal"
:id="pluginApp.id"
@close="showPluginInstallModal = false"
@saved="saved()"
/>
</a-modal>
<a-modal v-model:visible="showPluginUninstallModal" min-width="400px" max-width="700px" min-height="300" :footer="null">
<div class="flex flex-col">
<div class="flex">{{ `Please click on submit to reset ${pluginApp && pluginApp.title}` }}</div>
<div class="flex mt-6 justify-center space-x-2">
<a-button @click="showPluginUninstallModal = false"> Cancel </a-button>
<a-button type="primary" @click="confirmResetPlugin"> Submit </a-button>
</div>
</div>
</a-modal>
<v-dialog min-width="400px" max-width="700px" min-height="300">
<v-card v-if="pluginApp">
<v-card-text> Please confirm to reset {{ pluginApp.title }} </v-card-text>
<v-card-actions>
<v-btn color="primary" @click="confirmResetPlugin"> Yes </v-btn>
<v-btn @click="showPluginUninstallModal = false"> No </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<div class="h-full overflow-y-scroll grid grid-cols-2 gap-x-2 gap-y-4">
<a-card v-for="(app, i) in apps" :key="i" class="relative flex overflow-x-hidden app-item-card !shadow-sm rounded-md w-full">
<div class="install-btn flex flex-row justify-end space-x-1">
<a-button v-if="app.parsedInput" size="small" outlined class="!caption capitalize" @click="installApp(app)">
<div class="flex flex-row justify-center items-center">
<MdiEditIcon :height="12" />
Edit
</div>
</a-button>
<a-button v-if="app.parsedInput" size="small" outlined class="caption capitalize" @click="resetApp(app)">
<div class="flex flex-row justify-center items-center">
<MdiCloseCircleIcon />
<div class="flex ml-0.5">Reset</div>
</div>
</a-button>
<a-button v-else size="small" outlined class="caption capitalize" @click="installApp(app)">
<div class="flex flex-row justify-center items-center">
<MdiPlusIcon />
Install
</div>
</a-button>
</div>
<div class="flex flex-row space-x-2 items-center justify-start w-full">
<div class="flex w-20 px-4">
<a-avatar
shape="circle"
:style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}"
:src="`/${app.logo}`"
:color="app.title === 'SES' ? '#242f3e' : ''"
>
</a-avatar>
</div>
<div class="flex flex-col flex-grow-1">
<a-typography-title :level="5">{{ app.title }}</a-typography-title>
{{ app.description }}
</div>
</div>
</a-card>
</div>
</template>
<style scoped lang="scss">
.app-item-card {
position: relative;
transition: 0.4s background-color;
.install-btn {
position: absolute;
opacity: 1;
right: -100%;
top: 10px;
transition: 0.4s opacity, 0.4s right;
}
&:hover .install-btn {
right: 10px;
opacity: 1;
}
}
.app-item-card {
transition: 0.4s background-color, 0.4s transform;
&:hover {
background: rgba(123, 126, 136, 0.1) !important;
}
}
.caption {
font-size: 12px;
color: #242f3e;
}
</style>
Loading…
Cancel
Save