mirror of https://github.com/nocodb/nocodb
Muhammed Mustafa
2 years ago
3 changed files with 421 additions and 0 deletions
@ -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> |
@ -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…
Reference in new issue