Browse Source

feat: copy shared base

pull/6634/head
mertmit 1 year ago
parent
commit
4d8c149fab
  1. 100
      packages/nc-gui/components/dlg/SharedBaseDuplicate.vue
  2. 2
      packages/nc-gui/components/general/ShareProject.vue
  3. 9
      packages/nc-gui/composables/useCopySharedBase.ts
  4. 21
      packages/nc-gui/pages/copy-shared-base.vue
  5. 72
      packages/nc-gui/pages/index.vue
  6. 67
      packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts
  7. 2
      packages/nocodb/src/utils/acl.ts

100
packages/nc-gui/components/dlg/SharedBaseDuplicate.vue

@ -0,0 +1,100 @@
<script setup lang="ts">
import { ProjectTypes } from 'nocodb-sdk'
import { isEeUI, useApi, useVModel, useWorkspace } from '#imports'
const props = defineProps<{
modelValue: boolean
onOk: (jobData: { name: string; id: string }) => Promise<void>
}>()
const emit = defineEmits(['update:modelValue'])
const { api } = useApi()
const { sharedBaseId } = useCopySharedBase()
const { workspacesList } = storeToRefs(useWorkspace())
const dialogShow = useVModel(props, 'modelValue', emit)
const options = ref({
includeData: true,
includeViews: true,
})
const optionsToExclude = computed(() => {
const { includeData, includeViews } = options.value
return {
excludeData: !includeData,
excludeViews: !includeViews,
}
})
const isLoading = ref(false)
const selectedWorkspace = ref<string>()
const _duplicate = async () => {
if (!selectedWorkspace.value && isEeUI) return
try {
isLoading.value = true
const jobData = await api.base.duplicateShared(selectedWorkspace.value ?? 'nc', sharedBaseId.value, {
options: optionsToExclude.value,
base: isEeUI
? {
fk_workspace_id: selectedWorkspace.value,
type: ProjectTypes.DATABASE,
}
: {},
})
sharedBaseId.value = null
props.onOk({ ...jobData, workspace_id: selectedWorkspace.value } as any)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
dialogShow.value = false
}
}
</script>
<template>
<GeneralModal v-model:visible="dialogShow" class="!w-[30rem]" wrap-class-name="nc-modal-project-duplicate">
<div>
<div class="prose-xl font-bold self-center">{{ $t('general.duplicate') }} {{ $t('labels.sharedBase') }}</div>
<template v-if="isEeUI">
<div class="my-4">Select workspace to duplicate shared base to:</div>
<NcSelect
v-model:value="selectedWorkspace"
class="w-full"
:options="workspacesList.map((w) => ({ label: w.title, value: w.id }))"
placeholder="Select Workspace"
/>
</template>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>
<a-divider class="!m-0 !p-0 !my-2" />
<div class="text-xs p-2">
<a-checkbox v-model:checked="options.includeData">{{ $t('labels.includeData') }}</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">{{ $t('labels.includeView') }}</a-checkbox>
</div>
</div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
key="submit"
v-e="['a:shared-base:duplicate']"
:loading="isLoading"
:disabled="!selectedWorkspace && isEeUI"
@click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>
</div>
</GeneralModal>
</template>

2
packages/nc-gui/components/general/ShareProject.vue

