Merge pull request #13415 from nocodb/nc-worktree-stateful-chasing-jellyfish

This commit is contained in:
Anbarasu
2026-03-31 20:24:09 +05:30
committed by GitHub
26 changed files with 1659 additions and 30 deletions

View File

@@ -0,0 +1,218 @@
<script setup lang="ts">
import type { IntegrationType } from 'nocodb-sdk'
interface IntegrationLinkedBaseListResponse {
all_bases: boolean
bases: { id: string; title: string }[]
}
interface Props {
visible: boolean
integration: IntegrationType
}
const props = withDefaults(defineProps<Props>(), {})
const emits = defineEmits<{
'update:visible': [value: boolean]
'updated': []
}>()
const { visible, integration } = toRefs(props)
const { $api } = useNuxtApp()
const { t } = useI18n()
const workspaceStore = useWorkspace()
const { activeWorkspaceId } = storeToRefs(workspaceStore)
const basesStore = useBases()
const { basesList } = storeToRefs(basesStore)
const isLoading = ref(true)
const isSaving = ref(false)
const allBases = ref(true)
const selectedBaseIds = ref<Set<string>>(new Set())
const initialAllBases = ref(true)
const initialBaseIds = ref<Set<string>>(new Set())
const hasChanges = computed(() => {
if (allBases.value !== initialAllBases.value) return true
if (allBases.value) return false
if (selectedBaseIds.value.size !== initialBaseIds.value.size) return true
for (const id of selectedBaseIds.value) {
if (!initialBaseIds.value.has(id)) return true
}
return false
})
const listRef = ref<HTMLDivElement>()
const hasScrollableContent = ref(false)
function checkScrollable() {
if (!listRef.value) return
const { scrollTop, scrollHeight, clientHeight } = listRef.value
hasScrollableContent.value = scrollTop + clientHeight < scrollHeight - 4
}
const isOpen = computed({
get: () => visible.value,
set: (val) => emits('update:visible', val),
})
async function loadCurrentState() {
if (!activeWorkspaceId.value || !integration.value?.id) {
isLoading.value = false
return
}
try {
isLoading.value = true
const result = (await $api.internal.getOperation(activeWorkspaceId.value, NO_SCOPE, {
operation: 'integrationLinkedBaseList',
integrationId: integration.value.id,
})) as IntegrationLinkedBaseListResponse
if (result.all_bases) {
allBases.value = true
selectedBaseIds.value = new Set()
} else {
allBases.value = false
selectedBaseIds.value = new Set((result.bases || []).map((b) => b.id))
}
initialAllBases.value = allBases.value
initialBaseIds.value = new Set(selectedBaseIds.value)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
}
}
async function save() {
if (!activeWorkspaceId.value || !integration.value?.id) return
try {
isSaving.value = true
const payload = allBases.value ? { all_bases: true } : { base_ids: Array.from(selectedBaseIds.value) }
await $api.internal.postOperation(
activeWorkspaceId.value,
NO_SCOPE,
{ operation: 'integrationUpdateLinkedBases', integrationId: integration.value.id },
payload,
)
message.success(t('msg.success.updated'))
emits('updated')
isOpen.value = false
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isSaving.value = false
}
}
function toggleBase(baseId: string) {
if (selectedBaseIds.value.has(baseId)) {
selectedBaseIds.value.delete(baseId)
} else {
selectedBaseIds.value.add(baseId)
}
selectedBaseIds.value = new Set(selectedBaseIds.value)
}
watch(
visible,
(val) => {
if (val) {
loadCurrentState()
}
},
{ immediate: true },
)
watch(allBases, () => {
nextTick(checkScrollable)
})
</script>
<template>
<NcModal v-model:visible="isOpen" size="sm" wrap-class-name="nc-modal-base-assignment">
<template #header>
<span class="text-heading3">
{{ t('labels.manageBaseAccess') }}
</span>
</template>
<div v-if="isLoading" class="flex items-center justify-center py-8">
<GeneralLoader />
</div>
<div v-else class="flex flex-col flex-1 min-h-0 overflow-hidden">
<div
class="flex items-center justify-between p-3 rounded-lg border-1 cursor-pointer"
:class="allBases ? 'border-nc-border-brand bg-nc-bg-brand-soft' : 'border-nc-border-gray-medium'"
@click="allBases = !allBases"
>
<div class="flex flex-col gap-1">
<span class="text-sm font-semibold text-nc-content-gray">{{ t('activity.allBases') }}</span>
<span class="text-bodySm text-nc-content-gray-subtle">{{ t('labels.grantAccessToAllBases') }}</span>
</div>
<span @click.stop>
<NcSwitch v-model:checked="allBases" size="small" :disabled="isLoading" />
</span>
</div>
<template v-if="!allBases">
<div class="flex items-center justify-between mt-4 mb-2">
<span class="text-captionSm text-nc-content-gray-subtle2 uppercase tracking-wide">
{{ t('labels.selectBases') }}
</span>
<span class="text-bodySm text-nc-content-gray-subtle">
{{ selectedBaseIds.size }} / {{ basesList.length }}
</span>
</div>
<div class="relative flex-1 min-h-0">
<div
ref="listRef"
class="flex flex-col gap-1 h-full overflow-auto nc-scrollbar-thin"
@scroll="checkScrollable"
>
<div
v-for="base in basesList"
:key="base.id"
class="flex items-center gap-2.5 p-2 rounded-lg hover:bg-nc-bg-gray-light cursor-pointer"
@click="toggleBase(base.id!)"
>
<NcCheckbox :checked="selectedBaseIds.has(base.id!)" />
<GeneralProjectIcon :color="parseProp(base.meta).iconColor" :type="base.type" class="h-4.5 w-4.5 flex-none" />
<NcTooltip show-on-truncate-only class="truncate text-sm font-medium text-nc-content-gray">
{{ base.title }}
</NcTooltip>
</div>
<div v-if="!basesList.length" class="text-sm text-nc-content-gray-subtle2 py-2 text-center">
{{ t('labels.noData') }}
</div>
</div>
<div
v-if="hasScrollableContent"
class="absolute bottom-0 left-0 right-0 h-5 pointer-events-none"
style="background: linear-gradient(transparent, var(--nc-bg-default))"
/>
</div>
</template>
<!-- Footer -->
<div class="flex items-center justify-end gap-2 mt-auto pt-4">
<NcButton size="small" type="secondary" @click="isOpen = false">
{{ $t('general.cancel') }}
</NcButton>
<NcButton size="small" type="primary" :loading="isSaving" :disabled="!hasChanges" @click="save">
{{ $t('general.save') }}
</NcButton>
</div>
</div>
</NcModal>
</template>

