Browse Source

Merge pull request #3236 from nocodb/refactor/gui-v2-new-layout

refactor(gui-v2): new layout
pull/3242/head
navi 2 years ago committed by GitHub
parent
commit
36c0d9819c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 34
      packages/nc-gui-v2/assets/style-v2.scss
  2. 4
      packages/nc-gui-v2/components.d.ts
  3. 154
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  4. 88
      packages/nc-gui-v2/components/general/Language.vue
  5. 4
      packages/nc-gui-v2/components/general/ShareBaseButton.vue
  6. 52
      packages/nc-gui-v2/components/general/language/Menu.vue
  7. 33
      packages/nc-gui-v2/components/general/language/index.vue
  8. 2
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  9. 32
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  10. 18
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue
  11. 7
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue
  12. 6
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  13. 2
      packages/nc-gui-v2/layouts/base.vue
  14. 136
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  15. 192
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index.vue

34
packages/nc-gui-v2/assets/style-v2.scss

@ -3,6 +3,7 @@
:root {
--header-height: 50px;
--toolbar-height: 48px;
}
.ant-layout-header {
@ -51,7 +52,7 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
.nc-menu-item {
@apply cursor-pointer text-xs flex items-center gap-2 px-4 py-3 relative after:(content-[''] absolute top-0 left-0 bottom-0 w-full h-full right-0 bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
&.disabled {
&.disabled {
@apply text-black text-opacity-25 bg-[#f5f5f5] cursor-not-allowed text-shadow-none box-shadow-none border-[#d9d9d9];
}
}
@ -85,7 +86,7 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
svg {
@apply z-1 text-xl p-1 text-gray-500;
}
.disabled {
@apply cursor-not-allowed border-none;
}
@ -175,6 +176,7 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
}
.slide-enter,
.slide-leave-active {
transform: translate(-100%, 0);
}
@ -209,4 +211,30 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
&:active::after {
@apply ring ring-pink-500;
}
}
}
.ant-tabs-dropdown-menu-title-content{
@apply flex items-center;
}
.ant-dropdown-menu-item-group-list {
@apply !mx-0;
}
.ant-dropdown-menu-item-group-title {
@apply border-b-1;
}
.ant-dropdown-menu-item-group-list {
@apply m-0;
}
.ant-dropdown-menu-item {
@apply !py-0 active:(ring ring-pink-500);
}
.ant-dropdown-menu-title-conten{
@apply !py-0;
}

4
packages/nc-gui-v2/components.d.ts vendored

@ -87,6 +87,7 @@ declare module '@vue/runtime-core' {
MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default']
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MdiAccount: typeof import('~icons/mdi/account')['default']
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default']
MdiAccountGroup: typeof import('~icons/mdi/account-group')['default']
MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default']
@ -97,6 +98,7 @@ declare module '@vue/runtime-core' {
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default']
MdiAt: typeof import('~icons/mdi/at')['default']
MdiBackburger: typeof import('~icons/mdi/backburger')['default']
MdiBugOutline: typeof import('~icons/mdi/bug-outline')['default']
MdiCalculator: typeof import('~icons/mdi/calculator')['default']
MdiCalendarMonth: typeof import('~icons/mdi/calendar-month')['default']
@ -104,6 +106,7 @@ declare module '@vue/runtime-core' {
MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default']
MdiChat: typeof import('~icons/mdi/chat')['default']
MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDoubleLeft: typeof import('~icons/mdi/chevron-double-left')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiClose: typeof import('~icons/mdi/close')['default']
MdiCloseBox: typeof import('~icons/mdi/close-box')['default']
@ -158,6 +161,7 @@ declare module '@vue/runtime-core' {
MdiLogin: typeof import('~icons/mdi/login')['default']
MdiLogout: typeof import('~icons/mdi/logout')['default']
MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMenu: typeof import('~icons/mdi/menu')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default']

154
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -4,12 +4,14 @@ import Sortable from 'sortablejs'
import { Empty } from 'ant-design-vue'
import { useNuxtApp } from '#app'
import { computed, useProject, useTable, useTabs, useUIPermission, watchEffect } from '#imports'
import DlgAirtableImport from '~/components/dlg/AirtableImport.vue'
import DlgQuickImport from '~/components/dlg/QuickImport.vue'
import DlgTableCreate from '~/components/dlg/TableCreate.vue'
import { TabType } from '~/composables'
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large'
import MdiMenuIcon from '~icons/mdi/dots-vertical'
import MdiDrag from '~icons/mdi/drag-vertical'
import GithubStarButton from '~/components/dashboard/GithubStarButton.vue'
const { addTab } = useTabs()
@ -32,8 +34,6 @@ const tablesById = $computed<Record<string, TableType>>(() =>
}, {}),
)
const tableCreateDlg = ref(false)
let key = $ref(0)
const menuRef = $ref<HTMLLIElement>()
@ -135,21 +135,62 @@ const addTableTab = (table: TableType) => {
const activeTable = computed(() => {
return [TabType.TABLE, TabType.VIEW].includes(activeTab.value?.type) ? activeTab.value.title : null
})
function openQuickImportDialog(type: string) {
$e(`a:actions:import-${type}`)
const isOpen = ref(true)
const { close } = useDialog(DlgQuickImport, {
'modelValue': isOpen,
'importType': type,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
function openAirtableImportDialog() {
$e('a:actions:import-airtable')
const isOpen = ref(true)
const { close } = useDialog(DlgAirtableImport, {
'modelValue': isOpen,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
function openTableCreateDialog() {
$e('a:actions:create-table')
const isOpen = ref(true)
const { close } = useDialog(DlgTableCreate, {
'modelValue': isOpen,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
</script>
<template>
<div class="nc-treeview-container flex flex-col">
<div class="px-6 py-[8.5px] border-b-1 nc-filter-input">
<div class="flex items-center bg-gray-50 rounded relative">
<a-input
v-model:value="filterQuery"
class="nc-filter-input !bg-transparent"
:placeholder="$t('placeholder.searchProjectTree')"
/>
<MdiSearch class="nc-filter-input-icon text-gray-400 mx-3 absolute right-[-4px] top-[7px]" />
</div>
</div>
<a-dropdown :trigger="['contextmenu']">
<div
class="pt-2 pl-2 pb-2 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull"
@ -168,6 +209,77 @@ const activeTable = computed(() => {
</span>
</div>
<div style="direction: ltr" class="flex-1">
<div
class="group flex items-center gap-2 pl-5 pr-3 py-2 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
@click="openTableCreateDialog"
>
<MdiPlus />
<span class="text-gray-500 group-hover:(text-primary/100) flex-1">{{ $t('tooltip.addTable') }}</span>
<a-dropdown :trigger="['click']" @click.stop>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay>
<a-menu class="nc-add-project-menu !py-0 ml-6 rounded text-sm">
<a-menu-item-group title="QUICK IMPORT FROM" class="!px-0 !mx-0">
<a-menu-item
v-if="isUIAllowed('airtableImport')"
key="quick-import-airtable"
@click="openAirtableImportDialog"
>
<div class="color-transition nc-project-menu-item group">
<MdiTableLarge class="group-hover:text-pink-500" />
<!-- TODO: i18n -->
Airtable
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('csvImport')" key="quick-import-csv" @click="openQuickImportDialog('csv')">
<div class="color-transition nc-project-menu-item group">
<MdiFileDocumentOutline class="group-hover:text-pink-500" />
<!-- TODO: i18n -->
CSV file
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('jsonImport')" key="quick-import-json" @click="openQuickImportDialog('json')">
<div class="color-transition nc-project-menu-item group">
<MdiCodeJson class="group-hover:text-pink-500" />
<!-- TODO: i18n -->
JSON file
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('excelImport')"
key="quick-import-excel"
@click="openQuickImportDialog('excel')"
>
<div class="color-transition nc-project-menu-item group">
<MdiFileExcel class="group-hover:text-pink-500" />
<!-- TODO: i18n -->
Microsoft Excel
</div>
</a-menu-item>
</a-menu-item-group>
<a-menu-divider class="my-0" />
<a-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b">
<a
v-t="['e:datasource:import-request']"
href="https://github.com/nocodb/nocodb/issues/2052"
target="_blank"
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)"
>
<MdiOpenInNew class="group-hover:text-pink-500" />
<!-- TODO: i18n -->
Request a data source you need?
</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div v-if="tables.length" class="transition-height duration-200 overflow-hidden">
<div :key="key" ref="menuRef" class="border-none sortable-list">
<div
@ -238,7 +350,7 @@ const activeTable = computed(() => {
<div class="flex flex-col align-center">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<a-button type="primary" @click.stop="tableCreateDlg = true">{{ $t('tooltip.addTable') }}</a-button>
<a-button type="primary" @click.stop="openTableCreateDialog">{{ $t('tooltip.addTable') }}</a-button>
</div>
</a-card>
</div>
@ -273,13 +385,17 @@ const activeTable = computed(() => {
</template>
</a-dropdown>
<a-divider class="mt-0 mb-2" />
<a-divider class="mt-0 mb-0" />
<div class="items-center flex justify-center p-2">
<!--
Todo : move the component
<GithubStarButton />
-->
<div class="items-center flex justify-center mb-1">
<GithubStarButton />
<GeneralShareBaseButton class="!mr-0" />
</div>
<DlgTableCreate v-if="tableCreateDlg" v-model="tableCreateDlg" />
<DlgTableRename v-if="renameTableMeta" v-model="renameTableDlg" :table-meta="renameTableMeta" />
</div>
</template>

88
packages/nc-gui-v2/components/general/Language.vue

@ -1,88 +0,0 @@
<script lang="ts" setup>
import { Language } from '~/lib'
import { onMounted, useGlobal, useI18n, useNuxtApp } from '#imports'
const { $e } = useNuxtApp()
const { lang: currentLang } = useGlobal()
const { availableLocales = ['en'], locale } = useI18n()
const languages = $computed(() => availableLocales.sort())
const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
function applyDirection() {
const targetDirection = isRtlLang ? 'rtl' : 'ltr'
const oppositeDirection = targetDirection === 'ltr' ? 'rtl' : 'ltr'
document.body.classList.remove(oppositeDirection)
document.body.classList.add(targetDirection)
document.body.style.direction = targetDirection
}
function changeLanguage(lang: string) {
currentLang.value = lang
locale.value = lang
applyDirection()
$e('c:navbar:lang', { lang })
}
onMounted(() => {
applyDirection()
})
</script>
<template>
<a-dropdown class="select-none color-transition" :trigger="['click']">
<MaterialSymbolsTranslate v-bind="$attrs" class="md:text-xl cursor-pointer nc-menu-translate" />
<template #overlay>
<a-menu class="scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0 rounded">
<a-menu-item
v-for="lang of languages"
:key="lang"
:class="lang === locale ? '!bg-primary/10 text-primary' : ''"
class="group"
:value="lang"
@click="changeLanguage(lang)"
>
<div :class="lang === locale ? '!font-semibold !text-primary' : ''" class="nc-project-menu-item capitalize">
{{ Language[lang] || lang }}
</div>
</a-menu-item>
<a-menu-item class="mt-1">
<a
href="https://docs.nocodb.com/engineering/translation/#how-to-contribute--for-community-members"
target="_blank"
class="caption py-2 text-primary underline hover:opacity-75"
>
{{ $t('activity.translate') }}
</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<style scoped>
:deep(.ant-dropdown-menu-item-group-list) {
@apply !mx-0;
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-pink-500);
}
</style>

4
packages/nc-gui-v2/components/general/ShareBaseButton.vue

@ -9,7 +9,7 @@ const { isUIAllowed } = useUIPermission()
</script>
<template>
<div class="flex items-center mr-4">
<div class="flex items-center">
<a-button
v-if="
isUIAllowed('newUser') &&
@ -20,7 +20,7 @@ const { isUIAllowed } = useUIPermission()
"
size="middle"
type="primary"
class="!bg-white !text-primary rounded"
class="rounded"
@click="showUserModal = true"
>
<div class="flex items-center space-x-1">

52
packages/nc-gui-v2/components/general/language/Menu.vue

@ -0,0 +1,52 @@
<script lang="ts" setup>
import { Language } from '~/lib'
import { onMounted, useGlobal, useI18n, useNuxtApp } from '#imports'
const { $e } = useNuxtApp()
const { lang: currentLang } = useGlobal()
const { availableLocales = ['en'], locale } = useI18n()
const languages = $computed(() => availableLocales.sort())
const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
function applyDirection() {
const targetDirection = isRtlLang ? 'rtl' : 'ltr'
const oppositeDirection = targetDirection === 'ltr' ? 'rtl' : 'ltr'
document.body.classList.remove(oppositeDirection)
document.body.classList.add(targetDirection)
document.body.style.direction = targetDirection
}
function changeLanguage(lang: string) {
currentLang.value = lang
locale.value = lang
applyDirection()
$e('c:navbar:lang', { lang })
}
onMounted(() => {
applyDirection()
})
</script>
<template>
<a-menu class="scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0 rounded">
<a-menu-item
v-for="lang of languages"
:key="lang"
:class="lang === locale ? '!bg-primary/10 text-primary' : ''"
class="group"
:value="lang"
@click="changeLanguage(lang)"
>
<div :class="lang === locale ? '!font-semibold !text-primary' : ''" class="nc-project-menu-item capitalize">
{{ Language[lang] || lang }}
</div>
</a-menu-item>
<a-menu-item class="mt-1">
<a
href="https://docs.nocodb.com/engineering/translation/#how-to-contribute--for-community-members"
target="_blank"
class="caption py-2 text-primary underline hover:opacity-75"
>
{{ $t('activity.translate') }}
</a>
</a-menu-item>
</a-menu>
</template>

33
packages/nc-gui-v2/components/general/language/index.vue

@ -0,0 +1,33 @@
<script lang="ts" setup>
const { subMenu } = defineProps<{ subMenu?: boolean }>()
</script>
<template>
<GeneralLanguageMenu v-if="subMenu" />
<a-dropdown v-else class="select-none color-transition" :trigger="['click']">
<MaterialSymbolsTranslate v-bind="$attrs" class="md:text-xl cursor-pointer nc-menu-translate" />
<template #overlay>
<GeneralLanguageMenu />
</template>
</a-dropdown>
</template>
<style scoped>
:deep(.ant-dropdown-menu-item-group-list) {
@apply !mx-0;
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-pink-500);
}
</style>

2
packages/nc-gui-v2/components/smartsheet/Toolbar.vue

@ -7,7 +7,7 @@ const isPublic = inject(IsPublicInj, ref(false))
</script>
<template>
<div class="nc-table-toolbar w-full py-1 flex gap-1 items-center h-[48px] px-2 border-b" style="z-index: 7">
<div class="nc-table-toolbar w-full py-1 flex gap-1 items-center h-[var(--toolbar-height)] px-2 border-b" style="z-index: 7">
<SmartsheetToolbarFieldsMenu v-if="isGrid || isGallery" :show-system-fields="false" class="ml-1" />
<SmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery" />

32
packages/nc-gui-v2/components/smartsheet/sidebar/index.vue

@ -12,7 +12,6 @@ import {
inject,
provide,
ref,
useElementHover,
useRoute,
useRouter,
useViews,
@ -55,8 +54,6 @@ let selectedViewId = $ref('')
/** is view creation modal open */
let modalOpen = $ref(false)
const isHovered = useElementHover(sidebar)
/** Watch route param and change active view based on `viewTitle` */
watch(
[views, () => route.params.viewTitle],
@ -102,30 +99,11 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
class="relative shadow-md h-full"
theme="light"
>
<a-tooltip :mouse-enter-delay="1" placement="left">
<template #title> Toggle sidebar </template>
<Transition name="glow">
<div
v-show="sidebarCollapsed || isHovered"
class="group color-transition cursor-pointer hover:ring active:ring-pink-500 z-1 flex items-center p-[1px] absolute top-1/2 left-[-1rem] shadow bg-gray-100 rounded-full"
>
<MaterialSymbolsChevronRightRounded
v-if="isOpen"
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400 nc-right-sidebar-toggle"
@click="isOpen = false"
/>
<MaterialSymbolsChevronLeftRounded
v-else
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400 nc-right-sidebar-toggle"
@click="isOpen = true"
/>
</div>
</Transition>
</a-tooltip>
<Toolbar v-if="isOpen" :class="{ 'flex items-center py-3 px-3 justify-between border-b-1': !isForm }" />
<Toolbar
v-if="isOpen"
class="min-h-[var(--toolbar-height)] max-h-[var(--toolbar-height)]"
:class="{ 'flex items-center py-3 px-3 justify-between border-b-1': !isForm }"
/>
<Toolbar v-else class="py-3 px-2 max-w-[50px] flex !flex-col-reverse gap-4 items-center mt-[-1px]">
<template #start>

18
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue

@ -0,0 +1,18 @@
<script setup lang="ts">
/** Sidebar visible */
const { isOpen, toggle } = useSidebar({ storageKey: 'nc-right-sidebar' })
</script>
<template>
<a-tooltip :placement="isOpen ? 'bottomRight' : 'left'" :mouse-enter-delay="0.8">
<template #title> Toggle sidebar</template>
<div class="nc-sidebar-right-item hover:after:bg-primary/75 group nc-sidebar-add-row">
<MdiChevronDoubleLeft
class="cursor-pointer group-hover:(!text-white) transform transition-transform"
:class="{ 'rotate-180': isOpen }"
@click="toggle(!isOpen)"
/>
</div>
</a-tooltip>
</template>

7
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue

@ -5,6 +5,7 @@ import Reload from './Reload.vue'
import ExportCache from './ExportCache.vue'
import DeleteCache from './DeleteCache.vue'
import DebugMeta from './DebugMeta.vue'
import ToggleDrawer from './ToggleDrawer.vue'
import { IsFormInj } from '#imports'
const { isUIAllowed } = useUIPermission()
@ -14,6 +15,8 @@ const isForm = inject(IsFormInj)
const debug = $ref(false)
const clickCount = $ref(0)
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
</script>
<template>
@ -53,6 +56,10 @@ const clickCount = $ref(0)
<AddRow v-if="isUIAllowed('xcDatatableEditable')" @click.stop />
<div :class="{ 'w-[calc(100%_+_16px)] h-[1px] bg-gray-200 mt-1 -ml-1': !isOpen, 'dot': isOpen }" />
<ToggleDrawer />
<slot name="end" />
</div>
<div v-else>

6
packages/nc-gui-v2/components/tabs/Smartsheet.vue

@ -84,11 +84,9 @@ watch(isLocked, (nextValue) => (treeViewIsLockedInj.value = nextValue), { immedi
<SmartsheetForm v-else-if="isForm" />
</div>
</div>
<teleport to="#content">
<SmartsheetSidebar />
</teleport>
</template>
</div>
<SmartsheetSidebar v-if="meta" />
</div>
</template>

2
packages/nc-gui-v2/layouts/base.vue

@ -23,7 +23,7 @@ const logout = () => {
<a-layout class="!flex-col">
<Transition name="layout">
<a-layout-header
v-if="!route.meta.public && signedIn"
v-if="!route.meta.public && signedIn && !route.meta.hideHeader"
class="flex !bg-primary items-center text-white pl-4 pr-5 shadow-lg"
>
<div

136
packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue

@ -8,7 +8,6 @@ import {
provideSidebar,
ref,
useClipboard,
useElementHover,
useGlobal,
useProject,
useRoute,
@ -19,7 +18,7 @@ import { TabType } from '~/composables'
const route = useRoute()
const { appInfo, token } = useGlobal()
const { appInfo, token, signOut, signedIn, user } = useGlobal()
const { project, loadProject, loadTables, isSharedBase, loadProjectMetaInfo, projectMetaInfo } = useProject()
@ -42,6 +41,13 @@ const openDialogKey = ref<string>()
const dropdownOpen = ref(false)
const email = computed(() => user.value?.email ?? '---')
const logout = () => {
signOut()
navigateTo('/signin')
}
/** Sidebar ref */
const sidebar = ref()
@ -68,8 +74,6 @@ await loadProject()
await loadTables()
const isHovered = useElementHover(sidebar)
const copyProjectInfo = async () => {
try {
await loadProjectMetaInfo()
@ -97,6 +101,10 @@ const copyAuthToken = async () => {
message.error(e.message)
}
}
definePageMeta({
hideHeader: true,
})
</script>
<template>
@ -107,7 +115,7 @@ const copyAuthToken = async () => {
:collapsed="!isOpen"
width="250"
collapsed-width="50"
class="relative shadow-md h-full z-1"
class="relative shadow-md h-full z-1 nc-left-sidebar"
:trigger="null"
collapsible
theme="light"
@ -144,14 +152,20 @@ const copyAuthToken = async () => {
</template>
</div>
<a-dropdown v-else class="h-full" :trigger="['click']" placement="bottom">
<a-dropdown v-else class="h-full min-w-0 flex-1" :trigger="['click']" placement="bottom">
<div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']"
class="group cursor-pointer flex gap-4 items-center nc-project-menu overflow-hidden"
class="group cursor-pointer flex gap-1 items-center nc-project-menu overflow-hidden"
>
<template v-if="isOpen">
<div class="text-xl font-semibold truncate">{{ project.title }}</div>
<a-tooltip v-if="project.title?.length > 12" placement="bottom">
<div class="text-lg font-semibold truncate">{{ project.title }}</div>
<template #title>
<div class="text-sm">{{ project.title }}</div>
</template>
</a-tooltip>
<div v-else class="text-lg font-semibold truncate">{{ project.title }}</div>
<MdiChevronDown class="min-w-[28.5px] group-hover:text-pink-500 text-2xl" />
</template>
@ -224,6 +238,44 @@ const copyAuthToken = async () => {
<a-menu-divider />
<template v-if="signedIn && !isSharedBase">
<a-sub-menu v-if="isUIAllowed('previewAs')" key="account">
<template #title>
<div class="nc-project-menu-item group">
<MdiAccount class="group-hover:text-pink-500 nc-project-preview" />
Account
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<a-menu class="!py-0 dark:(!bg-gray-800) leading-8 !rounded">
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="nc-project-menu-item group no-underline" to="/user">
<MdiAt class="mt-1 group-hover:text-pink-500" />&nbsp;
<span class="prose">{{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-item key="1" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout">
<MdiLogout class="group-hover:(!text-pink-500)" />&nbsp;
<span class="prose">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu>
</a-sub-menu>
</template>
<a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as" v-t="['c:navdraw:preview-as']">
<template #title>
<div class="nc-project-menu-item group">
@ -242,35 +294,36 @@ const copyAuthToken = async () => {
<GeneralPreviewAs />
</a-sub-menu>
<a-sub-menu v-if="isUIAllowed('previewAs')" key="language">
<template #title>
<div class="nc-project-menu-item group">
<MaterialSymbolsTranslate class="group-hover:text-pink-500 nc-language" />
Language
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<GeneralLanguage sub-menu />
</a-sub-menu>
</a-menu-item-group>
</a-menu>
</template>
</a-dropdown>
<div class="nc-sidebar-left-toggle-icon hover:after:bg-primary/75 group nc-sidebar-add-row flex align-center px-2">
<MdiBackburger
class="cursor-pointer transform transition-transform duration-500"
:class="{ 'rotate-180': !isOpen }"
@click="toggle(!isOpen)"
/>
</div>
</div>
<a-tooltip :mouse-enter-delay="1" placement="right">
<template #title> Toggle table list </template>
<Transition name="glow">
<div
v-show="!isOpen || isHovered"
class="group color-transition cursor-pointer hover:ring active:ring-pink-500 z-1 flex items-center absolute top-1/2 right-[-0.75rem] shadow bg-gray-100 rounded-full"
>
<MaterialSymbolsChevronLeftRounded
v-if="isOpen"
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
@click="toggle(false)"
/>
<MaterialSymbolsChevronRightRounded
v-else
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
@click="toggle(true)"
/>
</div>
</Transition>
</a-tooltip>
<DashboardTreeView v-show="isOpen" />
</a-layout-sider>
</template>
@ -295,4 +348,25 @@ const copyAuthToken = async () => {
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-pink-500);
}
:global(#nc-sidebar-left .ant-layout-sider-collapsed) {
@apply !w-0 !max-w-0 !min-w-0 overflow-x-hidden;
}
.nc-left-sidebar {
.nc-sidebar-left-toggle-icon {
@apply opacity-0 transition-opactity duration-200 transition-color text-white/80 hover:text-white/100;
.nc-left-sidebar {
@apply !border-r-0;
}
}
&:hover .nc-sidebar-left-toggle-icon {
@apply opacity-100;
}
}
:deep(.ant-dropdown-menu-submenu-title) {
@apply py-0;
}
</style>

192
packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index.vue

@ -1,19 +1,14 @@
<script setup lang="ts">
import type { TabItem } from '~/composables'
import { TabMetaInj, provide, ref, useDialog, useNuxtApp, useTabs, useUIPermission } from '#imports'
import DlgTableCreate from '~/components/dlg/TableCreate.vue'
import DlgAirtableImport from '~/components/dlg/AirtableImport.vue'
import DlgQuickImport from '~/components/dlg/QuickImport.vue'
import { TabType } from '~/composables'
import { TabMetaInj, provide, useSidebar, useTabs } from '#imports'
import { TabType, useGlobal } from '~/composables'
import MdiAirTableIcon from '~icons/mdi/table-large'
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiAccountGroup from '~icons/mdi/account-group'
const { $e } = useNuxtApp()
const { tabs, activeTabIndex, activeTab, closeTab } = useTabs()
const { isUIAllowed } = useUIPermission()
const { isLoading } = useGlobal()
provide(TabMetaInj, activeTab)
@ -28,63 +23,21 @@ const icon = (tab: TabItem) => {
}
}
function openQuickImportDialog(type: string) {
$e(`a:actions:import-${type}`)
const isOpen = ref(true)
const { close } = useDialog(DlgQuickImport, {
'modelValue': isOpen,
'importType': type,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
function openTableCreateDialog() {
$e('a:actions:create-table')
const isOpen = ref(true)
const { close } = useDialog(DlgTableCreate, {
'modelValue': isOpen,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
function openAirtableImportDialog() {
$e('a:actions:import-airtable')
const isOpen = ref(true)
const { close } = useDialog(DlgAirtableImport, {
'modelValue': isOpen,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
const { isOpen, toggle } = useSidebar()
</script>
<template>
<div class="h-full w-full nc-container pt-[9px]">
<div class="h-full w-full nc-container">
<div class="h-full w-full flex flex-col">
<div>
<div class="flex items-end !min-h-[50px] bg-primary/100">
<div v-if="!isOpen" class="nc-sidebar-left-toggle-icon hover:after:bg-primary/75 group nc-sidebar-add-row py-2 px-3">
<MdiMenu
class="cursor-pointer transform transition-transform duration-500 text-white"
:class="{ 'rotate-180': !isOpen }"
@click="toggle(!isOpen)"
/>
</div>
<a-tabs v-model:activeKey="activeTabIndex" class="nc-root-tabs" type="editable-card" @edit="closeTab(activeTabIndex)">
<a-tab-pane v-for="(tab, i) in tabs" :key="i">
<template #tab>
@ -95,87 +48,15 @@ function openAirtableImportDialog() {
</div>
</template>
</a-tab-pane>
<template #leftExtra>
<a-dropdown v-if="isUIAllowed('addOrImport')" :trigger="['click']">
<div
class="cursor-pointer color-transition group hover:text-primary text-sm flex items-center gap-2 py-[9.5px] px-[20px]"
>
<MdiPlusBoxOutline class="group-hover:text-pink-500" />
Add / Import
</div>
<template #overlay>
<a-menu class="nc-add-project-menu !py-0 ml-6 rounded text-sm">
<a-menu-item v-if="isUIAllowed('addTable')" key="add-new-table" @click="openTableCreateDialog">
<div class="color-transition nc-project-menu-item after:(!rounded-t) group">
<MdiTable class="group-hover:text-pink-500" />
<!-- Add new table -->
{{ $t('tooltip.addTable') }}
</div>
</a-menu-item>
<a-menu-item-group title="QUICK IMPORT FROM" class="!px-0 !mx-0">
<a-menu-item
v-if="isUIAllowed('airtableImport')"
key="quick-import-airtable"
@click="openAirtableImportDialog"
>
<div class="color-transition nc-project-menu-item group">
<MdiTableLarge class="group-hover:text-pink-500" />
<!-- TODO: i18n -->
Airtable
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('csvImport')" key="quick-import-csv" @click="openQuickImportDialog('csv')">
<div class="color-transition nc-project-menu-item group">
<MdiFileDocumentOutline class="group-hover:text-pink-500" />
<!-- TODO: i18n -->
CSV file
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('jsonImport')" key="quick-import-json" @click="openQuickImportDialog('json')">
<div class="color-transition nc-project-menu-item group">
<MdiCodeJson class="group-hover:text-pink-500" />
<!-- TODO: i18n -->
JSON file
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('excelImport')"
key="quick-import-excel"
@click="openQuickImportDialog('excel')"
>
<div class="color-transition nc-project-menu-item group">
<MdiFileExcel class="group-hover:text-pink-500" />
<!-- TODO: i18n -->
Microsoft Excel
</div>
</a-menu-item>
</a-menu-item-group>
<a-menu-divider class="my-0" />
<a-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b">
<a
v-t="['e:datasource:import-request']"
href="https://github.com/nocodb/nocodb/issues/2052"
target="_blank"
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)"
>
<MdiOpenInNew class="group-hover:text-pink-500" />
<!-- TODO: i18n -->
Request a data source you need?
</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-tabs>
<span class="flex-1" />
<div class="flex justify-center align-self-center mr-2 min-w-[115px]">
<div v-show="isLoading" class="flex items-center gap-2 ml-3 text-white">
{{ $t('general.loading') }}
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" />
</div>
</div>
</div>
<div class="w-full min-h-[300px] flex-auto">
@ -187,24 +68,39 @@ function openAirtableImportDialog() {
<style scoped lang="scss">
.nc-container {
height: calc(100vh - var(--header-height));
height: 100vh;
flex: 1 1 100%;
}
:deep(.nc-root-tabs) {
& > .ant-tabs-nav {
@apply !mb-0;
@apply !mb-0 before:(!border-b-0);
.ant-tabs-extra-content {
@apply !bg-white/0;
}
& > .ant-tabs-nav-wrap > .ant-tabs-nav-list {
& > .ant-tabs-nav-add {
@apply !hidden;
}
.ant-tabs-nav-add {
@apply !hidden;
}
.ant-tabs-nav-more {
@apply text-white;
}
& > .ant-tabs-nav-wrap > .ant-tabs-nav-list {
& > .ant-tabs-tab-active {
@apply font-weight-medium;
}
& > .ant-tabs-tab {
@apply border-0;
}
& > .ant-tabs-tab:not(.ant-tabs-tab-active) {
@apply bg-gray-100 text-gray-500;
//@apply bg-gray-100 text-gray-500;
@apply bg-white/10 text-white/90;
.ant-tabs-tab-remove {
@apply !text-white;
}
}
}
}

Loading…
Cancel
Save