Browse Source

feat: duplicate options

Signed-off-by: mertmit <mertmit99@gmail.com>
feat/export-nest
mertmit 2 years ago
parent
commit
0ba68de46b
  1. 17
      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. 24
      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

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

@ -392,7 +392,13 @@ const setIcon = async (icon: string, table: TableType) => {
const duplicateTable = async (table: TableType) => { const duplicateTable = async (table: TableType) => {
if (!table || !table.id || !table.project_id) return if (!table || !table.id || !table.project_id) return
const jobData = await $api.dbTable.duplicate(table.project_id, table.id)
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) => { $jobs.subscribe({ name: jobData.name, id: jobData.id }, undefined, async (status: string) => {
if (status === 'completed') { if (status === 'completed') {
await loadTables() await loadTables()
@ -401,6 +407,15 @@ const duplicateTable = async (table: TableType) => {
await loadTables() await loadTables()
} }
}) })
},
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
} }
</script> </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) => { const duplicateProject = (project: ProjectType) => {
Modal.confirm({ const isOpen = ref(true)
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 { close } = useDialog(resolveComponent('DlgProjectDuplicate'), {
'modelValue': isOpen,
'project': project,
'onOk': async (jobData: { name: string; id: string }) => {
try {
await loadProjects() 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') { if (status === 'completed') {
await loadProjects() await loadProjects()
} else if (status === 'failed') { } else if (status === 'failed') {
@ -109,7 +106,14 @@ const duplicateProject = (project: ProjectType) => {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
}, },
'onUpdate:modelValue': closeDialog,
}) })
function closeDialog() {
isOpen.value = false
close(1000)
}
} }
const handleProjectColor = async (projectId: string, color: string) => { 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> <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> <template #overlay>
<a-menu class="!py-0 rounded text-sm"> <a-menu class="!py-0 rounded text-sm">
@ -340,7 +348,7 @@ const copyProjectMeta = async () => {
</div> </div>
</template> </template>
<style scoped> <style lang="scss" scoped>
.nc-action-btn { .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; @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: ( baseDuplicate: (
projectId: IdType, projectId: IdType,
data: {
excludeData?: boolean;
excludeViews?: boolean;
excludeHooks?: boolean;
},
baseId?: IdType, baseId?: IdType,
params: RequestParams = {} params: RequestParams = {}
) => ) =>
@ -4046,6 +4051,8 @@ export class Api<
>({ >({
path: `/api/v1/db/meta/duplicate/${projectId}/${baseId}`, path: `/api/v1/db/meta/duplicate/${projectId}/${baseId}`,
method: 'POST', method: 'POST',
body: data,
type: ContentType.Json,
format: 'json', format: 'json',
...params, ...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< this.request<
{ {
name?: string; name?: string;
@ -4081,6 +4096,8 @@ export class Api<
>({ >({
path: `/api/v1/db/meta/duplicate/${projectId}`, path: `/api/v1/db/meta/duplicate/${projectId}`,
method: 'POST', method: 'POST',
body: data,
type: ContentType.Json,
format: 'json', format: 'json',
...params, ...params,
}), }),
@ -5100,6 +5117,10 @@ export class Api<
duplicate: ( duplicate: (
projectId: IdType, projectId: IdType,
tableId: IdType, tableId: IdType,
data: {
excludeData?: boolean;
excludeViews?: boolean;
},
params: RequestParams = {} params: RequestParams = {}
) => ) =>
this.request< this.request<
@ -5114,6 +5135,8 @@ export class Api<
>({ >({
path: `/api/v1/db/meta/duplicate/${projectId}/table/${tableId}`, path: `/api/v1/db/meta/duplicate/${projectId}/table/${tableId}`,
method: 'POST', method: 'POST',
body: data,
type: ContentType.Json,
format: 'json', format: 'json',
...params, ...params,
}), }),

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

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

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

@ -49,7 +49,11 @@ export class DuplicateProcessor {
async duplicateBase(job: Job) { async duplicateBase(job: Job) {
const hrTime = initTime(); 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 project = await Project.get(projectId);
const dupProject = await Project.get(dupProjectId); const dupProject = await Project.get(dupProjectId);
@ -69,6 +73,8 @@ export class DuplicateProcessor {
const exportedModels = await this.exportService.serializeModels({ const exportedModels = await this.exportService.serializeModels({
modelIds: models.map((m) => m.id), modelIds: models.map((m) => m.id),
excludeViews,
excludeHooks,
}); });
elapsedTime(hrTime, 'serializeModels'); elapsedTime(hrTime, 'serializeModels');
@ -81,8 +87,6 @@ export class DuplicateProcessor {
const dupBase = dupProject.bases[0]; const dupBase = dupProject.bases[0];
elapsedTime(hrTime, 'projectCreate');
const idMap = await this.importService.importModels({ const idMap = await this.importService.importModels({
user, user,
projectId: dupProject.id, projectId: dupProject.id,
@ -97,6 +101,7 @@ export class DuplicateProcessor {
throw new Error(`Import failed for base '${base.id}'`); throw new Error(`Import failed for base '${base.id}'`);
} }
if (!excludeData) {
await this.importModelsData({ await this.importModelsData({
idMap, idMap,
sourceProject: project, sourceProject: project,
@ -105,6 +110,7 @@ export class DuplicateProcessor {
destBase: dupBase, destBase: dupBase,
hrTime, hrTime,
}); });
}
await this.projectsService.projectUpdate({ await this.projectsService.projectUpdate({
projectId: dupProject.id, projectId: dupProject.id,
@ -126,7 +132,11 @@ export class DuplicateProcessor {
async duplicateModel(job: Job) { async duplicateModel(job: Job) {
const hrTime = initTime(); 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 project = await Project.get(projectId);
const base = await Base.get(baseId); const base = await Base.get(baseId);
@ -151,6 +161,8 @@ export class DuplicateProcessor {
const exportedModel = ( const exportedModel = (
await this.exportService.serializeModels({ await this.exportService.serializeModels({
modelIds: [modelId], modelIds: [modelId],
excludeViews,
excludeHooks,
}) })
)[0]; )[0];
@ -178,6 +190,7 @@ export class DuplicateProcessor {
throw new Error(`Import failed for model '${modelId}'`); throw new Error(`Import failed for model '${modelId}'`);
} }
if (!excludeData) {
const fields: Record<string, string[]> = {}; const fields: Record<string, string[]> = {};
for (const md of relatedModels) { for (const md of relatedModels) {
@ -208,8 +221,7 @@ export class DuplicateProcessor {
}); });
elapsedTime(hrTime, 'reimportModelData'); elapsedTime(hrTime, 'reimportModelData');
}
// console.log('exportedModel', exportedModel);
} }
async importModelsData(param: { 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 { export class ExportService {
constructor(private datasService: DatasService) {} constructor(private datasService: DatasService) {}
async serializeModels(param: { modelIds: string[] }) { async serializeModels(param: {
modelIds: string[];
excludeViews?: boolean;
excludeHooks?: boolean;
}) {
const { modelIds } = param; const { modelIds } = param;
const excludeViews = param?.excludeViews || false;
const serializedModels = []; const serializedModels = [];
// db id to structured id // db id to structured id
@ -53,6 +59,11 @@ export class ExportService {
await model.getColumns(); await model.getColumns();
await model.getViews(); 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) { for (const column of model.columns) {
await column.getColOptions(); await column.getColOptions();
if (column.colOptions) { if (column.colOptions) {

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

@ -2127,6 +2127,38 @@
"$ref": "#/components/responses/BadRequest" "$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"], "tags": ["Project"],
"description": "Duplicate a project", "description": "Duplicate a project",
"parameters": [ "parameters": [
@ -2185,6 +2217,38 @@
"$ref": "#/components/responses/BadRequest" "$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"], "tags": ["Project"],
"description": "Duplicate a project", "description": "Duplicate a project",
"parameters": [ "parameters": [
@ -3908,6 +3972,33 @@
"$ref": "#/components/responses/BadRequest" "$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"], "tags": ["DB Table"],
"description": "Duplicate a table", "description": "Duplicate a table",
"parameters": [ "parameters": [

Loading…
Cancel
Save