View File

@@ -55,6 +55,19 @@ const toBeDeletedIntegration = ref<
const isLoadingGetLinkedSources = ref(false)
// Base assignment dialog state
const isBaseAssignmentOpen = ref(false)
const baseAssignmentIntegration = ref<IntegrationType | null>(null)
function openBaseAssignment(integration: IntegrationType) {
baseAssignmentIntegration.value = integration
isBaseAssignmentOpen.value = true
}
function onBaseAssignmentUpdated() {
loadIntegrations()
}
const tableWrapper = ref<HTMLDivElement>()
const orderBy = ref<Partial<Record<SortFields, 'asc' | 'desc' | undefined>>>({})
@@ -288,6 +301,16 @@ const columns = [
dataIndex: 'source_count',
showOrderBy: true,
},
...(isEeUI
? [
{
key: 'base_access',
title: t('labels.baseAccess'),
minWidth: 140,
width: 160,
},
]
: []),
{
key: 'action',
title: t('labels.actions'),
@@ -450,6 +473,29 @@ const customRow = (record: Record<string, any>) => ({
</NcTooltip>
</template>
<div v-if="column.key === 'base_access'" class="text-sm">
<NcBadge
v-if="!integration.is_restricted"
size="xs"
color="green"
:border="false"
class="cursor-pointer"
@click.stop="openBaseAssignment(integration)"
>
{{ $t('activity.allBases') }}
</NcBadge>
<NcBadge
v-else
size="xs"
color="gray"
:border="false"
class="cursor-pointer"
@click.stop="openBaseAssignment(integration)"
>
{{ $t('labels.restricted') }}
</NcBadge>
</div>
<div v-if="column.key === 'action'" @click.stop>
<NcDropdown placement="bottomRight">
<NcButton size="small" type="secondary">
@@ -464,6 +510,10 @@ const customRow = (record: Record<string, any>) => ({
<GeneralIcon class="text-current opacity-80" icon="star" />
<span>Set as default</span>
</NcMenuItem>
<NcMenuItem v-if="isEeUI" @click="openBaseAssignment(integration)">
<GeneralIcon class="text-current opacity-80" icon="ncDatabase" />
<span>{{ $t('labels.manageBaseAccess') }}</span>
</NcMenuItem>
<NcMenuItem
:disabled="!isFeatureEnabled(FEATURE_FLAG.DATA_REFLECTION) && integration.sub_type === SyncDataType.NOCODB"
@click="openEditIntegration(integration)"
@@ -644,6 +694,14 @@ const customRow = (record: Record<string, any>) => ({
</div>
</div>
</NcModal>
<!-- Base Assignment Dialog -->
<WorkspaceIntegrationsBaseAssignment
v-if="isEeUI && baseAssignmentIntegration"
v-model:visible="isBaseAssignmentOpen"
:integration="baseAssignmentIntegration"
@updated="onBaseAssignmentUpdated"
/>
</div>
</template>

View File

@@ -305,14 +305,17 @@ const createOrUpdateIntegration = async () => {
props.baseId,
)
} else {
await updateIntegration({
id: activeIntegration.value.id,
title: formState.value.title,
type: IntegrationsType.Database,
sub_type: formState.value.dataSource.client,
config,
is_private: formState.value.is_private,
})
await updateIntegration(
{
id: activeIntegration.value.id,
title: formState.value.title,
type: IntegrationsType.Database,
sub_type: formState.value.dataSource.client,
config,
is_private: formState.value.is_private,
},
props.baseId,
)
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))