Repository: nocodb/nocodb
Pull request #10853: feat: duplicate base ui refresh
Author: login: DarkPhoenix2704
Created: 2025-03-15T06:28:29Z | Merged: 2025-03-17T15:16:51Z

## Change Summary

Duplicate Base UI
Duplicate Table UI
Duplicate Column UI

## Change type

- [x] feat: (new feature for the user, not a new feature for build script)



Diff:
diff --git a/packages/nc-gui/assets/style.scss b/packages/nc-gui/assets/style.scss
index 07885f913a47..1a8c4f7cf137 100644
--- a/packages/nc-gui/assets/style.scss
+++ b/packages/nc-gui/assets/style.scss
@@ -985,7 +985,12 @@ svg.nc-virtual-cell-icon {
   }
 }
 
+.ant-switch {
+  background: #E7E7E9 !important;
+}
+
 .ant-switch-checked {
+  @apply !bg-nc-fill-primary;
   &.nc-ai-input {
     @apply !bg-nc-fill-purple-medium;
   }
diff --git a/packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue b/packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
index ac934a93a093..7ba69453c4fe 100644
--- a/packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
+++ b/packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
@@ -103,7 +103,7 @@ const [searchActive] = useToggle()
 const filterQuery = ref('')
 const keys = ref<Record<string, number>>({})
 const isTableDeleteDialogVisible = ref(false)
-const isProjectDeleteDialogVisible = ref(false)
+const isBaseDeleteDialogVisible = ref(false)
 
 const { refreshViewTabTitle } = useViewsStore()
 
@@ -388,7 +388,7 @@ function openErdView(source: SourceType) {
 
   const isOpen = ref(true)
 
-  const { close } = useDialog(resolveComponent('DlgProjectErd'), {
+  const { close } = useDialog(resolveComponent('DlgBaseErd'), {
     'modelValue': isOpen,
     'sourceId': source!.id,
     'onUpdate:modelValue': () => closeDialog(),
@@ -453,7 +453,7 @@ const tableDelete = () => {
 }
 
 const projectDelete = () => {
-  isProjectDeleteDialogVisible.value = true
+  isBaseDeleteDialogVisible.value = true
   $e('c:project:delete')
 }
 
@@ -1057,8 +1057,8 @@ const shouldOpenContextMenu = computed(() => {
     :table-id="contextMenuTarget.value?.id"
     :base-id="base?.id"
   />
-  <DlgProjectDelete v-model:visible="isProjectDeleteDialogVisible" :base-id="base?.id" />
-  <DlgProjectDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
+  <DlgBaseDelete v-model:visible="isBaseDeleteDialogVisible" :base-id="base?.id" />
+  <DlgBaseDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
   <GeneralModal v-model:visible="isErdModalOpen" size="large">
     <div class="h-[80vh]">
       <LazyDashboardSettingsErd :base-id="base?.id" :source-id="activeBaseId" />
diff --git a/packages/nc-gui/components/dlg/ProjectDelete.vue b/packages/nc-gui/components/dlg/Base/Delete.vue
similarity index 100%
rename from packages/nc-gui/components/dlg/ProjectDelete.vue
rename to packages/nc-gui/components/dlg/Base/Delete.vue
diff --git a/packages/nc-gui/components/dlg/Base/Duplicate.vue b/packages/nc-gui/components/dlg/Base/Duplicate.vue
new file mode 100644
index 000000000000..de5ef105254d
--- /dev/null
+++ b/packages/nc-gui/components/dlg/Base/Duplicate.vue
@@ -0,0 +1,389 @@
+<script setup lang="ts">
+import tinycolor from 'tinycolor2'
+import { type BaseType, WorkspaceUserRoles } from 'nocodb-sdk'
+
+const props = defineProps<{
+  modelValue: boolean
+  base: BaseType
+}>()
+
+const emit = defineEmits(['update:modelValue'])
+
+const dialogShow = useVModel(props, 'modelValue', emit)
+
+const { navigateToProject } = useGlobal()
+
+const { refreshCommandPalette } = useCommandPalette()
+
+const { api } = useApi()
+
+const { $e, $poller } = useNuxtApp()
+
+const basesStore = useBases()
+
+const { workspacesList, activeWorkspace } = useWorkspace()
+
+const { loadProjects, createProject: _createProject } = basesStore
+
+const { bases } = storeToRefs(basesStore)
+
+const options = ref({
+  includeData: true,
+  includeViews: true,
+  includeHooks: true,
+  includeComments: true,
+})
+const targetWorkspace = ref(activeWorkspace)
+
+const errorMessage = ref()
+
+// Used to handle different Action in different states in Modal
+// pending -> Initial state
+// loading -> Set when duplicate is triggered
+const status = ref<'pending' | 'success' | 'error' | 'loading'>('pending')
+
+const isEaster = ref(false)
+
+const dropdownOpen = ref(false)
+
+const optionsToExclude = computed(() => {
+  const { includeData, includeViews, includeHooks, includeComments } = options.value
+  return {
+    excludeData: !includeData,
+    excludeViews: !includeViews,
+    excludeHooks: !includeHooks,
+    excludeComments: !includeComments,
+  }
+})
+
+const workspaceOptions = computed(() => {
+  if (!isEeUI) return []
+  return workspacesList.filter((ws) =>
+    [WorkspaceUserRoles.CREATOR, WorkspaceUserRoles.OWNER].includes(ws.roles as WorkspaceUserRoles),
+  )
+})
+
+const isLoading = computed(() => status.value === 'loading')
+
+const targetBase = ref()
+
+const _duplicate = async () => {
+  try {
+    status.value = 'loading'
+    // pick a random color from array and assign to base
+    const color = baseThemeColors[Math.floor(Math.random() * 1000) % baseThemeColors.length]
+    const tcolor = tinycolor(color)
+
+    const complement = tcolor.complement()
+
+    const jobData = await api.base.duplicate(props.base.id as string, {
+      options: optionsToExclude.value,
+      base: {
+        fk_workspace_id: isEeUI
+          ? targetWorkspace.value?.id
+            ? targetWorkspace.value.id
+            : props.base.fk_workspace_id
+          : null,
+        type: props.base.type,
+        color,
+        meta: JSON.stringify({
+          theme: {
+            primaryColor: color,
+            accentColor: complement.toHex8String(),
+          },
+          iconColor: parseProp(props.base.meta).iconColor,
+        }),
+      },
+    })
+
+    $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) {
+            const resBases = await loadProjects('workspace', targetWorkspace?.value?.id)
+            targetBase.value = resBases.find((b) => b.id === jobData.base_id)
+            status.value = 'success'
+
+            refreshCommandPalette()
+          } else if (data.status === JobStatus.FAILED) {
+            status.value = 'error'
+            await loadProjects('workspace')
+            refreshCommandPalette()
+          }
+        }
+      },
+    )
+
+    $e('a:base:duplicate')
+  } catch (e: any) {
+    message.error(await extractSdkResponseErrorMsg(e))
+    errorMessage.value = await extractSdkResponseErrorMsg(e)
+    status.value = 'error'
+    dialogShow.value = false
+  }
+}
+
+const selectOption = (option: WorkspaceType) => {
+  targetWorkspace.value = option
+  dropdownOpen.value = false
+}
+
+const handleActionClick = () => {
+  switch (status.value) {
+    case 'pending': {
+      _duplicate()
+      break
+    }
+    case 'error': {
+      targetBase.value = null
+      errorMessage.value = null
+      status.value = 'pending'
+      break
+    }
+    case 'success': {
+      const base = targetBase.value
+      navigateToProject({
+        workspaceId: isEeUI ? base.fk_workspace_id : undefined,
+        baseId: base.id,
+        type: base.type,
+      })
+      dialogShow.value = false
+      break
+    }
+  }
+}
+
+watch(dialogShow, (newVal) => {
+  if (!newVal) {
+    status.value = 'pending'
+  }
+})
+
+onKeyStroke('Enter', () => {
+  // should only trigger this when our modal is open
+  if (dialogShow.value) {
+    _duplicate()
+  }
+})
+</script>
+
+<template>
+  <GeneralModal
+    v-if="base"
+    v-model:visible="dialogShow"
+    :mask-style="{
+      'background-color': 'rgba(0, 0, 0, 0.08)',
+    }"
+    :mask-closable="!isLoading"
+    :keyboard="!isLoading"
+    class="!w-[30rem]"
+    wrap-class-name="nc-modal-base-duplicate"
+  >
+    <div>
+      <div class="text-base text-nc-content-gray-emphasis leading-6 font-bold self-center" @dblclick="isEaster = !isEaster">
+        <template v-if="['pending', 'loading'].includes(status)">
+          {{ $t('labels.duplicateBaseBaseTitle', { baseTitle: base.title }) }}
+        </template>
+
+        <template v-else-if="status === 'success'">
+          <div class="flex items-center gap-2">
+            <GeneralIcon class="text-white w-6 h-6" icon="checkFill" />
+            <div class="text-nc-content-gray-emphasis font-semibold">
+              {{ $t('labels.duplicateBaseSuccessfull') }}
+            </div>
+          </div>
+        </template>
+        <template v-else-if="status === 'error'">
+          <div class="flex items-center gap-2">
+            <GeneralIcon icon="ncInfoSolid" class="flex-none !text-nc-content-red-dark w-6 h-6" />
+            <div class="text-nc-content-gray-emphasis font-semibold">
+              {{ $t('labels.duplicateBaseFailed') }}
+            </div>
+          </div>
+        </template>
+      </div>
+
+      <template v-if="['pending', 'loading'].includes(status)">
+        <div class="mt-5 flex gap-3 flex-col">
+          <div
+            class="flex gap-3 cursor-pointer leading-5 text-nc-content-gray font-medium items-center"
+            @click="options.includeData = !options.includeData"
+          >
+            <NcSwitch :checked="options.includeData" />
+            {{ $t('labels.includeRecords') }}
+          </div>
+
+          <template v-if="isEaster">
+            <div
+              class="flex gap-3 cursor-pointer leading-5 text-nc-content-gray font-medium items-center"
+              @click="options.includeViews = !options.includeViews"
+            >
+              <NcSwitch :checked="options.includeViews" />
+              {{ $t('labels.includeView') }}
+            </div>
+
+            <div
+              class="flex gap-3 cursor-pointer leading-5 text-nc-content-gray font-medium items-center"
+              @click="options.includeHooks = !options.includeHooks"
+            >
+              <NcSwitch :checked="options.includeHooks" />
+              {{ $t('labels.includeWebhook') }}
+            </div>
+          </template>
+
+          <div
+            class="flex gap-3 cursor-pointer leading-5 text-nc-content-gray font-medium items-center"
+            @click="options.includeComments = !options.includeComments"
+          >
+            <NcSwitch :checked="options.includeComments" />
+            {{ $t('labels.includeComments') }}
+          </div>
+        </div>
+
+        <div
+          :class="{
+            'mb-5': !isEeUI,
+          }"
+          class="mt-5 text-nc-content-gray-subtle2 font-medium"
+        >
+          {{ $t('labels.baseDuplicateMessage') }}
+        </div>
+
+        <div v-if="isEeUI" class="mb-5">
+          <NcDivider divider-class="!my-5" />
+
+          <div class="text-nc-content-gray font-medium leading-5">
+            {{ $t('labels.workspace') }}
+
+            <NcDropdown v-model:visible="dropdownOpen" class="mt-2">
+              <div
+                class="rounded-lg border-1 transition-all cursor-pointer flex items-center border-nc-border-grey-medium h-8 py-1 gap-2 px-3"
+                style="box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08)"
+                :class="{
+                  '!border-brand-500 !shadow-selected': dropdownOpen,
+                }"
+              >
+                <GeneralWorkspaceIcon size="small" :workspace="targetWorkspace" />
+
+                <div class="flex-1 capitalize truncate">
+                  {{ targetWorkspace?.title }}
+                </div>
+
+                <div class="flex gap-2 items-center">
+                  <div v-if="activeWorkspace?.id === targetWorkspace?.id" class="text-nc-content-gray-muted leading-4.5 text-xs">
+                    {{ $t('labels.currentWorkspace') }}
+                  </div>
+                  <GeneralIcon
+                    :class="{
+                      'transform rotate-180': dropdownOpen,
+                    }"
+                    class="text-nc-content-gray transition-all w-4 h-4"
+                    icon="ncChevronDown"
+                  />
+                </div>
+              </div>
+
+              <template #overlay>
+                <NcList
+                  :value="targetWorkspace"
+                  :item-height="28"
+                  close-on-select
+                  class="nc-base-workspace-selection"
+                  :min-items-for-search="6"
+                  container-class-name="w-full"
+                  :list="workspaceOptions"
+                  option-label-key="title"
+                >
+                  <template #listHeader>
+                    <div class="text-nc-content-gray-muted text-[13px] px-3 pt-2.5 pb-1.5 font-medium leading-5">
+                      {{ $t('labels.duplicateBaseMessage') }}
+                    </div>
+
+                    <NcDivider />
+                  </template>
+
+                  <template #listItem="{ option }">
+                    <div class="flex gap-2 w-full items-center" @click="selectOption(option)">
+                      <GeneralWorkspaceIcon :workspace="option" size="small" />
+
+                      <div class="flex-1 text-[13px] truncate font-semibold leading-5 capitalize w-full">
+                        {{ option.title }}
+                      </div>
+
+                      <div class="flex items-center gap-2">
+                        <div v-if="activeWorkspace?.id === option.id" class="text-nc-content-gray-muted leading-4.5 text-xs">
+                          {{ $t('labels.currentWorkspace') }}
+                        </div>
+                        <GeneralIcon v-if="option.id === targetWorkspace?.id" class="text-brand-500 w-4 h-4" icon="ncCheck" />
+                      </div>
+                    </div>
+                  </template>
+                </NcList>
+              </template>
+            </NcDropdown>
+          </div>
+        </div>
+      </template>
+
+      <template v-else-if="status === 'success'">
+        <div class="text-nc-content-gray-emphasis my-5 font-medium">
+          Base <span class="font-bold leading-5">"{{ base.title }}"</span> has finished duplication.
+        </div>
+      </template>
+
+      <template v-else-if="status === 'error'">
+        <div class="text-nc-content-gray-emphasis my-5 font-medium">{{ $t('labels.errorMessage') }} {{ errorMessage }}</div>
+      </template>
+    </div>
+    <div class="flex flex-row gap-x-2 justify-end">
+      <NcButton v-if="!isLoading" key="back" type="secondary" size="small" @click="dialogShow = false">
+        {{ $t('general.cancel') }}
+      </NcButton>
+      <NcButton
+        key="submit"
+        v-e="['a:base:duplicate']"
+        size="small"
+        :loading="isLoading"
+        :disabled="isLoading"
+        @click="handleActionClick"
+      >
+        <template v-if="status === 'pending'"> {{ $t('general.duplicate') }} {{ $t('objects.project') }} </template>
+        <template v-else-if="status === 'loading'"> Duplicating {{ $t('objects.project') }} </template>
+        <template v-else-if="status === 'success'"> {{ $t('labels.goToBase') }} </template>
+        <template v-else-if="status === 'error'"> {{ $t('labels.tryAgain') }} </template>
+      </NcButton>
+    </div>
+  </GeneralModal>
+</template>
+
+<style scoped lang="scss">
+:deep(.ant-modal-mask) {
+  @apply !bg-black !bg-opacity-[8%];
+}
+
+.nc-list-root {
+  @apply !w-[432px] !pt-0;
+}
+</style>
+
+<style lang="scss">
+.nc-base-workspace-selection {
+  .nc-list {
+    @apply !px-1;
+    .nc-list-item {
+      @apply !py-1;
+    }
+  }
+}
+</style>
diff --git a/packages/nc-gui/components/dlg/ProjectErd.vue b/packages/nc-gui/components/dlg/Base/Erd.vue
similarity index 100%
rename from packages/nc-gui/components/dlg/ProjectErd.vue
rename to packages/nc-gui/components/dlg/Base/Erd.vue
diff --git a/packages/nc-gui/components/dlg/ColumnDuplicate.vue b/packages/nc-gui/components/dlg/Column/Duplicate.vue
similarity index 73%
rename from packages/nc-gui/components/dlg/ColumnDuplicate.vue
rename to packages/nc-gui/components/dlg/Column/Duplicate.vue
index 3b500e7bc5c0..ede6d1ae87e4 100644
--- a/packages/nc-gui/components/dlg/ColumnDuplicate.vue
+++ b/packages/nc-gui/components/dlg/Column/Duplicate.vue
@@ -105,31 +105,39 @@ defineExpose({
   <GeneralModal
     v-model:visible="dialogShow"
     :class="{ active: dialogShow }"
-    :closable="!isLoading"
     :mask-closable="!isLoading"
     :keyboard="!isLoading"
     centered
+    :mask-style="{
+      'background-color': 'rgba(0, 0, 0, 0.08)',
+    }"
     wrap-class-name="nc-modal-column-duplicate"
     :footer="null"
     class="!w-[30rem]"
     @keydown.esc="dialogShow = false"
   >
     <div>
-      <div class="prose-xl font-bold self-center">{{ $t('general.duplicate') }} {{ $t('objects.column') }}</div>
-
-      <div class="mt-4">Are you sure you want to duplicate the field?</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-base text-nc-content-gray-emphasis leading-6 font-bold self-center">
+        {{ $t('general.duplicate') }} {{ $t('objects.column') }} "{{ column.title }}"
+      </div>
 
-      <div class="text-xs p-2">
-        <a-checkbox v-model:checked="options.includeData" :disabled="isLoading">{{ $t('labels.includeData') }}</a-checkbox>
+      <div class="mt-5 flex gap-3 flex-col">
+        <div
+          class="flex gap-3 cursor-pointer leading-5 text-nc-content-gray font-medium items-center"
+          @click="options.includeData = !options.includeData"
+        >
+          <NcSwitch :checked="options.includeData" />
+          {{ $t('labels.includeData') }}
+        </div>
       </div>
     </div>
-    <div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
-      <NcButton v-if="!isLoading" key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
-      <NcButton key="submit" type="primary" :loading="isLoading" @click="_duplicate">{{ $t('general.confirm') }} </NcButton>
+    <div class="flex flex-row gap-x-2 mt-5 justify-end">
+      <NcButton v-if="!isLoading" key="back" type="secondary" size="small" @click="dialogShow = false">
+        {{ $t('general.cancel') }}
+      </NcButton>
+      <NcButton key="submit" type="primary" size="small" :loading="isLoading" @click="_duplicate">
+        Duplicate Field
+      </NcButton>
     </div>
   </GeneralModal>
 </template>
diff --git a/packages/nc-gui/components/dlg/ColumnUpdateConfirm.vue b/packages/nc-gui/components/dlg/Column/UpdateConfirm.vue
similarity index 100%
rename from packages/nc-gui/components/dlg/ColumnUpdateConfirm.vue
rename to packages/nc-gui/components/dlg/Column/UpdateConfirm.vue
diff --git a/packages/nc-gui/components/dlg/ProjectDuplicate.vue b/packages/nc-gui/components/dlg/ProjectDuplicate.vue
deleted file mode 100644
index 2331bf6069ef..000000000000
--- a/packages/nc-gui/components/dlg/ProjectDuplicate.vue
+++ /dev/null
@@ -1,164 +0,0 @@
-<script setup lang="ts">
-import tinycolor from 'tinycolor2'
-import type { BaseType } from 'nocodb-sdk'
-
-const props = defineProps<{
-  modelValue: boolean
-  base: BaseType
-}>()
-
-const emit = defineEmits(['update:modelValue'])
-
-const { refreshCommandPalette } = useCommandPalette()
-
-const { api } = useApi()
-
-const { $e, $poller } = useNuxtApp()
-
-const basesStore = useBases()
-
-const { loadProjects, createProject: _createProject } = basesStore
-const { bases } = storeToRefs(basesStore)
-
-const { navigateToProject } = useGlobal()
-
-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 () => {
-  try {
-    isLoading.value = true
-    // pick a random color from array and assign to base
-    const color = baseThemeColors[Math.floor(Math.random() * 1000) % baseThemeColors.length]
-    const tcolor = tinycolor(color)
-
-    const complement = tcolor.complement()
-
-    const jobData = await api.base.duplicate(props.base.id as string, {
-      options: optionsToExclude.value,
-      base: {
-        fk_workspace_id: props.base.fk_workspace_id,
-        type: props.base.type,
-        color,
-        meta: JSON.stringify({
-          theme: {
-            primaryColor: color,
-            accentColor: complement.toHex8String(),
-          },
-          iconColor: parseProp(props.base.meta).iconColor,
-        }),
-      },
-    })
-
-    $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 loadProjects('workspace')
-            const base = bases.value.get(jobData.base_id)
-
-            // open project after duplication
-            if (base) {
-              await navigateToProject({
-                workspaceId: isEeUI ? base.fk_workspace_id : undefined,
-                baseId: base.id,
-                type: base.type,
-              })
-            }
-            refreshCommandPalette()
-            isLoading.value = false
-            dialogShow.value = false
-          } else if (data.status === JobStatus.FAILED) {
-            message.error('Failed to duplicate project')
-            await loadProjects('workspace')
-            refreshCommandPalette()
-            isLoading.value = false
-            dialogShow.value = false
-          }
-        }
-      },
-    )
-
-    $e('a:base:duplicate')
-  } catch (e: any) {
-    message.error(await extractSdkResponseErrorMsg(e))
-    isLoading.value = false
-    dialogShow.value = false
-  }
-}
-
-onKeyStroke('Enter', () => {
-  // should only trigger this when our modal is open
-  if (dialogShow.value) {
-    _duplicate()
-  }
-})
-
-const isEaster = ref(false)
-</script>
-
-<template>
-  <GeneralModal
-    v-if="base"
-    v-model:visible="dialogShow"
-    :mask-closable="!isLoading"
-    :keyboard="!isLoading"
-    class="!w-[30rem]"
-    wrap-class-name="nc-modal-base-duplicate"
-  >
-    <div>
-      <div class="font-medium text-lg text-gray-800 self-center" @dblclick="isEaster = !isEaster">
-        {{ $t('general.duplicate') }} {{ $t('objects.project') }}
-      </div>
-
-      <div class="mt-5">{{ $t('msg.warning.duplicateProject') }}</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" :disabled="isLoading">{{ $t('labels.includeData') }}</a-checkbox>
-        <a-checkbox v-model:checked="options.includeViews" :disabled="isLoading">{{ $t('labels.includeView') }}</a-checkbox>
-        <a-checkbox v-show="isEaster" v-model:checked="options.includeHooks" :disabled="isLoading">
-          {{ $t('labels.includeWebhook') }}
-        </a-checkbox>
-      </div>
-    </div>
-    <div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
-      <NcButton v-if="!isLoading" key="back" type="secondary" size="small" @click="dialogShow = false">{{
-        $t('general.cancel')
-      }}</NcButton>
-      <NcButton key="submit" v-e="['a:base:duplicate']" size="small" :loading="isLoading" @click="_duplicate"
-        >{{ $t('general.confirm') }}
-      </NcButton>
-    </div>
-  </GeneralModal>
-</template>
diff --git a/packages/nc-gui/components/dlg/TableCreate.vue b/packages/nc-gui/components/dlg/Table/Create.vue
similarity index 99%
rename from packages/nc-gui/components/dlg/TableCreate.vue
rename to packages/nc-gui/components/dlg/Table/Create.vue
index 9d4da5887022..ed3425281b07 100644
--- a/packages/nc-gui/components/dlg/TableCreate.vue
+++ b/packages/nc-gui/components/dlg/Table/Create.vue
@@ -781,8 +781,8 @@ const handleRefreshOnError = () => {
           <div v-else></div>
           <div class="flex gap-2 items-center">
             <NcButton type="secondary" size="small" :disabled="creating || isAiSaving" @click="dialogShow = false">{{
-              $t('general.cancel')
-            }}</NcButton>
+                $t('general.cancel')
+              }}</NcButton>
 
             <NcButton
               v-if="!aiMode"
