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.
400 lines
12 KiB
400 lines
12 KiB
<script setup lang="ts"> |
|
interface Prop { |
|
extensionId: string |
|
error?: any |
|
} |
|
|
|
const { extensionId, error } = defineProps<Prop>() |
|
|
|
const { |
|
extensionList, |
|
extensionsLoaded, |
|
availableExtensions, |
|
eventBus, |
|
getExtensionAssetsUrl, |
|
duplicateExtension, |
|
showExtensionDetails, |
|
} = useExtensions() |
|
|
|
const activeError = ref(error) |
|
|
|
const extensionRef = ref<HTMLElement>() |
|
|
|
const extensionModalRef = ref<HTMLElement>() |
|
|
|
const isMouseDown = ref(false) |
|
|
|
const extension = computed(() => { |
|
const ext = extensionList.value.find((ext) => ext.id === extensionId) |
|
if (!ext) { |
|
throw new Error('Extension not found') |
|
} |
|
return ext |
|
}) |
|
|
|
const titleInput = ref<HTMLInputElement | null>(null) |
|
|
|
const titleEditMode = ref<boolean>(false) |
|
|
|
const tempTitle = ref<string>(extension.value.title) |
|
|
|
const enableEditMode = () => { |
|
titleEditMode.value = true |
|
tempTitle.value = extension.value.title |
|
nextTick(() => { |
|
titleInput.value?.focus() |
|
titleInput.value?.select() |
|
}) |
|
} |
|
|
|
const updateExtensionTitle = async () => { |
|
await extension.value.setTitle(tempTitle.value) |
|
titleEditMode.value = false |
|
} |
|
|
|
const { fullscreen, collapsed } = useProvideExtensionHelper(extension) |
|
|
|
const component = ref<any>(null) |
|
|
|
const extensionManifest = ref<ExtensionManifest | undefined>() |
|
|
|
const fullscreenModalMaxWidth = computed(() => { |
|
const modalMaxWidth = { |
|
xs: 'min(calc(100vw - 32px), 448px)', |
|
sm: 'min(calc(100vw - 32px), 640px)', |
|
md: 'min(calc(100vw - 48px), 900px)', |
|
lg: 'min(calc(100vw - 48px), 1280px)', |
|
} |
|
|
|
return extensionManifest.value?.config?.modalMaxWith |
|
? modalMaxWidth[extensionManifest.value?.config?.modalMaxWith] || modalMaxWidth.lg |
|
: modalMaxWidth.lg |
|
}) |
|
|
|
const expandExtension = () => { |
|
if (!collapsed.value) return |
|
|
|
collapsed.value = false |
|
} |
|
|
|
onMounted(() => { |
|
until(extensionsLoaded) |
|
.toMatch((v) => v) |
|
.then(() => { |
|
extensionManifest.value = availableExtensions.value.find((ext) => ext.id === extension.value.extensionId) |
|
|
|
if (!extensionManifest.value) { |
|
return |
|
} |
|
|
|
import(`../../extensions/${extensionManifest.value.entry}/index.vue`).then((mod) => { |
|
component.value = markRaw(mod.default) |
|
}) |
|
}) |
|
.catch((err) => { |
|
if (!extensionManifest.value) { |
|
activeError.value = 'There was an error loading the extension' |
|
return |
|
} |
|
activeError.value = err |
|
}) |
|
}) |
|
|
|
// 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 |
|
} |
|
}) |
|
|
|
// close fullscreen on clicking extensionModalRef directly |
|
const closeFullscreen = (e: MouseEvent) => { |
|
if (e.target === extensionModalRef.value) { |
|
fullscreen.value = false |
|
} |
|
} |
|
|
|
const handleDuplicateExtension = async (id: string, open: boolean = false) => { |
|
const duplicatedExt = await duplicateExtension(id) |
|
|
|
if (duplicatedExt?.id && open) { |
|
fullscreen.value = false |
|
eventBus.emit(ExtensionsEvents.DUPLICATE, duplicatedExt.id) |
|
} |
|
} |
|
|
|
// #Listeners |
|
eventBus.on((event, payload) => { |
|
if (event === ExtensionsEvents.DUPLICATE && extension.value.id === payload) { |
|
setTimeout(() => { |
|
nextTick(() => { |
|
extensionRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' }) |
|
}) |
|
}, 500) |
|
} |
|
}) |
|
</script> |
|
|
|
<template> |
|
<div ref="extensionRef" class="w-full px-4" :data-testid="extension.id"> |
|
<div |
|
class="extension-wrapper" |
|
:class="[ |
|
{ |
|
'!h-auto': collapsed, |
|
'isOpen': !collapsed, |
|
'mousedown': isMouseDown, |
|
}, |
|
]" |
|
:style=" |
|
!collapsed |
|
? { |
|
height: extensionManifest?.config?.contentMinHeight, |
|
minHeight: extensionManifest?.config?.contentMinHeight, |
|
} |
|
: {} |
|
" |
|
@mousedown="isMouseDown = true" |
|
@mouseup="isMouseDown = false" |
|
> |
|
<div |
|
class="extension-header px-3 py-2" |
|
:class="{ |
|
'border-b-1 border-gray-200 h-[49px]': !collapsed, |
|
'collapsed border-transparent h-[48px]': collapsed, |
|
}" |
|
@click="expandExtension" |
|
> |
|
<div class="extension-header-left max-w-[calc(100%_-_100px)]"> |
|
<!-- Todo: enable later when we support extension reordering --> |
|
<!-- eslint-disable vue/no-constant-condition --> |
|
<NcButton size="xs" type="text" class="nc-extension-drag-handler !px-1" @click.stop> |
|
<GeneralIcon icon="ncDrag" class="flex-none text-gray-500" /> |
|
</NcButton> |
|
|
|
<img |
|
v-if="extensionManifest" |
|
:src="getExtensionAssetsUrl(extensionManifest.iconUrl)" |
|
alt="icon" |
|
class="h-8 w-8 object-contain" |
|
/> |
|
<a-input |
|
v-if="titleEditMode && !fullscreen" |
|
ref="titleInput" |
|
v-model:value="tempTitle" |
|
type="text" |
|
class="flex-grow !h-8 !px-1 !py-1 !-ml-1 !rounded-lg w-4/5 extension-title" |
|
@click.stop |
|
@keyup.enter="updateExtensionTitle" |
|
@keyup.esc="updateExtensionTitle" |
|
@blur="updateExtensionTitle" |
|
> |
|
</a-input> |
|
|
|
<NcTooltip v-else show-on-truncate-only class="truncate"> |
|
<template #title> |
|
{{ extension.title }} |
|
</template> |
|
<span class="extension-title cursor-pointer" @dblclick.stop="enableEditMode" @click.stop> |
|
{{ extension.title }} |
|
</span> |
|
</NcTooltip> |
|
</div> |
|
<div class="extension-header-right" @click.stop> |
|
<ExtensionsExtensionMenu |
|
:active-error="activeError" |
|
class="nc-extension-menu" |
|
@rename="enableEditMode" |
|
@duplicate="handleDuplicateExtension(extension.id, true)" |
|
@show-details="showExtensionDetails(extension.extensionId, 'extension')" |
|
@clear-data="extension.clear()" |
|
@delete="extension.delete()" |
|
/> |
|
<NcButton v-if="!activeError" type="text" size="xs" class="nc-extension-expand-btn !px-1" @click="fullscreen = true"> |
|
<GeneralIcon icon="ncMaximize2" class="h-3.5 w-3.5" /> |
|
</NcButton> |
|
<NcButton size="xs" type="text" class="!px-1" @click="collapsed = !collapsed"> |
|
<GeneralIcon :icon="collapsed ? 'arrowDown' : 'arrowUp'" class="flex-none" /> |
|
</NcButton> |
|
</div> |
|
</div> |
|
|
|
<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>{{ activeError }}</template> |
|
<template #extra> |
|
<NcButton size="small" @click="extension.clear()"> |
|
<div class="flex items-center gap-2"> |
|
<GeneralIcon icon="reload" /> |
|
Clear Data |
|
</div> |
|
</NcButton> |
|
<NcButton 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="{ 'extension-modal': fullscreen, 'h-[calc(100%_-_50px)]': !fullscreen }" |
|
@click="closeFullscreen" |
|
> |
|
<div |
|
:class="{ 'extension-modal-content': fullscreen, 'h-full': !fullscreen }" |
|
:style=" |
|
fullscreen |
|
? { |
|
maxWidth: fullscreenModalMaxWidth, |
|
} |
|
: {} |
|
" |
|
> |
|
<div v-if="fullscreen" class="flex items-center justify-between cursor-default"> |
|
<div class="flex-1 max-w-[calc(100%_-_96px)] flex items-center gap-2 text-gray-800 font-semibold"> |
|
<img |
|
v-if="extensionManifest" |
|
:src="getExtensionAssetsUrl(extensionManifest.iconUrl)" |
|
alt="icon" |
|
class="flex-none w-8 h-8" |
|
/> |
|
|
|
<a-input |
|
v-if="titleEditMode" |
|
ref="titleInput" |
|
v-model:value="tempTitle" |
|
type="text" |
|
class="flex-grow !h-8 !px-1 !py-1 !-ml-1 !rounded-lg !text-lg font-semibold extension-title max-w-[420px]" |
|
@click.stop |
|
@keyup.enter.stop="updateExtensionTitle" |
|
@keyup.esc.stop="updateExtensionTitle" |
|
@blur="updateExtensionTitle" |
|
> |
|
</a-input> |
|
<NcTooltip v-else show-on-truncate-only class="extension-title truncate text-lg"> |
|
<template #title> |
|
{{ extension.title }} |
|
</template> |
|
<span class="cursor-pointer" @dblclick="enableEditMode"> |
|
{{ extension.title }} |
|
</span> |
|
</NcTooltip> |
|
</div> |
|
<div class="flex items-center gap-4"> |
|
<ExtensionsExtensionMenu |
|
:active-error="activeError" |
|
:fullscreen="fullscreen" |
|
@rename="enableEditMode" |
|
@duplicate="handleDuplicateExtension(extension.id, true)" |
|
@show-details="showExtensionDetails(extension.extensionId, 'extension')" |
|
@clear-data="extension.clear()" |
|
@delete="extension.delete()" |
|
/> |
|
<NcButton size="small" type="text" class="flex-none" @click="fullscreen = false"> |
|
<GeneralIcon icon="close" /> |
|
</NcButton> |
|
</div> |
|
</div> |
|
<div |
|
v-show="fullscreen || !collapsed" |
|
class="extension-content" |
|
:class="{ 'fullscreen h-[calc(100%-40px)]': fullscreen, 'h-full': !fullscreen }" |
|
> |
|
<component :is="component" :key="extension.uiKey" class="h-full" /> |
|
</div> |
|
</div> |
|
</div> |
|
</Teleport> |
|
</template> |
|
</div> |
|
</div> |
|
</template> |
|
|
|
<style scoped lang="scss"> |
|
.extension-wrapper { |
|
@apply bg-white rounded-xl w-full border-1 relative; |
|
|
|
&.isOpen { |
|
resize: vertical; |
|
|
|
&:hover, |
|
&.mousedown { |
|
overflow-y: auto; |
|
} |
|
} |
|
} |
|
|
|
.extension-header { |
|
@apply flex justify-between; |
|
|
|
&.collapsed:not(:hover) { |
|
.nc-extension-expand-btn, |
|
.nc-extension-menu { |
|
@apply hidden; |
|
} |
|
} |
|
|
|
.extension-header-left { |
|
@apply flex-1 flex items-center gap-2; |
|
} |
|
|
|
.extension-header-right { |
|
@apply flex items-center gap-1; |
|
} |
|
|
|
.extension-title { |
|
@apply font-weight-600; |
|
} |
|
} |
|
|
|
.extension-content { |
|
@apply rounded-lg; |
|
|
|
&:not(.fullscreen) { |
|
@apply p-3; |
|
} |
|
} |
|
|
|
.extension-modal { |
|
@apply absolute top-0 left-0 z-1000 w-full h-full bg-black bg-opacity-50; |
|
|
|
.extension-modal-content { |
|
@apply bg-white rounded-2xl w-[90%] h-[90vh] mt-[5vh] mx-auto p-6 flex flex-col gap-3; |
|
} |
|
} |
|
|
|
: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-gray-800 font-semibold; |
|
} |
|
|
|
.ant-result-extra { |
|
@apply mt-3; |
|
} |
|
} |
|
</style>
|
|
|