mirror of https://github.com/nocodb/nocodb
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.
377 lines
11 KiB
377 lines
11 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) => { |
|
hook.active = !hook.active |
|
await saveHooks({ hook }) |
|
} |
|
|
|
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'] }, |
|
]) |
|
|
|
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" |
|
@click="createWebhook" |
|
data-testid="nc-new-webhook" |
|
> |
|
<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 }"> |
|
<div v-if="column.key === 'active'" v-e="['c:actions:webhook']" @click.stop> |
|
<NcSwitch size="small" :checked="!!hook.active" @change="toggleHook(hook)" /> |
|
</div> |
|
<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" @click.stop data-testid="nc-webhook-item-action"> |
|
<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> |
|
</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>
|
|
|