* feat: webhook wip * feat: webhook wip custom theme * fix: handle scroll * chore: clean up * fix: ux fixes * fix: font corrections * fix: webhook docs links fix: pr review comments * fix: box-shadow * fix(nc-gui): webhook css fixes * fix(nc-gui): reduce btn width from webhook modal * fix(nc-gui): update webhook page json editor * fix(nc-gui): add webhook docs link * fix(nc-gui): webhook parameter, headers input gap * fix(nc-gui): update webhook list table * fix(nc-gui): remove beautify json btn * fix(nc-gui): webhook header, parameters styles * fix(nc-gui): warning issue * fix(nc-gui): upate test webhook btn icons and enable save changes btn by default * fix(nc-gui): update hook type text in table * fix(nc-gui): focus webhook title on modal open * fix(nc-gui): minor changes * fix(nc-gui): update filter and params btn type * fix(nc-gui): update webhook oss ui * fix(nc-gui): add sortby webhook operation type option * fix(nc-gui): update webhook notification type icons * fix(nc-gui): invalid props issue * fix(nc-gui): update webhook condition text * fix(nc-gui): update monaco editor font color * test: webhook class name fix * fix(nc-gui): add missing webhook header key dropdown options * fix(nc-gui): update webhook header key placeholder text color * fix(nc-gui): update webhook modal min width * test(nc-gui): update some of the webhook related test * test(nc-gui): update create webhook test case * text(nc-gui): fixed some of the webhook test cases * test(nc-gui): update webhook conditional test cases * docs: update * fix(nc-gui): small changes --------- Co-authored-by: DarkPhoenix2704 <anbarasun123@gmail.com> Co-authored-by: Ramesh Mane <101566080+rameshmane7218@users.noreply.github.com>pull/9166/head
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.5 KiB |
@ -1,168 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import type { RequestParams } from 'nocodb-sdk' |
||||
import { ExportTypes } from 'nocodb-sdk' |
||||
import { saveAs } from 'file-saver' |
||||
import * as XLSX from 'xlsx' |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const sharedViewListDlg = ref(false) |
||||
|
||||
const isPublicView = inject(IsPublicInj, ref(false)) |
||||
|
||||
const isView = false |
||||
|
||||
const { base } = storeToRefs(useBase()) |
||||
|
||||
const { $api } = useNuxtApp() |
||||
|
||||
const meta = inject(MetaInj, ref()) |
||||
|
||||
const fields = inject(FieldsInj, ref([])) |
||||
|
||||
const selectedView = inject(ActiveViewInj, ref()) |
||||
|
||||
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow() |
||||
|
||||
const { exportFile: sharedViewExportFile } = useSharedView() |
||||
|
||||
const isLocked = inject(IsLockedInj) |
||||
|
||||
const showWebhookDrawer = ref(false) |
||||
|
||||
const quickImportDialog = ref(false) |
||||
|
||||
const { isUIAllowed } = useRoles() |
||||
|
||||
const exportFile = async (exportType: ExportTypes) => { |
||||
let offset = 0 |
||||
let c = 1 |
||||
const responseType = exportType === ExportTypes.EXCEL ? 'base64' : 'blob' |
||||
|
||||
try { |
||||
while (!isNaN(offset) && offset > -1) { |
||||
let res |
||||
if (isPublicView.value) { |
||||
res = await sharedViewExportFile(fields.value, offset, exportType, responseType) |
||||
} else { |
||||
res = await $api.dbViewRow.export( |
||||
'noco', |
||||
base?.value.id as string, |
||||
meta.value?.id as string, |
||||
selectedView.value?.id as string, |
||||
exportType, |
||||
{ |
||||
responseType, |
||||
query: { |
||||
fields: fields.value.map((field) => field.title), |
||||
offset, |
||||
sortArrJson: JSON.stringify(sorts.value), |
||||
filterArrJson: JSON.stringify(nestedFilters.value), |
||||
}, |
||||
} as RequestParams, |
||||
) |
||||
} |
||||
const { data, headers } = res |
||||
if (exportType === ExportTypes.EXCEL) { |
||||
const workbook = XLSX.read(data, { type: 'base64' }) |
||||
XLSX.writeFile(workbook, `${meta.value?.title}_exported_${c++}.xlsx`) |
||||
} else if (exportType === ExportTypes.CSV) { |
||||
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' }) |
||||
saveAs(blob, `${meta.value?.title}_exported_${c++}.csv`) |
||||
} |
||||
offset = +headers['nc-export-offset'] |
||||
if (offset > -1) { |
||||
// Downloading more files |
||||
message.info(t('msg.info.downloadingMoreFiles')) |
||||
} else { |
||||
// Successfully exported all table data |
||||
message.success(t('msg.success.tableDataExported')) |
||||
} |
||||
} |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<a-dropdown> |
||||
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn"> |
||||
<div class="flex gap-1 items-center"> |
||||
<MdiFlashOutline /> |
||||
|
||||
<!-- More --> |
||||
<span class="!text-sm font-weight-medium">{{ $t('general.more') }}</span> |
||||
|
||||
<MdiMenuDown class="text-grey" /> |
||||
</div> |
||||
</a-button> |
||||
|
||||
<template #overlay> |
||||
<div class="bg-gray-50 py-2 shadow-lg !border"> |
||||
<div> |
||||
<div v-e="['a:actions:download-csv']" class="nc-menu-item" @click="exportFile(ExportTypes.CSV)"> |
||||
<MdiDownloadOutline class="text-gray-500" /> |
||||
<!-- Download as CSV --> |
||||
{{ $t('activity.downloadCSV') }} |
||||
</div> |
||||
|
||||
<div v-e="['a:actions:download-excel']" class="nc-menu-item" @click="exportFile(ExportTypes.EXCEL)"> |
||||
<MdiDownloadOutline class="text-gray-500" /> |
||||
<!-- Download as XLSX --> |
||||
{{ $t('activity.downloadExcel') }} |
||||
</div> |
||||
|
||||
<div |
||||
v-if="isUIAllowed('csvImport') && !isView && !isPublicView" |
||||
v-e="['a:actions:upload-csv']" |
||||
class="nc-menu-item" |
||||
:class="{ disabled: isLocked }" |
||||
@click="!isLocked ? (quickImportDialog = true) : {}" |
||||
> |
||||
<MdiUploadOutline class="text-gray-500" /> |
||||
<!-- Upload CSV --> |
||||
{{ $t('activity.uploadCSV') }} |
||||
</div> |
||||
|
||||
<div |
||||
v-if="isUIAllowed('viewShare') && !isView && !isPublicView" |
||||
v-e="['a:actions:shared-view-list']" |
||||
class="nc-menu-item" |
||||
@click="sharedViewListDlg = true" |
||||
> |
||||
<MdiViewListOutline class="text-gray-500" /> |
||||
<!-- Shared View List --> |
||||
{{ $t('activity.listSharedView') }} |
||||
</div> |
||||
<div |
||||
v-if="isUIAllowed('webhook') && !isView && !isPublicView" |
||||
v-e="['c:actions:webhook']" |
||||
class="nc-menu-item" |
||||
@click="showWebhookDrawer = true" |
||||
> |
||||
<MdiHook class="text-gray-500" /> |
||||
{{ $t('objects.webhooks') }} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</a-dropdown> |
||||
|
||||
<LazyDlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" import-type="csv" :import-data-only="true" /> |
||||
|
||||
<LazyWebhookDrawer v-if="showWebhookDrawer" v-model="showWebhookDrawer" /> |
||||
|
||||
<a-modal |
||||
v-model:visible="sharedViewListDlg" |
||||
:class="{ active: sharedViewListDlg }" |
||||
:title="$t('activity.listSharedView')" |
||||
width="max(900px,60vw)" |
||||
:footer="null" |
||||
wrap-class-name="nc-modal-shared-view-list" |
||||
> |
||||
<LazySmartsheetToolbarSharedViewList v-if="sharedViewListDlg" /> |
||||
</a-modal> |
||||
</div> |
||||
</template> |
@ -1,216 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import type { Ref } from '@vue/reactivity' |
||||
import { LockType } from '#imports' |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const sharedViewListDlg = ref(false) |
||||
|
||||
const isPublicView = inject(IsPublicInj, ref(false)) |
||||
|
||||
const isView = false |
||||
|
||||
const { $api, $e } = useNuxtApp() |
||||
|
||||
const { isSqlView } = useSmartsheetStoreOrThrow() |
||||
|
||||
const selectedView = inject(ActiveViewInj, ref()) |
||||
|
||||
const isLocked = inject(IsLockedInj, ref(false)) |
||||
|
||||
const showWebhookDrawer = ref(false) |
||||
|
||||
const showApiSnippetDrawer = ref(false) |
||||
|
||||
const showErd = ref(false) |
||||
|
||||
type QuickImportDialogType = 'csv' | 'excel' | 'json' |
||||
|
||||
// TODO: add 'json' when it's ready |
||||
const quickImportDialogTypes: QuickImportDialogType[] = ['csv', 'excel'] |
||||
|
||||
const quickImportDialogs: Record<(typeof quickImportDialogTypes)[number], Ref<boolean>> = quickImportDialogTypes.reduce( |
||||
(acc: any, curr) => { |
||||
acc[curr] = ref(false) |
||||
return acc |
||||
}, |
||||
{}, |
||||
) as Record<QuickImportDialogType, Ref<boolean>> |
||||
|
||||
const { isUIAllowed } = useRoles() |
||||
|
||||
useBase() |
||||
|
||||
const meta = inject(MetaInj, ref()) |
||||
|
||||
const currentBaseId = computed(() => meta.value?.base_id) |
||||
|
||||
const currentSourceId = computed(() => meta.value?.source_id) |
||||
|
||||
/* |
||||
const Icon = computed(() => { |
||||
switch (selectedView.value?.lock_type) { |
||||
case LockType.Personal: |
||||
return iconMap.account |
||||
case LockType.Locked: |
||||
return iconMap.lock |
||||
case LockType.Collaborative: |
||||
default: |
||||
return iconMap.users |
||||
} |
||||
}) |
||||
*/ |
||||
|
||||
const lockType = computed(() => (selectedView.value?.lock_type as LockType) || LockType.Collaborative) |
||||
|
||||
async function changeLockType(type: LockType) { |
||||
$e('a:grid:lockmenu', { lockType: type }) |
||||
|
||||
if (!selectedView.value) return |
||||
|
||||
if (type === 'personal') { |
||||
// Coming soon |
||||
return message.info(t('msg.toast.futureRelease')) |
||||
} |
||||
try { |
||||
selectedView.value.lock_type = type |
||||
await $api.dbView.update(selectedView.value.id as string, { |
||||
lock_type: type, |
||||
}) |
||||
|
||||
message.success(`Successfully Switched to ${type} view`) |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
|
||||
const open = ref(false) |
||||
|
||||
useMenuCloseOnEsc(open) |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<a-dropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu" placement="bottomRight"> |
||||
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn !border-1 !border-gray-200 !rounded-md !py-1 !px-2"> |
||||
<MdiDotsHorizontal class="!w-4 !h-4" /> |
||||
</a-button> |
||||
|
||||
<template #overlay> |
||||
<a-menu class="!py-0 !rounded !text-gray-800 text-sm" data-testid="toolbar-actions" @click="open = false"> |
||||
<a-menu-item-group> |
||||
<template v-if="isUIAllowed('csvTableImport') && !isView && !isPublicView && !isSqlView"> |
||||
<a-sub-menu key="upload"> |
||||
<template #title> |
||||
<div v-e="['c:navdraw:preview-as']" class="nc-base-menu-item group"> |
||||
<GeneralIcon type="upload" /> |
||||
{{ $t('general.upload') }} |
||||
<div class="flex-1" /> |
||||
|
||||
<component :is="iconMap.arrowRight" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<template #expandIcon></template> |
||||
<template v-for="(dialog, type) in quickImportDialogs"> |
||||
<a-menu-item v-if="isUIAllowed(`${type}TableImport`) && !isView && !isPublicView" :key="type"> |
||||
<div |
||||
v-e="[`a:upload:${type}`]" |
||||
class="nc-base-menu-item" |
||||
:class="{ disabled: isLocked }" |
||||
@click="!isLocked ? (dialog.value = true) : {}" |
||||
> |
||||
<component :is="iconMap.upload" /> |
||||
{{ `${$t('general.upload')} ${type.toUpperCase()}` }} |
||||
</div> |
||||
</a-menu-item> |
||||
</template> |
||||
</a-sub-menu> |
||||
</template> |
||||
<a-sub-menu key="download"> |
||||
<template #title> |
||||
<div v-e="['c:download']" class="nc-base-menu-item group"> |
||||
<GeneralIcon icon="download" /> |
||||
{{ $t('general.download') }} |
||||
<div class="flex-1" /> |
||||
|
||||
<component :is="iconMap.arrowRight" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<template #expandIcon></template> |
||||
|
||||
<LazySmartsheetToolbarExportSubActions /> |
||||
</a-sub-menu> |
||||
|
||||
<a-sub-menu |
||||
v-if="isUIAllowed('viewCreateOrEdit')" |
||||
key="lock-type" |
||||
class="scrollbar-thin-dull max-h-90vh overflow-auto !py-0" |
||||
> |
||||
<template #title> |
||||
<div v-e="['c:navdraw:preview-as']" class="nc-base-menu-item group px-0 !py-0"> |
||||
<LazySmartsheetToolbarLockType hide-tick :type="lockType" /> |
||||
|
||||
<component :is="iconMap.arrowRight" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<template #expandIcon></template> |
||||
<a-menu-item @click="changeLockType(LockType.Collaborative)"> |
||||
<LazySmartsheetToolbarLockType :type="LockType.Collaborative" /> |
||||
</a-menu-item> |
||||
|
||||
<a-menu-item @click="changeLockType(LockType.Locked)"> |
||||
<LazySmartsheetToolbarLockType :type="LockType.Locked" /> |
||||
</a-menu-item> |
||||
|
||||
<!-- <a-menu-item @click="changeLockType(LockType.Personal)"> |
||||
<LazySmartsheetToolbarLockType :type="LockType.Personal" /> |
||||
</a-menu-item> --> |
||||
</a-sub-menu> |
||||
</a-menu-item-group> |
||||
</a-menu> |
||||
</template> |
||||
</a-dropdown> |
||||
|
||||
<template v-if="currentSourceId && currentBaseId"> |
||||
<LazyDlgQuickImport |
||||
v-for="tp in quickImportDialogTypes" |
||||
:key="tp" |
||||
v-model="quickImportDialogs[tp].value" |
||||
:import-type="tp" |
||||
:base-id="currentBaseId" |
||||
:source-id="currentSourceId" |
||||
:import-data-only="true" |
||||
/> |
||||
</template> |
||||
|
||||
<LazyWebhookDrawer v-if="showWebhookDrawer" v-model="showWebhookDrawer" /> |
||||
|
||||
<LazySmartsheetToolbarErd v-model="showErd" /> |
||||
|
||||
<a-modal |
||||
v-model:visible="sharedViewListDlg" |
||||
:class="{ active: sharedViewListDlg }" |
||||
:title="$t('activity.listSharedView')" |
||||
width="max(900px,60vw)" |
||||
:footer="null" |
||||
wrap-class-name="nc-modal-shared-view-list" |
||||
> |
||||
<LazySmartsheetToolbarSharedViewList v-if="sharedViewListDlg" /> |
||||
</a-modal> |
||||
|
||||
<LazySmartsheetApiSnippet v-model="showApiSnippetDrawer" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
:deep(.ant-dropdown-menu-submenu-title) { |
||||
@apply py-0; |
||||
} |
||||
|
||||
:deep(.ant-dropdown-menu-item-group-title) { |
||||
@apply hidden; |
||||
} |
||||
</style> |
@ -1,58 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
interface Props { |
||||
modelValue: boolean |
||||
} |
||||
|
||||
const props = defineProps<Props>() |
||||
|
||||
const emits = defineEmits(['update:modelValue']) |
||||
|
||||
const editOrAdd = ref(false) |
||||
|
||||
const vModel = useVModel(props, 'modelValue', emits) |
||||
|
||||
const currentHook = ref<Record<string, any>>() |
||||
|
||||
async function editHook(hook: Record<string, any>) { |
||||
editOrAdd.value = true |
||||
currentHook.value = hook |
||||
} |
||||
|
||||
async function addHook() { |
||||
editOrAdd.value = true |
||||
currentHook.value = undefined |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-drawer |
||||
v-model:visible="vModel" |
||||
:closable="false" |
||||
placement="right" |
||||
width="700px" |
||||
:body-style="{ background: 'rgba(67, 81, 232, 0.05)', padding: '0px 0px', overflow: 'hidden' }" |
||||
class="nc-drawer-webhook" |
||||
@keydown.esc="vModel = false" |
||||
> |
||||
<a-layout class="nc-drawer-webhook-body"> |
||||
<a-layout-content class="px-10 py-5 scrollbar-thin-primary"> |
||||
<LazyWebhookEditor v-if="editOrAdd" :hook="currentHook" @back-to-list="editOrAdd = false" /> |
||||
|
||||
<LazyWebhookList v-else @edit="editHook" @add="addHook" /> |
||||
</a-layout-content> |
||||
|
||||
<a-layout-footer class="!bg-white border-t flex"> |
||||
<a-button |
||||
v-e="['e:hiring']" |
||||
class="mx-auto mb-4 !rounded-md" |
||||
href="https://angel.co/company/nocodb" |
||||
target="_blank" |
||||
size="large" |
||||
rel="noopener noreferrer" |
||||
> |
||||
🚀 {{ $t('labels.weAreHiring') }}! 🚀 |
||||
</a-button> |
||||
</a-layout-footer> |
||||
</a-layout> |
||||
</a-drawer> |
||||
</template> |
@ -1,885 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import type { Ref } from 'vue' |
||||
import type { HookReqType, HookType } from 'nocodb-sdk' |
||||
import { |
||||
Form, |
||||
MetaInj, |
||||
computed, |
||||
extractSdkResponseErrorMsg, |
||||
fieldRequiredValidator, |
||||
iconMap, |
||||
inject, |
||||
isEeUI, |
||||
message, |
||||
onMounted, |
||||
parseProp, |
||||
reactive, |
||||
ref, |
||||
useApi, |
||||
// useGlobal, |
||||
useI18n, |
||||
useNuxtApp, |
||||
watch, |
||||
} from '#imports' |
||||
import { extractNextDefaultName } from '~/helpers/parsers/parserHelpers' |
||||
|
||||
interface Props { |
||||
hook?: HookType |
||||
} |
||||
|
||||
const props = defineProps<Props>() |
||||
|
||||
const emit = defineEmits(['close', 'delete']) |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const { $e } = useNuxtApp() |
||||
|
||||
const { api, isLoading: loading } = useApi() |
||||
|
||||
// const { appInfo } = useGlobal() |
||||
|
||||
const { hooks } = storeToRefs(useWebhooksStore()) |
||||
|
||||
const { base } = storeToRefs(useBase()) |
||||
|
||||
const meta = inject(MetaInj, ref()) |
||||
|
||||
const titleDomRef = ref<HTMLInputElement | undefined>() |
||||
|
||||
// const hookTabKey = ref('hook-edit') |
||||
|
||||
const useForm = Form.useForm |
||||
|
||||
const defaultHookName = t('labels.webhook') |
||||
|
||||
let hookRef = reactive< |
||||
Omit<HookType, 'notification'> & { notification: Record<string, any>; eventOperation?: string; condition: boolean } |
||||
>({ |
||||
id: '', |
||||
title: defaultHookName, |
||||
event: undefined, |
||||
operation: undefined, |
||||
eventOperation: undefined, |
||||
notification: { |
||||
type: 'URL', |
||||
payload: { |
||||
method: 'POST', |
||||
body: '{{ json event }}', |
||||
headers: [{}], |
||||
parameters: [{}], |
||||
path: '', |
||||
}, |
||||
}, |
||||
condition: false, |
||||
active: true, |
||||
version: 'v2', |
||||
}) |
||||
|
||||
const isBodyShown = ref(hookRef.version === 'v1' || isEeUI) |
||||
|
||||
const urlTabKey = ref<'params' | 'headers' | 'body'>('params') |
||||
|
||||
const apps: Record<string, any> = ref() |
||||
|
||||
const webhookTestRef = ref() |
||||
|
||||
const slackChannels = ref<Record<string, any>[]>([]) |
||||
|
||||
const teamsChannels = ref<Record<string, any>[]>([]) |
||||
|
||||
const discordChannels = ref<Record<string, any>[]>([]) |
||||
|
||||
const mattermostChannels = ref<Record<string, any>[]>([]) |
||||
|
||||
const filterRef = ref() |
||||
|
||||
const formInput = ref({ |
||||
'Email': [ |
||||
{ |
||||
key: 'to', |
||||
label: t('labels.toAddress'), |
||||
placeholder: t('labels.toAddress'), |
||||
type: 'SingleLineText', |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'subject', |
||||
label: t('labels.subject'), |
||||
placeholder: t('labels.subject'), |
||||
type: 'SingleLineText', |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'body', |
||||
label: t('labels.body'), |
||||
placeholder: t('labels.body'), |
||||
type: 'LongText', |
||||
required: true, |
||||
}, |
||||
], |
||||
'Slack': [ |
||||
{ |
||||
key: 'body', |
||||
label: t('labels.body'), |
||||
placeholder: t('labels.body'), |
||||
type: 'LongText', |
||||
required: true, |
||||
}, |
||||
], |
||||
'Microsoft Teams': [ |
||||
{ |
||||
key: 'body', |
||||
label: t('labels.body'), |
||||
placeholder: t('labels.body'), |
||||
type: 'LongText', |
||||
required: true, |
||||
}, |
||||
], |
||||
'Discord': [ |
||||
{ |
||||
key: 'body', |
||||
label: t('labels.body'), |
||||
placeholder: t('labels.body'), |
||||
type: 'LongText', |
||||
required: true, |
||||
}, |
||||
], |
||||
'Mattermost': [ |
||||
{ |
||||
key: 'body', |
||||
label: t('labels.body'), |
||||
placeholder: t('labels.body'), |
||||
type: 'LongText', |
||||
required: true, |
||||
}, |
||||
], |
||||
'Twilio': [ |
||||
{ |
||||
key: 'body', |
||||
label: t('labels.body'), |
||||
placeholder: t('labels.body'), |
||||
type: 'LongText', |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'to', |
||||
label: t('labels.commaSeparatedMobileNumber'), |
||||
placeholder: t('labels.commaSeparatedMobileNumber'), |
||||
type: 'LongText', |
||||
required: true, |
||||
}, |
||||
], |
||||
'Whatsapp Twilio': [ |
||||
{ |
||||
key: 'body', |
||||
label: t('labels.body'), |
||||
placeholder: t('labels.body'), |
||||
type: 'LongText', |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'to', |
||||
label: t('labels.commaSeparatedMobileNumber'), |
||||
placeholder: t('labels.commaSeparatedMobileNumber'), |
||||
type: 'LongText', |
||||
required: true, |
||||
}, |
||||
], |
||||
}) |
||||
|
||||
const isRenaming = ref(false) |
||||
|
||||
// TODO: Add back when show logs is working |
||||
const showLogs = computed( |
||||
() => false, |
||||
// !( |
||||
// appInfo.automationLogLevel === AutomationLogLevel.OFF || |
||||
// (appInfo.automationLogLevel === AutomationLogLevel.ALL && !appInfo.ee) |
||||
// ), |
||||
) |
||||
|
||||
const eventList = ref<Record<string, any>[]>([ |
||||
{ text: [t('general.after'), t('general.insert')], value: ['after', 'insert'] }, |
||||
{ text: [t('general.after'), t('general.update')], value: ['after', 'update'] }, |
||||
{ text: [t('general.after'), t('general.delete')], value: ['after', 'delete'] }, |
||||
{ text: [t('general.after'), t('general.bulkInsert')], value: ['after', 'bulkInsert'] }, |
||||
{ text: [t('general.after'), t('general.bulkUpdate')], value: ['after', 'bulkUpdate'] }, |
||||
{ text: [t('general.after'), t('general.bulkDelete')], value: ['after', 'bulkDelete'] }, |
||||
]) |
||||
|
||||
const notificationList = computed(() => { |
||||
return isEeUI |
||||
? [{ type: 'URL', text: t('datatype.URL') }] |
||||
: [ |
||||
{ type: 'URL', text: t('datatype.URL') }, |
||||
{ type: 'Email', text: t('datatype.Email') }, |
||||
{ type: 'Slack', text: t('general.slack') }, |
||||
{ type: 'Microsoft Teams', text: t('general.microsoftTeams') }, |
||||
{ type: 'Discord', text: t('general.discord') }, |
||||
{ type: 'Mattermost', text: t('general.matterMost') }, |
||||
{ type: 'Twilio', text: t('general.twilio') }, |
||||
{ type: 'Whatsapp Twilio', text: t('general.whatsappTwilio') }, |
||||
] |
||||
}) |
||||
|
||||
const methodList = [ |
||||
{ title: 'GET' }, |
||||
{ title: 'POST' }, |
||||
{ title: 'DELETE' }, |
||||
{ title: 'PUT' }, |
||||
{ title: 'HEAD' }, |
||||
{ title: 'PATCH' }, |
||||
] |
||||
|
||||
const validators = computed(() => { |
||||
return { |
||||
'title': [fieldRequiredValidator()], |
||||
'eventOperation': [fieldRequiredValidator()], |
||||
'notification.type': [fieldRequiredValidator()], |
||||
...(hookRef.notification.type === 'URL' && { |
||||
'notification.payload.method': [fieldRequiredValidator()], |
||||
'notification.payload.path': [fieldRequiredValidator()], |
||||
}), |
||||
...(hookRef.notification.type === 'Email' && { |
||||
'notification.payload.to': [fieldRequiredValidator()], |
||||
'notification.payload.subject': [fieldRequiredValidator()], |
||||
'notification.payload.body': [fieldRequiredValidator()], |
||||
}), |
||||
...(['Slack', 'Microsoft Teams', 'Discord', 'Mattermost'].includes(hookRef.notification.type) && { |
||||
'notification.payload.channels': [fieldRequiredValidator()], |
||||
'notification.payload.body': [fieldRequiredValidator()], |
||||
}), |
||||
...((hookRef.notification.type === 'Twilio' || hookRef.notification.type === 'Whatsapp Twilio') && { |
||||
'notification.payload.body': [fieldRequiredValidator()], |
||||
'notification.payload.to': [fieldRequiredValidator()], |
||||
}), |
||||
} |
||||
}) |
||||
const { validate, validateInfos } = useForm(hookRef, validators) |
||||
|
||||
const isValid = computed(() => { |
||||
// Recursively check if all the fields are valid |
||||
const check = (obj: Record<string, any>) => { |
||||
for (const key in obj) { |
||||
if (typeof obj[key] === 'object') { |
||||
if (!check(obj[key])) { |
||||
return false |
||||
} |
||||
} else if (obj && key === 'validateStatus' && obj[key] === 'error') { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
return hookRef && check(validateInfos) |
||||
}) |
||||
|
||||
function onNotificationTypeChange(reset = false) { |
||||
if (reset) { |
||||
hookRef.notification.payload = {} as Record<string, any> |
||||
if (['Slack', 'Microsoft Teams', 'Discord', 'Mattermost'].includes(hookRef.notification.type)) { |
||||
hookRef.notification.payload.channels = [] |
||||
hookRef.notification.payload.body = '' |
||||
} |
||||
} |
||||
|
||||
if (hookRef.notification.type === 'Slack') { |
||||
slackChannels.value = (apps.value && apps.value.Slack && apps.value.Slack.parsedInput) || [] |
||||
} |
||||
|
||||
if (hookRef.notification.type === 'Microsoft Teams') { |
||||
teamsChannels.value = (apps.value && apps.value['Microsoft Teams'] && apps.value['Microsoft Teams'].parsedInput) || [] |
||||
} |
||||
|
||||
if (hookRef.notification.type === 'Discord') { |
||||
discordChannels.value = (apps.value && apps.value.Discord && apps.value.Discord.parsedInput) || [] |
||||
} |
||||
|
||||
if (hookRef.notification.type === 'Mattermost') { |
||||
mattermostChannels.value = (apps.value && apps.value.Mattermost && apps.value.Mattermost.parsedInput) || [] |
||||
} |
||||
|
||||
if (hookRef.notification.type === 'URL') { |
||||
const body = hookRef.notification.payload.body |
||||
hookRef.notification.payload.body = body ? (body === '{{ json data }}' ? '{{ json event }}' : body) : '{{ json event }}' |
||||
hookRef.notification.payload.parameters = hookRef.notification.payload.parameters || [{}] |
||||
hookRef.notification.payload.headers = hookRef.notification.payload.headers || [{}] |
||||
hookRef.notification.payload.method = hookRef.notification.payload.method || 'POST' |
||||
hookRef.notification.payload.auth = hookRef.notification.payload.auth || '' |
||||
} |
||||
} |
||||
|
||||
function setHook(newHook: HookType) { |
||||
const notification = newHook.notification as Record<string, any> |
||||
Object.assign(hookRef, { |
||||
...newHook, |
||||
notification: { |
||||
...notification, |
||||
payload: notification.payload, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
function onEventChange() { |
||||
const { notification: { payload = {}, type = {} } = {} } = hookRef |
||||
|
||||
Object.assign(hookRef, { |
||||
...hookRef, |
||||
notification: { |
||||
type, |
||||
payload, |
||||
}, |
||||
}) |
||||
|
||||
hookRef.notification.payload = payload |
||||
|
||||
const channels: Ref<Record<string, any>[] | null> = ref(null) |
||||
|
||||
switch (hookRef.notification.type) { |
||||
case 'Slack': |
||||
channels.value = slackChannels.value |
||||
break |
||||
case 'Microsoft Teams': |
||||
channels.value = teamsChannels.value |
||||
break |
||||
case 'Discord': |
||||
channels.value = discordChannels.value |
||||
break |
||||
case 'Mattermost': |
||||
channels.value = mattermostChannels.value |
||||
break |
||||
} |
||||
|
||||
if (channels) { |
||||
hookRef.notification.payload.webhook_url = |
||||
hookRef.notification.payload.webhook_url && |
||||
hookRef.notification.payload.webhook_url.map((v: { webhook_url: string }) => |
||||
channels.value?.find((s) => v.webhook_url === s.webhook_url), |
||||
) |
||||
} |
||||
|
||||
if (hookRef.notification.type === 'URL') { |
||||
hookRef.notification.payload = hookRef.notification.payload || {} |
||||
hookRef.notification.payload.parameters = hookRef.notification.payload.parameters || [{}] |
||||
hookRef.notification.payload.headers = hookRef.notification.payload.headers || [{}] |
||||
hookRef.notification.payload.method = hookRef.notification.payload.method || 'POST' |
||||
} |
||||
} |
||||
|
||||
async function loadPluginList() { |
||||
if (isEeUI) return |
||||
try { |
||||
const plugins = ( |
||||
await api.plugin.webhookList({ |
||||
query: { |
||||
base_id: base.value.id, |
||||
}, |
||||
}) |
||||
).list! |
||||
|
||||
apps.value = plugins.reduce((o, p) => { |
||||
const plugin: { title: string; tags: string[]; parsedInput: Record<string, any> } = { |
||||
title: '', |
||||
tags: [], |
||||
parsedInput: {}, |
||||
...(p as any), |
||||
} |
||||
plugin.tags = p.tags ? p.tags.split(',') : [] |
||||
plugin.parsedInput = parseProp(p.input) |
||||
o[plugin.title] = plugin |
||||
|
||||
return o |
||||
}, {} as Record<string, any>) |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
|
||||
const isConditionSupport = computed(() => { |
||||
return hookRef.eventOperation && !hookRef.eventOperation.includes('bulk') |
||||
}) |
||||
|
||||
async function saveHooks() { |
||||
loading.value = true |
||||
try { |
||||
await validate() |
||||
} catch (_: any) { |
||||
message.error(t('msg.error.invalidForm')) |
||||
|
||||
loading.value = false |
||||
|
||||
return |
||||
} |
||||
|
||||
try { |
||||
let res |
||||
if (hookRef.id) { |
||||
res = await api.dbTableWebhook.update(hookRef.id, { |
||||
...hookRef, |
||||
notification: { |
||||
...hookRef.notification, |
||||
payload: hookRef.notification.payload, |
||||
}, |
||||
}) |
||||
} else { |
||||
res = await api.dbTableWebhook.create(meta.value!.id!, { |
||||
...hookRef, |
||||
notification: { |
||||
...hookRef.notification, |
||||
payload: hookRef.notification.payload, |
||||
}, |
||||
} as HookReqType) |
||||
|
||||
hooks.value.push(res) |
||||
} |
||||
|
||||
if (res && typeof res.notification === 'string') { |
||||
res.notification = JSON.parse(res.notification) |
||||
} |
||||
|
||||
if (!hookRef.id && res) { |
||||
hookRef = { ...hookRef, ...res } as any |
||||
} |
||||
|
||||
if (filterRef.value) { |
||||
await filterRef.value.applyChanges(hookRef.id, false, isConditionSupport.value) |
||||
} |
||||
|
||||
// Webhook details updated successfully |
||||
hooks.value = hooks.value.map((h) => { |
||||
if (h.id === hookRef.id) { |
||||
return hookRef |
||||
} |
||||
return h |
||||
}) |
||||
|
||||
emit('close') |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} finally { |
||||
loading.value = false |
||||
} |
||||
|
||||
$e('a:webhook:add', { |
||||
operation: hookRef.operation, |
||||
condition: hookRef.condition, |
||||
notification: hookRef.notification.type, |
||||
}) |
||||
} |
||||
|
||||
async function testWebhook() { |
||||
await webhookTestRef.value.testWebhook() |
||||
} |
||||
|
||||
const getDefaultHookName = (hooks: HookType[]) => { |
||||
return extractNextDefaultName([...hooks.map((el) => el?.title || '')], defaultHookName) |
||||
} |
||||
|
||||
watch( |
||||
() => hookRef.eventOperation, |
||||
() => { |
||||
if (!hookRef.eventOperation) return |
||||
|
||||
const [event, operation] = hookRef.eventOperation.split(' ') |
||||
hookRef.event = event as HookType['event'] |
||||
hookRef.operation = operation as HookType['operation'] |
||||
}, |
||||
) |
||||
|
||||
watch( |
||||
() => props.hook, |
||||
() => { |
||||
if (props.hook) { |
||||
setHook(props.hook) |
||||
onEventChange() |
||||
} else { |
||||
// Set the default hook title only when creating a new hook. |
||||
hookRef.title = getDefaultHookName(hooks.value) |
||||
} |
||||
}, |
||||
{ immediate: true }, |
||||
) |
||||
|
||||
onMounted(async () => { |
||||
await loadPluginList() |
||||
|
||||
if (hookRef.event && hookRef.operation) { |
||||
hookRef.eventOperation = `${hookRef.event} ${hookRef.operation}` |
||||
} else { |
||||
hookRef.eventOperation = eventList.value[0].value.join(' ') |
||||
} |
||||
|
||||
onNotificationTypeChange() |
||||
|
||||
setTimeout(() => { |
||||
if (hookRef.id === '') { |
||||
titleDomRef.value?.click() |
||||
titleDomRef.value?.select() |
||||
} |
||||
}, 50) |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex nc-webhook-header pb-3 gap-x-2 items-start"> |
||||
<div class="flex flex-1"> |
||||
<a-form-item v-bind="validateInfos.title" class="flex flex-grow"> |
||||
<div |
||||
class="flex flex-grow px-1.5 py-0.125 items-center rounded-md border-gray-200 bg-gray-50 outline-gray-200" |
||||
style="outline-style: solid; outline-width: thin" |
||||
> |
||||
<input |
||||
ref="titleDomRef" |
||||
v-model="hookRef.title" |
||||
class="flex flex-grow text-lg font-medium capitalize outline-none bg-inherit nc-text-field-hook-title" |
||||
:placeholder="$t('placeholder.webhookTitle')" |
||||
:contenteditable="true" |
||||
@blur="isRenaming = false" |
||||
@focus="isRenaming = true" |
||||
@keydown.enter.prevent="titleDomRef?.blur()" |
||||
/> |
||||
</div> |
||||
</a-form-item> |
||||
</div> |
||||
<div class="flex flex-row gap-2"> |
||||
<NcButton class="nc-btn-webhook-test" type="secondary" size="small" @click="testWebhook"> |
||||
<div class="flex items-center px-1">{{ $t('activity.testWebhook') }}</div> |
||||
</NcButton> |
||||
|
||||
<NcButton |
||||
class="nc-btn-webhook-save" |
||||
type="primary" |
||||
:loading="loading" |
||||
size="small" |
||||
:disabled="!isValid" |
||||
@click.prevent="saveHooks" |
||||
> |
||||
<template #loading> {{ $t('general.saving') }} </template> |
||||
<div class="flex items-center px-1">{{ $t('general.save') }}</div> |
||||
</NcButton> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="flex flex-row"> |
||||
<div |
||||
class="nc-webhook-form flex flex-col" |
||||
:class="{ |
||||
'w-1/2': showLogs, |
||||
'w-full': !showLogs, |
||||
}" |
||||
> |
||||
<a-form :model="hookRef" name="create-or-edit-webhook"> |
||||
<a-form-item> |
||||
<div class="form-field-header">{{ $t('general.event') }}</div> |
||||
<a-row type="flex" :gutter="[16, 16]"> |
||||
<a-col :span="12"> |
||||
<a-form-item v-bind="validateInfos.eventOperation"> |
||||
<NcSelect |
||||
v-model:value="hookRef.eventOperation" |
||||
size="large" |
||||
:placeholder="$t('general.event')" |
||||
class="nc-text-field-hook-event capitalize" |
||||
dropdown-class-name="nc-dropdown-webhook-event" |
||||
> |
||||
<a-select-option |
||||
v-for="(event, i) in eventList" |
||||
:key="i" |
||||
class="capitalize" |
||||
:value="event.value.join(' ')" |
||||
:disabled="hookRef.version === 'v1' && ['bulkInsert', 'bulkUpdate', 'bulkDelete'].includes(event.value[1])" |
||||
> |
||||
<div class="flex items-center gap-2 justify-between"> |
||||
<div>{{ event.text.join(' ') }}</div> |
||||
<component |
||||
:is="iconMap.check" |
||||
v-if="hookRef.eventOperation === event.value.join(' ')" |
||||
id="nc-selected-item-icon" |
||||
class="text-primary w-4 h-4" |
||||
/> |
||||
</div> |
||||
</a-select-option> |
||||
</NcSelect> |
||||
</a-form-item> |
||||
</a-col> |
||||
|
||||
<a-col :span="12"> |
||||
<a-form-item v-bind="validateInfos['notification.type']"> |
||||
<NcSelect |
||||
v-model:value="hookRef.notification.type" |
||||
size="large" |
||||
class="nc-select-hook-notification-type" |
||||
:placeholder="$t('general.notification')" |
||||
dropdown-class-name="nc-dropdown-webhook-notification" |
||||
@change="onNotificationTypeChange(true)" |
||||
> |
||||
<a-select-option v-for="(notificationOption, i) in notificationList" :key="i" :value="notificationOption.type"> |
||||
<div class="flex items-center gap-2"> |
||||
<component :is="iconMap.link" v-if="notificationOption.type === 'URL'" class="mr-2" /> |
||||
|
||||
<component :is="iconMap.email" v-if="notificationOption.type === 'Email'" class="mr-2" /> |
||||
|
||||
<MdiSlack v-if="notificationOption.type === 'Slack'" class="mr-2" /> |
||||
|
||||
<MdiMicrosoftTeams v-if="notificationOption.type === 'Microsoft Teams'" class="mr-2" /> |
||||
|
||||
<MdiDiscord v-if="notificationOption.type === 'Discord'" class="mr-2" /> |
||||
|
||||
<MdiChat v-if="notificationOption.type === 'Mattermost'" class="mr-2" /> |
||||
|
||||
<MdiWhatsapp v-if="notificationOption.type === 'Whatsapp Twilio'" class="mr-2" /> |
||||
|
||||
<MdiCellphoneMessage v-if="notificationOption.type === 'Twilio'" class="mr-2" /> |
||||
|
||||
<div class="flex-1">{{ notificationOption.text }}</div> |
||||
<component |
||||
:is="iconMap.check" |
||||
v-if="hookRef.notification.type === notificationOption.type" |
||||
id="nc-selected-item-icon" |
||||
class="text-primary w-4 h-4" |
||||
/> |
||||
</div> |
||||
</a-select-option> |
||||
</NcSelect> |
||||
</a-form-item> |
||||
</a-col> |
||||
</a-row> |
||||
|
||||
<a-row v-if="hookRef.notification.type === 'URL'" class="mb-5" type="flex" :gutter="[16, 0]"> |
||||
<a-col :span="6"> |
||||
<div>Action</div> |
||||
<NcSelect |
||||
v-model:value="hookRef.notification.payload.method" |
||||
size="large" |
||||
class="nc-select-hook-url-method" |
||||
dropdown-class-name="nc-dropdown-hook-notification-url-method" |
||||
> |
||||
<a-select-option v-for="(method, i) in methodList" :key="i" :value="method.title"> |
||||
<div class="flex items-center gap-2 justify-between"> |
||||
<div>{{ method.title }}</div> |
||||
<component |
||||
:is="iconMap.check" |
||||
v-if="hookRef.notification.payload.method === method.title" |
||||
id="nc-selected-item-icon" |
||||
class="text-primary w-4 h-4" |
||||
/> |
||||
</div> |
||||
</a-select-option> |
||||
</NcSelect> |
||||
</a-col> |
||||
|
||||
<a-col :span="18"> |
||||
<div>Link</div> |
||||
<a-form-item v-bind="validateInfos['notification.payload.path']"> |
||||
<a-input |
||||
v-model:value="hookRef.notification.payload.path" |
||||
size="large" |
||||
placeholder="http://example.com" |
||||
class="nc-text-field-hook-url-path !rounded-md" |
||||
/> |
||||
</a-form-item> |
||||
</a-col> |
||||
|
||||
<a-col :span="24"> |
||||
<NcTabs v-model:activeKey="urlTabKey" type="card" closeable="false" class="border-1 !pb-2 !rounded-lg"> |
||||
<a-tab-pane key="params" :tab="$t('title.parameter')" force-render> |
||||
<LazyApiClientParams v-model="hookRef.notification.payload.parameters" class="p-4" /> |
||||
</a-tab-pane> |
||||
|
||||
<a-tab-pane key="headers" :tab="$t('title.headers')" class="nc-tab-headers"> |
||||
<LazyApiClientHeaders v-model="hookRef.notification.payload.headers" class="!p-4" /> |
||||
</a-tab-pane> |
||||
|
||||
<a-tab-pane v-if="isBodyShown" key="body" tab="Body"> |
||||
<LazyMonacoEditor |
||||
v-model="hookRef.notification.payload.body" |
||||
disable-deep-compare |
||||
:validate="false" |
||||
class="min-h-60 max-h-80" |
||||
/> |
||||
</a-tab-pane> |
||||
|
||||
<!-- No in use at this moment --> |
||||
<!-- <a-tab-pane key="auth" tab="Auth"> --> |
||||
<!-- <LazyMonacoEditor v-model="hook.notification.payload.auth" class="min-h-60 max-h-80" /> --> |
||||
|
||||
<!-- <span class="text-gray-500 prose-sm p-2"> --> |
||||
<!-- For more about auth option refer --> |
||||
<!-- <a class="prose-sm" href ="https://github.com/axios/axios#request-config" target="_blank">axios docs</a>. --> |
||||
<!-- </span> --> |
||||
<!-- </a-tab-pane> --> |
||||
</NcTabs> |
||||
</a-col> |
||||
</a-row> |
||||
|
||||
<a-row v-if="hookRef.notification.type === 'Slack'" type="flex"> |
||||
<a-col :span="24"> |
||||
<a-form-item v-bind="validateInfos['notification.payload.channels']"> |
||||
<LazyWebhookChannelMultiSelect |
||||
v-model="hookRef.notification.payload.channels" |
||||
:selected-channel-list="hookRef.notification.payload.channels" |
||||
:available-channel-list="slackChannels" |
||||
:placeholder="$t('placeholder.selectSlackChannels')" |
||||
/> |
||||
</a-form-item> |
||||
</a-col> |
||||
</a-row> |
||||
|
||||
<a-row v-if="hookRef.notification.type === 'Microsoft Teams'" type="flex"> |
||||
<a-col :span="24"> |
||||
<a-form-item v-bind="validateInfos['notification.payload.channels']"> |
||||
<LazyWebhookChannelMultiSelect |
||||
v-model="hookRef.notification.payload.channels" |
||||
:selected-channel-list="hookRef.notification.payload.channels" |
||||
:available-channel-list="teamsChannels" |
||||
:placeholder="$t('placeholder.selectTeamsChannels')" |
||||
/> |
||||
</a-form-item> |
||||
</a-col> |
||||
</a-row> |
||||
|
||||
<a-row v-if="hookRef.notification.type === 'Discord'" type="flex"> |
||||
<a-col :span="24"> |
||||
<a-form-item v-bind="validateInfos['notification.payload.channels']"> |
||||
<LazyWebhookChannelMultiSelect |
||||
v-model="hookRef.notification.payload.channels" |
||||
:selected-channel-list="hookRef.notification.payload.channels" |
||||
:available-channel-list="discordChannels" |
||||
:placeholder="$t('placeholder.selectDiscordChannels')" |
||||
/> |
||||
</a-form-item> |
||||
</a-col> |
||||
</a-row> |
||||
|
||||
<a-row v-if="hookRef.notification.type === 'Mattermost'" type="flex"> |
||||
<a-col :span="24"> |
||||
<a-form-item v-bind="validateInfos['notification.payload.channels']"> |
||||
<LazyWebhookChannelMultiSelect |
||||
v-model="hookRef.notification.payload.channels" |
||||
:selected-channel-list="hookRef.notification.payload.channels" |
||||
:available-channel-list="mattermostChannels" |
||||
:placeholder="$t('placeholder.selectMattermostChannels')" |
||||
/> |
||||
</a-form-item> |
||||
</a-col> |
||||
</a-row> |
||||
|
||||
<a-row v-if="formInput[hookRef.notification.type] && hookRef.notification.payload" type="flex"> |
||||
<a-col v-for="(input, i) in formInput[hookRef.notification.type]" :key="i" :span="24"> |
||||
<a-form-item v-if="input.type === 'LongText'" v-bind="validateInfos[`notification.payload.${input.key}`]"> |
||||
<a-textarea v-model:value="hookRef.notification.payload[input.key]" :placeholder="input.label" size="large" /> |
||||
</a-form-item> |
||||
|
||||
<a-form-item v-else v-bind="validateInfos[`notification.payload.${input.key}`]"> |
||||
<a-input v-model:value="hookRef.notification.payload[input.key]" :placeholder="input.label" size="large" /> |
||||
</a-form-item> |
||||
</a-col> |
||||
</a-row> |
||||
<a-row v-show="isConditionSupport" class="mb-5" type="flex"> |
||||
<a-col :span="24"> |
||||
<div class="rounded-lg border-1 p-6"> |
||||
<a-checkbox |
||||
:checked="Boolean(hookRef.condition)" |
||||
class="nc-check-box-hook-condition" |
||||
@update:checked="hookRef.condition = $event" |
||||
> |
||||
{{ $t('activity.onCondition') }} |
||||
</a-checkbox> |
||||
|
||||
<LazySmartsheetToolbarColumnFilter |
||||
v-if="hookRef.condition" |
||||
ref="filterRef" |
||||
class="!p-0 mt-4" |
||||
:auto-save="false" |
||||
:show-loading="false" |
||||
:hook-id="hookRef.id" |
||||
:web-hook="true" |
||||
@update:filters-length="hookRef.condition = $event > 0" |
||||
/> |
||||
</div> |
||||
</a-col> |
||||
</a-row> |
||||
|
||||
<a-row> |
||||
<a-col :span="24"> |
||||
<div v-if="isBodyShown" class="text-gray-600"> |
||||
<div class="flex items-center"> |
||||
<em |
||||
>{{ $t('msg.webhookBodyMsg1') }} <strong>{{ $t('msg.webhookBodyMsg2') }}</strong> |
||||
{{ $t('msg.webhookBodyMsg3') }}</em |
||||
> |
||||
|
||||
<a-tooltip bottom> |
||||
<template #title> |
||||
<span> |
||||
<strong>{{ $t('general.data') }}</strong> : {{ $t('title.rowData') }} <br /> |
||||
</span> |
||||
</template> |
||||
<component :is="iconMap.info" class="ml-2" /> |
||||
</a-tooltip> |
||||
</div> |
||||
|
||||
<div class="my-3"> |
||||
<a |
||||
href="https://docs.nocodb.com/automation/webhook/create-webhook/#webhook-with-custom-payload-" |
||||
target="_blank" |
||||
rel="noopener" |
||||
> |
||||
<!-- Document Reference --> |
||||
{{ $t('labels.docReference') }} |
||||
</a> |
||||
</div> |
||||
</div> |
||||
|
||||
<LazyWebhookTest |
||||
ref="webhookTestRef" |
||||
:hook="{ |
||||
...hookRef, |
||||
notification: { |
||||
...hookRef.notification, |
||||
payload: hookRef.notification.payload, |
||||
}, |
||||
}" |
||||
/> |
||||
</a-col> |
||||
</a-row> |
||||
</a-form-item> |
||||
</a-form> |
||||
</div> |
||||
<div v-if="showLogs" class="nc-webhook-calllog flex w-1/2"> |
||||
<LazyWebhookCallLog :hook="hookRef" /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped> |
||||
.circle { |
||||
width: 0.6rem; |
||||
height: 0.6rem; |
||||
background-color: #ffffff; |
||||
border-radius: 50%; |
||||
position: relative; |
||||
} |
||||
|
||||
.dot { |
||||
width: 0.4rem; |
||||
height: 0.4rem; |
||||
border-radius: 50%; |
||||
position: absolute; |
||||
top: 50%; |
||||
left: 50%; |
||||
transform: translate(-52.5%, -52.5%); |
||||
} |
||||
|
||||
:deep(.ant-tabs-tab) { |
||||
@apply border-r-0 border-l-0 border-t-0 !px-4 !bg-inherit !border-b-2 border-transparent text-gray-600; |
||||
} |
||||
:deep(.ant-tabs-tab-active) { |
||||
@apply !px-4 !border-primary; |
||||
} |
||||
|
||||
.form-field-header { |
||||
@apply mb-1; |
||||
} |
||||
</style> |
@ -1,169 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import type { FilterReqType, HookReqType, HookType } from 'nocodb-sdk' |
||||
|
||||
const emit = defineEmits(['edit', 'add']) |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const { $api, $e } = useNuxtApp() |
||||
|
||||
const hooks = ref<HookType[]>([]) |
||||
|
||||
const meta = inject(MetaInj, ref()) |
||||
|
||||
async function loadHooksList() { |
||||
try { |
||||
const hookList = (await $api.dbTableWebhook.list(meta.value?.id as string)).list |
||||
hooks.value = hookList.map((hook) => { |
||||
hook.notification = parseProp(hook.notification) |
||||
return hook |
||||
}) |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
|
||||
async function deleteHook(item: HookType, index: number) { |
||||
Modal.confirm({ |
||||
title: `Do you want to delete '${item.title}'?`, |
||||
wrapClassName: 'nc-modal-hook-delete', |
||||
okText: 'Yes', |
||||
okType: 'danger', |
||||
cancelText: 'No', |
||||
async onOk() { |
||||
try { |
||||
if (item.id) { |
||||
await $api.dbTableWebhook.delete(item.id) |
||||
hooks.value.splice(index, 1) |
||||
} else { |
||||
hooks.value.splice(index, 1) |
||||
} |
||||
|
||||
// Hook deleted successfully |
||||
message.success(t('msg.success.webhookDeleted')) |
||||
if (!hooks.value.length) { |
||||
hooks.value = [] |
||||
} |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
|
||||
$e('a:webhook:delete') |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
async function copyHook(hook: HookType) { |
||||
try { |
||||
const newHook = await $api.dbTableWebhook.create(hook.fk_model_id!, { |
||||
...hook, |
||||
title: `${hook.title} - Copy`, |
||||
active: false, |
||||
} as HookReqType) |
||||
|
||||
if (newHook) { |
||||
$e('a:webhook:copy') |
||||
// create the corresponding filters |
||||
const hookFilters = (await $api.dbTableWebhookFilter.read(hook.id!, {})).list |
||||
for (const hookFilter of hookFilters) { |
||||
await $api.dbTableWebhookFilter.create(newHook.id!, { |
||||
comparison_op: hookFilter.comparison_op, |
||||
comparison_sub_op: hookFilter.comparison_sub_op, |
||||
fk_column_id: hookFilter.fk_column_id, |
||||
fk_parent_id: hookFilter.fk_parent_id, |
||||
is_group: hookFilter.is_group, |
||||
logical_op: hookFilter.logical_op, |
||||
value: hookFilter.value, |
||||
} as FilterReqType) |
||||
} |
||||
newHook.notification = parseProp(newHook.notification) |
||||
hooks.value = [newHook, ...hooks.value] |
||||
} |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
|
||||
onMounted(() => { |
||||
loadHooksList() |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class=""> |
||||
<div class="mb-2"> |
||||
<div class="float-left font-bold text-xl mt-2 mb-4">{{ meta?.title }} : Webhooks</div> |
||||
|
||||
<a-button |
||||
v-e="['c:webhook:add']" |
||||
class="float-right !rounded-md nc-btn-create-webhook" |
||||
type="primary" |
||||
size="middle" |
||||
@click="emit('add')" |
||||
> |
||||
{{ $t('activity.addWebhook') }} |
||||
</a-button> |
||||
</div> |
||||
|
||||
<a-divider /> |
||||
|
||||
<div v-if="hooks.length" class=""> |
||||
<a-list item-layout="horizontal" :data-source="hooks" class="cursor-pointer scrollbar-thin-primary"> |
||||
<template #renderItem="{ item, index }"> |
||||
<a-list-item class="p-2 nc-hook" @click="emit('edit', item)"> |
||||
<a-list-item-meta> |
||||
<template #description> |
||||
<span class="uppercase"> {{ item.event }} {{ item.operation.replace(/[A-Z]/g, ' $&') }}</span> |
||||
</template> |
||||
|
||||
<template #title> |
||||
<div class="text-xl normal-case"> |
||||
<span class="text-gray-400 text-sm"> ({{ item.version }}) </span> |
||||
{{ item.title }} |
||||
</div> |
||||
</template> |
||||
|
||||
<template #avatar> |
||||
<div class="px-2"> |
||||
<component :is="iconMap.hook" class="text-xl" /> |
||||
</div> |
||||
<div class="px-2 text-white rounded" :class="{ 'bg-green-500': item.active, 'bg-gray-500': !item.active }"> |
||||
{{ item.active ? 'ON' : 'OFF' }} |
||||
</div> |
||||
</template> |
||||
</a-list-item-meta> |
||||
|
||||
<template #extra> |
||||
<div> |
||||
<!-- Notify Via --> |
||||
<div class="mr-2">{{ $t('labels.notifyVia') }} : {{ item?.notification?.type }}</div> |
||||
|
||||
<div class="float-right pt-2 pr-1"> |
||||
<a-tooltip v-if="item.version === 'v2'" placement="left"> |
||||
<template #title> |
||||
{{ $t('activity.copyWebhook') }} |
||||
</template> |
||||
<component :is="iconMap.copy" class="text-xl nc-hook-copy-icon" @click.stop="copyHook(item)" /> |
||||
</a-tooltip> |
||||
|
||||
<a-tooltip placement="left"> |
||||
<template #title> |
||||
{{ $t('activity.deleteWebhook') }} |
||||
</template> |
||||
<component :is="iconMap.delete" class="text-xl nc-hook-delete-icon" @click.stop="deleteHook(item, index)" /> |
||||
</a-tooltip> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</a-list-item> |
||||
</template> |
||||
</a-list> |
||||
</div> |
||||
|
||||
<div v-else class="min-h-[75vh]"> |
||||
<div class="p-4 bg-gray-100 text-gray-600"> |
||||
Webhooks list is empty, create new webhook by clicking 'Create webhook' button. |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -1,24 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
const isOpen = ref(false) |
||||
|
||||
const editOrAdd = ref(false) |
||||
const currentHook = ref<Record<string, any>>() |
||||
|
||||
async function editHook(hook: Record<string, any>) { |
||||
editOrAdd.value = true |
||||
currentHook.value = hook |
||||
} |
||||
|
||||
async function addHook() { |
||||
editOrAdd.value = true |
||||
currentHook.value = undefined |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<GeneralModal v-model:visible="isOpen"> |
||||
<LazyWebhookEditor v-if="editOrAdd" :hook="currentHook" @back-to-list="editOrAdd = false" /> |
||||
|
||||
<LazyWebhookList v-else @edit="editHook" @add="addHook" /> |
||||
</GeneralModal> |
||||
</template> |
@ -1,62 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import type { HookTestReqType, HookType } from 'nocodb-sdk' |
||||
|
||||
interface Props { |
||||
hook: HookType |
||||
} |
||||
|
||||
const { hook } = defineProps<Props>() |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const { $api } = useNuxtApp() |
||||
|
||||
const meta = inject(MetaInj, ref()) |
||||
|
||||
const sampleData = ref() |
||||
|
||||
watch( |
||||
() => hook?.operation, |
||||
async () => { |
||||
await loadSampleData() |
||||
}, |
||||
) |
||||
|
||||
async function loadSampleData() { |
||||
sampleData.value = await $api.dbTableWebhook.samplePayloadGet( |
||||
meta?.value?.id as string, |
||||
hook?.operation || 'insert', |
||||
hook.version!, |
||||
) |
||||
} |
||||
|
||||
async function testWebhook() { |
||||
try { |
||||
await $api.dbTableWebhook.test( |
||||
meta.value?.id as string, |
||||
{ |
||||
hook, |
||||
payload: sampleData.value, |
||||
} as HookTestReqType, |
||||
) |
||||
|
||||
// Webhook tested successfully |
||||
message.success(t('msg.success.webhookTested')) |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
|
||||
defineExpose({ |
||||
testWebhook, |
||||
}) |
||||
|
||||
onMounted(async () => { |
||||
await loadSampleData() |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="mb-4 font-weight-medium">Sample Payload</div> |
||||
<LazyMonacoEditor v-model="sampleData" class="min-h-60 max-h-80" /> |
||||
</template> |
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 260 KiB |
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 221 KiB |
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 223 KiB |