Browse Source

feat: setup page - WIP

pull/9314/head
Pranav C 5 months ago
parent
commit
478f54d229
  1. 60
      packages/nc-gui/components/account/Setup.vue
  2. 122
      packages/nc-gui/components/account/setup/ConfigModal.vue
  3. 115
      packages/nc-gui/components/account/setup/Email.vue
  4. 75
      packages/nc-gui/components/account/setup/ListModal.vue
  5. 114
      packages/nc-gui/components/account/setup/Storage.vue
  6. 2
      packages/nc-gui/components/nc/Modal.vue
  7. 118
      packages/nc-gui/composables/useAccountSetupStore.ts
  8. 1
      packages/nc-gui/lang/en.json
  9. 4
      packages/nc-gui/pages/account/index/setup-old/[[nestedPage]].vue

60
packages/nc-gui/components/account/Setup.vue

@ -1,28 +1,48 @@
<script setup lang="ts">
const { t } = useI18n()
const configs = ref([
const { loadSetupApps, emailConfigured, storageConfigured, listModalDlg } = useProvideAccountSetupStore()
const openedCategory = ref<string | null>(null)
const configs = computed(() => [
{
title: t('labels.setupLabel', { label: t('labels.email') }),
key: 'email',
description: 'Configure an email account to send system notifications to your organisation’s users.',
docsLink: '',
path: '/account/setup/email'
onClick: () => {
listModalDlg.value = true
openedCategory.value = 'Email'
},
configured: emailConfigured.value,
},
{
title: t('labels.setupLabel', { label: t('labels.storage') }),
key: 'storage',
description: 'Configure a storage service to store your organisation’s data.',
docsLink: '',
path: '/account/setup/storage'
onClick: () => {
listModalDlg.value = true
openedCategory.value = 'Storage'
},
configured: storageConfigured.value,
},
{
title: t('labels.switchToProd'),
key: 'switchToProd',
description: 'Configure a production-ready app database to port from the existing built-in application database.',
docsLink: ''
docsLink: '',
onClick: () => {
// TODO: Implement the logic to switch to production
},
isPending: !isEeUI,
},
])
onMounted(async () => {
await loadSetupApps()
})
</script>
<template>
@ -45,17 +65,39 @@ const configs = ref([
>
<div class="flex flex-col gap-6 w-150">
<div v-for="config of configs" class="flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-y-2">
<div class="flex font-bold text-base" data-rec="true">{{ config.title }}</div>
<div class="text-gray-600 text-tiny">{{config.description}}</div>
<div class="flex flex justify-between" data-rec="true">
<span class="font-bold text-base"> {{ config.title }}</span>
<div class="flex justify-between">
<NcButton size="small !text-tiny" type="text">Go to docs</NcButton>
<NcButton size="small" @click="navigateTo(config.path)">Configure</NcButton>
<div
v-if="!config.configured || config.isPending"
class="flex items-center gap-1 text-orange-500 bg-orange-50 border-1 border-orange-500 px-1 rounded"
>
<GeneralIcon icon="warning" class="text-orange-500" />
{{ $t('activity.pending') }}
</div>
</div>
<div class="text-gray-600 text-sm">{{ config.description }}</div>
<div class="flex justify-between mt-4" v-if="config.configured">
<div class="flex gap-2">
<GeneralIcon icon="check" class="text-green-500" />
</div>
</div>
<div v-else class="flex justify-between mt-4" >
<NcButton size="small" type="text">
<div class="flex gap-2 items-center">
Go to docs
<GeneralIcon icon="ncExternalLink" />
</div>
</NcButton>
<NcButton size="small" @click="config.onClick">Configure</NcButton>
</div>
</div>
</div>
</div>
<LazyAccountSetupListModal v-if="openedCategory" :category="openedCategory" v-model="listModalDlg" />
</div>
</template>

122
packages/nc-gui/components/account/setup/ConfigModal.vue

@ -0,0 +1,122 @@
<script setup lang="ts">
import { Action } from '../../../composables/useAccountSetupStore'
const props = defineProps<{
id: string
modelValue?: boolean
}>()
const emit = defineEmits(['saved', 'close', 'update:modelValue'])
const vOpen = useVModel(props, 'modelValue', emit)
const {
readPluginDetails,
activePluginFormData: pluginFormData,
activePlugin: plugin,
isLoading,
loadingAction,
testSettings,
saveSettings,
} = useAccountSetupStoreOrThrow()
if (!plugin.value) {
await readPluginDetails(props.id)
}
const pluginTypeMap = {
Input: FormBuilderInputType.Input,
Select: FormBuilderInputType.Select,
Checkbox: FormBuilderInputType.Switch,
LongText: FormBuilderInputType.Input,
Password: FormBuilderInputType.Input,
}
const { formState, validate } = useProvideFormBuilderHelper({
formSchema: [
...plugin.value.formDetails.items.map((item) => ({
type: pluginTypeMap[item.type] || FormBuilderInputType.Input,
label: item.label,
placeholder: item.placeholder,
model: item.key,
required: item.required,
})),
],
onSubmit: async () => {
console.log('submit', formState)
},
initialState: pluginFormData,
})
const doAction = async (action: Action) => {
try {
switch (action) {
case Action.Save:
await validate()
await saveSettings()
break
case Action.Test:
await validate()
await testSettings()
break
default:
// noop
break
}
} catch (e: any) {
console.log(e)
} finally {
loadingAction.value = null
}
}
</script>
<template>
<NcModal
:visible="vOpen"
:keyboard="isModalClosable"
centered
width="70rem"
wrap-class-name="nc-modal-create-source"
@keydown.esc="vOpen = false"
>
<div class="flex-1 flex flex-col max-h-full min-h-400px">
<div class="px-4 pb-4 w-full flex items-center gap-3 border-b-1 border-gray-200">
<GeneralIcon icon="arrowLeft" class="flex-none text-[20px]" @click="vOpen = false" />
<div
v-if="plugin.logo"
class="mr-1 flex items-center justify-center"
:class="[plugin.title === 'SES' ? 'p-2 bg-[#242f3e]' : '']"
>
<img :alt="plugin.title || 'plugin'" :src="plugin.logo" class="h-3" />
</div>
<span class="font-semibold text-lg">{{ plugin.formDetails.title }}</span>
<div class="flex-grow" />
<div class="flex gap-2">
<NcButton
v-for="(action, i) in plugin.formDetails.actions"
:key="i"
class="!px-5"
:loading="loadingAction === action.key"
:type="action.key === Action.Save ? 'primary' : 'default'"
size="small"
:disabled="!!loadingAction"
@click="doAction(action.key)"
>
{{ action.label }}
</NcButton>
</div>
</div>
<div class="h-[calc(100%_-_58px)] flex py-4 flex-col">
<div v-if="isLoading || !plugin" class="flex flex-row w-full justify-center items-center h-52">
<a-spin size="large" />
</div>
<div v-else class="flex">
<NcFormBuilder class="w-500px mx-auto" />
</div>
</div>
</div>
</NcModal>
</template>

115
packages/nc-gui/components/account/setup/Email.vue

@ -1,115 +0,0 @@
<script setup lang="ts">
const { emailApps } = useAccountSetupStoreOrThrow()
</script>
<template>
<div class="w-full">
<div class="p-4 flex flex-wrap w-full gap-5 mx-auto my-2">
<div v-for="(app) in emailApps" :key="app.title" class="w-296px max-w-296px flex gap-6 border-1 border-gray-100 py-3 px-6 rounded items-center">
<img
v-if="app.title !== 'SMTP'"
class="max-w-32px max-h-32px"
alt="logo"
:style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}"
:src="app.logo"
/>
<GeneralIcon v-else icon="mail" />
<span class="font-weight-bold">{{ app.title }}</span>
</div>
<!--
<a-card
v-for="(app, i) in emailApps"
:key="i"
class="sm:w-100 md:w-130"
:class="`relative flex overflow-x-hidden app-item-card !shadow-sm rounded-md w-full nc-app-store-card-${app.title}`"
>
<div class="install-btn flex flex-row justify-end space-x-1">
<a-button v-if="app.parsedInput" size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-edit">
<IcRoundEdit class="pr-0.5" :height="12" />
{{ $t('general.edit') }}
</div>
</a-button>
<a-button v-if="app.parsedInput" size="small" outlined @click="showResetPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-reset">
<component :is="iconMap.closeCircle" />
<div class="flex ml-0.5">{{ $t('general.reset') }}</div>
</div>
</a-button>
<a-button v-else size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install">
<component :is="iconMap.plus" />
{{ $t('general.install') }}
</div>
</a-button>
</div>
<div class="flex flex-row space-x-2 items-center justify-start w-full">
<div class="flex w-[68px]">
<img
v-if="app.title !== 'SMTP'"
class="avatar"
alt="logo"
:style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}"
:src="app.logo"
/>
<div v-else />
</div>
<div class="flex flex-col flex-1 w-3/5 pl-3">
<a-typography-title :level="5">{{ app.title }}</a-typography-title>
{{ app.description }}
</div>
</div>
</a-card>-->
</div>
</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: 0.7rem;
}
.avatar {
width: 5rem;
height: 5rem;
padding: 0.25rem;
object-fit: contain;
}
</style>

75
packages/nc-gui/components/account/setup/ListModal.vue

@ -0,0 +1,75 @@
<script setup lang="ts">
const props = defineProps<{
category: string
modelValue?: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const vOpen = useVModel(props, 'modelValue', emit)
const selectedApp = ref<string | null>(null)
const { categorizeApps, confirmModalDlg } = useAccountSetupStoreOrThrow()
const selectApp = (app: any) => {
selectedApp.value = app
confirmModalDlg.value = true
}
</script>
<template>
<NcModal
:visible="vOpen"
:keyboard="isModalClosable"
centered
width="70rem"
wrap-class-name="nc-modal-create-source"
@keydown.esc="vOpen = false"
>
<div class="flex-1 flex flex-col max-h-full min-h-400px">
<div class="px-4 py-3 w-full flex items-center gap-3 border-b-1 border-gray-200">
<div class="flex-1 text-base font-weight-700">Setup {{ category }}</div>
</div>
<div class="h-[calc(100%_-_58px)] flex">
<div class="w-full">
<div class="container">
<div v-for="app in categorizeApps[category] || []" :key="app.title" class="item" @click="selectApp(app)">
<img
v-if="app.title !== 'SMTP'"
class="icon"
:alt="app.title"
:style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}"
:src="app.logo"
/>
<GeneralIcon v-else icon="mail" />
<span class="title">{{ app.title }}</span>
</div>
</div>
</div>
</div>
</div>
<AccountSetupConfigModal v-if="selectedApp" :id="selectedApp.id" v-model="confirmModalDlg" />
</NcModal>
</template>
<style scoped lang="scss">
.container {
@apply p-4 flex flex-wrap w-full gap-5 mx-auto my-2 justify-center;
.item {
@apply w-296px max-w-296px flex gap-6 border-1 border-gray-100 py-3 px-6 rounded items-center cursor-pointer hover:(shadow bg-gray-50);
.icon {
@apply max-w-32px max-h-32px;
}
.title {
@apply font-weight-bold;
}
}
}
</style>

114
packages/nc-gui/components/account/setup/Storage.vue

@ -1,114 +0,0 @@
<script setup lang="ts">
const { storageApps } = useAccountSetupStoreOrThrow()
</script>
<template>
<div class="w-full">
<div class="p-4 flex flex-wrap w-full gap-5 mx-auto my-2">
<div v-for="(app) in storageApps" :key="app.title" class="w-296px max-w-296px flex gap-6 border-1 border-gray-100 py-3 px-6 rounded items-center">
<img
v-if="app.title !== 'SMTP'"
class="max-w-32px max-h-32px"
alt="logo"
:style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}"
:src="app.logo"
/>
<GeneralIcon v-else icon="mail" />
<span class="font-weight-bold">{{ app.title }}</span>
</div>
<!--
<a-card
v-for="(app, i) in storageApps"
:key="i"
class="sm:w-100 md:w-130"
:class="`relative flex overflow-x-hidden app-item-card !shadow-sm rounded-md w-full nc-app-store-card-${app.title}`"
>
<div class="install-btn flex flex-row justify-end space-x-1">
<a-button v-if="app.parsedInput" size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-edit">
<IcRoundEdit class="pr-0.5" :height="12" />
{{ $t('general.edit') }}
</div>
</a-button>
<a-button v-if="app.parsedInput" size="small" outlined @click="showResetPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-reset">
<component :is="iconMap.closeCircle" />
<div class="flex ml-0.5">{{ $t('general.reset') }}</div>
</div>
</a-button>
<a-button v-else size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install">
<component :is="iconMap.plus" />
{{ $t('general.install') }}
</div>
</a-button>
</div>
<div class="flex flex-row space-x-2 items-center justify-start w-full">
<div class="flex w-[68px]">
<img
v-if="app.title !== 'SMTP'"
class="avatar"
alt="logo"
:style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}"
:src="app.logo"
/>
<div v-else />
</div>
<div class="flex flex-col flex-1 w-3/5 pl-3">
<a-typography-title :level="5">{{ app.title }}</a-typography-title>
{{ app.description }}
</div>
</div>
</a-card>-->
</div>
</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: 0.7rem;
}
.avatar {
width: 5rem;
height: 5rem;
padding: 0.25rem;
object-fit: contain;
}
</style>

