+
+
diff --git a/packages/nc-gui/components/workspace/View.vue b/packages/nc-gui/components/workspace/View.vue
index 821dfbffe7..06dd9925c6 100644
--- a/packages/nc-gui/components/workspace/View.vue
+++ b/packages/nc-gui/components/workspace/View.vue
@@ -4,6 +4,7 @@ import { PlanFeatureTypes, PlanTitles } from 'nocodb-sdk'
const props = defineProps<{
workspaceId?: string
+ isNewWsPage?: boolean
}>()
const router = useRouter()
@@ -35,6 +36,13 @@ const {
isEEFeatureBlocked,
} = useEeConfig()
+const { isFromIntegrationPage, integrationPaginationData, loadIntegrations } = useProvideIntegrationViewStore()
+
+// Local ref for inner integrations sub-tabs in settings sidebar mode.
+// Cannot use activeViewTab (which writes to route.query.tab) because the outer NcTabs
+// also reads route.query.tab — changing it to 'connections' makes the outer pane blank.
+const integrationsSubTab = ref
('integrations')
+
const hasTeamsEditPermission = computed(() => {
return isEeUI && isTeamsEnabled.value && isUIAllowed('teamCreate')
})
@@ -55,9 +63,25 @@ const currentWorkspace = computedAsync(async () => {
return ws
})
+const routeNameToWsTab: Record = {
+ 'index-typeOrId-index': 'bases',
+ 'index-typeOrId': 'bases',
+ 'index-typeOrId-members': 'collaborators',
+ 'index-typeOrId-teams': 'teams',
+ 'index-typeOrId-integrations': 'integrations',
+ 'index-typeOrId-audits': 'audits',
+ 'index-typeOrId-billing': 'billing',
+ 'index-typeOrId-sso': 'sso',
+ 'index-typeOrId-settings': 'settings',
+}
+
+const wsTabToRouteName: Record = Object.fromEntries(Object.entries(routeNameToWsTab).map(([k, v]) => [v, k]))
+
const tab = computed({
get() {
- return route.value.query?.tab ?? 'collaborators'
+ return props.isNewWsPage
+ ? routeNameToWsTab[route.value.name as string] || 'collaborators'
+ : route.value.query?.tab ?? 'collaborators'
},
set(tab: string) {
if (!isWsAuditEnabled.value && tab === 'audits') {
@@ -76,7 +100,11 @@ const tab = computed({
loadCollaborators({} as any, props.workspaceId)
}
- router.push({ query: { ...route.value.query, tab } })
+ if (props.isNewWsPage) {
+ router.push({ name: wsTabToRouteName[tab] || 'index-typeOrId' })
+ } else {
+ router.push({ query: { ...route.value.query, tab } })
+ }
},
})
@@ -113,7 +141,12 @@ onMounted(() => {
watch(
() => route.value.query?.tab,
- async (newTab) => {
+ async (newTab, oldTab) => {
+ if (oldTab === 'integrations') {
+ isFromIntegrationPage.value = false
+ integrationsSubTab.value = 'integrations'
+ }
+
await until(() => isBaseRolesLoaded.value).toBeTruthy()
if (!isUIAllowed('workspaceCollaborators') && !isEEFeatureBlocked.value) {
@@ -130,19 +163,21 @@ watch(
},
)
-onMounted(() => {
- hideSidebar.value = true
-})
+if (!props.isNewWsPage) {
+ onMounted(() => {
+ hideSidebar.value = true
+ })
-onBeforeUnmount(() => {
- hideSidebar.value = false
-})
+ onBeforeUnmount(() => {
+ hideSidebar.value = false
+ })
+}
@@ -152,19 +187,26 @@ onBeforeUnmount(() => {
'max-w-[calc(100%_-_52px)]': isMobileMode,
}"
>
-
+
{{ currentWorkspace?.title }}
-
+
+
-
- {{ $t('title.teamAndSettings') }}
-
+
+ {{ $t('title.teamAndSettings') }}
+
+
-
+
{{ org.title }}
@@ -199,10 +241,21 @@ onBeforeUnmount(() => {
-
+
+
+
+
+
+ {{ $t('objects.projects') }}
+
+
+
+ bases
+
+
@@ -232,6 +285,59 @@ onBeforeUnmount(() => {
+
+
+
+
+ {{ $t('general.integrations') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('general.integrations') }}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('general.connections') }}
+
+ {{ integrationPaginationData.totalRows }}
+
+
+
+
+
+
+
+
+
+
+
+
+import { PlanFeatureTypes, PlanTitles } from 'nocodb-sdk'
+
+const router = useRouter()
+const route = router.currentRoute
+
+const { t } = useI18n()
+
+const workspaceStore = useWorkspace()
+
+const { activeWorkspace, isTeamsEnabled } = storeToRefs(workspaceStore)
+const { loadCollaborators } = workspaceStore
+
+const { appInfo, isMobileMode } = useGlobal()
+
+const { isUIAllowed } = useRoles()
+
+const {
+ isWsAuditEnabled,
+ isPaymentEnabled,
+ isEEFeatureBlocked,
+ getFeature,
+ handleUpgradePlan,
+ showUpgradeToUseTeams,
+} = useEeConfig()
+
+const hasTeamsEditPermission = computed(() => {
+ return isEeUI && isTeamsEnabled.value && isUIAllowed('teamCreate')
+})
+
+const isWorkspaceSsoAvail = computed(() => {
+ if (isEeUI && appInfo.value?.isCloud && getFeature(PlanFeatureTypes.FEATURE_SSO)) {
+ return true
+ }
+ return false
+})
+
+const routeNameToWsTab: Record = {
+ 'index-typeOrId-index': 'bases',
+ 'index-typeOrId': 'bases',
+ 'index-typeOrId-members': 'collaborators',
+ 'index-typeOrId-teams': 'teams',
+ 'index-typeOrId-integrations': 'integrations',
+ 'index-typeOrId-audits': 'audits',
+ 'index-typeOrId-billing': 'billing',
+ 'index-typeOrId-sso': 'sso',
+ 'index-typeOrId-settings': 'settings',
+}
+
+const wsTabToRouteName: Record = Object.fromEntries(
+ Object.entries(routeNameToWsTab).map(([k, v]) => [v, k]),
+)
+
+const activeTab = computed(() => {
+ return routeNameToWsTab[route.value.name as string] || 'bases'
+})
+
+const onTabChange = (tabKey: string) => {
+ if (!isWsAuditEnabled.value && tabKey === 'audits') {
+ handleUpgradePlan({
+ title: t('upgrade.upgradeToAccessWsAudit'),
+ content: t('upgrade.upgradeToAccessWsAuditSubtitle', { plan: PlanTitles.ENTERPRISE }),
+ limitOrFeature: PlanFeatureTypes.FEATURE_AUDIT_WORKSPACE,
+ })
+ return
+ }
+
+ if (isEeUI && tabKey === 'teams' && hasTeamsEditPermission.value && showUpgradeToUseTeams()) return
+
+ if (['collaborators', 'teams'].includes(tabKey) && isUIAllowed('workspaceCollaborators')) {
+ loadCollaborators({}, activeWorkspace.value?.id)
+ }
+
+ router.push({ name: wsTabToRouteName[tabKey] || 'index-typeOrId' })
+}
+
+// Tab definitions — built dynamically based on permissions
+interface TabItem {
+ key: string
+ icon: string
+ label: string
+ badge?: any
+}
+
+const tabItems = computed(() => {
+ const items: TabItem[] = [
+ { key: 'bases', icon: 'ncDatabase', label: t('objects.projects') },
+ ]
+
+ if (isUIAllowed('workspaceCollaborators')) {
+ items.push({ key: 'collaborators', icon: 'users', label: t('labels.members') })
+ }
+
+ if (isEeUI && hasTeamsEditPermission.value) {
+ items.push({ key: 'teams', icon: 'ncBuilding', label: t('general.teams') })
+ }
+
+ if (!isMobileMode.value) {
+ if (isUIAllowed('workspaceIntegrations')) {
+ items.push({ key: 'integrations', icon: 'integration', label: t('general.integrations') })
+ }
+
+ if (isEeUI && isPaymentEnabled.value && isUIAllowed('workspaceBilling')) {
+ items.push({ key: 'billing', icon: 'ncDollarSign', label: t('general.billing') })
+ }
+
+ if (isEeUI && isUIAllowed('workspaceAuditList')) {
+ items.push({ key: 'audits', icon: 'audit', label: t('title.audits') })
+ }
+
+ if (isWorkspaceSsoAvail.value && isUIAllowed('workspaceSSO')) {
+ items.push({ key: 'sso', icon: 'sso', label: t('title.sso') })
+ }
+ }
+
+ if (!isEEFeatureBlocked.value) {
+ items.push({ key: 'settings', icon: 'ncSettings', label: t('labels.settings') })
+ }
+
+ return items
+})
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/workspace/ViewTopbar.vue b/packages/nc-gui/components/workspace/ViewTopbar.vue
new file mode 100644
index 0000000000..a64e10b677
--- /dev/null
+++ b/packages/nc-gui/components/workspace/ViewTopbar.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+ {{ activeWorkspace?.title }}
+
+
+ {{ activePlanTitle }} {{ $t('general.plan') }}
+
+ ·
+ {{ $t('general.upgrade') }}
+
+
+
+
+
+
+
+
+
{{ $t('activity.searchWorkspaceBases') }}...
+
+ {{ renderCmdOrCtrlKey() }}
+ K
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/composables/useBackToBase.ts b/packages/nc-gui/composables/useBackToBase.ts
index 4127e055e3..7c6732a55c 100644
--- a/packages/nc-gui/composables/useBackToBase.ts
+++ b/packages/nc-gui/composables/useBackToBase.ts
@@ -22,7 +22,7 @@ export const useBackToBase = () => {
// Show on base settings full-page mode (even though technically on a base page)
if (isBaseSettingsFullPage.value) return !!lastVisitedBase.value
- return !isOnBasePage.value && !hideMiniSidebar.value && !!lastVisitedBase.value
+ return !isOnBasePage.value && !!lastVisitedBase.value
})
const navigateToBase = () => {
diff --git a/packages/nc-gui/composables/useWsBaseListAll.ts b/packages/nc-gui/composables/useWsBaseListAll.ts
new file mode 100644
index 0000000000..3898bda77c
--- /dev/null
+++ b/packages/nc-gui/composables/useWsBaseListAll.ts
@@ -0,0 +1,69 @@
+import { NO_SCOPE } from 'nocodb-sdk'
+
+export interface BaseListAllData {
+ workspaces: {
+ id: string
+ title: string
+ meta: Record
+ plan_title: string | null
+ bases: {
+ id: string
+ title: string
+ meta: Record
+ role: string
+ order: number
+ managed_app_master?: boolean
+ managed_app_id?: string | null
+ }[]
+ }[]
+}
+
+export const useWsBaseListAll = createSharedComposable(() => {
+ const { $api } = useNuxtApp()
+
+ const baseListAllData = ref(null)
+ const isBaseListAllLoading = ref(false)
+
+ const loadBaseListAll = async (force = false) => {
+ if (!force && (baseListAllData.value || isBaseListAllLoading.value)) return
+
+ isBaseListAllLoading.value = true
+ try {
+ baseListAllData.value = (await $api.internal.getOperation(NO_SCOPE, NO_SCOPE, {
+ operation: 'baseListAll',
+ })) as BaseListAllData
+ } catch {
+ // silently fail — cross-workspace search won't be available
+ } finally {
+ isBaseListAllLoading.value = false
+ }
+ }
+
+ // Map of workspace ID → plan info
+ const baseListAllWsMap = computed(() => {
+ const map = new Map()
+ for (const ws of baseListAllData.value?.workspaces ?? []) {
+ map.set(ws.id, { plan_title: ws.plan_title })
+ }
+ return map
+ })
+
+ // Workspace IDs that have at least one base title matching a search query
+ const getBaseMatchCountByWs = (query: string) => {
+ if (!query || !baseListAllData.value) return new Map()
+ const map = new Map()
+ for (const ws of baseListAllData.value.workspaces) {
+ const count = ws.bases.filter((b) => searchCompare(b.title, query)).length
+ if (count > 0) map.set(ws.id, count)
+ }
+ return map
+ }
+
+ return {
+ baseListAllData,
+ isBaseListAllLoading,
+ loadBaseListAll,
+ baseListAllWsMap,
+ getBaseMatchCountByWs,
+ }
+})
diff --git a/packages/nc-gui/pages/index/[typeOrId]/settings/index.vue b/packages/nc-gui/pages/index/[typeOrId]/settings/index.vue
index 425cbfbb8b..0d9ed804c0 100644
--- a/packages/nc-gui/pages/index/[typeOrId]/settings/index.vue
+++ b/packages/nc-gui/pages/index/[typeOrId]/settings/index.vue
@@ -3,13 +3,8 @@ definePageMeta({
hideHeader: true,
hasSidebar: true,
})
-
-const route = useRoute()
-
-// Redirect old settings URL to new flat URL
-navigateTo(`/${route.params.typeOrId}/members`, { replace: true })
-
+