Browse Source

feat(nc-gui): customizable extension header

pull/9351/head
Ramesh Mane 3 months ago
parent
commit
d09e2785eb
  1. 50
      packages/nc-gui/components/extensions/Extension.vue
  2. 113
      packages/nc-gui/components/extensions/Extension/Header.vue
  3. 12
      packages/nc-gui/components/extensions/Extension/HeaderMenu.vue
  4. 30
      packages/nc-gui/components/extensions/Extension/Wrapper.vue
  5. 320
      packages/nc-gui/extensions/data-exporter/index.vue
  6. 12
      packages/nc-gui/extensions/json-exporter/index.vue

50
packages/nc-gui/components/extensions/Extension.vue

@ -222,56 +222,10 @@ eventBus.on((event, payload) => {
: {}
"
>
<div
v-if="fullscreen"
class="flex items-center gap-3 cursor-default px-4 pt-4 pb-[15px] border-b-1 border-nc-gray-medium"
>
<img
v-if="extensionManifest"
:src="getExtensionAssetsUrl(extensionManifest.iconUrl)"
alt="icon"
class="flex-none w-8 h-8"
/>
<div v-if="titleEditMode" class="flex-1 flex">
<a-input
ref="titleInput"
v-model:value="tempTitle"
type="text"
class="flex-1 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>
</div>
<NcTooltip v-else show-on-truncate-only class="extension-title truncate text-lg flex-1">
<template #title>
{{ extension.title }}
</template>
<span class="cursor-pointer text-gray-800 font-semibold" @dblclick="enableEditMode">
{{ extension.title }}
</span>
</NcTooltip>
<ExtensionsExtensionHeaderMenu
: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="xs" type="text" class="flex-none !px-1" @click="fullscreen = false">
<GeneralIcon icon="close" />
</NcButton>
</div>
<div
v-show="fullscreen || !collapsed"
class="extension-content"
:class="{ 'fullscreen h-[calc(100%-40px)] p-6': fullscreen, 'h-full': !fullscreen }"
class="extension-content h-full"
:class="{ 'fullscreen': fullscreen, 'h-full': !fullscreen }"
>
<component :is="component" :key="extension.uiKey" class="h-full" />
</div>

113
packages/nc-gui/components/extensions/Extension/Header.vue

@ -1,4 +1,11 @@
<script lang="ts" setup>
/**
* ExtensionHeader component.
*
* @slot prefix - Slot for custom content to be displayed at the start of the header when in fullscreen mode.
* @slot extra - Slot for additional custom content to be displayed before the options and close button in fullscreen mode.
*/
interface Props {
isFullscreen?: boolean
}
@ -36,6 +43,12 @@ const expandExtension = () => {
collapsed.value = false
}
/**
* Handles the duplication of an extension.
*
* @param id - The ID of the extension to duplicate.
* @param open - Optional. If true, the duplicated extension will be opened.
*/
const handleDuplicateExtension = async (id: string, open: boolean = false) => {
const duplicatedExt = await duplicateExtension(id)
@ -49,72 +62,88 @@ const handleDuplicateExtension = async (id: string, open: boolean = false) => {
<template>
<div
class="extension-header px-3 py-2"
v-if="(isFullscreen && fullscreen) || !isFullscreen"
class="extension-header flex items-center"
:class="{
'border-b-1 border-gray-200 h-[49px]': !collapsed,
'collapsed border-transparent h-[48px]': collapsed,
'border-b-1 border-nc-gray-medium h-[49px]': !collapsed && !isFullscreen,
'collapsed border-transparent h-[48px]': collapsed && !isFullscreen,
'px-3 py-2 gap-1': !isFullscreen,
'gap-3 px-4 pt-4 pb-[15px] border-b-1 border-nc-gray-medium': isFullscreen,
}"
@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"
/>
<slot v-if="isFullscreen" name="prefix"></slot>
<NcButton v-if="!isFullscreen" 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 flex-none"
/>
<div v-if="titleEditMode" class="flex-1">
<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"
class="flex-1 !h-8 !px-1 !py-1 !-ml-1 !rounded-lg extension-title"
:class="{
'w-4/5': !isFullscreen,
'!text-lg !font-semibold max-w-[420px]': isFullscreen,
}"
@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>
<ExtensionsExtensionHeaderMenu
: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">
<NcTooltip v-else show-on-truncate-only class="truncate flex-1">
<template #title>
{{ extension.title }}
</template>
<span
class="extension-title cursor-pointer"
:class="{
'text-lg font-semibold ': isFullscreen,
}"
@dblclick.stop="enableEditMode"
@click.stop
>
{{ extension.title }}
</span>
</NcTooltip>
<slot v-if="isFullscreen" name="extra"></slot>
<ExtensionsExtensionHeaderMenu
:is-fullscreen="isFullscreen"
class="nc-extension-menu"
@rename="enableEditMode"
@duplicate="handleDuplicateExtension(extension.id, true)"
@show-details="showExtensionDetails(extension.extensionId, 'extension')"
@clear-data="extension.clear()"
@delete="extension.delete()"
/>
<template v-if="!isFullscreen">
<NcButton v-if="!activeError" size="xs" type="text" class="nc-extension-expand-btn !px-1" @click.stop="fullscreen = true">
<GeneralIcon icon="ncMaximize2" class="h-3.5 w-3.5" />
</NcButton>
<NcButton size="xs" type="text" class="!px-1" @click="collapsed = !collapsed">
<NcButton size="xs" type="text" class="!px-1" @click.stop="collapsed = !collapsed">
<GeneralIcon :icon="collapsed ? 'arrowDown' : 'arrowUp'" class="flex-none" />
</NcButton>
</div>
</template>
<template v-else>
<NcButton :size="isFullscreen ? 'small' : 'xs'" type="text" class="flex-none !px-1" @click="fullscreen = false">
<GeneralIcon icon="close" />
</NcButton>
</template>
</div>
</template>
<style lang="scss" scoped>
.extension-header {
@apply flex justify-between;
&.collapsed:not(:hover) {
.nc-extension-expand-btn,
.nc-extension-menu {

12
packages/nc-gui/components/extensions/Extension/HeaderMenu.vue

@ -1,17 +1,19 @@
<script setup lang="ts">
interface Props {
fullscreen?: boolean
activeError?: boolean
isFullscreen?: boolean
}
const { fullscreen, activeError } = defineProps<Props>()
defineProps<Props>()
const { activeError } = useExtensionHelperOrThrow()
const emits = defineEmits(['rename', 'duplicate', 'showDetails', 'clearData', 'delete'])
</script>
<template>
<div class="flex items-center">
<div class="flex items-center" @click.stop>
<NcDropdown :trigger="['click']" placement="bottomRight">
<NcButton type="text" size="xs" class="!px-1">
<NcButton type="text" :size="isFullscreen ? 'small' : 'xs'" class="!px-1">
<GeneralIcon icon="threeDotVertical" />
</NcButton>

30
packages/nc-gui/components/extensions/Extension/Wrapper.vue

@ -0,0 +1,30 @@
<script lang="ts" setup>
const { fullscreen } = useExtensionHelperOrThrow()
const headerRef = ref<HTMLDivElement>()
const { height } = useElementSize(headerRef)
</script>
<template>
<div>
<div ref="headerRef">
<ExtensionsExtensionHeader>
<template #extra>
<slot name="headerExtra"></slot>
</template>
</ExtensionsExtensionHeader>
</div>
<div
:class="{
'p-6 max': fullscreen,
'h-full': !fullscreen,
}"
:style="fullscreen ? { height: height ? `calc(100% - ${height}px)` : 'calc(100% - 64px)' } : {}"
>
<slot />
</div>
</div>
</template>
<style lang="scss" scoped></style>

320
packages/nc-gui/extensions/data-exporter/index.vue

@ -203,191 +203,193 @@ onMounted(() => {
</script>
<template>
<div ref="dataExporterRef" class="data-exporter">
<div class="pb-3 flex items-center justify-between gap-2.5 flex-wrap">
<div
class="flex-1 flex items-center"
:class="{
'max-w-[min(350px,calc(100%-124px))]': isExporting && !fullscreen && width > 325,
'max-w-[min(350px,calc(100%_-_84px))]': !isExporting && !fullscreen && width > 325,
'max-w-full': width <= 325,
'max-w-[900px]': fullscreen,
}"
>
<NcSelect
v-model:value="exportPayload.tableId"
placeholder="-select table-"
:disabled="isExporting"
class="nc-data-exporter-table-select"
<ExtensionsExtensionWrapper>
<div ref="dataExporterRef" class="data-exporter">
<div class="pb-3 flex items-center justify-between gap-2.5 flex-wrap">
<div
class="flex-1 flex items-center"
:class="{
'flex-1 max-w-[240px]': fullscreen,
'min-w-1/2 max-w-[175px]': !fullscreen,
'max-w-[min(350px,calc(100%-124px))]': isExporting && !fullscreen && width > 325,
'max-w-[min(350px,calc(100%_-_84px))]': !isExporting && !fullscreen && width > 325,
'max-w-full': width <= 325,
'max-w-[900px]': fullscreen,
}"
:filter-option="filterOption"
dropdown-class-name="w-[250px]"
show-search
@change="onTableSelect"
>
<a-select-option v-for="table of tableList" :key="table.label" :value="table.value">
<div class="w-full flex items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="{ meta: table.meta }" class="text-gray-500" />
</div>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>{{ table.label }}</template>
<span>{{ table.label }}</span>
</NcTooltip>
<component
:is="iconMap.check"
v-if="exportPayload.tableId === table.value"
id="nc-selected-item-icon"
class="flex-none text-primary w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
<NcSelect
v-model:value="exportPayload.viewId"
placeholder="-select view-"
:disabled="isExporting"
class="nc-data-exporter-view-select"
:class="{
'flex-1 max-w-[240px]': fullscreen,
'min-w-1/2 max-w-[175px]': !fullscreen,
}"
dropdown-class-name="w-[250px]"
:filter-option="filterOption"
show-search
@change="onViewSelect"
>
<a-select-option v-for="view of viewList" :key="view.label" :value="view.value">
<div class="w-full flex items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralViewIcon :meta="{ meta: view.meta, type: view.type }" class="flex-none text-gray-500" />
<NcSelect
v-model:value="exportPayload.tableId"
placeholder="-select table-"
:disabled="isExporting"
class="nc-data-exporter-table-select"
:class="{
'flex-1 max-w-[240px]': fullscreen,
'min-w-1/2 max-w-[175px]': !fullscreen,
}"
:filter-option="filterOption"
dropdown-class-name="w-[250px]"
show-search
@change="onTableSelect"
>
<a-select-option v-for="table of tableList" :key="table.label" :value="table.value">
<div class="w-full flex items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="{ meta: table.meta }" class="text-gray-500" />
</div>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>{{ table.label }}</template>
<span>{{ table.label }}</span>
</NcTooltip>
<component
:is="iconMap.check"
v-if="exportPayload.tableId === table.value"
id="nc-selected-item-icon"
class="flex-none text-primary w-4 h-4"
/>
</div>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>{{ view.label }}</template>
<span>{{ view.label }}</span>
</NcTooltip>
<component
:is="iconMap.check"
v-if="exportPayload.viewId === view.value"
id="nc-selected-item-icon"
class="flex-none text-primary w-4 h-4"
/>
</div> </a-select-option
></NcSelect>
</div>
<div class="flex-none flex justify-end">
<NcTooltip class="flex" placement="topRight" :disabled="!isExporting">
<template #title> The CSV file is being prepared in the background. You'll be notified once it's ready. </template>
<NcButton :disabled="!exportPayload?.viewId" :loading="isExporting" size="xs" @click="exportDataAsync">{{
isExporting ? 'Generating' : 'Export'
}}</NcButton>
</NcTooltip>
</div>
</div>
<div class="data-exporter-body flex-1 flex flex-col">
<div class="data-exporter-header">Recent Exports</div>
<div v-if="exportedFiles.length" class="flex-1 flex flex-col nc-scrollbar-thin max-h-[calc(100%_-_25px)]">
<template v-for="exp of exportedFiles">
<div
v-if="exp.status === JobStatus.COMPLETED ? exp.result : true"
:key="exp.id"
class="px-3 py-2 flex gap-2 justify-between border-b-1 hover:bg-gray-50"
</a-select-option>
</NcSelect>
<NcSelect
v-model:value="exportPayload.viewId"
placeholder="-select view-"
:disabled="isExporting"
class="nc-data-exporter-view-select"
:class="{
'px-4 py-3': fullscreen,
'px-3 py-2': !fullscreen,
'flex-1 max-w-[240px]': fullscreen,
'min-w-1/2 max-w-[175px]': !fullscreen,
}"
dropdown-class-name="w-[250px]"
:filter-option="filterOption"
show-search
@change="onViewSelect"
>
<a-select-option v-for="view of viewList" :key="view.label" :value="view.value">
<div class="w-full flex items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralViewIcon :meta="{ meta: view.meta, type: view.type }" class="flex-none text-gray-500" />
</div>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>{{ view.label }}</template>
<span>{{ view.label }}</span>
</NcTooltip>
<component
:is="iconMap.check"
v-if="exportPayload.viewId === view.value"
id="nc-selected-item-icon"
class="flex-none text-primary w-4 h-4"
/>
</div> </a-select-option
></NcSelect>
</div>
<div class="flex-none flex justify-end">
<NcTooltip class="flex" placement="topRight" :disabled="!isExporting">
<template #title> The CSV file is being prepared in the background. You'll be notified once it's ready. </template>
<NcButton :disabled="!exportPayload?.viewId" :loading="isExporting" size="xs" @click="exportDataAsync">{{
isExporting ? 'Generating' : 'Export'
}}</NcButton>
</NcTooltip>
</div>
</div>
<div class="data-exporter-body flex-1 flex flex-col">
<div class="data-exporter-header">Recent Exports</div>
<div v-if="exportedFiles.length" class="flex-1 flex flex-col nc-scrollbar-thin max-h-[calc(100%_-_25px)]">
<template v-for="exp of exportedFiles">
<div
class="flex-1 flex items-center gap-3"
v-if="exp.status === JobStatus.COMPLETED ? exp.result : true"
:key="exp.id"
class="px-3 py-2 flex gap-2 justify-between border-b-1 hover:bg-gray-50"
:class="{
'max-w-[calc(100%_-_74px)]': exp.status === JobStatus.COMPLETED,
'max-w-[calc(100%_-_38px)]': exp.status !== JobStatus.COMPLETED,
'px-4 py-3': fullscreen,
'px-3 py-2': !fullscreen,
}"
>
<NcTooltip v-if="[JobStatus.COMPLETED, JobStatus.FAILED].includes(exp.status)" class="flex">
<template #title>
{{ jobStatusTooltip[exp.status] }}
</template>
<GeneralIcon
:icon="exp.status === JobStatus.COMPLETED ? 'circleCheckSolid' : 'alertTriangleSolid'"
class="flex-none h-4 w-4"
:class="{
'!text-green-700': exp.status === JobStatus.COMPLETED,
'!text-red-700': exp.status === JobStatus.FAILED,
}"
/>
</NcTooltip>
<div v-else class="h-5 flex items-center">
<GeneralLoader size="regular" class="flex-none" />
</div>
<div
class="flex-1 flex items-center gap-3"
:class="{
'max-w-[calc(100%_-_74px)]': exp.status === JobStatus.COMPLETED,
'max-w-[calc(100%_-_38px)]': exp.status !== JobStatus.COMPLETED,
}"
>
<NcTooltip v-if="[JobStatus.COMPLETED, JobStatus.FAILED].includes(exp.status)" class="flex">
<template #title>
{{ jobStatusTooltip[exp.status] }}
</template>
<GeneralIcon
:icon="exp.status === JobStatus.COMPLETED ? 'circleCheckSolid' : 'alertTriangleSolid'"
class="flex-none h-4 w-4"
:class="{
'!text-green-700': exp.status === JobStatus.COMPLETED,
'!text-red-700': exp.status === JobStatus.FAILED,
}"
/>
</NcTooltip>
<div v-else class="h-5 flex items-center">
<GeneralLoader size="regular" class="flex-none" />
</div>
<div class="flex-1 max-w-[calc(100%_-_28px)] flex flex-col gap-1">
<div class="inline-flex gap-1 text-sm text-gray-800 -ml-[1px]">
<span class="inline-flex items-center h-5">
<GeneralIcon icon="file" class="flex-none text-gray-600/80 h-3.5 w-3.5" />
</span>
<NcTooltip class="truncate max-w-[calc(100%_-_20px)]" show-on-truncate-only>
<template #title>
<div class="flex-1 max-w-[calc(100%_-_28px)] flex flex-col gap-1">
<div class="inline-flex gap-1 text-sm text-gray-800 -ml-[1px]">
<span class="inline-flex items-center h-5">
<GeneralIcon icon="file" class="flex-none text-gray-600/80 h-3.5 w-3.5" />
</span>
<NcTooltip class="truncate max-w-[calc(100%_-_20px)]" show-on-truncate-only>
<template #title>
{{ exp.result.title || titleHelper() }}
</template>
{{ exp.result.title || titleHelper() }}
</template>
{{ exp.result.title || titleHelper() }}
</NcTooltip>
</div>
<div v-if="exp.result.timestamp" class="text-[10px] leading-4 text-gray-600">
<NcTooltip class="truncate" show-on-truncate-only>
<template #title>
</NcTooltip>
</div>
<div v-if="exp.result.timestamp" class="text-[10px] leading-4 text-gray-600">
<NcTooltip class="truncate" show-on-truncate-only>
<template #title>
{{ dayjs(exp.result.timestamp).format('MM/DD/YYYY [at] hh:mm A') }}
</template>
{{ dayjs(exp.result.timestamp).format('MM/DD/YYYY [at] hh:mm A') }}
</template>
{{ dayjs(exp.result.timestamp).format('MM/DD/YYYY [at] hh:mm A') }}
</NcTooltip>
</NcTooltip>
</div>
</div>
</div>
</div>
<div
v-if="exp.status === JobStatus.COMPLETED"
class="flex items-center"
@click="handleDownload(urlHelper(exp.result.url))"
>
<NcTooltip class="flex items-center">
<template #title>
{{ $t('general.download') }}
</template>
<NcButton type="secondary" size="xs" class="!px-[5px]">
<div class="flex items-center gap-2">
<GeneralIcon icon="download" />
</div>
</NcButton>
</NcTooltip>
</div>
<div
v-if="exp.status === JobStatus.COMPLETED"
class="flex items-center"
@click="handleDownload(urlHelper(exp.result.url))"
>
<NcTooltip class="flex items-center">
<template #title>
{{ $t('general.download') }}
</template>
<NcButton type="secondary" size="xs" class="!px-[5px]">
<div class="flex items-center gap-2">
<GeneralIcon icon="download" />
</div>
</NcButton>
</NcTooltip>
</div>
<div class="flex items-center">
<NcTooltip class="flex">
<template #title>
{{ $t('general.remove') }}
</template>
<div class="flex items-center">
<NcTooltip class="flex">
<template #title>
{{ $t('general.remove') }}
</template>
<NcButton type="text" size="xs" class="!px-[5px]" @click="onRemoveExportedFile(exp.id)">
<GeneralIcon icon="close" />
</NcButton>
</NcTooltip>
<NcButton type="text" size="xs" class="!px-[5px]" @click="onRemoveExportedFile(exp.id)">
<GeneralIcon icon="close" />
</NcButton>
</NcTooltip>
</div>
</div>
</div>
</template>
</template>
</div>
<div v-else class="px-3 py-2 flex-1 flex items-center justify-center text-gray-600">No exports</div>
</div>
<div v-else class="px-3 py-2 flex-1 flex items-center justify-center text-gray-600">No exports</div>
</div>
</div>
</ExtensionsExtensionWrapper>
</template>
<style lang="scss" scoped>
.data-exporter {
@apply flex flex-col overflow-hidden h-full;
@apply flex flex-col overflow-hidden h-full;
.data-exporter-header {
@apply px-3 py-1 bg-gray-100 text-[11px] leading-4 text-gray-600 border-b-1;
}

12
packages/nc-gui/extensions/json-exporter/index.vue

@ -92,11 +92,13 @@ onMounted(() => {
</script>
<template>
<div class="flex flex-col gap-2 p-2 border-1 rounded-lg">
<NcSelect v-model:value="exportPayload.tableId" :options="tableList" placeholder="-select table-" @change="onTableSelect" />
<NcSelect v-model:value="exportPayload.viewId" :options="viewList" placeholder="-select view-" @change="onViewSelect" />
<NcButton @click="exportJson">Export</NcButton>
</div>
<ExtensionsExtensionWrapper>
<div class="flex flex-col gap-2 p-2 border-1 rounded-lg">
<NcSelect v-model:value="exportPayload.tableId" :options="tableList" placeholder="-select table-" @change="onTableSelect" />
<NcSelect v-model:value="exportPayload.viewId" :options="viewList" placeholder="-select view-" @change="onViewSelect" />
<NcButton @click="exportJson">Export</NcButton>
</div>
</ExtensionsExtensionWrapper>
</template>
<style lang="scss"></style>

Loading…
Cancel
Save