Browse Source

Merge pull request #3129 from nocodb/fix/gui-v2-webhook

fix(gui-v2): webhook follow up issues
pull/3162/head
navi 2 years ago committed by GitHub
parent
commit
2d41fa2d6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      packages/nc-gui-v2/components/api-client/Headers.vue
  2. 17
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  3. 66
      packages/nc-gui-v2/components/webhook/ChannelMultiSelect.vue
  4. 2
      packages/nc-gui-v2/components/webhook/Drawer.vue
  5. 93
      packages/nc-gui-v2/components/webhook/Editor.vue
  6. 6
      packages/nc-gui-v2/components/webhook/List.vue
  7. 2
      packages/nc-gui-v2/components/webhook/Test.vue
  8. 19
      packages/nc-gui-v2/composables/useViewFilters.ts

4
packages/nc-gui-v2/components/api-client/Headers.vue

@ -85,7 +85,7 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
<a-checkbox v-model:checked="headerRow.enabled" /> <a-checkbox v-model:checked="headerRow.enabled" />
</a-form-item> </a-form-item>
</td> </td>
<td class="px-2"> <td class="px-2 w-min-[400px]">
<a-form-item> <a-form-item>
<a-select v-model:value="headerRow.name" size="large" placeholder="Key"> <a-select v-model:value="headerRow.name" size="large" placeholder="Key">
<a-select-option v-for="(header, i) in headerList" :key="i" :value="header"> <a-select-option v-for="(header, i) in headerList" :key="i" :value="header">
@ -94,7 +94,7 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
</a-select> </a-select>
</a-form-item> </a-form-item>
</td> </td>
<td class="px-2"> <td class="px-2 w-min-[400px]">
<a-form-item> <a-form-item>
<a-input v-model:value="headerRow.value" size="large" placeholder="Value" /> <a-input v-model:value="headerRow.value" size="large" placeholder="Value" />
</a-form-item> </a-form-item>

17
packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue

@ -9,12 +9,19 @@ import { ActiveViewInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import MdiDeleteIcon from '~icons/mdi/close-box' import MdiDeleteIcon from '~icons/mdi/close-box'
import MdiAddIcon from '~icons/mdi/plus' import MdiAddIcon from '~icons/mdi/plus'
const { nested = false, parentId, autoSave = true } = defineProps<{ nested?: boolean; parentId?: string; autoSave: boolean }>() const {
nested = false,
parentId,
autoSave = true,
hookId = null,
} = defineProps<{ nested?: boolean; parentId?: string; autoSave: boolean; hookId?: string }>()
const emit = defineEmits(['update:filtersLength']) const emit = defineEmits(['update:filtersLength'])
const meta = inject(MetaInj) const meta = inject(MetaInj)
const activeView = inject(ActiveViewInj) const activeView = inject(ActiveViewInj)
const reloadDataHook = inject(ReloadViewDataHookInj) const reloadDataHook = inject(ReloadViewDataHookInj)
// todo: replace with inject or get from state // todo: replace with inject or get from state
@ -76,7 +83,7 @@ const types = computed(() => {
watch( watch(
() => (activeView?.value as any)?.id, () => (activeView?.value as any)?.id,
(n, o) => { (n, o) => {
if (n !== o) loadFilters() if (n !== o) loadFilters(hookId as string)
}, },
{ immediate: true }, { immediate: true },
) )
@ -95,11 +102,11 @@ watch(
}, },
) )
const applyChanges = async () => { const applyChanges = async (hookId?: string) => {
await sync() await sync(hookId)
for (const nestedFilter of nestedFilters?.value || []) { for (const nestedFilter of nestedFilters?.value || []) {
if (nestedFilter.parentId) { if (nestedFilter.parentId) {
await nestedFilter.applyChanges(true) await nestedFilter.applyChanges(hookId, true)
} }
} }
} }

66
packages/nc-gui-v2/components/webhook/ChannelMultiSelect.vue

