mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-01 05:26:53 +00:00
298 lines
8.2 KiB
Vue
298 lines
8.2 KiB
Vue
<script setup lang="ts">
|
|
interface Prop {
|
|
extensionId: string
|
|
error?: any
|
|
clearError?: () => void
|
|
}
|
|
|
|
const props = defineProps<Prop>()
|
|
|
|
const { extensionList, extensionsLoaded, availableExtensions, userHasAccessToExtension, extensionAccess } = useExtensions()
|
|
|
|
const activeError = toRef(props, 'error')
|
|
|
|
const isLoadedExtension = ref<boolean>(true)
|
|
|
|
const extensionRef = ref<HTMLElement>()
|
|
|
|
const extensionModalRef = ref<HTMLElement>()
|
|
|
|
const isMouseDown = ref(false)
|
|
|
|
const extension = computed(() => {
|
|
const ext = extensionList.value.find((ext) => ext.id === props.extensionId)
|
|
if (!ext) {
|
|
throw new Error('Extension not found')
|
|
}
|
|
return ext
|
|
})
|
|
|
|
const extensionManifest = computed<ExtensionManifest | undefined>(() => {
|
|
return availableExtensions.value.find((ext) => ext.id === extension.value?.extensionId)
|
|
})
|
|
|
|
const activeExtensionId = computed(() => extensionManifest.value?.id ?? '')
|
|
|
|
const hasAccessToExtension = computed(() => {
|
|
return userHasAccessToExtension(activeExtensionId.value)
|
|
})
|
|
|
|
provide(ExtensionConfigInj, ref({ activeExtensionId }))
|
|
|
|
const {
|
|
fullscreen,
|
|
fullscreenModalSize: currentExtensionModalSize,
|
|
collapsed,
|
|
} = useProvideExtensionHelper(extension, extensionManifest, activeError, hasAccessToExtension)
|
|
|
|
const { height } = useElementSize(extensionRef)
|
|
|
|
const component = ref<any>(null)
|
|
|
|
const extensionHeight = computed(() => {
|
|
const heigthInInt = parseInt(extensionManifest.value?.config?.contentMinHeight || '') || undefined
|
|
|
|
if (!heigthInInt || height.value > heigthInInt) return `${height.value}px`
|
|
|
|
return extensionManifest.value?.config?.contentMinHeight
|
|
})
|
|
|
|
const fullscreenModalSize = computed(() => {
|
|
return currentExtensionModalSize.value ? modalSizes[currentExtensionModalSize.value] || modalSizes.lg : modalSizes.lg
|
|
})
|
|
|
|
// close fullscreen on clicking extensionModalRef directly
|
|
const closeFullscreen = (e: MouseEvent) => {
|
|
if (e.target === extensionModalRef.value) {
|
|
fullscreen.value = false
|
|
}
|
|
}
|
|
|
|
const onClearData = () => {
|
|
if (extensionAccess.value.update) {
|
|
extension.value.clear()
|
|
}
|
|
|
|
props.clearError?.()
|
|
}
|
|
|
|
onMounted(() => {
|
|
until(extensionsLoaded)
|
|
.toMatch((v) => v)
|
|
.then(() => {
|
|
if (!extensionManifest.value) {
|
|
return
|
|
}
|
|
|
|
import(`../../extensions/${extensionManifest.value.entry}/index.vue`)
|
|
.then((mod) => {
|
|
component.value = markRaw(mod.default)
|
|
isLoadedExtension.value = false
|
|
})
|
|
.catch((e) => {
|
|
isLoadedExtension.value = false
|
|
throw new Error(e)
|
|
})
|
|
})
|
|
.catch((err) => {
|
|
if (!extensionManifest.value) {
|
|
throw new Error('There was an error loading the extension')
|
|
}
|
|
isLoadedExtension.value = false
|
|
throw new Error(err)
|
|
})
|
|
})
|
|
|
|
// #Listeners
|
|
|
|
// close fullscreen on escape key press
|
|
useEventListener('keydown', (e) => {
|
|
// Check if the event target or its closest parent is an input, select, or textarea
|
|
const isFormElement = (e?.target as HTMLElement)?.closest('input, select, textarea')
|
|
|
|
// If the target is not a form element and the key is 'Escape', close fullscreen
|
|
if (e.key === 'Escape' && !isFormElement) {
|
|
fullscreen.value = false
|
|
}
|
|
})
|
|
|
|
const noExplicitHeightExtensions = ['nc-data-exporter']
|
|
|
|
const isNoExplicitHeightExtension = computed(() => noExplicitHeightExtensions.includes(extension.value.extensionId))
|
|
|
|
/**
|
|
* Log extension error so that we can debug easily.
|
|
*/
|
|
watch(
|
|
activeError,
|
|
(newVal) => {
|
|
if (!newVal) return
|
|
|
|
console.error(newVal)
|
|
},
|
|
{
|
|
immediate: true,
|
|
},
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="extensionRef" class="w-full px-4" :class="`nc-${extensionManifest?.id}`" :data-testid="extension.id">
|
|
<div
|
|
class="extension-wrapper"
|
|
:class="[
|
|
{
|
|
'!h-auto': collapsed,
|
|
'isOpen': !collapsed,
|
|
'mousedown': isMouseDown,
|
|
},
|
|
]"
|
|
:style="
|
|
!collapsed
|
|
? {
|
|
height: isNoExplicitHeightExtension ? '100%' : extensionHeight,
|
|
minHeight: extensionManifest?.config?.contentMinHeight,
|
|
}
|
|
: {}
|
|
"
|
|
@mousedown="isMouseDown = true"
|
|
@mouseup="isMouseDown = false"
|
|
>
|
|
<ExtensionsExtensionHeader :is-fullscreen="false" />
|
|
|
|
<template v-if="activeError">
|
|
<div
|
|
v-show="!collapsed"
|
|
class="extension-content nc-scrollbar-thin h-[calc(100%_-_50px)] flex items-center justify-center"
|
|
:class="{
|
|
fullscreen,
|
|
}"
|
|
>
|
|
<a-result status="error" title="Extension Error" class="nc-extension-error">
|
|
<template #subTitle>
|
|
<span class="text-nc-content-gray-muted">
|
|
{{ activeError }}
|
|
</span>
|
|
</template>
|
|
<template #extra>
|
|
<NcButton size="small" @click="onClearData">
|
|
<div class="flex items-center gap-2">
|
|
<GeneralIcon icon="reload" />
|
|
{{ extensionAccess.update ? 'Clear Data' : 'Reload Extension' }}
|
|
</div>
|
|
</NcButton>
|
|
<NcButton v-if="extensionAccess.delete" size="small" type="danger" @click="extension.delete()">
|
|
<div class="flex items-center gap-2">
|
|
<GeneralIcon icon="delete" />
|
|
Delete
|
|
</div>
|
|
</NcButton>
|
|
</template>
|
|
</a-result>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<Teleport to="body" :disabled="!fullscreen">
|
|
<div
|
|
ref="extensionModalRef"
|
|
:class="[
|
|
fullscreen ? `nc-${extensionManifest?.id}` : '',
|
|
{ 'extension-modal': fullscreen, 'h-[calc(100%_-_50px)]': !fullscreen },
|
|
]"
|
|
@click="closeFullscreen"
|
|
>
|
|
<div
|
|
:class="{
|
|
'extension-modal-content': fullscreen,
|
|
'h-full': !fullscreen,
|
|
'!nc-h-screen !nc-w-screen': fullscreen && currentExtensionModalSize === 'fullscreen',
|
|
'nc-extension-fullscreen': fullscreen && currentExtensionModalSize === 'extensionFullscreen',
|
|
}"
|
|
:style="
|
|
fullscreen
|
|
? {
|
|
maxWidth: fullscreenModalSize.width,
|
|
maxHeight: fullscreenModalSize.height,
|
|
}
|
|
: {}
|
|
"
|
|
>
|
|
<div
|
|
v-show="fullscreen || !collapsed"
|
|
class="extension-content h-full"
|
|
:class="{ 'fullscreen': fullscreen, 'h-full nc-scrollbar-thin': !fullscreen }"
|
|
>
|
|
<component :is="component" :key="extension.uiKey" class="h-full" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<general-overlay :model-value="isLoadedExtension" inline transition class="!bg-opacity-15 rounded-xl overflow-hidden">
|
|
<div class="flex flex-col items-center justify-center h-full w-full !bg-nc-bg-default !bg-opacity-80">
|
|
<a-spin size="large" />
|
|
</div>
|
|
</general-overlay>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.extension-wrapper {
|
|
@apply bg-nc-bg-default rounded-xl w-full border-1 relative;
|
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.08);
|
|
|
|
&.isOpen {
|
|
resize: vertical;
|
|
|
|
&:hover,
|
|
&.mousedown {
|
|
overflow-y: auto;
|
|
}
|
|
}
|
|
}
|
|
|
|
.extension-content {
|
|
@apply rounded-b-lg;
|
|
}
|
|
|
|
.extension-modal {
|
|
@apply absolute top-0 left-0 z-1000 w-full h-full bg-black/50 flex items-center justify-center;
|
|
|
|
.extension-modal-content {
|
|
@apply bg-nc-bg-default rounded-2xl mx-auto flex flex-col overflow-hidden dark:(border-1 border-nc-border-gray-medium);
|
|
|
|
&:not(.nc-extension-fullscreen) {
|
|
@apply w-[90%] h-[90vh];
|
|
}
|
|
|
|
&.nc-extension-fullscreen {
|
|
@apply w-[calc(100vw-32px)] h-[calc(100vh-var(--topbar-height)-16px)];
|
|
}
|
|
}
|
|
|
|
&:has(.nc-extension-fullscreen) {
|
|
@apply pt-[var(--topbar-height)] !items-start;
|
|
}
|
|
}
|
|
|
|
:deep(.nc-extension-error.ant-result) {
|
|
@apply p-0;
|
|
.ant-result-icon {
|
|
@apply mb-3;
|
|
& > span {
|
|
@apply text-[32px];
|
|
}
|
|
}
|
|
|
|
.ant-result-title {
|
|
@apply text-base text-nc-content-gray font-semibold;
|
|
}
|
|
|
|
.ant-result-extra {
|
|
@apply mt-3;
|
|
}
|
|
}
|
|
</style>
|