@@ -810,11 +810,11 @@ const handleRefreshOnError = () => {
                   activeTabSelectedTables.length
                     ? activeTabSelectedTables.length > 1
                       ? $t('activity.createTables_plural', {
-                          count: activeTabSelectedTables.length,
-                        })
+                        count: activeTabSelectedTables.length,
+                      })
                       : $t('activity.createTables', {
-                          count: activeTabSelectedTables.length,
-                        })
+                        count: activeTabSelectedTables.length,
+                      })
                     : $t('activity.createTable')
                 }}
               </div>
diff --git a/packages/nc-gui/components/dlg/TableDelete.vue b/packages/nc-gui/components/dlg/Table/Delete.vue
similarity index 100%
rename from packages/nc-gui/components/dlg/TableDelete.vue
rename to packages/nc-gui/components/dlg/Table/Delete.vue
diff --git a/packages/nc-gui/components/dlg/TableDescriptionUpdate.vue b/packages/nc-gui/components/dlg/Table/DescriptionUpdate.vue
similarity index 100%
rename from packages/nc-gui/components/dlg/TableDescriptionUpdate.vue
rename to packages/nc-gui/components/dlg/Table/DescriptionUpdate.vue
diff --git a/packages/nc-gui/components/dlg/TableDuplicate.vue b/packages/nc-gui/components/dlg/Table/Duplicate.vue
similarity index 74%
rename from packages/nc-gui/components/dlg/TableDuplicate.vue
rename to packages/nc-gui/components/dlg/Table/Duplicate.vue
index 89599705d966..ce3eb7f41745 100644
--- a/packages/nc-gui/components/dlg/TableDuplicate.vue
+++ b/packages/nc-gui/components/dlg/Table/Duplicate.vue
@@ -133,35 +133,50 @@ const isEaster = ref(false)
     :keyboard="!isLoading"
     centered
     wrap-class-name="nc-modal-table-duplicate"
