Browse Source

Merge pull request #3123 from nocodb/feat/gui-v2-shared-base

wip(gui-v2): shared base
pull/3133/head
navi 2 years ago committed by GitHub
parent
commit
50ad4f16d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  2. 2
      packages/nc-gui-v2/components/general/MiniSidebar.vue
  3. 2
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  4. 9
      packages/nc-gui-v2/components/smartsheet/Gallery.vue
  5. 13
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  6. 17
      packages/nc-gui-v2/components/smartsheet/Pagination.vue
  7. 10
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  8. 2
      packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue
  9. 2
      packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue
  10. 2
      packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue
  11. 3
      packages/nc-gui-v2/composables/useApi/interceptors.ts
  12. 46
      packages/nc-gui-v2/composables/useMetas.ts
  13. 30
      packages/nc-gui-v2/composables/useProject.ts
  14. 18
      packages/nc-gui-v2/composables/useTabs.ts
  15. 1
      packages/nc-gui-v2/context/index.ts
  16. 4
      packages/nc-gui-v2/layouts/base.vue
  17. 3
      packages/nc-gui-v2/middleware/auth.global.ts
  18. 26
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  19. 5
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index.vue
  20. 0
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue
  21. 0
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/auth.vue
  22. 0
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/index.vue

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