@ -0,0 +1,66 @@
<script setup lang="ts">
import { onMounted } from '@vue/runtime-core'
interface Props {
modelValue: Record<string, any>[]
availableChannelList: Record<string, any>[]
placeholder: string
}
const { availableChannelList, placeholder, ...rest } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const vModel = useVModel(rest, 'modelValue', emit)
// idx of selected channels
const localChannelValues = $ref<number[]>([])
// availableChannelList with idx enriched
let availableChannelWithIdxList = $ref<Record<string, any>[]>()
watch(
() => localChannelValues,
(v) => {
const res = []
for (const channelIdx of v) {
const target = availableChannelWithIdxList.find((availableChannel) => availableChannel.idx === channelIdx)
if (target) {
// push without target.idx
res.push({ webhook_url: target.webhook_url, channel: target.channel })
}
}
vModel.value = res
},
)
onMounted(() => {
if (availableChannelList.length) {
// enrich idx
let idx = 0
availableChannelWithIdxList = availableChannelList.map((channel) => ({
...channel,
idx: idx++,
}))
// build localChannelValues from modelValue
for (const channel of rest.modelValue || []) {
const target = availableChannelWithIdxList.find(
(availableChannelWithIdx) =>
availableChannelWithIdx.webhook_url === channel.webhook_url && availableChannelWithIdx.channel === channel.channel,
)
if (target) {
localChannelValues.push(target.idx)
}
}
}
})
</script>
<template>
<a-select v-model:value="localChannelValues" mode="multiple" :placeholder="placeholder" max-tag-count="responsive">
<a-select-option v-for="channel of availableChannelWithIdxList" :key="channel.idx" :value="channel.idx">{{
channel.channel
}}</a-select-option>
</a-select>
</template>

2
packages/nc-gui-v2/components/webhook/Drawer.vue