+    :mask-style="{
+      'background-color': 'rgba(0, 0, 0, 0.08)',
+    }"
     :footer="null"
     class="!w-[30rem]"
     @keydown.esc="dialogShow = false"
   >
     <div>
-      <div class="font-medium text-lg text-gray-800 self-center" @dblclick="isEaster = !isEaster">
-        {{ $t('general.duplicate') }} {{ $t('objects.table') }}
+      <div class="text-base text-nc-content-gray-emphasis leading-6 font-bold self-center" @dblclick="isEaster = !isEaster">
+        {{ $t('general.duplicate') }} {{ $t('objects.table') }} "{{ table.title }}"
       </div>
 
-      <div class="mt-5">{{ $t('msg.warning.duplicateTable') }}</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" :disabled="isLoading">{{ $t('labels.includeData') }}</a-checkbox>
-        <a-checkbox v-model:checked="options.includeViews" :disabled="isLoading">{{ $t('labels.includeView') }}</a-checkbox>
-        <a-checkbox v-show="isEaster" v-model:checked="options.includeHooks" :disabled="isLoading">
+      <div class="mt-5 flex gap-3 flex-col">
+        <div
+          class="flex gap-3 cursor-pointer leading-5 text-nc-content-gray font-medium items-center"
+          @click="options.includeData = !options.includeData"
+        >
+          <NcSwitch :checked="options.includeData" />
+          {{ $t('labels.includeRecords') }}
+        </div>
+        <div
+          class="flex gap-3 cursor-pointer leading-5 text-nc-content-gray font-medium items-center"
+          @click="options.includeViews = !options.includeViews"
+        >
+          <NcSwitch :checked="options.includeViews" />
+          {{ $t('labels.includeView') }}
+        </div>
+
+        <div
+          v-show="isEaster"
+          class="flex gap-3 cursor-pointer leading-5 text-nc-content-gray font-medium items-center"
+          @click="options.includeHooks = !options.includeHooks"
+        >
+          <NcSwitch :checked="options.includeHooks" />
           {{ $t('labels.includeWebhook') }}
-        </a-checkbox>
+        </div>
       </div>
     </div>
-    <div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
+    <div class="flex flex-row gap-x-2 mt-5 justify-end">
       <NcButton v-if="!isLoading" key="back" type="secondary" size="small" @click="dialogShow = false">{{
         $t('general.cancel')
       }}</NcButton>
-      <NcButton key="submit" v-e="['a:table:duplicate']" type="primary" size="small" :loading="isLoading" @click="_duplicate"
-        >{{ $t('general.confirm') }}
+      <NcButton key="submit" v-e="['a:table:duplicate']" type="primary" size="small" :loading="isLoading" @click="_duplicate">
+        Duplicate Table
       </NcButton>
     </div>
   </GeneralModal>
