Files
nocodb/packages/nc-gui/components/dashboard/MiniSidebarV2/Rail.vue
mertmit afdf6fce0b chore: sync changes
Signed-off-by: mertmit <mertmit99@gmail.com>
2026-04-07 14:06:03 +03:00

408 lines
11 KiB
Vue

<script lang="ts" setup>
import { extractBaseRoleFromWorkspaceRole } from 'nocodb-sdk'
interface NavItem {
key: string
icon: string
label: string
disabled?: boolean
onClick?: () => void
}
const router = useRouter()
const route = router.currentRoute
const { navigateToProject, isMobileMode } = useGlobal()
const { $e: _$e } = useNuxtApp()
const workspaceStore = useWorkspace()
const { activeWorkspaceId, activeWorkspace } = storeToRefs(workspaceStore)
const basesStore = useBases()
const { basesList, resolvedProject } = storeToRefs(basesStore)
const sidebarStore = useSidebarStore()
const { activeSidebarTab } = storeToRefs(sidebarStore)
const { isUIAllowed, workspaceRoles } = useRoles()
const notificationStore = useNotification()
const { unreadCount } = toRefs(notificationStore)
const isNotificationOpen = ref(false)
const {
isPanelExpanded: isChatPanelExpanded,
isFullScreen: isChatFullScreen,
hasWorkspaceContext: hasChatWorkspaceContext,
hasBaseContext: hasChatBaseContext,
toggleChatPanel,
} = useChatPanel()
const { blockAiChat, showEEFeatures } = useEeConfig()
const { isRtl } = useRtl()
const handleChatToggle = () => {
toggleChatPanel()
}
const isBaseOpen = computed(() => {
return route.value.name?.toString().startsWith('index-typeOrId-baseId-')
})
watch(
isBaseOpen,
(val) => {
if (val && ncBackRoute().get() !== '/') ncBackRoute().set('')
},
{ immediate: true },
)
const isBaseListModalOpen = ref(false)
const navigateToProjectPage = () => {
if (route.value.name?.toString().startsWith('index-typeOrId-baseId-')) {
return
}
const lastVisitedBase = ncLastVisitedBase().get()
const baseToNavigate = lastVisitedBase
? basesList.value?.find((b) => b.id === lastVisitedBase) ?? basesList.value[0]
: basesList.value[0]
navigateToProject({ workspaceId: isEeUI ? activeWorkspaceId.value : undefined, baseId: baseToNavigate?.id })
}
const hasAvailableBases = computed(() => !!basesList.value?.length)
const getBasePath = () => {
const wsId = route.value.params.typeOrId || activeWorkspaceId.value
const baseId = route.value.params.baseId
if (baseId) return `/${wsId}/${baseId}`
return resolvedProject.value?.id ? `/${wsId}/${resolvedProject.value.id}` : ''
}
const onTabClick = async (tabKey: string) => {
if (isChatFullScreen.value) isChatFullScreen.value = false
if (tabKey === 'settings') {
activeSidebarTab.value = 'settings'
// If a base is open, navigate to base settings; otherwise ws-level settings
if (isBaseOpen.value) {
navigateTo(`${getBasePath()}/settings`)
} else {
const wsId = route.value.params.typeOrId || activeWorkspaceId.value
navigateTo(`/${wsId}/members`)
}
return
}
// Navigate first, then update tab — avoids stale API calls from the current page
const basePath = getBasePath()
if (!basePath) return
if (tabKey === 'workflows') {
await navigateTo(`${basePath}/workflows`)
} else if (tabKey === 'docs') {
await navigateTo(`${basePath}/docs`)
} else {
await navigateTo(basePath)
}
activeSidebarTab.value = tabKey as typeof activeSidebarTab.value
}
useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
const isBaseSearchInput = e.target instanceof HTMLInputElement && e.target.closest('.nc-base-search-input')
if (
!e.altKey ||
(!isBaseSearchInput &&
(isActiveInputElementExist(e) ||
cmdKActive() ||
isCmdJActive() ||
isNcDropdownOpen() ||
isActiveElementInsideExtension() ||
isActiveElementInsideScriptPane() ||
isDrawerOrModalExist() ||
isExpandedFormOpenExist()))
) {
return
}
switch (e.code) {
case 'KeyB': {
e.preventDefault()
navigateToProjectPage()
break
}
}
})
// Cmd/Ctrl + Shift + A — toggle AI chat
useEventListener(document, 'keydown', (e: KeyboardEvent) => {
if (!isEeUI || blockAiChat.value) return
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
if (
cmdOrCtrl &&
e.shiftKey &&
e.code === 'KeyA' &&
!isActiveInputElementExist(e) &&
!isNcDropdownOpen() &&
!isDrawerOrModalExist()
) {
e.preventDefault()
handleChatToggle()
}
})
// ── Main nav items (add/remove/reorder here) ──
const mainItems = computed<NavItem[]>(() => [
{
key: 'data',
icon: 'ncTable',
label: 'Data',
disabled: !hasAvailableBases.value,
onClick: () => {
onTabClick('data')
},
},
...(isEeUI && !isMobileMode.value && showEEFeatures.value
? [
{
key: 'workflows',
icon: 'ncAutomation',
label: 'Workflows',
disabled:
!hasAvailableBases.value ||
!isUIAllowed('scriptList', {
roles: resolvedProject.value?.project_role || extractBaseRoleFromWorkspaceRole(workspaceRoles.value),
}),
onClick: () => {
onTabClick('workflows')
},
},
]
: []),
...(isEeUI && showEEFeatures.value
? [
{
key: 'docs',
icon: 'ncFileText',
label: 'Docs',
disabled: !hasAvailableBases.value,
onClick: () => {
onTabClick('docs')
},
},
]
: []),
])
</script>
<template>
<nav class="nc-rail" data-testid="nc-mini-sidebar-v2-rail">
<div class="flex-none h-[var(--topbar-height)] w-full relative flex items-center justify-center">
<!-- Logo hover shows back arrow, click navigates to workspace -->
<NcTooltip placement="right" :arrow="false">
<template #title>{{ $t('labels.backToWorkspace') }}: {{ activeWorkspace?.title }}</template>
<div
class="nc-rail-logo nc-rail-logo-hover"
data-testid="nc-mini-sidebar-v2-logo"
:data-workspace-title="activeWorkspace?.title"
@click="navigateTo(`/${activeWorkspaceId}`)"
>
<GeneralProjectIcon
class="!h-7 !w-7 nc-logo-icon"
:color="parseProp(resolvedProject?.meta).iconColor"
:type="resolvedProject?.type"
:managed-app="
resolvedProject
? {
managed_app_master: resolvedProject?.managed_app_master,
managed_app_id: resolvedProject?.managed_app_id,
}
: undefined
"
/>
<div class="nc-back-icon">
<GeneralIcon icon="ncArrowLeft" class="!h-4.5 !w-4.5 text-nc-content-gray" />
</div>
</div>
</NcTooltip>
<div class="absolute bottom-0 left-0 right-0 flex justify-center">
<NcDivider class="!w-8 !min-w-8 !my-0 !border-nc-border-gray-medium" />
</div>
</div>
<!-- Main nav items -->
<DashboardMiniSidebarV2RailItem
v-for="(item, idx) of mainItems"
:key="idx"
:icon="item.icon"
:label="item.label"
:panel-key="item.key"
:active="activeSidebarTab === item.key && !isChatFullScreen"
:disabled="item.disabled"
:disable-tooltip="true"
@click="item.onClick?.()"
/>
<!-- AI Chat -->
<DashboardMiniSidebarV2RailItem
v-if="isEeUI && !blockAiChat && hasChatWorkspaceContext && hasChatBaseContext && !isMobileMode"
v-e="['c:chat:toggle']"
label="Chat"
panel-key="chat"
data-testid="nc-sidebar-chat-btn"
:active="isChatPanelExpanded"
:disable-tooltip="true"
:plain-active="!isChatFullScreen"
@click="handleChatToggle"
>
<template #icon>
<GeneralIcon icon="ncAutoAwesome" class="nc-rail-item-icon !text-nc-content-brand" />
</template>
</DashboardMiniSidebarV2RailItem>
<!-- Settings -->
<DashboardMiniSidebarV2RailItem
icon="ncSettings"
label="Settings"
panel-key="settings"
:active="activeSidebarTab === 'settings' && !isChatFullScreen"
:disable-tooltip="true"
@click="onTabClick('settings')"
/>
<!-- Bottom group -->
<div class="nc-rail-bottom-group">
<!-- Help -->
<DashboardMiniSidebarHelp>
<DashboardMiniSidebarV2RailItem icon="ncHelp" label="Help" panel-key="help" is-dropdown />
</DashboardMiniSidebarHelp>
</div>
<NcDivider class="!w-8 !min-w-8 !max-w-8 !my-0 !border-nc-border-gray-medium" />
<DashboardMiniSidebarCreateNewActionMenu v-if="!isMobileMode" />
<!-- Activity / Notifications -->
<NcDropdown
v-model:visible="isNotificationOpen"
:placement="isRtl ? 'left' : 'right'"
overlay-class-name="!shadow-none"
:overlay-style="isRtl ? { marginRight: '8px' } : { marginLeft: '8px' }"
:trigger="['click']"
>
<DashboardMiniSidebarV2RailItem
label="Activity"
tooltip="Activity"
panel-key="notification"
data-testid="nc-sidebar-notification-btn"
:active="isNotificationOpen"
:disable-tooltip="isNotificationOpen"
is-dropdown
>
<template #icon>
<div class="relative flex items-center justify-center">
<span
v-if="unreadCount"
class="absolute -top-1 -right-1 w-1.5 h-1.5 rounded-full border border-white dark:border-[#1a1a1a]"
style="background: #e75a8d"
/>
<GeneralIcon icon="notification" class="nc-rail-item-icon" />
</div>
</template>
</DashboardMiniSidebarV2RailItem>
<template #overlay>
<NotificationCard @close="isNotificationOpen = false" />
</template>
</NcDropdown>
<!-- User Avatar -->
<DashboardSidebarUserInfo />
<WorkspaceBaseListModal v-model:visible="isBaseListModalOpen" />
</nav>
</template>
<style lang="scss" scoped>
.nc-rail {
@apply flex flex-col gap-1.5 items-center h-full w-full;
}
.nc-rail-logo {
@apply flex items-center justify-center cursor-pointer opacity-90 hover:opacity-100 transition-all duration-150 p-1.5 rounded-lg;
}
.nc-rail-bottom-group {
@apply flex flex-col items-center w-full;
margin-top: auto;
}
.nc-rail-admin-wrapper {
@apply relative w-full flex justify-center;
.nc-notif-dot {
position: absolute;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
background: #e75a8d;
border-radius: 50%;
border: 2px solid #f0f0f0;
z-index: 1;
:root[theme='dark'] & {
border-color: #161616;
}
}
}
.nc-rail-logo-hover {
@apply relative;
// Fixed size so layout never shifts
width: 40px;
height: 40px;
.nc-logo-icon,
.nc-back-icon {
@apply absolute inset-0 m-auto;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.nc-logo-icon {
opacity: 1;
transform: scale(1);
}
.nc-back-icon {
@apply flex items-center justify-center;
opacity: 0;
transform: scale(0.8);
}
&:hover {
.nc-logo-icon {
opacity: 0;
transform: scale(0.8);
}
.nc-back-icon {
opacity: 1;
transform: scale(1);
}
}
}
</style>