@ -2,7 +2,7 @@
import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { Empty } from 'ant-design-vue'
import { useNuxtApp, useRoute } from '#app'
import { useNuxtApp } from '#app'
import { computed, useProject, useTable, useTabs, watchEffect } from '#imports'
import { TabType } from '~/composables'
import MdiView from '~icons/mdi/eye-circle-outline'
@ -14,9 +14,7 @@ const { addTab } = useTabs()
const { $api, $e } = useNuxtApp()
const route = useRoute()
const { tables, loadTables } = useProject(route.params.projectId as string)
const { tables, loadTables, isSharedBase } = useProject()
const { activeTab } = useTabs()
const { deleteTable } = useTable()
@ -145,7 +143,11 @@ const activeTable = computed(() => {
</div>
<a-dropdown :trigger="['contextmenu']">
<div class="p-2 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull" style="direction: rtl">
<div
class="p-2 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull"
:class="{ 'mb-[20px]': isSharedBase }"
style="direction: rtl"
>
<div
style="direction: ltr"
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"

2
packages/nc-gui-v2/components/general/MiniSidebar.vue

@ -106,7 +106,7 @@ const logout = () => {
<div
:class="[route.name.includes('nc-projectId') ? 'active' : 'pointer-events-none !text-gray-400']"
class="nc-mini-sidebar-item"
@click="navigateTo(`/nc/${route.params.projectId}`)"
@click="navigateTo(`/${route.params.projectType}/${route.params.projectId}`)"
>
<MdiDatabase class="cursor-pointer transform hover:scale-105 text-2xl" />
</div>

2
packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue

@ -5,7 +5,7 @@ import type { Ref } from 'vue'
import { ColumnInj, IsFormInj, MetaInj } from '~/context'
import { provide, toRef, useMetas, useProvideColumnCreateStore } from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean; required: boolean }>()
const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean; required?: boolean }>()
const column = toRef(props, 'column')
const hideMenu = toRef(props, 'hideMenu')

9
packages/nc-gui-v2/components/smartsheet/Gallery.vue

@ -1,6 +1,7 @@
<script lang="ts" setup>
import { isVirtualCol } from 'nocodb-sdk'
import { inject, provide, useViewData } from '#imports'
import Row from '~/components/smartsheet/Row.vue'
import { ActiveViewInj, ChangePageInj, FieldsInj, IsFormInj, IsGridInj, MetaInj, PaginationDataInj, ReadonlyInj } from '~/context'
import ImageIcon from '~icons/mdi/file-image-box'
@ -50,11 +51,11 @@ const attachments = (record: any): Array<Attachment> => {
}
</script>
<!-- TODO: Fix scrolling -->
<template>
<div class="flex flex-col h-full min-h-0 w-full">
<div class="nc-gallery-container min-h-0 flex-1 grid grid-cols-4 gap-4 my-4 px-3">
<div class="flex flex-col h-full w-full">
<div class="nc-gallery-container min-h-0 flex-1 grid grid-cols-4 gap-4 my-4 px-3 overflow-auto">
<div v-for="(record, recordIndex) in data" :key="recordIndex" class="flex flex-col">
<Row :row="record">
<a-card hoverable class="!rounded-lg h-full">
<template #cover>
<a-carousel v-if="attachments(record).length !== 0" autoplay>
@ -89,6 +90,7 @@ const attachments = (record: any): Array<Attachment> => {
</div>
</div>
</a-card>
</Row>
</div>
</div>
<SmartsheetPagination />
@ -97,7 +99,6 @@ const attachments = (record: any): Array<Attachment> => {
<style scoped>
.nc-gallery-container {
height: calc(100vh - 250px);
overflow: auto;
}
</style>

13
packages/nc-gui-v2/components/smartsheet/Grid.vue

@ -462,10 +462,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<style scoped lang="scss">
.nc-grid-wrapper {
width: 100%;
// todo : proper height calculation
height: calc(100vh - 215px);
overflow: auto;
@apply h-full w-full overflow-auto;
td,
th {
@ -482,10 +479,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
table,
td,
th {
border-right: 1px solid #f0f0f0 !important;
border-left: 1px solid #f0f0f0 !important;
border-bottom: 1px solid #f0f0f0 !important;
border-top: 1px solid #f0f0f0 !important;
@apply !border-1;
border-collapse: collapse;
}
@ -511,8 +505,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
}
td.active::before {
background: #0040bc;
opacity: 0.1;
@apply bg-primary/5;
}
}

17
packages/nc-gui-v2/components/smartsheet/Pagination.vue

@ -34,21 +34,12 @@ const page = computed({
:show-size-changer="false"
/>
<div v-else class="mx-auto d-flex align-center mt-n1" style="max-width: 250px">
<span class="caption" style="white-space: nowrap"> Change page:</span>
<v-text-field
:value="page"
class="ml-1 caption"
:full-width="false"
outlined
dense
hide-details
type="number"
@keydown.enter="changePage(page)"
>
<template #append>
<span class="text-xs" style="white-space: nowrap"> Change page:</span>
<a-input :value="page" size="small" class="ml-1 !text-xs" type="number" @keydown.enter="changePage(page)">
<template #suffix>
<MdiKeyboardIcon class="mt-1" @click="changePage(page)" />
</template>
</v-text-field>
</a-input>
</div>
<div class="flex-1" />

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

@ -1,22 +1,22 @@
<script setup lang="ts">
import { useSmartsheetStoreOrThrow } from '~/composables'
const { isGrid, isForm } = useSmartsheetStoreOrThrow()
const { isGrid, isForm, isGallery } = useSmartsheetStoreOrThrow()
</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">
<SmartsheetToolbarFieldsMenu v-if="isGrid" :show-system-fields="false" class="ml-1" />
<SmartsheetToolbarFieldsMenu v-if="isGrid || isGallery" :show-system-fields="false" class="ml-1" />
<SmartsheetToolbarColumnFilterMenu v-if="isGrid" />
<SmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery" />
<SmartsheetToolbarSortListMenu v-if="isGrid" />
<SmartsheetToolbarSortListMenu v-if="isGrid || isGallery" />
<SmartsheetToolbarShareView v-if="isForm || isGrid" />
<SmartsheetToolbarMoreActions v-if="isGrid" />
<div class="flex-1" />
<SmartsheetToolbarSearchData v-if="isGrid" class="shrink mr-2" />
<SmartsheetToolbarSearchData v-if="isGrid || isGallery" class="shrink mr-2" />
</div>
</template>

2
packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue

@ -32,7 +32,7 @@ const iconColor = '#1890ff'
<template>
<div class="flex p-2 align-center gap-2 p-4">
<h5 class="text-lg font-weight-medium flex align-center gap-1 mb-0">
<h5 class="text-lg font-weight-medium flex align-center gap-1 mb-0 min-w-0 overflow-x-hidden truncate">
<mdi-table-arrow-right :style="{ color: iconColor }" />
<template v-if="meta">

2
packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue

@ -82,7 +82,7 @@ const isExpanded = useVModel(props, 'modelValue', emits)
<template>
<a-modal v-model:visible="isExpanded" :footer="null" width="min(90vw,1000px)" :body-style="{ padding: 0 }" :closable="false">
<Header @cancel="isExpanded = false" />
<a-card class="!bg-gray-100">
<a-card class="!bg-gray-100 min-h-[70vh]">
<div class="flex h-full nc-form-wrapper items-stretch">
<div class="flex-grow overflow-auto scrollbar-thin-primary">
<div class="w-[500px] mx-auto">

2
packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue

@ -26,7 +26,7 @@ const { project } = useProject()
const { copy } = useClipboard()
const url = $computed(() => (base && base.uuid ? `${dashboardUrl}/nc/base/${base.uuid}` : null))
const url = $computed(() => (base && base.uuid ? `${dashboardUrl}/base/${base.uuid}` : null))
const loadBase = async () => {
try {

3
packages/nc-gui-v2/composables/useApi/interceptors.ts

@ -18,7 +18,8 @@ export function addAxiosInterceptors(api: Api<any>) {
}
if (!config.url?.endsWith('/user/me') && !config.url?.endsWith('/admin/roles')) {
if (route && route.params && route.params.shared_base_id) config.headers['xc-shared-base-id'] = route.params.shared_base_id
if (route && route.params && route.params.projectType === 'base')
config.headers['xc-shared-base-id'] = route.params.projectId
}
return config

46
packages/nc-gui-v2/composables/useMetas.ts

@ -1,6 +1,8 @@
import { message } from 'ant-design-vue'
import type { WatchStopHandle } from 'vue'
import type { TableInfoType, TableType } from 'nocodb-sdk'
import { useProject } from './useProject'
import { extractSdkResponseErrorMsg } from '~/utils'
import { useNuxtApp, useState } from '#app'
export function useMetas() {
@ -11,42 +13,48 @@ export function useMetas() {
const loadingState = useState<Record<string, boolean>>('metas-loading-state', () => ({}))
const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | TableInfoType | null> => {
if (!force && metas.value[tableIdOrTitle]) return metas.value[tableIdOrTitle]
const modelId = (tables.value.find((t) => t.title === tableIdOrTitle || t.id === tableIdOrTitle) || {}).id
if (!modelId) {
console.warn(`Table '${tableIdOrTitle}' is not found in the table list`)
return null
}
if (!tableIdOrTitle) return null
/** wait until loading is finished if requesting same meta */
if (!force) {
if (!force && loadingState.value[tableIdOrTitle]) {
await new Promise((resolve) => {
let unwatch: WatchStopHandle
// set maximum 20sec timeout to wait loading meta
const timeout = setTimeout(() => {
unwatch?.()
clearTimeout(timeout)
resolve(null)
}, 20000)
}, 10000)
// watch for loading state change
unwatch = watch(
() => loadingState.value[modelId],
() => !!loadingState.value[tableIdOrTitle],
(isLoading) => {
if (!isLoading) {
clearTimeout(timeout)
resolve(null)
unwatch?.()
resolve(null)
}
},
{ immediate: true },
)
})
if (metas.value[modelId]) return metas.value[modelId]
if (metas.value[tableIdOrTitle]) {
return metas.value[tableIdOrTitle]
}
}
loadingState.value[tableIdOrTitle] = true
try {
if (!force && metas.value[tableIdOrTitle]) {
return metas.value[tableIdOrTitle]
}
loadingState.value[modelId] = true
const modelId = tableIdOrTitle.startsWith('md_') ? tableIdOrTitle : tables.value.find((t) => t.title === tableIdOrTitle)?.id
if (!modelId) {
console.warn(`Table '${tableIdOrTitle}' is not found in the table list`)
return null
}
const model = await $api.dbTable.read(modelId)
metas.value = {
@ -55,9 +63,13 @@ export function useMetas() {
[model.title]: model,
}
loadingState.value[modelId] = false
return model
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
delete loadingState.value[tableIdOrTitle]
}
return null
}
const clearAllMeta = () => {

30
packages/nc-gui-v2/composables/useProject.ts

@ -1,17 +1,20 @@
import { SqlUiFactory } from 'nocodb-sdk'
import type { OracleUi, ProjectType, TableType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core'
import { useNuxtApp, useState } from '#app'
import { useNuxtApp, useRoute, useState } from '#app'
import { USER_PROJECT_ROLES } from '~/lib'
export function useProject(projectId?: MaybeRef<string>) {
const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({}))
const { $api } = useNuxtApp()
const _projectId = $computed(() => unref(projectId))
let _projectId = $ref('')
const project = useState<ProjectType>('project')
const tables = useState<TableType[]>('tables', () => [] as TableType[])
const route = useRoute()
// todo: refactor path param name and variable name
const projectType = $computed(() => route.params.projectType as string)
async function loadProjectRoles() {
projectRoles.value = {}
@ -29,17 +32,19 @@ export function useProject(projectId?: MaybeRef<string>) {
}
}
async function loadProject(id: string) {
project.value = await $api.project.read(id)
await loadProjectRoles()
async function loadProject() {
if (unref(projectId)) {
_projectId = unref(projectId)!
} else if (projectType === 'base') {
const baseData = await $api.public.sharedBaseGet(route.params.projectId as string)
_projectId = baseData.project_id!
} else {
_projectId = route.params.projectId as string
}
watchEffect(async () => {
if (_projectId) {
await loadProject(_projectId)
project.value = await $api.project.read(_projectId!)
await loadProjectRoles()
await loadTables()
}
})
const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '')
@ -48,6 +53,7 @@ export function useProject(projectId?: MaybeRef<string>) {
const sqlUi = computed(
() => SqlUiFactory.create({ client: projectBaseType }) as Exclude<ReturnType<typeof SqlUiFactory['create']>, typeof OracleUi>,
)
const isSharedBase = computed(() => projectType === 'base')
return { project, tables, loadProjectRoles, loadProject, loadTables, isMysql, isPg, sqlUi }
return { project, tables, loadProjectRoles, loadProject, loadTables, isMysql, isPg, sqlUi, isSharedBase }
}

18
packages/nc-gui-v2/composables/useTabs.ts

@ -29,9 +29,11 @@ export function useTabs() {
const router = useRouter()
const { tables } = useProject()
const projectType = $computed(() => route.params.projectType as string)
const activeTabIndex: WritableComputedRef<number> = computed({
get() {
if ((route.name as string)?.startsWith('nc-projectId-index-index-type-title-viewTitle') && tables.value?.length) {
if ((route.name as string)?.startsWith('projectType-projectId-index-index-type-title-viewTitle') && tables.value?.length) {
const tab: Partial<TabItem> = { type: route.params.type as TabType, title: route.params.title as string }
const id = tables.value?.find((t) => t.title === tab.title)?.id
@ -56,7 +58,7 @@ export function useTabs() {
},
set(index: number) {
if (index === -1) {
navigateTo(`/nc/${route.params.projectId}`)
navigateTo(`/${projectType}/${route.params.projectId}`)
} else {
const tab = tabs.value[index]
@ -91,7 +93,7 @@ export function useTabs() {
let newTabIndex = index - 1
if (newTabIndex < 0 && tabs.value?.length > 1) newTabIndex = index + 1
if (newTabIndex === -1) {
await navigateTo(`/nc/${route.params.projectId}`)
await navigateTo(`/${projectType}/${route.params.projectId}`)
} else {
await navigateToTab(tabs.value?.[newTabIndex])
}
@ -102,11 +104,15 @@ export function useTabs() {
function navigateToTab(tab: TabItem) {
switch (tab.type) {
case TabType.TABLE:
return navigateTo(`/nc/${route.params.projectId}/table/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`)
return navigateTo(
`/${projectType}/${route.params.projectId}/table/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`,
)
case TabType.VIEW:
return navigateTo(`/nc/${route.params.projectId}/view/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`)
return navigateTo(
`/${projectType}/${route.params.projectId}/view/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`,
)
case TabType.AUTH:
return navigateTo(`/nc/${route.params.projectId}/auth`)
return navigateTo(`/${projectType}/${route.params.projectId}/auth`)
}
}

1
packages/nc-gui-v2/context/index.ts

@ -5,7 +5,6 @@ import type { useViewData } from '#imports'
import type { Row } from '~/composables'
import type { TabItem } from '~/composables/useTabs'
export const EditEnabledInj: InjectionKey<boolean> = Symbol('edit-enabled')
export const ActiveCellInj: InjectionKey<Ref<boolean>> = Symbol('active-cell')
export const RowInj: InjectionKey<Ref<Row>> = Symbol('row')
export const ColumnInj: InjectionKey<Ref<ColumnType & { meta: any }>> = Symbol('column-injection')

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

@ -4,6 +4,8 @@ import { computed, useGlobal, useRoute } from '#imports'
const { signOut, signedIn, isLoading, user } = useGlobal()
const { isSharedBase } = useProject()
const route = useRoute()
const email = computed(() => user.value?.email ?? '---')
@ -49,7 +51,7 @@ const logout = () => {
</div>
</a-tooltip>
<template v-if="signedIn">
<template v-if="signedIn && !isSharedBase">
<a-dropdown :trigger="['click']">
<MdiDotsVertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent />

3
packages/nc-gui-v2/middleware/auth.global.ts

@ -23,6 +23,9 @@ import { useGlobal } from '#imports'
export default defineNuxtRouteMiddleware((to, from) => {
const state = useGlobal()
/** if shred base allow without validating */
if (to.params?.projectType === 'base') return
/** if auth is required or unspecified (same as required) and user is not signed in, redirect to signin page */
if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) {
return navigateTo('/signin')

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

@ -5,7 +5,7 @@ import { openLink } from '~/utils'
const route = useRoute()
const { project, loadProject, loadTables } = useProject(route.params.projectId as string)
const { project, loadProject, loadTables, isSharedBase } = useProject()
const { addTab, clearTabs } = useTabs()
@ -39,7 +39,7 @@ function toggleDialog(value?: boolean, key?: string) {
openDialogKey.value = key
}
await loadProject(route.params.projectId as string)
await loadProject()
await loadTables()
</script>
@ -58,14 +58,31 @@ await loadTables()
>
<div style="height: var(--header-height)" class="flex items-center !bg-primary text-white px-1 pl-5 gap-2">
<div
v-if="isOpen"
v-if="isOpen && !isSharedBase"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105"
@click="navigateTo('/')"
>
<img alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<a
v-if="isOpen && isSharedBase"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105"
href="https://github.com/nocodb/nocodb"
target="_blank"
>
<img alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</a>
<a-dropdown :trigger="['click']" placement="bottom">
<div v-if="isSharedBase">
<template v-if="isOpen">
<div class="text-xl font-semibold truncate">{{ project.title }}</div>
</template>
<template v-else>
<MdiFolder class="text-primary cursor-pointer transform hover:scale-105 text-2xl" />
</template>
</div>
<a-dropdown v-else :trigger="['click']" placement="bottom">
<div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']"
@ -220,7 +237,6 @@ await loadTables()
</template>
<dashboard-settings-modal v-model="dialogOpen" :open-key="openDialogKey" />
<NuxtPage />
<GeneralPreviewAs float />

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

@ -144,9 +144,10 @@ const icon = (tab: TabItem) => {
</template>
</a-tabs>
</div>
<div class="w-full min-h-[300px] grow">
<NuxtPage />
</div>
</div>
<DlgTableCreate v-if="tableCreateDialog" v-model="tableCreateDialog" />
<DlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" :import-type="importType" />
@ -156,7 +157,7 @@ const icon = (tab: TabItem) => {
<style scoped lang="scss">
.nc-container {
height: calc(100% - var(--header-height));
height: calc(100vh - var(--header-height));
flex: 1 1 100%;
}

0
packages/nc-gui-v2/pages/nc/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue → packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue

0
packages/nc-gui-v2/pages/nc/[projectId]/index/index/auth.vue → packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/auth.vue

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

Loading…
Cancel
Save