diff --git a/packages/nc-gui/components/dlg/TableMagic.vue b/packages/nc-gui/components/dlg/Table/Magic.vue
similarity index 100%
rename from packages/nc-gui/components/dlg/TableMagic.vue
rename to packages/nc-gui/components/dlg/Table/Magic.vue
diff --git a/packages/nc-gui/components/dlg/ViewCreate.vue b/packages/nc-gui/components/dlg/View/Create.vue
similarity index 98%
rename from packages/nc-gui/components/dlg/ViewCreate.vue
rename to packages/nc-gui/components/dlg/View/Create.vue
index 27261e39bd40..a1a204becace 100644
--- a/packages/nc-gui/components/dlg/ViewCreate.vue
+++ b/packages/nc-gui/components/dlg/View/Create.vue
@@ -428,8 +428,8 @@ onMounted(async () => {
           const relationColumn =
             metaId || meta.value?.id
               ? metas.value[metaId || meta.value?.id]?.columns?.find(
-                  (c: ColumnType) => c.id === (column?.colOptions as LookupType)?.fk_relation_column_id,
-                )
+                (c: ColumnType) => c.id === (column?.colOptions as LookupType)?.fk_relation_column_id,
+              )
               : undefined
 
           if (relationColumn?.colOptions?.fk_related_model_id) {
@@ -505,10 +505,10 @@ onMounted(async () => {
       if (props.type === ViewTypes.CALENDAR) {
         viewSelectFieldOptions.value = meta
           .value!.columns!.filter(
-            (el) =>
-              [UITypes.DateTime, UITypes.Date, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(el.uidt) ||
-              (el.uidt === UITypes.Formula && (el.colOptions as any)?.parsed_tree?.dataType === FormulaDataTypes.DATE),
-          )
+          (el) =>
+            [UITypes.DateTime, UITypes.Date, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(el.uidt) ||
+            (el.uidt === UITypes.Formula && (el.colOptions as any)?.parsed_tree?.dataType === FormulaDataTypes.DATE),
+        )
           .map((field) => {
             return {
               value: field.id,
@@ -1502,11 +1502,11 @@ const getPluralName = (name: string) => {
                 activeTabSelectedViews.length
                   ? activeTabSelectedViews.length > 1
                     ? $t('labels.createViews_plural', {
-                        count: activeTabSelectedViews.length,
-                      })
+                      count: activeTabSelectedViews.length,
+                    })
                     : $t('labels.createViews', {
-                        count: activeTabSelectedViews.length,
-                      })
+                      count: activeTabSelectedViews.length,
+                    })
                   : $t('labels.createView')
               }}
             </div>
diff --git a/packages/nc-gui/components/dlg/ViewDelete.vue b/packages/nc-gui/components/dlg/View/Delete.vue
similarity index 100%
rename from packages/nc-gui/components/dlg/ViewDelete.vue
rename to packages/nc-gui/components/dlg/View/Delete.vue
diff --git a/packages/nc-gui/components/dlg/ViewDescriptionUpdate.vue b/packages/nc-gui/components/dlg/View/DescriptionUpdate.vue
similarity index 100%
rename from packages/nc-gui/components/dlg/ViewDescriptionUpdate.vue
rename to packages/nc-gui/components/dlg/View/DescriptionUpdate.vue
diff --git a/packages/nc-gui/components/general/Modal.vue b/packages/nc-gui/components/general/Modal.vue
index 28279420ed7c..003aff278226 100644
--- a/packages/nc-gui/components/general/Modal.vue
+++ b/packages/nc-gui/components/general/Modal.vue
@@ -1,4 +1,6 @@
 <script lang="ts" setup>
+import type { CSSProperties } from '@vue/runtime-dom'
+
 const props = withDefaults(
   defineProps<{
     visible: boolean
@@ -6,6 +8,7 @@ const props = withDefaults(
     size?: 'small' | 'medium' | 'large' | 'xl'
     destroyOnClose?: boolean
     maskClosable?: boolean
+    maskStyle?: CSSProperties
     closable?: boolean
     keyboard?: boolean
   }>(),
@@ -77,6 +80,7 @@ const visible = useVModel(props, 'visible', emits)
     :closable="closable"
     :keyboard="keyboard"
     wrap-class-name="nc-modal-wrapper"
+    :mask-style="maskStyle"
     :footer="null"
     :destroy-on-close="destroyOnClose"
     :mask-closable="maskClosable"
diff --git a/packages/nc-gui/components/general/WorkspaceIcon.vue b/packages/nc-gui/components/general/WorkspaceIcon.vue
index 86097f071774..a4e6c7c93bfa 100644
--- a/packages/nc-gui/components/general/WorkspaceIcon.vue
+++ b/packages/nc-gui/components/general/WorkspaceIcon.vue
@@ -12,7 +12,7 @@ const props = withDefaults(
       iconType: IconType | string
     }
     hideLabel?: boolean
-    size?: 'small' | 'medium' | 'large' | 'xlarge'
+    size?: 'small' | 'medium' | 'large' | 'xlarge' | 'middle'
     isRounded?: boolean
     iconBgColor?: string
   }>(),
@@ -80,8 +80,10 @@ const size = computed(() => props.size || 'medium')
     :class="{
       'min-w-4 w-4 h-4 rounded': size === 'small',
       'min-w-6 w-6 h-6 rounded-md': size === 'medium',
+      'min-w-8 w-6 h-8 rounded-md': size === 'middle',
       'min-w-10 w-10 h-10 rounded-lg !text-base': size === 'large',
       'min-w-16 w-16 h-16 rounded-lg !text-4xl': size === 'xlarge',
+      'min-w-8 w-6 h-8 rounded-md': size === 'middle',
       '!rounded-[50%]': props.isRounded,
     }"
     :style="{
@@ -143,6 +145,7 @@ const size = computed(() => props.size || 'medium')
         :class="{
           'text-white': isColorDark(workspaceColor),
           'text-black': !isColorDark(workspaceColor),
+          'text-[8px]': size === 'small',
         }"
       >
         {{ workspace?.title?.slice(0, 2) }}
diff --git a/packages/nc-gui/components/nc/List/index.vue b/packages/nc-gui/components/nc/List/index.vue
index 0d5b6d644ef0..1dd80c7ece40 100644
--- a/packages/nc-gui/components/nc/List/index.vue
+++ b/packages/nc-gui/components/nc/List/index.vue
@@ -197,7 +197,6 @@ const handleResetHoverEffect = (clearActiveOption = false, newActiveIndex?: numb
  */
 const handleSelectOption = (option: NcListItemType, index?: number) => {
   if (!ncIsObject(option) || !(optionValueKey in option) || option.ncItemDisabled) return
-
   if (index !== undefined) {
     activeOptionIndex.value = index
   }
@@ -338,7 +337,7 @@ watch(
   <div
     ref="listRef"
     tabindex="0"
-    class="flex flex-col pt-2 w-64 !focus:(shadow-none outline-none)"
+    class="flex flex-col nc-list-root pt-2 w-64 !focus:(shadow-none outline-none)"
     @keydown.arrow-down.prevent="onArrowDown"
     @keydown.arrow-up.prevent="onArrowUp"
     @keydown.enter.prevent="handleSelectOption(list[activeOptionIndex])"
@@ -376,7 +375,7 @@ watch(
               <NcTooltip
                 v-for="{ data: option, index: idx } in virtualList"
                 :key="idx"
-                class="flex items-center gap-2 w-full py-2 px-2 rounded-md my-[2px] first-of-type:mt-0 last-of-type:mb-0"
+                class="flex items-center gap-2 nc-list-item w-full py-2 px-2 rounded-md my-[2px] first-of-type:mt-0 last-of-type:mb-0"
                 :class="[
                   `nc-list-option-${idx}`,
                   {
diff --git a/packages/nc-gui/components/nc/Switch.vue b/packages/nc-gui/components/nc/Switch.vue
index 0cffa9223f21..7cd0761420cb 100644
--- a/packages/nc-gui/components/nc/Switch.vue
+++ b/packages/nc-gui/components/nc/Switch.vue
@@ -57,6 +57,7 @@ const onChange = (e: boolean, updateValue = false) => {
     :class="{
       'size-xsmall': size === 'xsmall',
       'size-xxsmall': size === 'xxsmall',
+      'size-small': size === 'small',
     }"
     :loading="loading"
     v-bind="$attrs"
@@ -81,6 +82,28 @@ const onChange = (e: boolean, updateValue = false) => {
 </template>
 
 <style lang="scss" scoped>
+.size-small {
+  @apply h-4 min-w-[28px] leading-[14px];
+
+  :deep(.ant-switch-handle) {
+    @apply h-[12px] w-[12px] top-[2px] left-[calc(100%_-_26px)];
+  }
+
+  :deep(.ant-switch-inner) {
+    @apply !mr-[5px] !ml-[18px] !my-0;
+  }
+
+  &.ant-switch-checked {
+    :deep(.ant-switch-handle) {
+      @apply left-[calc(100%_-_14px)];
+    }
+
+    :deep(.ant-switch-inner) {
+      @apply !mr-[18px] !ml-[5px];
+    }
+  }
+}
+
 .size-xsmall {
   @apply h-3.5 min-w-[26px] leading-[14px];
 
@@ -123,4 +146,4 @@ const onChange = (e: boolean, updateValue = false) => {
     }
   }
 }
-</style>
+</style>
\ No newline at end of file
diff --git a/packages/nc-gui/components/workspace/ProjectList.vue b/packages/nc-gui/components/workspace/ProjectList.vue
index 340428f83f6b..6521eb993be2 100644
--- a/packages/nc-gui/components/workspace/ProjectList.vue
+++ b/packages/nc-gui/components/workspace/ProjectList.vue
@@ -20,11 +20,11 @@ const { $e } = useNuxtApp()
 
 const { isUIAllowed } = useRoles()
 
-const showProjectDeleteModal = ref(false)
+const showBaseDeleteModal = ref(false)
 
-const toBeDeletedProjectId = ref<string | undefined>()
+const toBeDeletedBaseId = ref<string | undefined>()
 
-const openProject = async (base: BaseType) => {
+const openBase = async (base: BaseType) => {
   navigateToProject({
     baseId: base.id!,
     type: base.type as NcProjectType,
@@ -44,11 +44,11 @@ const roleAlias = {
   [ProjectRoles.OWNER]: 'Base Owner',
 }
 
-const deleteProject = (base: BaseType) => {
+const deleteBase = (base: BaseType) => {
   $e('c:base:delete')
 
-  showProjectDeleteModal.value = true
-  toBeDeletedProjectId.value = base.id
+  showBaseDeleteModal.value = true
+  toBeDeletedBaseId.value = base.id
 }
 
 const renameInput = ref<HTMLInputElement>()
@@ -68,7 +68,7 @@ const disableEdit = (index: number) => {
 
 const customRow = (record: BaseType) => ({
   onClick: async () => {
-    openProject(record)
+    openBase(record)
 
     $e('a:base:open')
   },
@@ -156,7 +156,7 @@ function onProjectTitleClick(index: number) {
   clickCount++
   if (clickCount === 1) {
     timer = setTimeout(function () {
-      openProject(basesList.value![index])
+      openBase(basesList.value![index])
       clickCount = 0
     }, delay)
   } else {
@@ -352,7 +352,7 @@ const setColor = async (color: string, base: BaseType) => {
                 -->
                 <a-menu-item
                   v-if="isUIAllowed('baseDelete', { roles: [record.workspace_role, record.project_role].join() })"
-                  @click="deleteProject(record)"
+                  @click="deleteBase(record)"
                 >
                   <div class="nc-menu-item-wrapper text-red-500">
                     <GeneralIcon icon="delete" />
@@ -366,14 +366,14 @@ const setColor = async (color: string, base: BaseType) => {
         </template>
       </template>
     </a-table>
-    <DlgProjectDelete v-if="toBeDeletedProjectId" v-model:visible="showProjectDeleteModal" :base-id="toBeDeletedProjectId" />
+    <DlgBaseDelete v-if="toBeDeletedBaseId" v-model:visible="showBaseDeleteModal" :base-id="toBeDeletedBaseId" />
     <WorkspaceMoveProjectDlg
       v-if="selectedProjectToMove"
       v-model="isMoveDlgOpen"
       :base="selectedProjectToMove"
       @success="workspaceMoveProjectOnSuccess"
     />
-    <DlgProjectDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
+    <DlgBaseDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
   </div>
 </template>
 
diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json
index 52e719e97d35..282a10446720 100644
--- a/packages/nc-gui/lang/en.json
+++ b/packages/nc-gui/lang/en.json
@@ -513,6 +513,7 @@
     "isNotNull": "is not null"
   },
   "title": {
+    "confirmSnapshotRestore": "Confirm Snapshot Restore",
     "urlFilters": "URL Filters",
     "creatingScript": "Creating Script",
     "searchWebhook": "Search webhook",
@@ -685,6 +686,20 @@
     "reAssignViewModalSubtitle": "Once reassigned, current owner will no longer be able to edit the view configuration."
   },
   "labels": {
+    "restoringSnapshot": "Restoring Snapshot",
+    "note": "Note:",
+    "snapshotRestoreMessage1": "Restoring this snapshot will not affect the existing base.",
+    "snapshotRestoreMessage2": "On restore, a new base will be created in the selected workspace.",
+    "snapshotRestoreConfirmation": "Are you sure you want to restore this base snapshot.",
+    "tryAgain": "Try again",
+    "goToBase": "Go to Base",
+    "errorMessage": "Error message:",
+    "duplicateBaseBaseTitle": "Duplicate Base \"{baseTitle}\"",
+    "duplicateBaseSuccessfull": "Base Duplication Successfull",
+    "duplicateBaseFailed": "Base Duplication Failed",
+    "duplicateBaseMessage": "You can only duplicate bases into workspaces where you have creator access or above.",
+    "snapshotBaseRestoreMessage": "You can only restore into workspaces where you have creator access or above.",
+    "currentWorkspace": "Current workspace",
     "latitude": "Latitude",
     "longitude": "Longitude",
     "failed": "Failed",
@@ -1080,8 +1095,11 @@
     "apis": "APIs",
     "apiSnippet": "API Snippets",
     "includeData": "Include Data",
-    "includeView": "Include View",
-    "includeWebhook": "Include Webhook",
+    "includeRecords": "Include records",
+    "baseDuplicateMessage": "Note: Audit logs & integrations will not be duplicated.",
+    "includeView": "Include views",
+    "includeWebhook": "Include webhook",
+    "includeComments": "Include comments",
     "zoomInToViewColumns": "Zoom in to view columns",
     "embedInSite": "Embed this view in your site",
     "titleRequired": "title is required.",
diff --git a/packages/nc-gui/store/bases.ts b/packages/nc-gui/store/bases.ts
index 491314c9a54f..fb3d9a826892 100644
--- a/packages/nc-gui/store/bases.ts
+++ b/packages/nc-gui/store/bases.ts
@@ -108,7 +108,7 @@ export const useBases = defineStore('basesStore', () => {
     await api.auth.baseUserRemove(baseId, user.id)
   }
 
-  const loadProjects = async (page: 'recent' | 'shared' | 'starred' | 'workspace' = 'recent') => {
+  const loadProjects = async (page: 'recent' | 'shared' | 'starred' | 'workspace' = 'recent', _?: string) => {
     // if shared base then get the shared base and create a list
     if (route.value.params.typeOrId === 'base' && route.value.params.baseId) {
       try {
@@ -167,6 +167,8 @@ export const useBases = defineStore('basesStore', () => {
       }, new Map())
 
       await updateIfBaseOrderIsNullOrDuplicate()
+
+      return _projects
     } catch (e) {
       console.error(e)
       message.error(e.message)
diff --git a/packages/nocodb/src/modules/jobs/jobs.module.ts b/packages/nocodb/src/modules/jobs/jobs.module.ts
index 33c6fa37f41a..1b354b3d0b18 100644
--- a/packages/nocodb/src/modules/jobs/jobs.module.ts
+++ b/packages/nocodb/src/modules/jobs/jobs.module.ts
@@ -1,8 +1,8 @@
 import { forwardRef, Module } from '@nestjs/common';
 import { BullModule } from '@nestjs/bull';
-import { MigrateController } from 'src/modules/jobs/jobs/export-import/migrate.controller';
-import { MigrateService } from 'src/modules/jobs/jobs/export-import/migrate.service';
 import { RecoverDisconnectedTableNames } from './migration-jobs/nc_job_008_recover_disconnected_table_name';
+import { MigrateController } from '~/modules/jobs/jobs/export-import/migrate.controller';
+import { MigrateService } from '~/modules/jobs/jobs/export-import/migrate.service';
 import { NocoModule } from '~/modules/noco.module';
 import { getRedisURL, NC_REDIS_TYPE } from '~/helpers/redisHelpers';
 
@@ -13,6 +13,7 @@ import { AtImportController } from '~/modules/jobs/jobs/at-import/at-import.cont
 import { AtImportProcessor } from '~/modules/jobs/jobs/at-import/at-import.processor';
 import { DuplicateController } from '~/modules/jobs/jobs/export-import/duplicate.controller';
 import { DuplicateProcessor } from '~/modules/jobs/jobs/export-import/duplicate.processor';
+import { DuplicateService } from '~/modules/jobs/jobs/export-import/duplicate.service';
 import { MetaSyncController } from '~/modules/jobs/jobs/meta-sync/meta-sync.controller';
 import { MetaSyncProcessor } from '~/modules/jobs/jobs/meta-sync/meta-sync.processor';
 import { SourceCreateController } from '~/modules/jobs/jobs/source-create/source-create.controller';
@@ -101,6 +102,7 @@ export const JobsModuleMetadata = {
     ExportService,
     ImportService,
     DuplicateProcessor,
+    DuplicateService,
     MigrateService,
     AtImportProcessor,
     MetaSyncProcessor,
diff --git a/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts b/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts
index d78f1a00fdcf..bb596981bbf5 100644
--- a/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts
+++ b/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts
@@ -23,6 +23,7 @@ import { MetaTable, RootScopes } from '~/utils/globals';
 import { NcError } from '~/helpers/catchError';
 import Noco from '~/Noco';
 import { AppHooksService } from '~/services/app-hooks/app-hooks.service';
+import { DuplicateService } from '~/modules/jobs/jobs/export-import/duplicate.service';
 
 @Controller()
 @UseGuards(MetaApiLimiterGuard, GlobalGuard)
@@ -31,6 +32,7 @@ export class DuplicateController {
     @Inject('JobsService') protected readonly jobsService: IJobsService,
     protected readonly basesService: BasesService,
     protected readonly appHooksService: AppHooksService,
+    protected readonly duplicateService: DuplicateService,
   ) {}
 
   @Post([
@@ -135,71 +137,13 @@ export class DuplicateController {
       base?: any;
     },
   ) {
-    const base = await Base.get(context, baseId);
-
-    if (!base) {
-      throw new Error(`Base not found for id '${baseId}'`);
-    }
-
-    const source = sourceId
-      ? await Source.get(context, sourceId)
-      : (await base.getSources())[0];
-
-    if (!source) {
-      throw new Error(`Source not found!`);
-    }
-
-    const bases = await Base.list(context.workspace_id);
-
-    const uniqueTitle = generateUniqueName(
-      `${base.title} copy`,
-      bases.map((p) => p.title),
-    );
-
-    const parentAuditId = await Noco.ncMeta.genNanoid(MetaTable.AUDIT);
-
-    req.ncParentAuditId = parentAuditId;
-
-    const dupProject = await this.basesService.baseCreate({
-      base: {
-        title: uniqueTitle,
-        status: ProjectStatus.JOB,
-        ...(body.base || {}),
-      },
-      user: {
-        id: req.user.id,
-        email: req.user.email,
-        display_name: req.user.display_name,
-      },
-      req,
-    });
-
-    this.appHooksService.emit(AppEvents.BASE_DUPLICATE_START, {
-      sourceBase: base,
-      destBase: dupProject,
-      user: req.user,
-      req,
+    return await this.duplicateService.duplicateBase({
       context,
-      id: parentAuditId,
-      options: body?.options,
-    });
-
-    const job = await this.jobsService.add(JobTypes.DuplicateBase, {
-      context,
-      user: req.user,
-      baseId: base.id,
-      sourceId: source.id,
-      dupProjectId: dupProject.id,
-      options: body.options || {},
-      req: {
-        user: req.user,
-        clientIp: req.clientIp,
-        headers: req.headers,
-        ncParentAuditId: parentAuditId,
-      },
+      req,
+      baseId,
+      sourceId,
+      body,
     });
-
-    return { id: job.id, base_id: dupProject.id };
   }
 
   @Post([
diff --git a/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts b/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
index 3a66818466f0..9aee9fb33c59 100644
--- a/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
+++ b/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
@@ -8,7 +8,7 @@ import {
   isVirtualCol,
   RelationTypes,
 } from 'nocodb-sdk';
-import { Injectable } from '@nestjs/common';
+import { Injectable, NotImplementedException } from '@nestjs/common';
 import type { Job } from 'bull';
 import type { NcContext, NcRequest } from '~/interface/config';
 import type {
@@ -32,17 +32,35 @@ import { AppHooksService } from '~/services/app-hooks/app-hooks.service';
 
 @Injectable()
 export class DuplicateProcessor {
-  private readonly debugLog = debug('nc:jobs:duplicate');
+  protected readonly debugLog = debug('nc:jobs:duplicate');
 
   constructor(
-    private readonly exportService: ExportService,
-    private readonly importService: ImportService,
-    private readonly projectsService: BasesService,
-    private readonly bulkDataService: BulkDataAliasService,
-    private readonly columnsService: ColumnsService,
-    private readonly appHooksService: AppHooksService,
+    protected readonly exportService: ExportService,
+    protected readonly importService: ImportService,
+    protected readonly projectsService: BasesService,
+    protected readonly bulkDataService: BulkDataAliasService,
+    protected readonly columnsService: ColumnsService,
+    protected readonly appHooksService: AppHooksService,
   ) {}
 
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  protected async handleDuplicateDifferentWs(params: {
+    sourceBase: Base; // Base to duplicate
+    targetBase: Base; // Base to duplicate to
+    dataSource: Source; // Data source to duplicate from
+    req: NcRequest;
+    context: NcContext; // Context of the base to duplicate
+    targetContext?: NcContext; // Context of the base to duplicate to
+    options: {
+      excludeData?: boolean;
+      excludeHooks?: boolean;
+      excludeViews?: boolean;
+      excludeComments?: boolean;
+    };
+  }) {
+    throw new NotImplementedException();
+  }
+
   async duplicateBaseJob({
     sourceBase,
     targetBase,
@@ -65,7 +83,7 @@ export class DuplicateProcessor {
       excludeViews?: boolean;
       excludeComments?: boolean;
     };
-    operation: string;
+    operation: JobTypes;
   }) {
     const hrTime = initTime();
 
@@ -74,6 +92,21 @@ export class DuplicateProcessor {
       base_id: targetBase.id,
     };
 
+    if (
+      [JobTypes.DuplicateBase, JobTypes.RestoreSnapshot].includes(operation) &&
+      targetContext.workspace_id !== sourceBase.fk_workspace_id
+    ) {
+      await this.handleDuplicateDifferentWs({
+        sourceBase,
+        targetBase,
+        dataSource,
+        req,
+        context,
+        targetContext,
+        options,
+      });
+    }
+
     try {
       if (!sourceBase || !targetBase || !dataSource) {
         throw new Error(`Base or source not found!`);
diff --git a/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.service.ts b/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.service.ts
new file mode 100644
index 000000000000..68575f2dbfd1
--- /dev/null
+++ b/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.service.ts
@@ -0,0 +1,138 @@
+import { Inject, Injectable, NotImplementedException } from '@nestjs/common';
+import {
+  AppEvents,
+  type NcContext,
+  type NcRequest,
+  ProjectStatus,
+} from 'nocodb-sdk';
+import { Base, Source } from '~/models';
+import Noco from '~/Noco';
+import { MetaTable } from '~/cli';
+import { generateUniqueName } from '~/helpers/exportImportHelpers';
+import { JobTypes } from '~/interface/Jobs';
+import { AppHooksService } from '~/services/app-hooks/app-hooks.service';
+import { BasesService } from '~/services/bases.service';
+import { IJobsService } from '~/modules/jobs/jobs-service.interface';
+
+@Injectable()
+export class DuplicateService {
+  constructor(
+    @Inject('JobsService') protected readonly jobsService: IJobsService,
+    protected readonly basesService: BasesService,
+    protected readonly appHooksService: AppHooksService,
+  ) {}
+
+  async duplicateBase({
+    context,
+    req,
+    baseId,
+    sourceId,
+    body,
+  }: {
+    context: NcContext;
+    req: NcRequest;
+    baseId: string;
+    sourceId?: string;
+    body?: {
+      options?: {
+        excludeData?: boolean;
+        excludeViews?: boolean;
+        excludeHooks?: boolean;
+      };
+      // override duplicated base
+      base?: any;
+    };
+  }) {
+    const base = await Base.get(context, baseId);
+
+    if (!base) {
+      throw new Error(`Base not found for id '${baseId}'`);
+    }
+
+    const source = sourceId
+      ? await Source.get(context, sourceId)
+      : (await base.getSources())[0];
+
+    if (!source) {
+      throw new Error(`Source not found!`);
+    }
+
+    if (
+      body.base?.fk_workspace_id &&
+      body.base?.fk_workspace_id !== '' &&
+      (base.fk_workspace_id || body.base?.fk_workspace_id) &&
+      base.fk_workspace_id !== body.base?.fk_workspace_id
+    ) {
+      await this.handleDifferentWs({
+        context,
+        req,
+        sourceBase: base,
+        targetBase: body.base,
+      });
+    }
+    const targetWorkspaceId =
+      body.base?.fk_workspace_id ?? context.workspace_id;
+    const bases = await Base.list(targetWorkspaceId);
+
+    const targetBaseTitle = body.base?.title ?? base.title;
+    const uniqueTitle = generateUniqueName(
+      `${targetBaseTitle} copy`,
+      bases.map((p) => p.title),
+    );
+
+    const parentAuditId = await Noco.ncMeta.genNanoid(MetaTable.AUDIT);
+
+    req.ncParentAuditId = parentAuditId;
+
+    const dupProject = await this.basesService.baseCreate({
+      base: {
+        title: uniqueTitle,
+        status: ProjectStatus.JOB,
+        ...(body.base || {}),
+      },
+      user: {
+        id: req.user.id,
+        email: req.user.email,
+        display_name: req.user.display_name,
+      },
+      req,
+    });
+
+    this.appHooksService.emit(AppEvents.BASE_DUPLICATE_START, {
+      sourceBase: base,
+      destBase: dupProject,
+      user: req.user,
+      req,
+      context,
+      id: parentAuditId,
+      options: body?.options,
+    });
+
+    const job = await this.jobsService.add(JobTypes.DuplicateBase, {
+      context,
+      user: req.user,
+      baseId: base.id,
+      sourceId: source.id,
+      dupProjectId: dupProject.id,
+      options: body.options || {},
+      req: {
+        user: req.user,
+        clientIp: req.clientIp,
+        headers: req.headers,
+        ncParentAuditId: parentAuditId,
+      },
+    });
+
+    return { id: job.id, base_id: dupProject.id };
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  protected async handleDifferentWs(params: {
+    sourceBase: Base;
+    targetBase: Base;
+    context: NcContext;
+    req: NcRequest;
+  }) {
+    throw new NotImplementedException();
+  }
+}
diff --git a/packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts b/packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
index d8df2ba60dd7..9cbbd1f6f5c2 100644
--- a/packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
+++ b/packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
@@ -1,4 +1,4 @@
-import { Injectable, Logger } from '@nestjs/common';
+import { Injectable, Logger, NotImplementedException } from '@nestjs/common';
 import debug from 'debug';
 import {
   isAIPromptCol,
@@ -58,23 +58,38 @@ export class ImportService {
   protected readonly logger = new Logger(ImportService.name);
 
   constructor(
-    private tablesService: TablesService,
-    private columnsService: ColumnsService,
-    private filtersService: FiltersService,
-    private sortsService: SortsService,
-    private viewColumnsService: ViewColumnsService,
-    private gridColumnsService: GridColumnsService,
-    private formColumnsService: FormColumnsService,
-    private gridsService: GridsService,
-    private formsService: FormsService,
-    private galleriesService: GalleriesService,
-    private calendarsService: CalendarsService,
-    private kanbansService: KanbansService,
-    private bulkDataService: BulkDataAliasService,
-    private hooksService: HooksService,
-    private viewsService: ViewsService,
+    protected tablesService: TablesService,
+    protected columnsService: ColumnsService,
+    protected filtersService: FiltersService,
+    protected sortsService: SortsService,
+    protected viewColumnsService: ViewColumnsService,
+    protected gridColumnsService: GridColumnsService,
+    protected formColumnsService: FormColumnsService,
+    protected gridsService: GridsService,
+    protected formsService: FormsService,
+    protected galleriesService: GalleriesService,
+    protected calendarsService: CalendarsService,
+    protected kanbansService: KanbansService,
+    protected bulkDataService: BulkDataAliasService,
+    protected hooksService: HooksService,
+    protected viewsService: ViewsService,
   ) {}
 
+  async importUsers(
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    context: NcContext,
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    payload: {
+      users: {
+        email: string;
+        display_name?: string;
+      }[];
+      req: NcRequest;
+    },
+  ) {
+    throw new NotImplementedException();
+  }
+
   async importModels(
     context: NcContext,
     param: {
diff --git a/tests/playwright/pages/Dashboard/Grid/Column/index.ts b/tests/playwright/pages/Dashboard/Grid/Column/index.ts
index 7d01c2233b5c..4834e0460daa 100644
--- a/tests/playwright/pages/Dashboard/Grid/Column/index.ts
+++ b/tests/playwright/pages/Dashboard/Grid/Column/index.ts
@@ -463,7 +463,7 @@ export class ColumnPageObject extends BasePage {
     await this.grid.get().locator(`th[data-title="${title}"] .nc-ui-dt-dropdown`).click();
     await this.rootPage.locator('li[role="menuitem"]:has-text("Duplicate"):visible').click();
 
-    await this.rootPage.locator('.nc-modal-column-duplicate .nc-button:has-text("Confirm"):visible').click();
+    await this.rootPage.locator('.nc-modal-column-duplicate .nc-button:has-text("Duplicate Field"):visible').click();
 
     // await this.verifyToast({ message: 'Column duplicated successfully' });
     await this.grid.get().locator(`th[data-title="${expectedTitle}"]`).waitFor({ state: 'visible', timeout: 10000 });
diff --git a/tests/playwright/pages/Dashboard/TreeView.ts b/tests/playwright/pages/Dashboard/TreeView.ts
index a09c37f514af..7dce50d3f80c 100644
--- a/tests/playwright/pages/Dashboard/TreeView.ts
+++ b/tests/playwright/pages/Dashboard/TreeView.ts
@@ -370,9 +370,11 @@ export class TreeViewPage extends BasePage {
     await contextMenu.waitFor();
     await contextMenu.locator(`.ant-dropdown-menu-item:has-text("Duplicate")`).click();
 
-    await this.rootPage.locator('div.ant-modal-content').locator(`button.ant-btn:has-text("Confirm")`).click();
+    await this.rootPage.locator('div.ant-modal-content').locator(`button.ant-btn:has-text("Duplicate Base")`).click();
 
     await this.rootPage.waitForTimeout(10000);
+
+    await this.rootPage.locator('div.ant-modal-content').locator(`button.ant-btn:has-text("Go to Base")`).click();
   }
 
   async openProjectSourceSettings(param: { title: string; context: NcContext }) {


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>packages/nc-gui/components/nc/List/index.vue (1)</summary><blockquote>

`198-200`: _⚠️ Potential issue_

**Option validation removed from handleSelectOption function.**

The critical validation check `if (!ncIsObject(option) || !(optionValueKey in option) || option.ncItemDisabled) return` has been removed. This could potentially cause runtime errors when invalid options are processed.


Restore the validation check to prevent potential errors:

```diff
const handleSelectOption = (option: NcListItemType, index?: number) => {
+  if (!ncIsObject(option) || !(optionValueKey in option) || option.ncItemDisabled) return
  if (index !== undefined) {
    activeOptionIndex.value = index
  }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (7)</summary><blockquote>

<details>
<summary>packages/nc-gui/components/dlg/Base/Duplicate.vue (3)</summary><blockquote>

`70-132`: **Consider a completion or timeout mechanism in `_duplicate`.**

The poller subscription handles `COMPLETED` and `FAILED` statuses but does not explicitly handle situations where the duplicate job remains in a stale or otherwise unresponsive state. Consider adding a timeout or fallback to ensure that the UI transitions out of "loading" if the job never updates the status.

---

`197-200`: **Fix spelling of "Successful" in success message.**

There's a small typo in line 199 where "Successfull" should be "Successful".  

```diff
-            <div class="text-nc-content-gray-emphasis font-semibold">Base Duplication Successfull</div>
+            <div class="text-nc-content-gray-emphasis font-semibold">Base Duplication Successful</div>
```

---

`353-355`: **Ensure consistent wording for Base duplication.**

Within the duplication button text, `$t('objects.project')` is used instead of "Base," which may confuse users if the rest of the UI references "Base."  

```diff
-<template v-if="status === 'pending'"> {{ $t('general.duplicate') }} {{ $t('objects.project') }} </template>
+<template v-if="status === 'pending'"> {{ $t('general.duplicate') }} {{ $t('objects.base') }} </template>
```

</blockquote></details>
<details>
<summary>packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts (1)</summary><blockquote>

`140-146`: **Remove unnecessary `await` when directly returning the Promise.**

When returning the result of an async call without further processing, there's no strong need for `await`. You could simplify to `return this.duplicateService.duplicateBase(...)`.  

```diff
-    return await this.duplicateService.duplicateBase({
+    return this.duplicateService.duplicateBase({
       context,
       req,
       baseId,
       sourceId,
       body,
     });
```

</blockquote></details>
<details>
<summary>packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue (1)</summary><blockquote>

`1061-1061`: **Ensure naming consistency for duplication.**
The `<DlgBaseDuplicate>` component and `isDuplicateDlgOpen` align with the rename to “Base,” but consider renaming `selectedProjectToDuplicate` to `selectedBaseToDuplicate` for total consistency.

</blockquote></details>
<details>
<summary>packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.service.ts (1)</summary><blockquote>

`129-137`: **Unimplemented cross-workspace logic.**
Throwing `NotImplementedException` clarifies that cross-workspace duplications are not supported at the moment. Consider implementing or removing the possibility of specifying a different workspace if not truly supported.

</blockquote></details>
<details>
<summary>packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts (1)</summary><blockquote>

`47-62`: **Added new method for cross-workspace duplication.**

The new `handleDuplicateDifferentWs` method has been added to handle duplication of bases between different workspaces. Currently, it throws a `NotImplementedException`, indicating that this functionality will be implemented in the future.

This is good practice to create the method signature and document expected parameters, even before implementation is complete.

Consider adding a TODO comment or JIRA ticket reference to track the future implementation of this method.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 392c6e2ed2bd7b596067844047db336522bcdb6d and 80bbd27895d161b801d1951393a5f529d138eb7c.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `packages/nc-gui/lang/en.json` is excluded by `!**/*.json`

</details>

<details>
<summary>📒 Files selected for processing (21)</summary>

* `packages/nc-gui/assets/style.scss` (1 hunks)
* `packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue` (3 hunks)
* `packages/nc-gui/components/dlg/Base/Duplicate.vue` (1 hunks)
* `packages/nc-gui/components/dlg/Column/Duplicate.vue` (1 hunks)
* `packages/nc-gui/components/dlg/ProjectDuplicate.vue` (0 hunks)
* `packages/nc-gui/components/dlg/Table/Create.vue` (2 hunks)
* `packages/nc-gui/components/dlg/Table/Duplicate.vue` (1 hunks)
* `packages/nc-gui/components/dlg/View/Create.vue` (3 hunks)
* `packages/nc-gui/components/general/Modal.vue` (2 hunks)
* `packages/nc-gui/components/general/WorkspaceIcon.vue` (3 hunks)
* `packages/nc-gui/components/nc/List/index.vue` (2 hunks)
* `packages/nc-gui/components/nc/Switch.vue` (3 hunks)
* `packages/nc-gui/components/workspace/ProjectList.vue` (6 hunks)
* `packages/nc-gui/store/bases.ts` (1 hunks)
* `packages/nocodb/src/modules/jobs/jobs.module.ts` (3 hunks)
* `packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts` (3 hunks)
* `packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts` (3 hunks)
* `packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.service.ts` (1 hunks)
* `packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts` (2 hunks)
* `tests/playwright/pages/Dashboard/Grid/Column/index.ts` (1 hunks)
* `tests/playwright/pages/Dashboard/TreeView.ts` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

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

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: release-docker / buildx
* GitHub Check: pre-build-for-playwright / playwright
* GitHub Check: unit-tests-pg
* GitHub Check: unit-tests
* GitHub Check: jest-unit-test

</details>

<details>
<summary>🔇 Additional comments (57)</summary><blockquote>

<details>
<summary>packages/nc-gui/components/dlg/Table/Create.vue (2)</summary>

`784-785`: **LGTM - Improved formatting of translation string**

The formatting change enhances readability by placing the closing brackets on a new line.

---

`813-817`: **LGTM - Consistent formatting of translation parameters**

The formatting change for the translation parameters improves code readability and maintains consistency with other similar structures in the codebase.

</details>
<details>
<summary>packages/nc-gui/components/general/Modal.vue (3)</summary>

`2-3`: **LGTM - Added import for CSSProperties type**

Properly importing the CSSProperties type to support the new maskStyle prop.

---

`11-11`: **LGTM - Added new maskStyle prop**

Adding the maskStyle prop enhances the component's flexibility by allowing customization of the modal's mask appearance.

---

`83-83`: **LGTM - Applied maskStyle prop to modal**

The maskStyle prop is correctly bound to the :mask-style attribute of the a-modal component.

</details>
<details>
<summary>packages/nc-gui/store/bases.ts (1)</summary>

`111-111`: **LGTM - Updated function signature to support future enhancements**

The loadProjects function signature has been expanded to include an optional second parameter, which follows the naming convention for unused parameters. This change maintains backward compatibility while preparing for potential future enhancements.

</details>
<details>
<summary>packages/nc-gui/assets/style.scss (2)</summary>

`988-990`: **LGTM - Added default styling for switches**

New default background color for the switch component enhances visual consistency across the UI.

---

`993-993`: **LGTM - Updated styling for checked switches**

Applied the utility class for primary fill color to checked switches, ensuring consistent styling with the application's design system.

</details>
<details>
<summary>packages/nc-gui/components/general/WorkspaceIcon.vue (3)</summary>

`15-15`: **New size option added to the component API**

The size prop has been extended to include a 'middle' option, which increases the component's flexibility for different UI contexts.

---

`85-85`: **CSS class implementation for new 'middle' size option**

Appropriate CSS classes have been added for the new 'middle' size option with specific width, height, and border radius values.

---

`147-147`: **Added specific text sizing for 'small' size**

A precise text size has been defined for the 'small' size variant, improving text readability in the smallest workspace icon size.

</details>
<details>
<summary>tests/playwright/pages/Dashboard/Grid/Column/index.ts (1)</summary>

`466-466`: **Updated button selector for better specificity**

The button selector text has been changed from generic "Confirm" to the more descriptive "Duplicate Field", which better reflects the action being performed and makes the test more robust by using a more specific selector.

</details>
<details>
<summary>tests/playwright/pages/Dashboard/TreeView.ts (2)</summary>

`373-373`: **Updated button selector to use more specific text**

The button selector has been changed from generic "Confirm" to the more descriptive "Duplicate Base", which improves test specificity and aligns with the terminology change from "Project" to "Base" throughout the application.

---

`377-377`: **Added navigation step after base duplication**

A new step has been added to click the "Go to Base" button after duplication is complete, completing the workflow by navigating to the newly created base. This improves user experience and test coverage.

</details>
<details>
<summary>packages/nc-gui/components/nc/Switch.vue (2)</summary>

`60-60`: **Added class binding for 'small' size**

The component now correctly applies the 'size-small' CSS class when the size prop is set to 'small'.

---

`85-105`: **Implemented CSS styling for small switch size**

Comprehensive CSS styling has been added for the 'small' size variant of the switch, including proper dimensions, handle positioning, and text alignment for both checked and unchecked states. The implementation follows the same pattern as the existing size variants, maintaining consistency in the component design system.

</details>
<details>
<summary>packages/nc-gui/components/dlg/View/Create.vue (3)</summary>

`432-433`: **Improved code readability with proper line breaks.**

The closing parenthesis for the find method has been moved to a new line, which enhances code readability and follows better formatting practices.

---

`508-511`: **Improved filter expression formatting.**

Moving the closing parenthesis to a new line after the logical conditions makes the filter expression more readable while maintaining the same functionality.

---

`1505-1509`: **Improved template string interpolation formatting.**

The closing parenthesis for the `$t` function calls have been moved to a new line, which enhances readability when handling interpolated content with count parameters.

</details>
<details>
<summary>packages/nc-gui/components/dlg/Column/Duplicate.vue (4)</summary>

`111-113`: **Added consistent modal overlay styling.**

The addition of `:mask-style` with a specific background color enhances the visual hierarchy and provides a consistent appearance across modal dialogs.

---

`120-122`: **Enhanced modal title with better context.**

The title now includes the column name being duplicated, providing clearer context to the user. The styling has also been updated for better visual hierarchy.

---

`124-132`: **Replaced checkbox with toggle switch for better UX.**

Replaced the standard checkbox with a more interactive `NcSwitch` component, improving usability and visual feedback. The entire row is now clickable, enhancing the interactive area.

---

`134-140`: **Improved action buttons with consistent sizing and clear labels.**

The action buttons now use consistent sizing and the primary action button has a more specific label ("Duplicate Field") instead of a generic confirmation label, providing clearer guidance to users.

</details>
<details>
<summary>packages/nc-gui/components/nc/List/index.vue (2)</summary>

`340-340`: **Updated class name for better CSS organization.**

Changed from inline Tailwind focus utilities to a named class `nc-list-root`, which improves CSS organization and maintainability while preserving the same functionality.

---

`378-378`: **Added semantic class name to list items.**

Added the `nc-list-item` class to list items for better CSS targeting and consistency across the application.

</details>
<details>
<summary>packages/nc-gui/components/dlg/Table/Duplicate.vue (4)</summary>

`136-138`: **Added consistent modal overlay styling.**

The addition of `:mask-style` with a specific background color creates visual consistency with other modal dialogs in the application.

---

`144-146`: **Enhanced modal title with better context.**

The title now includes the table name being duplicated, providing better context to the user. The added easter egg functionality (toggling visibility of include hooks option on double click) is a nice touch but should be documented.

---

`148-172`: **Replaced checkboxes with toggle switches for better UX.**

Replaced all checkboxes with `NcSwitch` components and made entire rows clickable, creating a more interactive and modern UI. The options are now organized in a cleaner vertical layout with consistent spacing.

---

`174-180`: **Improved action buttons with consistent sizing and clear labels.**

The action buttons now use consistent sizing and the primary action button has a more specific label ("Duplicate Table") instead of a generic confirmation label, making the action clearer to users.

</details>
<details>
<summary>packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts (1)</summary>

`26-35`: **Good introduction of `DuplicateService`.**

Injecting `DuplicateService` here centralizes duplication logic and enhances maintainability. Ensure that potential errors thrown by the service are properly caught or handled by the global exception filters, if any, to avoid unhandled rejections.

</details>
<details>
<summary>packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts (1)</summary>

`61-76`: **Visibility change to protected is beneficial for subclass access.**

Switching service dependencies from private to protected increases extensibility. Ensure no unintended external usage or extended surface area that might compromise encapsulation.

</details>
<details>
<summary>packages/nocodb/src/modules/jobs/jobs.module.ts (3)</summary>

`4-5`: **No concerns with the updated import paths.**
These changes to `MigrateController` and `MigrateService` align with the tilde-based import convention across the codebase.

---

`16-16`: **Good addition of the `DuplicateService` import.**
This import seamlessly integrates the duplication functionality into the jobs module.

---

`105-105`: **Service provider registration is correct.**
Registering `DuplicateService` in `providers` ensures it's available for injection, aligning with NestJS best practices.

</details>
<details>
<summary>packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue (2)</summary>

`391-391`: **Renaming the ERD dialog from project to base is consistent.**
Switching to `DlgBaseErd` aligns well with the broader rename from “Project” to “Base.”

---

`1060-1060`: **Correct usage of the new base delete dialog.**
Replacing `<DlgProjectDelete>` with `<DlgBaseDelete>` is consistent with the naming convention throughout the file.

</details>
<details>
<summary>packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.service.ts (11)</summary>

`1-23`: **Imports and dependency injection look correct.**
All necessary dependencies (`IJobsService`, `BasesService`, `AppHooksService`, etc.) are injected. The usage of `@Inject('JobsService')` for the interface-based injection is also appropriate.

---

`17-23`: **Constructor injection is well-structured.**
Storing the references to `jobsService`, `basesService`, and `appHooksService` ensures you can orchestrate duplication logic effectively.

---

`25-45`: **Method signature clarity.**
The `duplicateBase` method parameters are explicitly typed and destructured, making the external usage straightforward. Good job including `body.base` overrides for further flexibility.

---

`46-50`: **Base existence check.**
Throwing an error for a missing base ID is critical for safe error handling and clearer debugging.

---

`52-58`: **Source presence validation.**
Raising an error if the provided or default source doesn't exist helps avoid undefined references later in the duplication process.

---

`73-82`: **Unique title generation.**
Applying `generateUniqueName` ensures collisions are resolved gracefully, preserving user-friendly naming for duplicated bases.

---

`83-86`: **Audit ID association.**
Tying `req.ncParentAuditId` to the new parent audit ID helps maintain a complete audit trail for the duplication process.

---

`87-100`: **Base creation logic is sound.**
Creating a new base in job status, merging additional user overrides, and capturing user data is well-structured. This call to `basesService.baseCreate` properly decouples the creation from the duplication process.

---

`101-109`: **Emitting duplication start event.**
`BASE_DUPLICATE_START` event emission keeps the rest of the system in sync with the duplication lifecycle, enabling watchers to hook into the duplication flow.

---

`111-124`: **Queueing the duplication job.**
Adding the duplication task to `jobsService` with relevant metadata and request fields ensures robust asynchronicity and traceability of the duplication task. Good job capturing headers, client IP, and user data.

---

`126-127`: **Returning job and base IDs is straightforward.**
Exposing the job's ID and the duplicated base’s ID allows clients to track and query the new base or job progress.

</details>
<details>
<summary>packages/nc-gui/components/workspace/ProjectList.vue (7)</summary>

`23-25`: **Renamed variables for better terminology consistency.**

Good job on renaming these variables from "Project" to "Base" to maintain consistent terminology throughout the codebase. This aligns with the PR objective of duplicating the Base UI.

---

`27-32`: **Function renamed for terminology consistency.**

The `openBase` function name now matches the "Base" terminology used throughout the application. The implementation remains functionally equivalent to the previous `openProject` function.

---

`47-52`: **Updated function name and variable references.**

The `deleteBase` function has been appropriately renamed from `deleteProject` and its internal variable references have been updated to use the new variable names. The event tracking call on line 48 has also been updated to use "base" instead of "project".

---

`159-159`: **Updated function call reference.**

The function call has been correctly updated to use `openBase` instead of `openProject`.

---

`355-355`: **Updated event handler reference.**

The event handler reference has been correctly updated to use `deleteBase` instead of `deleteProject`.

---

`369-369`: **Updated component reference.**

The dialog component has been correctly updated from `DlgProjectDelete` to `DlgBaseDelete`, and the prop passing has been updated to use `base-id` instead of `project-id`.

---

`376-376`: **Updated duplicate dialog component reference.**

The duplicate dialog component has been correctly updated from `DlgProjectDuplicate` to `DlgBaseDuplicate`.

</details>
<details>
<summary>packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts (3)</summary>

`11-11`: **Added NotImplementedException import.**

Added necessary import for the new method that will throw this exception.

---

`35-35`: **Changed access modifier from private to protected.**

The access modifier for `debugLog` has been changed from `private` to `protected`, which suggests this class might be extended by subclasses that need access to this property.

---

`38-44`: **Modified constructor parameter access modifiers.**

The access modifiers for all constructor parameters have been changed from `private` to `protected`. This is a good practice if the class is intended to be extended, allowing subclasses to access these properties.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T06:32:46Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts (1)</summary><blockquote>

`140-146`: **Well-executed refactoring for better separation of concerns.**

The `duplicateBase` method has been effectively refactored to delegate its functionality to the `DuplicateService`. This improves the code by:

1. Following the Single Responsibility Principle
2. Reducing code duplication
3. Making the controller thinner and moving business logic to the service layer
4. Using a clean approach for parameter passing with a single object

This change is part of the broader effort to rename "Project" to "Base" as mentioned in the PR description, while also improving the code structure.


Consider applying a similar refactoring pattern to the other duplication methods (`duplicateSharedBase`, `duplicateModel`, and `duplicateColumn`) to maintain consistency throughout the controller.

</blockquote></details>
<details>
<summary>packages/nc-gui/components/dlg/Base/Duplicate.vue (4)</summary><blockquote>

`1-29`: **Component looks well-structured but missing imports**

The component has proper TypeScript setup with props and emissions defined correctly. However, several variables and functions used throughout the component aren't explicitly imported or defined within this file:
- `useVModel`, `useGlobal`, `useCommandPalette`, `useApi` (lines 12-18)
- `useBases`, `useWorkspace`, `storeToRefs` (lines 22-28)


Consider adding the missing imports at the top of the file for better code clarity and to prevent potential issues with future refactoring:

```diff
<script setup lang="ts">
import tinycolor from 'tinycolor2'
import { type BaseType, WorkspaceUserRoles } from 'nocodb-sdk'
+import { useVModel } from '@vueuse/core'
+import { useGlobal, useCommandPalette, useApi } from '~/composables'
+import { useBases, useWorkspace } from '~/stores'
+import { storeToRefs } from 'pinia'
```

---

`45-65`: **Easter egg feature and missing variable definition**

There are two issues in this section:

1. An undocumented easter egg feature that toggles display of additional duplication options
2. The `isEeUI` variable is used but not defined in this file


For the first issue, consider adding a comment explaining the purpose of the easter egg feature. For the second issue, import the missing variable:

```diff
+// Import isEeUI from the appropriate source
+import { isEeUI } from '~/utils/eeLicense'

const isEaster = ref(false) // Controls visibility of additional duplication options when enabled with double-click
```

---

`134-138`: **Missing WorkspaceType type definition**

The `selectOption` function uses the `WorkspaceType` type which isn't defined or imported.


Add the missing type import:

```diff
import { type BaseType, WorkspaceUserRoles } from 'nocodb-sdk'
+import type { WorkspaceType } from '~/lib/types'
```

---

`336-338`: **Hardcoded text without translation**

The success message is hardcoded in English rather than using the translation system like other messages in the component.


Use the translation system for the success message to maintain consistency:

```diff
-        <div class="text-nc-content-gray-emphasis my-5 font-medium">
-          Base <span class="font-bold leading-5">"{{ base.title }}"</span> has finished duplication.
-        </div>
+        <div class="text-nc-content-gray-emphasis my-5 font-medium">
+          {{ $t('labels.baseDuplicateSuccess', { baseTitle: base.title }) }}
+        </div>
```

Don't forget to add the translation key to your localization files.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0fd434cd5a2c35d0fd548941e9ecc6d6b18d7286 and f0cfc8c2c39fdd62b1af15f7d192f35ad998013c.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `packages/nc-gui/lang/en.json` is excluded by `!**/*.json`

</details>

<details>
<summary>📒 Files selected for processing (21)</summary>

* `packages/nc-gui/assets/style.scss` (1 hunks)
* `packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue` (4 hunks)
* `packages/nc-gui/components/dlg/Base/Duplicate.vue` (1 hunks)
* `packages/nc-gui/components/dlg/Column/Duplicate.vue` (1 hunks)
* `packages/nc-gui/components/dlg/ProjectDuplicate.vue` (0 hunks)
* `packages/nc-gui/components/dlg/Table/Create.vue` (2 hunks)
* `packages/nc-gui/components/dlg/Table/Duplicate.vue` (1 hunks)
* `packages/nc-gui/components/dlg/View/Create.vue` (3 hunks)
* `packages/nc-gui/components/general/Modal.vue` (2 hunks)
* `packages/nc-gui/components/general/WorkspaceIcon.vue` (3 hunks)
* `packages/nc-gui/components/nc/List/index.vue` (2 hunks)
* `packages/nc-gui/components/nc/Switch.vue` (3 hunks)
* `packages/nc-gui/components/workspace/ProjectList.vue` (6 hunks)
* `packages/nc-gui/store/bases.ts` (1 hunks)
* `packages/nocodb/src/modules/jobs/jobs.module.ts` (3 hunks)
* `packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts` (3 hunks)
* `packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts` (3 hunks)
* `packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.service.ts` (1 hunks)
* `packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts` (2 hunks)
* `tests/playwright/pages/Dashboard/Grid/Column/index.ts` (1 hunks)
* `tests/playwright/pages/Dashboard/TreeView.ts` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

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

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (18)</summary>

* packages/nc-gui/components/dlg/Table/Create.vue
* packages/nc-gui/store/bases.ts
* packages/nc-gui/components/nc/Switch.vue
* packages/nc-gui/assets/style.scss
* tests/playwright/pages/Dashboard/TreeView.ts
* packages/nc-gui/components/dlg/Table/Duplicate.vue
* packages/nc-gui/components/general/WorkspaceIcon.vue
* packages/nc-gui/components/general/Modal.vue
* tests/playwright/pages/Dashboard/Grid/Column/index.ts
* packages/nc-gui/components/dlg/Column/Duplicate.vue
* packages/nc-gui/components/dlg/View/Create.vue
* packages/nc-gui/components/nc/List/index.vue
* packages/nocodb/src/modules/jobs/jobs.module.ts
* packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.service.ts
* packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
* packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
* packages/nc-gui/components/workspace/ProjectList.vue
* packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: release-docker / buildx
* GitHub Check: unit-tests-pg
* GitHub Check: unit-tests

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.controller.ts (2)</summary>

`26-26`: **Appropriate import for new service.**

Adding the `DuplicateService` import supports the refactoring approach of moving duplication logic to a dedicated service class.

---

`35-35`: **Good use of dependency injection.**

The `DuplicateService` is correctly injected into the constructor, following the established pattern in this controller.

</details>
<details>
<summary>packages/nc-gui/components/dlg/Base/Duplicate.vue (3)</summary>

`178-344`: **Well-structured UI with clear state handling**

The template section effectively handles different states (pending, loading, success, error) with appropriate UI elements and feedback. The component provides clear user guidance during the duplication process and offers workspace selection for enterprise users.

---

`366-382`: **Clean and well-structured styling**

The component uses scoped styles appropriately and leverages utility classes. The styles are well-organized and target specific elements with clear intent.

---

`357-360`: <details>
<summary>❓ Verification inconclusive</summary>

**Terminology inconsistency in button labels**

The button labels use "project" terminology even though this component is for duplicating a "Base". This inconsistency might confuse users.


Update the button labels to use consistent terminology:

```diff
-        <template v-if="status === 'pending'"> {{ $t('general.duplicate') }} {{ $t('objects.project') }} </template>
-        <template v-else-if="status === 'loading'"> Duplicating {{ $t('objects.project') }} </template>
+        <template v-if="status === 'pending'"> {{ $t('general.duplicate') }} {{ $t('objects.base') }} </template>
+        <template v-else-if="status === 'loading'"> Duplicating {{ $t('objects.base') }} </template>
```

Alternatively, confirm that the translation keys are correctly updated to reflect the terminology change from "project" to "base".

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if translation keys for objects.project and objects.base exist
fd --type file -e json | xargs grep -l "objects.project\|objects.base" | xargs cat
```

Length of output: 84

---



</details>

**Action Required: Verify and Update Translation Keys for Duplicate Labels**

The Duplicate.vue file still uses the "objects.project" key in the "pending" and "loading" states, even though this component is intended for duplicating a “Base.” To ensure consistency, please update these references to "objects.base" as shown below:

```diff
-        <template v-if="status === 'pending'"> {{ $t('general.duplicate') }} {{ $t('objects.project') }} </template>
-        <template v-else-if="status === 'loading'"> Duplicating {{ $t('objects.project') }} </template>
+        <template v-if="status === 'pending'"> {{ $t('general.duplicate') }} {{ $t('objects.base') }} </template>
+        <template v-else-if="status === 'loading'"> Duplicating {{ $t('objects.base') }} </template>
```

Our automated check for translation keys didn’t produce any output, so please manually verify that the "objects.base" key is available (or add it) and that it correctly replaces “project” across the codebase. This will ensure that the duplicate action labels are consistent with the component’s intended functionality.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T04:31:24Z
user: login: dstala
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-17T06:22:14Z

Review comments:
diff_hunk: @@ -58,23 +58,38 @@ export class ImportService {
   protected readonly logger = new Logger(ImportService.name);
 
   constructor(
-    private tablesService: TablesService,
-    private columnsService: ColumnsService,
-    private filtersService: FiltersService,
-    private sortsService: SortsService,
-    private viewColumnsService: ViewColumnsService,
-    private gridColumnsService: GridColumnsService,
-    private formColumnsService: FormColumnsService,
-    private gridsService: GridsService,
-    private formsService: FormsService,
-    private galleriesService: GalleriesService,
-    private calendarsService: CalendarsService,
-    private kanbansService: KanbansService,
-    private bulkDataService: BulkDataAliasService,
-    private hooksService: HooksService,
-    private viewsService: ViewsService,
+    protected tablesService: TablesService,
+    protected columnsService: ColumnsService,
+    protected filtersService: FiltersService,
+    protected sortsService: SortsService,
+    protected viewColumnsService: ViewColumnsService,
+    protected gridColumnsService: GridColumnsService,
+    protected formColumnsService: FormColumnsService,
+    protected gridsService: GridsService,
+    protected formsService: FormsService,
+    protected galleriesService: GalleriesService,
+    protected calendarsService: CalendarsService,
+    protected kanbansService: KanbansService,
+    protected bulkDataService: BulkDataAliasService,
+    protected hooksService: HooksService,
+    protected viewsService: ViewsService,
   ) {}
 
+  async importUsers(
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    context: NcContext,
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    payload: {
+      users: {
+        email: string;
+        display_name?: string;
+      }[];
+      req: NcRequest;
+    },
+  ) {
+    throw new NotImplementedException();
+  }
path: packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Implement or remove the placeholder for `importUsers`.**

The `importUsers` method currently throws a `NotImplementedException` and can cause unexpected runtime errors. Either provide an implementation or remove the method until functionality is ready.  

```diff
+// TODO: Provide implementation to handle user data import
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  async importUsers(
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    context: NcContext,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    payload: {
      users: {
        email: string;
        display_name?: string;
      }[];
      req: NcRequest;
    },
  ) {
    // TODO: Provide implementation to handle user data import
    throw new NotImplementedException();
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 31997be to 3e8c50d
created_at: 2025-03-15T06:32:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,7 +103,7 @@ const [searchActive] = useToggle()
 const filterQuery = ref('')
 const keys = ref<Record<string, number>>({})
 const isTableDeleteDialogVisible = ref(false)
-const isProjectDeleteDialogVisible = ref(false)
+const isBaseDeleteDialogVisible = ref(false)
path: packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Rename variables for consistency.**
Renaming `isProjectDeleteDialogVisible` to `isBaseDeleteDialogVisible` is a logical step. However, the existing `projectDelete()` function still sets `isProjectDeleteDialogVisible` (unchanged code) instead of `isBaseDeleteDialogVisible`. Consider aligning both the function name and its referenced variable to use “Base” consistently and remove any unused old variable declarations.

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 31997be to 3e8c50d
created_at: 2025-03-15T06:32:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+import { Inject, Injectable, NotImplementedException } from '@nestjs/common';
+import {
+  AppEvents,
+  type NcContext,
+  type NcRequest,
+  ProjectStatus,
+} from 'nocodb-sdk';
+import { Base, Source } from '~/models';
+import Noco from '~/Noco';
+import { MetaTable } from '~/cli';
+import { generateUniqueName } from '~/helpers/exportImportHelpers';
+import { JobTypes } from '~/interface/Jobs';
+import { AppHooksService } from '~/services/app-hooks/app-hooks.service';
+import { BasesService } from '~/services/bases.service';
+import { IJobsService } from '~/modules/jobs/jobs-service.interface';
+
+@Injectable()
+export class DuplicateService {
+  constructor(
+    @Inject('JobsService') protected readonly jobsService: IJobsService,
+    protected readonly basesService: BasesService,
+    protected readonly appHooksService: AppHooksService,
+  ) {}
+
+  async duplicateBase({
+    context,
+    req,
+    baseId,
+    sourceId,
+    body,
+  }: {
+    context: NcContext;
+    req: NcRequest;
+    baseId: string;
+    sourceId?: string;
+    body?: {
+      options?: {
+        excludeData?: boolean;
+        excludeViews?: boolean;
+        excludeHooks?: boolean;
+      };
+      // override duplicated base
+      base?: any;
+    };
+  }) {
+    const base = await Base.get(context, baseId);
+
+    if (!base) {
+      throw new Error(`Base not found for id '${baseId}'`);
+    }
+
+    const source = sourceId
+      ? await Source.get(context, sourceId)
+      : (await base.getSources())[0];
+
+    if (!source) {
+      throw new Error(`Source not found!`);
+    }
+
+    if (
+      body.base?.fk_workspace_id &&
+      body.base?.fk_workspace_id !== '' &&
+      (base.fk_workspace_id || body.base?.fk_workspace_id) &&
+      base.fk_workspace_id !== body.base?.fk_workspace_id
+    ) {
+      await this.handleDifferentWs({
+        context,
+        req,
+        sourceBase: base,
+        targetBase: body.base,
+      });
+    }
path: packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.service.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Workspace difference handling.**
The `handleDifferentWs` method is invoked when workspace IDs differ. Marking it as not implemented clarifies the current limitation, though it should be tracked for completion.

Would you like to open an issue to flesh out cross-workspace duplication?

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 31997be to 3e8c50d
created_at: 2025-03-15T06:32:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,6 +92,21 @@ export class DuplicateProcessor {
       base_id: targetBase.id,
     };
 
+    if (
+      operation === JobTypes.DuplicateBase &&
+      targetContext.workspace_id !== sourceBase.fk_workspace_id
+    ) {
+      await this.handleDuplicateDifferentWs({
+        sourceBase,
+        targetBase,
+        dataSource,
+        req,
+        context,
+        targetContext,
+        options,
+      });
+    }
path: packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Added cross-workspace duplication handling.**

A new conditional block has been added to check if the operation is duplicating a base between different workspaces. If so, it calls the new `handleDuplicateDifferentWs` method.

This prepares the codebase for future support of cross-workspace duplication.


Since this will currently throw a NotImplementedException, verify that the UI properly prevents users from attempting cross-workspace duplication or handles the error appropriately:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are UI checks that prevent cross-workspace duplication

# Look for conditions that might prevent cross-workspace duplication in the UI
echo "Searching for UI conditions that might prevent cross-workspace duplication:"
rg -A 5 -B 5 "workspace_id" --glob "*.vue" | grep -i "duplicate"
```

Length of output: 4072

---



</details>

**Action Required: UI Handling for Cross-Workspace Duplication**

The new conditional block in `duplicate.processor.ts` correctly routes cross-workspace duplication requests via `handleDuplicateDifferentWs`. However, our verification using the grep output indicates that the Vue components (in `packages/nc-gui/components/dlg/SharedBaseDuplicate.vue` and `packages/nc-gui/components/dlg/Base/Duplicate.vue`) do not include a pre-check to prevent users from attempting cross-workspace duplication. Instead, they always pass a workspace ID to the duplication API, which—when a cross-workspace attempt is made—will trigger a backend NotImplementedException.

Please ensure that one of the following is implemented:
- The UI prevents users from initiating cross-workspace duplication, or
- The UI gracefully handles the NotImplementedException (e.g., by displaying an appropriate error message).

This adjustment is critical to prevent unexpected failures for end users.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-15T06:32:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,382 @@
+<script setup lang="ts">
+import tinycolor from 'tinycolor2'
+import { type BaseType, WorkspaceUserRoles } from 'nocodb-sdk'
+
+const props = defineProps<{
+  modelValue: boolean
+  base: BaseType
+}>()
+
+const emit = defineEmits(['update:modelValue'])
+
+const dialogShow = useVModel(props, 'modelValue', emit)
+
+const { navigateToProject } = useGlobal()
+
+const { refreshCommandPalette } = useCommandPalette()
+
+const { api } = useApi()
+
+const { $e, $poller } = useNuxtApp()
+
+const basesStore = useBases()
+
+const { workspacesList, activeWorkspace } = useWorkspace()
+
+const { loadProjects, createProject: _createProject } = basesStore
+
+const { bases } = storeToRefs(basesStore)
+
+const options = ref({
+  includeData: true,
+  includeViews: true,
+  includeHooks: true,
+  includeComments: true,
+})
+const targetWorkspace = ref(activeWorkspace)
+
+const errorMessage = ref()
+
+// Used to handle different Action in different states in Modal
+// pending -> Initial state
+// loading -> Set when duplicate is triggered
+const status = ref<'pending' | 'success' | 'error' | 'loading'>('pending')
+
+const isEaster = ref(false)
+
+const dropdownOpen = ref(false)
+
+const optionsToExclude = computed(() => {
+  const { includeData, includeViews, includeHooks, includeComments } = options.value
+  return {
+    excludeData: !includeData,
+    excludeViews: !includeViews,
+    excludeHooks: !includeHooks,
+    excludeComments: !includeComments,
+  }
+})
+
+const workspaceOptions = computed(() => {
+  if (!isEeUI) return []
+  return workspacesList.filter((ws) =>
+    [WorkspaceUserRoles.CREATOR, WorkspaceUserRoles.OWNER].includes(ws.roles as WorkspaceUserRoles),
+  )
+})
+
+const isLoading = computed(() => status.value === 'loading')
+
+const targetBase = ref()
+
+const _duplicate = async () => {
+  try {
+    status.value = 'loading'
+    // pick a random color from array and assign to base
+    const color = baseThemeColors[Math.floor(Math.random() * 1000) % baseThemeColors.length]
+    const tcolor = tinycolor(color)
+
+    const complement = tcolor.complement()
+
+    const jobData = await api.base.duplicate(props.base.id as string, {
+      options: optionsToExclude.value,
+      base: {
+        fk_workspace_id:
+          targetWorkspace.value && targetWorkspace.value?.id ? targetWorkspace.value.id : props.base.fk_workspace_id,
+        type: props.base.type,
+        color,
+        meta: JSON.stringify({
+          theme: {
+            primaryColor: color,
+            accentColor: complement.toHex8String(),
+          },
+          iconColor: parseProp(props.base.meta).iconColor,
+        }),
+      },
+    })
+
+    $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) {
+            const resBases = await loadProjects('workspace', targetWorkspace?.value?.id)
+            targetBase.value = resBases.find((b) => b.id === jobData.base_id)
+            status.value = 'success'
+
+            refreshCommandPalette()
+          } else if (data.status === JobStatus.FAILED) {
+            status.value = 'error'
+            await loadProjects('workspace')
+            refreshCommandPalette()
+          }
+        }
+      },
+    )
+
+    $e('a:base:duplicate')
+  } catch (e: any) {
+    message.error(await extractSdkResponseErrorMsg(e))
+    errorMessage.value = await extractSdkResponseErrorMsg(e)
+    status.value = 'error'
+    dialogShow.value = false
+  }
+}
path: packages/nc-gui/components/dlg/Base/Duplicate.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Missing variable definitions in duplication logic**

The duplication logic is well-implemented but relies on several undefined variables and functions:

1. `baseThemeColors` array (line 74)
2. `parseProp` function (line 91)
3. `JobStatus` enum (lines 110, 116)
4. `message` object (line 127)
5. `extractSdkResponseErrorMsg` function (lines 127-128)


Add the missing imports to ensure the component functions correctly:

```diff
<script setup lang="ts">
import tinycolor from 'tinycolor2'
import { type BaseType, WorkspaceUserRoles } from 'nocodb-sdk'
+import { baseThemeColors } from '~/utils/colors'
+import { parseProp } from '~/utils'
+import { JobStatus } from '~/lib/enums'
+import { message } from 'ant-design-vue'
+import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
<script setup lang="ts">
import tinycolor from 'tinycolor2'
import { type BaseType, WorkspaceUserRoles } from 'nocodb-sdk'
import { baseThemeColors } from '~/utils/colors'
import { parseProp } from '~/utils'
import { JobStatus } from '~/lib/enums'
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'

const _duplicate = async () => {
  try {
    status.value = 'loading'
    // pick a random color from array and assign to base
    const color = baseThemeColors[Math.floor(Math.random() * 1000) % baseThemeColors.length]
    const tcolor = tinycolor(color)

    const complement = tcolor.complement()

    const jobData = await api.base.duplicate(props.base.id as string, {
      options: optionsToExclude.value,
      base: {
        fk_workspace_id:
          targetWorkspace.value && targetWorkspace.value?.id ? targetWorkspace.value.id : props.base.fk_workspace_id,
        type: props.base.type,
        color,
        meta: JSON.stringify({
          theme: {
            primaryColor: color,
            accentColor: complement.toHex8String(),
          },
          iconColor: parseProp(props.base.meta).iconColor,
        }),
      },
    })

    $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) {
            const resBases = await loadProjects('workspace', targetWorkspace?.value?.id)
            targetBase.value = resBases.find((b) => b.id === jobData.base_id)
            status.value = 'success'
            refreshCommandPalette()
          } else if (data.status === JobStatus.FAILED) {
            status.value = 'error'
            await loadProjects('workspace')
            refreshCommandPalette()
          }
        }
      },
    )

    $e('a:base:duplicate')
  } catch (e: any) {
    message.error(await extractSdkResponseErrorMsg(e))
    errorMessage.value = await extractSdkResponseErrorMsg(e)
    status.value = 'error'
    dialogShow.value = false
  }
}
</script>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits c99d451 to 23ce871
created_at: 2025-03-17T04:31:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-15T06:28:37Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

The pull request implements several cross-cutting changes. In the frontend, new styles are introduced for switch components and dialogs, with several components and variables renamed from “Project” to “Base” to generalize functionality. New duplication components (for bases and columns) are added while the old project-specific duplicate component is removed. There are also code formatting and class name updates across various dialogs and lists. In the backend, the duplication flow is refactored to delegate logic to a new `DuplicateService`, with corresponding updates in controllers, processors, and import services, along with adjusted import paths for clarity.

## Changes

| Files | Change Summary |
| --- | --- |
| `packages/nc-gui/assets/style.scss` | New SCSS styles introduced for `.ant-switch` and `.ant-switch-checked` with updated background colors. |
| `packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue`,<br>`packages/nc-gui/components/workspace/ProjectList.vue` | Variables, method names, and dialog components renamed from “Project” to “Base” for deletion, duplication, and ERD actions. |
| `packages/nc-gui/components/dlg/Base/Duplicate.vue`,<br>`packages/nc-gui/components/dlg/ProjectDuplicate.vue` (deleted),<br>`packages/nc-gui/components/dlg/Column/Duplicate.vue`,<br>`packages/nc-gui/components/dlg/Table/Duplicate.vue` | Added new duplication components for base entities; removed the project-specific duplicate; updated UI structure and button texts. |
| `packages/nc-gui/components/dlg/Table/Create.vue`,<br>`packages/nc-gui/components/dlg/View/Create.vue` | Formatting improvements and template string adjustments for better code readability. |
| `packages/nc-gui/components/general/Modal.vue`,<br>`packages/nc-gui/components/general/WorkspaceIcon.vue` | Added a new `maskStyle` prop in Modal and a new `middle` size option in WorkspaceIcon along with updated class bindings. |
| `packages/nc-gui/components/nc/List/index.vue`,<br>`packages/nc-gui/components/nc/Switch.vue` | Removal of validation logic in the list selection function; introduction of a new CSS class (`size-small`) in the Switch component. |
| `packages/nc-gui/store/bases.ts` | Extended the `loadProjects` method signature with an optional second parameter. |
| `packages/nocodb/src/modules/jobs/...` (multiple files) | Updated import paths; added a new `DuplicateService`; refactored duplication logic in the controller, processor, and service; added a new `importUsers` method and adjusted access modifiers. |
| `tests/playwright/pages/Dashboard/...` (multiple files) | Updated duplicate action button texts (e.g. “Duplicate Field” and “Duplicate Base”) and added a navigation step post-duplication. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Dialog
    participant Controller
    participant DuplicateService
    participant JobService

    User->>Dialog: Initiate duplicate base action
    Dialog->>Controller: Send duplication request
    Controller->>DuplicateService: duplicateBase(context, req, baseId, options)
    DuplicateService->>JobService: Create and enqueue duplication job
    JobService-->>DuplicateService: Return job ID
    DuplicateService-->>Controller: Return job ID & new base details
    Controller-->>Dialog: Deliver duplication result
```

## Suggested labels

`🔦 Type: Feature`, `size:XL`, `lgtm`

</details>

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡🧪 Multi-step agentic review comment chat (experimental)</summary>
> 
> - We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
> 						- To enable this feature, set early_access to true under in the settings.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5373fd7606578876f651957f3583bc742ea9f0a0 and d6e458707ed3ee19ebea1b54a2bafe1177298ac6.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/nc-gui/components/dlg/Base/Duplicate.vue` (1 hunks)
* `packages/nc-gui/store/bases.ts` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* packages/nc-gui/components/dlg/Base/Duplicate.vue
* packages/nc-gui/store/bases.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: release-docker / buildx
* GitHub Check: pre-build-for-playwright / playwright
* GitHub Check: unit-tests
* GitHub Check: unit-tests-pg

</details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=nocodb/nocodb&utm_content=10853):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-15T06:36:28Z
body: Uffizzi Preview `deployment-61946` was deleted.
author_association: CONTRIBUTOR