2
packages/nc-gui/components/nc/Modal.vue

@ -1,4 +1,6 @@
<script lang="ts" setup>
import {Action} from "../../composables/useAccountSetupStore";
const props = withDefaults(
defineProps<{
visible: boolean

118
packages/nc-gui/composables/useAccountSetupStore.ts

@ -1,17 +1,37 @@
import rfdc from 'rfdc'
import type { ColumnReqType, ColumnType, PluginType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import type { Ref } from 'vue'
import type { RuleObject } from 'ant-design-vue/es/form'
import { generateUniqueColumnName } from '~/helpers/parsers/parserHelpers'
import type { PluginTestReqType, PluginType } from 'nocodb-sdk'
export enum Action {
Save = 'save',
Test = 'test',
}
const [useProvideAccountSetupStore, useAccountSetupStore] = createInjectionState(() => {
const apps = ref<(PluginType & { parsedInput?: Record<string, any>; tags?: string[] })[]>([])
const { $api, $e } = useNuxtApp()
const activePlugin = ref<PluginType | null>(null)
const activePluginFormData = ref({})
const isLoading = ref(false)
const loadingAction = ref<null | Action>(null)
const emailApps = computed(() => apps.value.filter((app) => app.category === 'Email'))
const storageApps = computed(() => apps.value.filter((app) => app.category === ('Storage')))
const storageApps = computed(() => apps.value.filter((app) => app.category === 'Storage'))
const emailConfigured = computed(() => emailApps.value.find((app) => app.active))
const storageConfigured = computed(() => storageApps.value.find((app) => app.active))
const listModalDlg = ref(false)
const confirmModalDlg = ref(false)
const categorizeApps = computed(() => {
return apps.value.reduce((acc, app) => {
if (!acc[app.category]) {
acc[app.category] = []
}
acc[app.category].push(app)
return acc
}, {} as Record<string, PluginType[]>)
})
const loadSetupApps = async () => {
try {
@ -27,11 +47,95 @@ const [useProvideAccountSetupStore, useAccountSetupStore] = createInjectionState
}
}
const saveSettings = async () => {
loadingAction.value = Action.Save
try {
// todo: validate form fields
// await formRef.value?.validateFields()
await $api.plugin.update(activePlugin.value?.id, {
input: JSON.stringify(activePluginFormData.value),
active: true,
})
// Plugin settings saved successfully
message.success(activePlugin.value?.formDetails.msgOnInstall || t('msg.success.pluginSettingsSaved'))
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
loadingAction.value = null
}
}
const testSettings = async () => {
loadingAction.value = Action.Test
try {
if (activePlugin.value) {
const res = await $api.plugin.test({
input: JSON.stringify(activePluginFormData.value),
title: activePlugin.value.title,
category: activePlugin.value.category,
} as PluginTestReqType)
if (res) {
// Successfully tested plugin settings
message.success(t('msg.success.pluginTested'))
} else {
// Invalid credentials
message.info(t('msg.info.invalidCredentials'))
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
loadingAction.value = null
}
}
const readPluginDetails = async (id: string) => {
try {
isLoading.value = true
const res = await $api.plugin.read(id)
const formDetails = JSON.parse(res.input_schema ?? '{}')
const emptyParsedInput = formDetails.array ? [{}] : {}
const parsedInput = typeof res.input === 'string' ? JSON.parse(res.input) : emptyParsedInput
// the type of 'secure' was XcType.SingleLineText in 0.0.1
// and it has been changed to XcType.Checkbox, since 0.0.2
// hence, change the text value to boolean here
if ('secure' in parsedInput && typeof parsedInput.secure === 'string') {
parsedInput.secure = parsedInput.secure === 'true'
}
activePlugin.value = { ...res, formDetails, parsedInput }
activePluginFormData.value = activePlugin.value.parsedInput
} catch (e) {
console.log(e)
} finally {
isLoading.value = false
}
}
return {
apps,
emailApps,
storageApps,
loadSetupApps,
categorizeApps,
activePlugin,
readPluginDetails,
activePluginFormData,
isLoading,
testSettings,
saveSettings,
loadingAction,
emailConfigured,
storageConfigured,
listModalDlg,
confirmModalDlg,
}
})

1
packages/nc-gui/lang/en.json

@ -992,6 +992,7 @@
"redirectToUrl": "Redirect to URL"
},
"activity": {
"pending": "Pending",
"webhookDetails": "Webhook Details",
"hideWeekends": "Hide weekends",
"renameBase": "Rename Base",

4
packages/nc-gui/pages/account/index/setup/[[nestedPage]].vue → packages/nc-gui/pages/account/index/setup-old/[[nestedPage]].vue

@ -9,10 +9,10 @@ onMounted(async () => {
<template>
<div class="h-full">
<template v-if="$route.params.nestedPage === 'storage'">
<LazyAccountSetupStorage />
<LazyAccountSetupListModal category="Storage" />
</template>
<template v-else-if="$route.params.nestedPage === 'email'">
<LazyAccountSetupEmail />
<LazyAccountSetupListModal category="Email" />
</template>
<template v-else>
<LazyAccountSetup />
Loading…
Cancel
Save