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.
322 lines
9.8 KiB
322 lines
9.8 KiB
<script setup lang="ts"> |
|
interface Prop { |
|
extensionId: string |
|
error?: any |
|
} |
|
|
|
const { extensionId, error } = defineProps<Prop>() |
|
|
|
const { |
|
extensionList, |
|
extensionsLoaded, |
|
availableExtensions, |
|
eventBus, |
|
getExtensionIcon, |
|
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<any>(null) |
|
|
|
const extensionMinHeight = computed(() => { |
|
switch (extension.value.extensionId) { |
|
case 'nc-data-exporter': |
|
return 'min-h-[300px] h-[300px]' |
|
case 'nc-json-exporter': |
|
return 'min-h-[194px] h-[194px]' |
|
case 'nc-csv-import': |
|
return 'min-h-[180px] h-[180px]' |
|
} |
|
}) |
|
|
|
onMounted(() => { |
|
until(extensionsLoaded) |
|
.toMatch((v) => v) |
|
.then(() => { |
|
extensionManifest.value = availableExtensions.value.find((ext) => ext.id === extension.value.extensionId) |
|
|
|
if (!extensionManifest) { |
|
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) => { |
|
if (e.key === 'Escape') { |
|
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="[ |
|
`${!collapsed ? extensionMinHeight : ''}`, |
|
{ |
|
'!h-auto': collapsed, |
|
'isOpen': !collapsed, |
|
'mousedown': isMouseDown, |
|
}, |
|
]" |
|
@mousedown="isMouseDown = true" |
|
@mouseup="isMouseDown = false" |
|
> |
|
<div class="extension-header" :class="{ 'mb-2': !collapsed }"> |
|
<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 v-if="false" size="xxsmall" type="text"> |
|
<GeneralIcon icon="ncDrag" class="flex-none text-gray-500" /> |
|
</NcButton> |
|
|
|
<img |
|
v-if="extensionManifest" |
|
:src="getExtensionIcon(extensionManifest.iconUrl)" |
|
alt="icon" |
|
class="h-6 w-6 object-contain" |
|
/> |
|
<input |
|
v-if="titleEditMode && !fullscreen" |
|
ref="titleInput" |
|
v-model="tempTitle" |
|
class="flex-grow leading-1 outline-0 ring-none !text-inherit !bg-transparent w-4/5 extension-title" |
|
@click.stop |
|
@keyup.enter="updateExtensionTitle" |
|
@keyup.esc="updateExtensionTitle" |
|
@blur="updateExtensionTitle" |
|
/> |
|
<NcTooltip v-else show-on-truncate-only class="truncate"> |
|
<template #title> |
|
{{ extension.title }} |
|
</template> |
|
<span class="extension-title" @dblclick="enableEditMode"> |
|
{{ extension.title }} |
|
</span> |
|
</NcTooltip> |
|
</div> |
|
<div class="extension-header-right"> |
|
<NcButton v-if="!activeError" type="text" size="xxsmall" @click="fullscreen = true"> |
|
<GeneralIcon icon="expand" /> |
|
</NcButton> |
|
<ExtensionsExtensionMenu |
|
:active-error="activeError" |
|
@rename="enableEditMode" |
|
@duplicate="handleDuplicateExtension(extension.id, true)" |
|
@show-details="showExtensionDetails(extension.extensionId, 'extension')" |
|
@clear-data="extension.clear()" |
|
@delete="extension.delete()" |
|
/> |
|
<NcButton size="xxsmall" type="text" @click="collapsed = !collapsed"> |
|
<GeneralIcon :icon="collapsed ? 'arrowUp' : 'arrowDown'" class="flex-none" /> |
|
</NcButton> |
|
</div> |
|
</div> |
|
<template v-if="activeError"> |
|
<div v-show="!collapsed" class="extension-content"> |
|
<a-result status="error" title="Extension Error"> |
|
<template #subTitle>{{ activeError }}</template> |
|
<template #extra> |
|
<NcButton @click="extension.clear()"> |
|
<div class="flex items-center gap-2"> |
|
<GeneralIcon icon="reload" /> |
|
Clear Data |
|
</div> |
|
</NcButton> |
|
<NcButton 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%_-_32px)]': !fullscreen }" |
|
@click="closeFullscreen" |
|
> |
|
<div :class="{ 'extension-modal-content': fullscreen, 'h-full': !fullscreen }"> |
|
<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-weight-600"> |
|
<img |
|
v-if="extensionManifest" |
|
:src="getExtensionIcon(extensionManifest.iconUrl)" |
|
alt="icon" |
|
class="flex-none w-6 h-6" |
|
/> |
|
<input |
|
v-if="titleEditMode" |
|
ref="titleInput" |
|
v-model="tempTitle" |
|
class="flex-grow leading-1 outline-0 ring-none !text-xl !bg-transparent !font-weight-600" |
|
@click.stop |
|
@keyup.enter="updateExtensionTitle" |
|
@keyup.esc="updateExtensionTitle" |
|
@blur="updateExtensionTitle" |
|
/> |
|
<NcTooltip v-else show-on-truncate-only class="extension-title truncate text-xl"> |
|
<template #title> |
|
{{ extension.title }} |
|
</template> |
|
<span @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="{ '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 px-3 py-[11px] w-full border-1 relative; |
|
|
|
&.isOpen { |
|
resize: vertical; |
|
|
|
&:hover, |
|
&.mousedown { |
|
overflow-y: auto; |
|
} |
|
} |
|
} |
|
|
|
.extension-header { |
|
@apply flex justify-between; |
|
|
|
.extension-header-left { |
|
@apply flex-1 flex items-center gap-2; |
|
} |
|
|
|
.extension-header-right { |
|
@apply flex items-center gap-2; |
|
} |
|
|
|
.extension-title { |
|
@apply font-weight-600; |
|
} |
|
} |
|
|
|
.extension-content { |
|
@apply rounded-lg; |
|
} |
|
|
|
.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%] max-w-[1154px] h-[90vh] mt-[5vh] mx-auto p-6 flex flex-col gap-3; |
|
} |
|
} |
|
</style>
|
|
|