多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

398 lines
12 KiB

<script lang="ts" setup>
import type { HookType } from 'nocodb-sdk'
import { LoadingOutlined } from '@ant-design/icons-vue'
import dayjs from 'dayjs'
const { activeTable } = storeToRefs(useTablesStore())
const { sorts, sortDirection, loadSorts, handleGetSortedData, saveOrUpdate: saveOrUpdateSort } = useUserSorts('Webhook')
const selectedHook = ref<undefined | HookType>()
const { hooks, isHooksLoading } = storeToRefs(useWebhooksStore())
const { loadHooksList, deleteHook: _deleteHook, copyHook, saveHooks } = useWebhooksStore()
const { activeView } = storeToRefs(useViewsStore())
const { t } = useI18n()
const isWebhookModalOpen = ref(false)
const modalDeleteButtonRef = ref(null)
const indicator = h(LoadingOutlined, {
style: {
fontSize: '2.5rem',
},
spin: true,
})
const deleteHookId = ref('')
const showDeleteModal = ref(false)
const isDeleting = ref(false)
const toBeDeleteHook = computed(() => {
return hooks.value.find((hook) => hook.id === deleteHookId.value)
})
const deleteHook = async () => {
isDeleting.value = true
if (!deleteHookId.value) return
try {
await _deleteHook(deleteHookId.value)
} finally {
isDeleting.value = false
showDeleteModal.value = false
deleteHookId.value = ''
}
}
const selectedHookId = ref<string | undefined>(undefined)
const isCopying = ref(false)
const copyWebhook = async (hook: HookType) => {
if (isCopying.value) return
isCopying.value = true
try {
await copyHook(hook)
} finally {
isCopying.value = false
}
}
const openDeleteModal = (hookId: string) => {
deleteHookId.value = hookId
showDeleteModal.value = true
}
const webHookSearch = ref('')
const filteredHooks = computed(() =>
hooks.value.filter((hook) => hook.title?.toLowerCase().includes(webHookSearch.value.toLowerCase())),
)
const sortedHooks = computed(() => {
return handleGetSortedData(filteredHooks.value, sorts.value)
})
watch(showDeleteModal, () => {
if (!showDeleteModal.value) return
nextTick(() => {
;(modalDeleteButtonRef.value as any)?.$el?.focus()
})
})
watch(isWebhookModalOpen, (val) => {
if (!val) {
selectedHook.value = undefined
}
})
watch(
() => activeTable.value?.id,
async () => {
if (!activeTable.value?.id) return
selectedHookId.value = undefined
await loadHooksList()
},
{
immediate: true,
},
)
const toggleHook = async (hook: HookType) => {
if (hook.event === 'manual' && hook.operation === 'trigger') {
message.error(t('msg.error.manualTriggerHook'))
return
}
const ogHook = Object.assign({}, hook)
hook.active = !hook.active
await saveHooks({ hook, ogHook })
}
const createWebhook = async () => {
isWebhookModalOpen.value = true
}
const editHook = (hook: HookType) => {
selectedHook.value = hook
isWebhookModalOpen.value = true
}
const onModalClose = () => {
isWebhookModalOpen.value = false
selectedHook.value = undefined
}
onMounted(async () => {
loadSorts()
})
const orderBy = computed<Record<string, SordDirectionType>>({
get: () => {
return sortDirection.value
},
set: (value: Record<string, SordDirectionType>) => {
// Check if value is an empty object
if (Object.keys(value).length === 0) {
saveOrUpdateSort({})
return
}
const [field, direction] = Object.entries(value)[0]
saveOrUpdateSort({
field,
direction,
})
},
})
const eventList = ref<Record<string, any>[]>([
{ text: [t('general.on'), t('labels.recordInsert')], value: ['after', 'insert'] },
{ text: [t('general.on'), t('labels.recordUpdate')], value: ['after', 'update'] },
{ text: [t('general.on'), t('labels.recordDelete')], value: ['after', 'delete'] },
{ text: [t('general.onMultiple'), t('labels.recordInsert')], value: ['after', 'bulkInsert'] },
{ text: [t('general.onMultiple'), t('labels.recordUpdate')], value: ['after', 'bulkUpdate'] },
{ text: [t('general.onMultiple'), t('labels.recordDelete')], value: ['after', 'bulkDelete'] },
{ text: [t('general.manual'), t('general.trigger')], value: ['manual', 'trigger'] },
])
const columns: NcTableColumnProps[] = [
{
key: 'active',
title: t('general.active'),
width: 90,
minWidth: 90,
},
{
key: 'name',
title: t('general.name'),
minWidth: 252,
showOrderBy: true,
dataIndex: 'title',
},
{
key: 'type',
title: t('general.type'),
basis: '25%',
minWidth: 200,
showOrderBy: true,
dataIndex: 'webhook-operation-type',
},
{
key: 'created_at',
title: t('labels.addedOn'),
width: 180,
minWidth: 180,
showOrderBy: true,
dataIndex: 'created_at',
},
{
key: 'action',
title: '',
width: 80,
minWidth: 80,
},
]
const customRow = (hook: HookType) => {
return {
onClick: () => editHook(hook),
}
}
const getHookTypeText = (hook: HookType) => {
return (
eventList.value.find((e) => e.value.includes(hook.event) && e.value.includes(hook.operation))?.text?.join(' ') ||
`Before ${hook.operation}`
)
}
</script>
<template>
<div class="nc-webhook-wrapper w-full p-4">
<div class="max-w-250 h-full w-full mx-auto">
<div v-if="activeView && !isHooksLoading">
<div class="w-full mb-4 flex justify-between gap-3">
<div class="flex-1 flex gap-2">
<a-input
v-model:value="webHookSearch"
class="w-full nc-input-sm nc-input-border-on-value !max-w-84"
size="small"
:placeholder="$t('title.searchWebhook')"
allow-clear
>
<template #prefix>
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500" />
</template>
</a-input>
<NcButton
class="px-2"
type="text"
size="small"
@click="navigateTo('https://docs.nocodb.com/category/webhook/', { open: navigateToBlankTargetOpenOption })"
>
<div class="flex items-center gap-2">
{{ $t('title.docs') }}
<GeneralIcon icon="externalLink" />
</div>
</NcButton>
</div>
<NcButton
v-e="['c:actions:webhook']"
type="secondary"
size="small"
class="!text-brand-500 !hover:text-brand-600"
data-testid="nc-new-webhook"
@click="createWebhook"
>
<div class="flex gap-2 items-center">
<GeneralIcon icon="plus" />
{{ $t('activity.newWebhook') }}
</div>
</NcButton>
</div>
<div style="height: calc(100vh - (var(--topbar-height) * 3.5))" class="">
<div
v-if="!hooks.length"
class="flex-col flex items-center gap-6 justify-center w-full h-full py-12 px-4 border-1 rounded-xl border-gray-200"
>
<div class="text-gray-700 font-bold text-center text-2xl">{{ $t('msg.createWebhookMsg1') }}</div>
<div class="text-gray-700 text-center max-w-[24rem]">{{ $t('msg.createWebhookMsg2') }}</div>
<NcButton v-e="['c:actions:webhook']" class="flex max-w-40" type="primary" size="small" @click="createWebhook">
<div class="flex items-center gap-2">
<GeneralIcon icon="plus" class="flex-none" />
<span>{{ $t('activity.newWebhook') }}</span>
</div>
</NcButton>
</div>
<NcTable
v-else
v-model:order-by="orderBy"
:columns="columns"
:data="sortedHooks"
:custom-row="customRow"
class="h-full"
body-row-class-name="nc-view-sidebar-webhook-item"
>
<template #bodyCell="{ column, record: hook }">
<NcTooltip :disabled="hook.event !== 'manual'">
<template #title>
{{ $t('msg.error.manualTriggerHook') }}
</template>
<div v-if="column.key === 'active'" v-e="['c:actions:webhook']" @click.stop>
<NcSwitch
size="small"
:disabled="hook.event === 'manual'"
:checked="!!hook.active"
@change="toggleHook(hook)"
/>
</div>
</NcTooltip>
<template v-if="column.key === 'name'">
<NcTooltip class="truncate max-w-full text-gray-800 font-semibold text-sm" show-on-truncate-only>
{{ hook.title }}
<template #title>
{{ hook.title }}
</template>
</NcTooltip>
</template>
<template v-if="column.key === 'type'">
{{ getHookTypeText(hook) }}
</template>
<template v-if="column.key === 'created_at'">
{{ dayjs(hook.created_at).format('DD MMM YYYY') }}
</template>
<template v-if="column.key === 'action'">
<NcDropdown overlay-class-name="nc-webhook-item-action-dropdown">
<NcButton type="secondary" size="small" class="!w-8 !h-8" data-testid="nc-webhook-item-action" @click.stop>
<component :is="iconMap.threeDotVertical" class="text-gray-700" />
</NcButton>
<template #overlay>
<NcMenu class="w-48">
<NcMenuItem key="edit" data-testid="nc-webhook-item-action-edit" @click="editHook(hook)">
<GeneralIcon icon="edit" class="text-gray-800" />
<span>{{ $t('general.edit') }}</span>
</NcMenuItem>
<NcMenuItem key="duplicate" data-testid="nc-webhook-item-action-duplicate" @click="copyWebhook(hook)">
<GeneralIcon icon="duplicate" class="text-gray-800" />
<span>{{ $t('general.duplicate') }}</span>
</NcMenuItem>
<a-menu-divider class="my-1.5" />
<NcMenuItem
key="delete"
class="!hover:bg-red-50"
data-testid="nc-webhook-item-action-delete"
@click="openDeleteModal(hook.id)"
>
<div class="text-red-500">
<GeneralIcon icon="delete" class="group-hover:text-accent -ml-0.25 -mt-0.75 mr-0.5" />
{{ $t('general.delete') }}
</div>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</template>
</template>
</NcTable>
</div>
<GeneralDeleteModal v-model:visible="showDeleteModal" :entity-name="$t('objects.webhook')" :on-delete="deleteHook">
<template #entity-preview>
<div v-if="toBeDeleteHook" class="flex flex-row items-center py-2 px-3 bg-gray-50 rounded-lg text-gray-700 mb-4">
<component :is="iconMap.hook" class="text-gray-600" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-2.5"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ toBeDeleteHook.title }}
</div>
</div>
<span v-if="toBeDeleteHook?.event === 'manual'" class="text-small leading-[18px] mb-2 text-gray-500">
{{ $t('msg.warning.webhookDelete') }}
</span>
</template>
</GeneralDeleteModal>
<Webhook
v-if="isWebhookModalOpen"
v-model:value="isWebhookModalOpen"
:hook="selectedHook"
:event-list="eventList"
@close="onModalClose"
/>
</div>
<div
v-else
class="h-full w-full flex flex-col justify-center items-center"
style="height: calc(100vh - (var(--topbar-height) * 2))"
>
<a-spin size="large" :indicator="indicator" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
:deep(.ant-input::placeholder) {
@apply text-gray-500;
}
</style>