@ -31,8 +31,10 @@ async function editHook(hook: Record<string, any>) {
:body-style="{ background: 'rgba(67, 81, 232, 0.05)', padding: '50px' }" :body-style="{ background: 'rgba(67, 81, 232, 0.05)', padding: '50px' }"
@keydown.esc="vModel = false" @keydown.esc="vModel = false"
> >
<div>
<WebhookEditor v-if="editOrAdd" ref="webhookEditorRef" @back-to-list="editOrAdd = false" /> <WebhookEditor v-if="editOrAdd" ref="webhookEditorRef" @back-to-list="editOrAdd = false" />
<WebhookList v-else @edit="editHook" @add="editOrAdd = true" /> <WebhookList v-else @edit="editHook" @add="editOrAdd = true" />
</div>
<div class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center"> <div class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center">
<a-button v-t="['e:hiring']" href="https://angel.co/company/nocodb" target="_blank" size="large"> <a-button v-t="['e:hiring']" href="https://angel.co/company/nocodb" target="_blank" size="large">
🚀 We are Hiring! 🚀 🚀 We are Hiring! 🚀

93
packages/nc-gui-v2/components/webhook/Editor.vue

@ -1,13 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Form, message } from 'ant-design-vue' import { Form, message } from 'ant-design-vue'
import { MetaInj } from '~/context' import { MetaInj, extractSdkResponseErrorMsg, fieldRequiredValidator, inject, reactive, useApi, useNuxtApp } from '#imports'
import { extractSdkResponseErrorMsg, fieldRequiredValidator } from '~/utils'
import { inject, reactive, useApi, useNuxtApp } from '#imports'
interface Option {
label: string
value: string
}
const emit = defineEmits(['backToList', 'editOrAdd']) const emit = defineEmits(['backToList', 'editOrAdd'])
@ -51,7 +44,7 @@ const discordChannels = ref<Record<string, any>[]>([])
const mattermostChannels = ref<Record<string, any>[]>([]) const mattermostChannels = ref<Record<string, any>[]>([])
const filters = ref([]) const filterRef = ref()
const formInput = ref({ const formInput = ref({
'Email': [ 'Email': [
@ -205,21 +198,27 @@ const validators = computed(() => {
}) })
const { validate, validateInfos } = useForm(hook, validators) const { validate, validateInfos } = useForm(hook, validators)
function onNotTypeChange() { function onNotTypeChange(reset = false) {
hook.notification.payload = {} as any if (reset) {
hook.notification.payload = {} as Record<string, any>
}
if (hook.notification.type === 'Slack') { if (hook.notification.type === 'Slack') {
slackChannels.value = (apps && apps?.Slack && apps.Slack.parsedInput) || [] slackChannels.value = (apps.value && apps.value.Slack && apps.Slack.parsedInput) || []
} }
if (hook.notification.type === 'Microsoft Teams') { if (hook.notification.type === 'Microsoft Teams') {
teamsChannels.value = (apps && apps['Microsoft Teams'] && apps['Microsoft Teams'].parsedInput) || [] teamsChannels.value = (apps.value && apps.value['Microsoft Teams'] && apps.value['Microsoft Teams'].parsedInput) || []
} }
if (hook.notification.type === 'Discord') { if (hook.notification.type === 'Discord') {
discordChannels.value = (apps && apps.Discord && apps.Discord.parsedInput) || [] discordChannels.value = (apps.value && apps.value.Discord && apps.value.Discord.parsedInput) || []
} }
if (hook.notification.type === 'Mattermost') { if (hook.notification.type === 'Mattermost') {
mattermostChannels.value = (apps && apps.Mattermost && apps.Mattermost.parsedInput) || [] mattermostChannels.value = (apps.value && apps.value.Mattermost && apps.value.Mattermost.parsedInput) || []
} }
if (hook.notification.type === 'URL') { if (hook.notification.type === 'URL') {
hook.notification.payload.body = '{{ json data }}' hook.notification.payload.body = '{{ json data }}'
hook.notification.payload.parameters = [{}] hook.notification.payload.parameters = [{}]
@ -228,10 +227,6 @@ function onNotTypeChange() {
} }
} }
function filterOption(input: string, option: Option) {
return option.value.toUpperCase().includes(input.toUpperCase())
}
function setHook(newHook: any) { function setHook(newHook: any) {
Object.assign(hook, { Object.assign(hook, {
...newHook, ...newHook,
@ -301,6 +296,8 @@ async function loadPluginList() {
if (hook.event && hook.operation) { if (hook.event && hook.operation) {
hook.eventOperation = `${hook.event} ${hook.operation}` hook.eventOperation = `${hook.event} ${hook.operation}`
} }
onNotTypeChange()
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -342,12 +339,9 @@ async function saveHooks() {
hook.id = res.id hook.id = res.id
} }
// TODO: wait for filter implementation if (filterRef.value) {
// if ($refs.filter) { await filterRef.value.applyChanges(hook.id)
// await $refs.filter.applyChanges(false, { }
// hookId: hook.id,
// });
// }
message.success('Webhook details updated successfully') message.success('Webhook details updated successfully')
} catch (e: any) { } catch (e: any) {
@ -383,8 +377,8 @@ watch(
}, },
) )
onMounted(() => { onMounted(async () => {
loadPluginList() await loadPluginList()
}) })
</script> </script>
@ -439,7 +433,7 @@ onMounted(() => {
v-model:value="hook.notification.type" v-model:value="hook.notification.type"
size="large" size="large"
:placeholder="$t('general.notification')" :placeholder="$t('general.notification')"
@change="onNotTypeChange" @change="onNotTypeChange(true)"
> >
<a-select-option v-for="(notificationOption, i) in notificationList" :key="i" :value="notificationOption.type"> <a-select-option v-for="(notificationOption, i) in notificationList" :key="i" :value="notificationOption.type">
<div class="flex items-center"> <div class="flex items-center">
@ -505,12 +499,12 @@ onMounted(() => {
<a-row v-if="hook.notification.type === 'Slack'" type="flex"> <a-row v-if="hook.notification.type === 'Slack'" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']"> <a-form-item v-bind="validateInfos['notification.channels']">
<a-auto-complete <WebhookChannelMultiSelect
v-model:value="hook.notification.payload.channels" v-if="slackChannels.length > 0"
size="large" v-model="hook.notification.payload.channels"
:options="slackChannels" :selected-channel-list="hook.notification.payload.channels"
:available-channel-list="slackChannels"
placeholder="Select Slack channels" placeholder="Select Slack channels"
:filter-option="filterOption"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -519,12 +513,12 @@ onMounted(() => {
<a-row v-if="hook.notification.type === 'Microsoft Teams'" type="flex"> <a-row v-if="hook.notification.type === 'Microsoft Teams'" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']"> <a-form-item v-bind="validateInfos['notification.channels']">
<a-auto-complete <WebhookChannelMultiSelect
v-model:value="hook.notification.payload.channels" v-if="teamsChannels.length > 0"
size="large" v-model="hook.notification.payload.channels"
:options="teamsChannels" :selected-channel-list="hook.notification.payload.channels"
:available-channel-list="teamsChannels"
placeholder="Select Microsoft Teams channels" placeholder="Select Microsoft Teams channels"
:filter-option="filterOption"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -533,12 +527,12 @@ onMounted(() => {
<a-row v-if="hook.notification.type === 'Discord'" type="flex"> <a-row v-if="hook.notification.type === 'Discord'" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']"> <a-form-item v-bind="validateInfos['notification.channels']">
<a-auto-complete <WebhookChannelMultiSelect
v-model:value="hook.notification.payload.channels" v-if="discordChannels.length > 0"
size="large" v-model="hook.notification.payload.channels"
:options="discordChannels" :selected-channel-list="hook.notification.payload.channels"
:available-channel-list="discordChannels"
placeholder="Select Discord channels" placeholder="Select Discord channels"
:filter-option="filterOption"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -547,12 +541,12 @@ onMounted(() => {
<a-row v-if="hook.notification.type === 'Mattermost'" type="flex"> <a-row v-if="hook.notification.type === 'Mattermost'" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']"> <a-form-item v-bind="validateInfos['notification.channels']">
<a-auto-complete <WebhookChannelMultiSelect
v-model:value="hook.notification.payload.channels" v-if="mattermostChannels.length > 0"
size="large" v-model="hook.notification.payload.channels"
:options="mattermostChannels" :selected-channel-list="hook.notification.payload.channels"
:available-channel-list="mattermostChannels"
placeholder="Select Mattermost channels" placeholder="Select Mattermost channels"
:filter-option="filterOption"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -573,7 +567,7 @@ onMounted(() => {
<a-col :span="24"> <a-col :span="24">
<a-card> <a-card>
<a-checkbox v-model:checked="hook.condition">On Condition</a-checkbox> <a-checkbox v-model:checked="hook.condition">On Condition</a-checkbox>
<SmartsheetToolbarColumnFilter v-if="hook.condition" /> <SmartsheetToolbarColumnFilter v-if="hook.condition" ref="filterRef" :auto-save="false" :hook-id="hook.id" />
</a-card> </a-card>
</a-col> </a-col>
</a-row> </a-row>
@ -601,7 +595,6 @@ onMounted(() => {
ref="webhookTestRef" ref="webhookTestRef"
:hook="{ :hook="{
...hook, ...hook,
filters,
notification: { notification: {
...hook.notification, ...hook.notification,
payload: hook.notification.payload, payload: hook.notification.payload,

6
packages/nc-gui-v2/components/webhook/List.vue

@ -58,7 +58,11 @@ onMounted(() => {
</div> </div>
<a-divider /> <a-divider />
<div v-if="hooks.length"> <div v-if="hooks.length">
<a-list item-layout="horizontal" :data-source="hooks" class="cursor-pointer pl-5 pr-5 pt-2 pb-2"> <a-list
item-layout="horizontal"
:data-source="hooks"
class="cursor-pointer max-h-[75vh] overflow-y-auto scrollbar-thin-primary"
>
<template #renderItem="{ item, index }"> <template #renderItem="{ item, index }">
<a-list-item class="pa-2" @click="emit('edit', item)"> <a-list-item class="pa-2" @click="emit('edit', item)">
<a-list-item-meta> <a-list-item-meta>

2
packages/nc-gui-v2/components/webhook/Test.vue

@ -28,7 +28,7 @@ watch(
async function loadSampleData() { async function loadSampleData() {
sampleData.value = { sampleData.value = {
data: await $api.dbTableWebhook.samplePayloadGet(meta?.value?.id as string, hook?.operation), data: await $api.dbTableWebhook.samplePayloadGet(meta?.value?.id as string, hook?.operation || 'insert'),
} }
} }

19
packages/nc-gui-v2/composables/useViewFilters.ts

@ -16,15 +16,23 @@ export function useViewFilters(
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { metas } = useMetas() const { metas } = useMetas()
const loadFilters = async () => { const loadFilters = async (hookId?: string) => {
if (hookId) {
if (parentId) {
filters.value = await $api.dbTableFilter.childrenRead(parentId)
} else {
filters.value = (await $api.dbTableWebhookFilter.read(hookId as string)) as any
}
} else {
if (parentId) { if (parentId) {
filters.value = await $api.dbTableFilter.childrenRead(parentId) filters.value = await $api.dbTableFilter.childrenRead(parentId)
} else { } else {
filters.value = await $api.dbTableFilter.read(view?.value?.id as string) filters.value = await $api.dbTableFilter.read(view?.value?.id as string)
} }
} }
}
const sync = async (_nested = false) => { const sync = async (hookId?: string, _nested = false) => {
for (const [i, filter] of Object.entries(filters.value)) { for (const [i, filter] of Object.entries(filters.value)) {
if (filter.status === 'delete') { if (filter.status === 'delete') {
await $api.dbTableFilter.delete(filter.id as string) await $api.dbTableFilter.delete(filter.id as string)
@ -34,12 +42,19 @@ export function useViewFilters(
fk_parent_id: parentId, fk_parent_id: parentId,
}) })
} else if (filter.status === 'create') { } else if (filter.status === 'create') {
if (hookId) {
filters.value[+i] = (await $api.dbTableWebhookFilter.create(hookId, {
...filter,
fk_parent_id: parentId,
})) as any
} else {
filters.value[+i] = (await $api.dbTableFilter.create(view?.value?.id as string, { filters.value[+i] = (await $api.dbTableFilter.create(view?.value?.id as string, {
...filter, ...filter,
fk_parent_id: parentId, fk_parent_id: parentId,
})) as any })) as any
} }
} }
}
reloadData?.() reloadData?.()
} }

Loading…
Cancel
Save