@ -71,7 +71,7 @@ const copySharedBase = async () => {
</NcButton> </NcButton>
</div> </div>
<template v-else-if="isSharedBase && isEeUI"> <template v-else-if="isSharedBase">
<div class="flex-1"></div> <div class="flex-1"></div>
<div class="flex flex-col justify-center h-full"> <div class="flex flex-col justify-center h-full">
<div class="flex flex-row items-center w-full"> <div class="flex flex-row items-center w-full">

9
packages/nc-gui/composables/useCopySharedBase.ts

@ -0,0 +1,9 @@
import { createSharedComposable, ref } from '#imports'
export const useCopySharedBase = createSharedComposable(() => {
const sharedBaseId = ref<string | null>(null)
return {
sharedBaseId,
}
})

21
packages/nc-gui/pages/copy-shared-base.vue

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { useBase, useCopySharedBase, useRoute } from '#imports'
const route = useRoute()
const { sharedBaseId } = useCopySharedBase()
const { forcedProjectId } = storeToRefs(useBase())
onMounted(() => {
sharedBaseId.value = route.query.base as string
if (forcedProjectId?.value) forcedProjectId.value = undefined
navigateTo(`/`)
})
</script>
<template>
<div></div>
</template>
<style scoped></style>

72
packages/nc-gui/pages/index.vue

@ -16,7 +16,9 @@ const basesStore = useBases()
const { populateWorkspace } = useWorkspace() const { populateWorkspace } = useWorkspace()
const { signedIn } = useGlobal() const { signedIn, ncNavigateTo } = useGlobal()
const { isUIAllowed } = useRoles()
const router = useRouter() const router = useRouter()
@ -46,6 +48,10 @@ const isSharedFormView = computed(() => {
return routeName.startsWith('index-typeOrId-form-viewId') return routeName.startsWith('index-typeOrId-form-viewId')
}) })
const { sharedBaseId } = useCopySharedBase()
const isDuplicateDlgOpen = ref(false)
async function handleRouteTypeIdChange() { async function handleRouteTypeIdChange() {
// avoid loading bases for shared views // avoid loading bases for shared views
if (isSharedView.value) { if (isSharedView.value) {
@ -82,7 +88,29 @@ watch(
// immediate watch, because if route is changed during page transition // immediate watch, because if route is changed during page transition
// It will error out nuxt // It will error out nuxt
onMounted(() => { onMounted(() => {
handleRouteTypeIdChange() if (route.value.query?.continueAfterSignIn) {
localStorage.removeItem('continueAfterSignIn')
return navigateTo(route.value.query.continueAfterSignIn as string)
} else {
const continueAfterSignIn = localStorage.getItem('continueAfterSignIn')
if (continueAfterSignIn) {
return navigateTo({
path: continueAfterSignIn,
query: route.value.query,
})
}
}
handleRouteTypeIdChange().then(() => {
if (sharedBaseId.value) {
if (!isUIAllowed('baseDuplicate')) {
message.error('You are not allowed to create base')
return
}
isDuplicateDlgOpen.value = true
}
})
}) })
function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: string) { function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: string) {
@ -93,6 +121,40 @@ function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: str
} }
provide(ToggleDialogInj, toggleDialog) provide(ToggleDialogInj, toggleDialog)
const { $e, $poller } = useNuxtApp()
const DlgSharedBaseDuplicateOnOk = async (jobData: { id: string; base_id: string; workspace_id: string }) => {
await populateWorkspace()
$poller.subscribe(
{ id: jobData.id },
async (data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
if (data.status === JobStatus.COMPLETED) {
await ncNavigateTo({
baseId: jobData.base_id,
})
} else if (data.status === JobStatus.FAILED) {
message.error('Failed to duplicate shared base')
await populateWorkspace()
}
}
},
)
$e('a:base:duplicate-shared-base')
}
</script> </script>
<template> <template>
@ -117,6 +179,12 @@ provide(ToggleDialogInj, toggleDialog)
v-model:data-sources-state="dataSourcesState" v-model:data-sources-state="dataSourcesState"
:base-id="baseId" :base-id="baseId"
/> />
<DlgSharedBaseDuplicate
v-if="isUIAllowed('baseDuplicate')"
v-model="isDuplicateDlgOpen"
:shared-base-id="sharedBaseId"
:on-ok="DlgSharedBaseDuplicateOnOk"
/>
</div> </div>
</template> </template>

67
packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts

@ -25,6 +25,73 @@ export class DuplicateController {
protected readonly basesService: BasesService, protected readonly basesService: BasesService,
) {} ) {}
@Post([
'/api/v1/db/meta/duplicate/:workspaceId/shared/:sharedBaseId',
'/api/v1/meta/duplicate/:workspaceId/shared/:sharedBaseId',
])
@HttpCode(200)
@Acl('duplicateSharedBase', {
scope: 'org',
})
public async duplicateSharedBase(
@Request() req,
@Param('workspaceId') _workspaceId: string,
@Param('sharedBaseId') sharedBaseId: string,
@Body()
body?: {
options?: {
excludeData?: boolean;
excludeViews?: boolean;
};
base?: any;
},
) {
const base = await Base.getByUuid(sharedBaseId);
if (!base) {
throw new Error(`Base not found for id '${sharedBaseId}'`);
}
const source = (await base.getBases())[0];
if (!source) {
throw new Error(`Source not found!`);
}
const bases = await Base.list({});
const uniqueTitle = generateUniqueName(
`${base.title} copy`,
bases.map((p) => p.title),
);
const dupProject = await this.basesService.baseCreate({
base: {
title: uniqueTitle,
status: ProjectStatus.JOB,
...(body.base || {}),
},
user: { id: req.user.id },
});
const job = await this.jobsService.add(JobTypes.DuplicateBase, {
baseId: base.id,
sourceId: source.id,
dupProjectId: dupProject.id,
options:
{
...body.options,
excludeHooks: true,
} || {},
req: {
user: req.user,
clientIp: req.clientIp,
},
});
return { id: job.id, base_id: dupProject.id };
}
@Post([ @Post([
'/api/v1/db/meta/duplicate/:baseId/:sourceId?', '/api/v1/db/meta/duplicate/:baseId/:sourceId?',
'/api/v1/meta/duplicate/:baseId/:sourceId?', '/api/v1/meta/duplicate/:baseId/:sourceId?',

2
packages/nocodb/src/utils/acl.ts

@ -36,6 +36,7 @@ const permissionScopes = {
'commandPalette', 'commandPalette',
'testConnection', 'testConnection',
'genericGPT', 'genericGPT',
'duplicateSharedBase',
// Cache // Cache
'cacheGet', 'cacheGet',
@ -245,6 +246,7 @@ const rolePermissions:
uploadViaURL: true, uploadViaURL: true,
isPluginActive: true, isPluginActive: true,
baseCreate: true, baseCreate: true,
duplicateSharedBase: true,
}, },
}, },
}; };

Loading…
Cancel
Save