Browse Source

feat: duplicate options

Signed-off-by: mertmit <mertmit99@gmail.com>
feat/export-nest
mertmit 2 years ago
parent
commit
0ba68de46b
  1. 31
      packages/nc-gui/components/dashboard/TreeView.vue
  2. 78
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  3. 78
      packages/nc-gui/components/dlg/TableDuplicate.vue
  4. 32
      packages/nc-gui/pages/index/index/index.vue
  5. 25
      packages/nocodb-sdk/src/lib/Api.ts
  6. 16
      packages/nocodb/src/modules/jobs/export-import/duplicate.controller.ts
  7. 94
      packages/nocodb/src/modules/jobs/export-import/duplicate.processor.ts
  8. 13
      packages/nocodb/src/modules/jobs/export-import/export.service.ts
  9. 91
      packages/nocodb/src/schema/swagger.json

31
packages/nc-gui/components/dashboard/TreeView.vue

@ -392,15 +392,30 @@ const setIcon = async (icon: string, table: TableType) => {
const duplicateTable = async (table: TableType) => {
if (!table || !table.id || !table.project_id) return
const jobData = await $api.dbTable.duplicate(table.project_id, table.id)
$jobs.subscribe({ name: jobData.name, id: jobData.id }, undefined, async (status: string) => {
if (status === 'completed') {
await loadTables()
} else if (status === 'failed') {
message.error('Failed to duplicate table')
await loadTables()
}
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgTableDuplicate'), {
'modelValue': isOpen,
'table': table,
'onOk': async (jobData: { name: string; id: string }) => {
$jobs.subscribe({ name: jobData.name, id: jobData.id }, undefined, async (status: string) => {
if (status === 'completed') {
await loadTables()
} else if (status === 'failed') {
message.error('Failed to duplicate table')
await loadTables()
}
})
},
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
</script>

78
packages/nc-gui/components/dlg/ProjectDuplicate.vue

@ -0,0 +1,78 @@
<script setup lang="ts">
import type { ProjectType } from 'nocodb-sdk'
import { useVModel } from '#imports'
const props = defineProps<{
modelValue: boolean
project: ProjectType
onOk: (jobData: { name: string; id: string }) => Promise<void>
}>()
const emit = defineEmits(['update:modelValue'])
const { api } = useApi()
const dialogShow = useVModel(props, 'modelValue', emit)
const options = ref({
includeData: true,
includeViews: true,
includeHooks: true,
})
const optionsToExclude = computed(() => {
const { includeData, includeViews, includeHooks } = options.value
return {
excludeData: !includeData,
excludeViews: !includeViews,
excludeHooks: !includeHooks,
}
})
const isLoading = ref(false)
const _duplicate = async () => {
isLoading.value = true
const jobData = await api.project.duplicate(props.project.id as string, optionsToExclude.value)
props.onOk(jobData as any)
isLoading.value = false
dialogShow.value = false
}
</script>
<template>
<a-modal
v-model:visible="dialogShow"
:class="{ active: dialogShow }"
width="max(30vw, 600px)"
centered
wrap-class-name="nc-modal-project-duplicate"
@keydown.esc="dialogShow = false"
>
<template #footer>
<a-button key="back" size="large" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" size="large" type="primary" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</a-button>
</template>
<div class="pl-10 pr-10 pt-5">
<div class="prose-xl font-bold self-center my-4">{{ $t('general.duplicate') }}</div>
<div class="mb-2">Are you sure you want to duplicate the `{{ project.title }}` project?</div>
<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">Include data</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">Include views</a-checkbox>
<a-checkbox v-model:checked="options.includeHooks">Include webhooks</a-checkbox>
</div>
</div>
</a-modal>
</template>
<style scoped lang="scss"></style>

78
packages/nc-gui/components/dlg/TableDuplicate.vue

@ -0,0 +1,78 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import { useVModel } from '#imports'
const props = defineProps<{
modelValue: boolean
table: TableType
onOk: (jobData: { name: string; id: string }) => Promise<void>
}>()
const emit = defineEmits(['update:modelValue'])
const { api } = useApi()
const dialogShow = useVModel(props, 'modelValue', emit)
const options = ref({
includeData: true,
includeViews: true,
includeHooks: true,
})
const optionsToExclude = computed(() => {
const { includeData, includeViews, includeHooks } = options.value
return {
excludeData: !includeData,
excludeViews: !includeViews,
excludeHooks: !includeHooks,
}
})
const isLoading = ref(false)
const _duplicate = async () => {
isLoading.value = true
const jobData = await api.dbTable.duplicate(props.table.project_id!, props.table.id!, optionsToExclude.value)
props.onOk(jobData as any)
isLoading.value = false
dialogShow.value = false
}
</script>
<template>
<a-modal
v-model:visible="dialogShow"
:class="{ active: dialogShow }"
width="max(30vw, 600px)"
centered
wrap-class-name="nc-modal-table-duplicate"
@keydown.esc="dialogShow = false"
>
<template #footer>
<a-button key="back" size="large" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" size="large" type="primary" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</a-button>
</template>
<div class="pl-10 pr-10 pt-5">
<div class="prose-xl font-bold self-center my-4">{{ $t('general.duplicate') }}</div>
<div class="mb-2">Are you sure you want to duplicate the `{{ table.title }}` table?</div>
<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">Include data</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">Include views</a-checkbox>
<a-checkbox v-model:checked="options.includeHooks">Include hooks</a-checkbox>
</div>
</div>
</a-modal>
</template>
<style scoped lang="scss"></style>

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

@ -83,19 +83,16 @@ const deleteProject = (project: ProjectType) => {
}
const duplicateProject = (project: ProjectType) => {
Modal.confirm({
title: `Do you want to duplicate '${project.title}' project?`,
wrapClassName: 'nc-modal-project-duplicate',
okText: 'Yes',
okType: 'primary',
cancelText: 'No',
async onOk() {
try {
const jobData = await api.project.duplicate(project.id as string)
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgProjectDuplicate'), {
'modelValue': isOpen,
'project': project,
'onOk': async (jobData: { name: string; id: string }) => {
try {
await loadProjects()
$jobs.subscribe({ name: jobData.name, id: jobData.id }, null, async (status: string) => {
$jobs.subscribe({ name: jobData.name, id: jobData.id }, undefined, async (status: string) => {
if (status === 'completed') {
await loadProjects()
} else if (status === 'failed') {
@ -109,7 +106,14 @@ const duplicateProject = (project: ProjectType) => {
message.error(await extractSdkResponseErrorMsg(e))
}
},
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
const handleProjectColor = async (projectId: string, color: string) => {
@ -319,7 +323,11 @@ const copyProjectMeta = async () => {
/>
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-import-menu" @click.stop>
<GeneralIcon icon="threeDotVertical" class="nc-import-menu outline-0" :data-testid="`p-three-dot-${record.title}`"/>
<GeneralIcon
icon="threeDotVertical"
class="nc-import-menu outline-0"
:data-testid="`p-three-dot-${record.title}`"
/>
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
@ -340,7 +348,7 @@ const copyProjectMeta = async () => {
</div>
</template>
<style scoped>
<style lang="scss" scoped>
.nc-action-btn {
@apply text-gray-500 group-hover:text-accent active:(ring ring-accent ring-opacity-100) cursor-pointer p-2 w-[30px] h-[30px] hover:bg-gray-300/50 rounded-full;
}

25
packages/nocodb-sdk/src/lib/Api.ts

@ -4031,6 +4031,11 @@ export class Api<
*/
baseDuplicate: (
projectId: IdType,
data: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
},
baseId?: IdType,
params: RequestParams = {}
) =>
@ -4046,6 +4051,8 @@ export class Api<
>({
path: `/api/v1/db/meta/duplicate/${projectId}/${baseId}`,
method: 'POST',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),
@ -4068,7 +4075,15 @@ export class Api<
}`
*/
duplicate: (projectId: IdType, params: RequestParams = {}) =>
duplicate: (
projectId: IdType,
data: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
},
params: RequestParams = {}
) =>
this.request<
{
name?: string;
@ -4081,6 +4096,8 @@ export class Api<
>({
path: `/api/v1/db/meta/duplicate/${projectId}`,
method: 'POST',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),
@ -5100,6 +5117,10 @@ export class Api<
duplicate: (
projectId: IdType,
tableId: IdType,
data: {
excludeData?: boolean;
excludeViews?: boolean;
},
params: RequestParams = {}
) =>
this.request<
@ -5114,6 +5135,8 @@ export class Api<
>({
path: `/api/v1/db/meta/duplicate/${projectId}/table/${tableId}`,
method: 'POST',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),

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

@ -41,6 +41,12 @@ export class DuplicateController {
@Request() req,
@Param('projectId') projectId: string,
@Param('baseId') baseId?: string,
@Body()
options?: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
},
) {
const project = await Project.get(projectId);
@ -72,6 +78,7 @@ export class DuplicateController {
projectId: project.id,
baseId: base.id,
dupProjectId: dupProject.id,
options,
req: {
user: req.user,
clientIp: req.clientIp,
@ -88,6 +95,12 @@ export class DuplicateController {
@Request() req,
@Param('projectId') projectId: string,
@Param('modelId') modelId?: string,
@Body()
options?: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
},
) {
const project = await Project.get(projectId);
@ -114,11 +127,12 @@ export class DuplicateController {
projectId: project.id,
baseId: base.id,
modelId: model.id,
title: uniqueTitle,
options,
req: {
user: req.user,
clientIp: req.clientIp,
},
title: uniqueTitle,
});
return { id: job.id, name: job.name };

94
packages/nocodb/src/modules/jobs/export-import/duplicate.processor.ts

@ -49,7 +49,11 @@ export class DuplicateProcessor {
async duplicateBase(job: Job) {
const hrTime = initTime();
const { projectId, baseId, dupProjectId, req } = job.data;
const { projectId, baseId, dupProjectId, req, options } = job.data;
const excludeData = options?.excludeData || false;
const excludeHooks = options?.excludeHooks || false;
const excludeViews = options?.excludeViews || false;
const project = await Project.get(projectId);
const dupProject = await Project.get(dupProjectId);
@ -69,6 +73,8 @@ export class DuplicateProcessor {
const exportedModels = await this.exportService.serializeModels({
modelIds: models.map((m) => m.id),
excludeViews,
excludeHooks,
});
elapsedTime(hrTime, 'serializeModels');
@ -81,8 +87,6 @@ export class DuplicateProcessor {
const dupBase = dupProject.bases[0];
elapsedTime(hrTime, 'projectCreate');
const idMap = await this.importService.importModels({
user,
projectId: dupProject.id,
@ -97,14 +101,16 @@ export class DuplicateProcessor {
throw new Error(`Import failed for base '${base.id}'`);
}
await this.importModelsData({
idMap,
sourceProject: project,
sourceModels: models,
destProject: dupProject,
destBase: dupBase,
hrTime,
});
if (!excludeData) {
await this.importModelsData({
idMap,
sourceProject: project,
sourceModels: models,
destProject: dupProject,
destBase: dupBase,
hrTime,
});
}
await this.projectsService.projectUpdate({
projectId: dupProject.id,
@ -126,7 +132,11 @@ export class DuplicateProcessor {
async duplicateModel(job: Job) {
const hrTime = initTime();
const { projectId, baseId, modelId, title, req } = job.data;
const { projectId, baseId, modelId, title, req, options } = job.data;
const excludeData = options?.excludeData || false;
const excludeHooks = options?.excludeHooks || false;
const excludeViews = options?.excludeViews || false;
const project = await Project.get(projectId);
const base = await Base.get(baseId);
@ -151,6 +161,8 @@ export class DuplicateProcessor {
const exportedModel = (
await this.exportService.serializeModels({
modelIds: [modelId],
excludeViews,
excludeHooks,
})
)[0];
@ -178,38 +190,38 @@ export class DuplicateProcessor {
throw new Error(`Import failed for model '${modelId}'`);
}
const fields: Record<string, string[]> = {};
for (const md of relatedModels) {
const bts = md.columns
.filter(
(c) =>
c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions.type === 'bt' &&
c.colOptions.fk_related_model_id === modelId,
)
.map((c) => c.id);
if (bts.length > 0) {
fields[md.id] = [md.primaryKey.id];
fields[md.id].push(...bts);
if (!excludeData) {
const fields: Record<string, string[]> = {};
for (const md of relatedModels) {
const bts = md.columns
.filter(
(c) =>
c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions.type === 'bt' &&
c.colOptions.fk_related_model_id === modelId,
)
.map((c) => c.id);
if (bts.length > 0) {
fields[md.id] = [md.primaryKey.id];
fields[md.id].push(...bts);
}
}
}
await this.importModelsData({
idMap,
sourceProject: project,
sourceModels: [sourceModel],
destProject: project,
destBase: base,
hrTime,
modelFieldIds: fields,
externalModels: relatedModels,
});
elapsedTime(hrTime, 'reimportModelData');
await this.importModelsData({
idMap,
sourceProject: project,
sourceModels: [sourceModel],
destProject: project,
destBase: base,
hrTime,
modelFieldIds: fields,
externalModels: relatedModels,
});
// console.log('exportedModel', exportedModel);
elapsedTime(hrTime, 'reimportModelData');
}
}
async importModelsData(param: {

13
packages/nocodb/src/modules/jobs/export-import/export.service.ts

@ -19,9 +19,15 @@ import type { LinkToAnotherRecordColumn, View } from '../../../models';
export class ExportService {
constructor(private datasService: DatasService) {}
async serializeModels(param: { modelIds: string[] }) {
async serializeModels(param: {
modelIds: string[];
excludeViews?: boolean;
excludeHooks?: boolean;
}) {
const { modelIds } = param;
const excludeViews = param?.excludeViews || false;
const serializedModels = [];
// db id to structured id
@ -53,6 +59,11 @@ export class ExportService {
await model.getColumns();
await model.getViews();
// if views are excluded, filter all views except default
if (excludeViews) {
model.views = model.views.filter((v) => v.is_default);
}
for (const column of model.columns) {
await column.getColOptions();
if (column.colOptions) {

91
packages/nocodb/src/schema/swagger.json

@ -2127,6 +2127,38 @@
"$ref": "#/components/responses/BadRequest"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"excludeData": {
"type": "boolean",
"required": false
},
"excludeViews": {
"type": "boolean",
"required": false
},
"excludeHooks": {
"type": "boolean",
"required": false
}
}
},
"examples": {
"Example 1": {
"value": {
"excludeData": true,
"excludeViews": true,
"excludeHooks": true
}
}
}
}
}
},
"tags": ["Project"],
"description": "Duplicate a project",
"parameters": [
@ -2185,6 +2217,38 @@
"$ref": "#/components/responses/BadRequest"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"excludeData": {
"type": "boolean",
"required": false
},
"excludeViews": {
"type": "boolean",
"required": false
},
"excludeHooks": {
"type": "boolean",
"required": false
}
}
},
"examples": {
"Example 1": {
"value": {
"excludeData": true,
"excludeViews": true,
"excludeHooks": true
}
}
}
}
}
},
"tags": ["Project"],
"description": "Duplicate a project",
"parameters": [
@ -3908,6 +3972,33 @@
"$ref": "#/components/responses/BadRequest"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"excludeData": {
"type": "boolean",
"required": false
},
"excludeViews": {
"type": "boolean",
"required": false
}
}
},
"examples": {
"Example 1": {
"value": {
"excludeData": true,
"excludeViews": true
}
}
}
}
}
},
"tags": ["DB Table"],
"description": "Duplicate a table",
"parameters": [

Loading…
Cancel
Save