Browse Source

Nc fix: skip confirmation modal for duplicate view (#9586)

* feat(nocodb): add view copy api backend changes

* fix(nc-gui): skip duplicate view modal

* revert(nocodb): duplicate view api changes

* chore: lint

* fix(nc-gui): update duplicate view return type

* fix(nc-gui): ai review changes

* fix(nc-gui): update duplicate view test cases

* fix(nc-gui): pr review changes
pull/9592/head
Ramesh Mane 2 months ago committed by GitHub
parent
commit
c7fad698be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 55
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  2. 67
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  3. 74
      packages/nc-gui/store/views.ts
  4. 13
      tests/playwright/pages/Dashboard/ViewSidebar/index.ts

55
packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type BaseType, type TableType, type ViewType, ViewTypes } from 'nocodb-sdk' import { type BaseType, type TableType, ViewTypes } from 'nocodb-sdk'
import { toRef } from '@vue/reactivity' import { toRef } from '@vue/reactivity'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
@ -52,7 +52,7 @@ const {
duplicateTable: _duplicateTable, duplicateTable: _duplicateTable,
} = inject(TreeViewInj)! } = inject(TreeViewInj)!
const { loadViews: _loadViews, navigateToView } = useViewsStore() const { loadViews: _loadViews, navigateToView, duplicateView } = useViewsStore()
const { activeView, activeViewTitleOrId, viewsByTable } = storeToRefs(useViewsStore()) const { activeView, activeViewTitleOrId, viewsByTable } = storeToRefs(useViewsStore())
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore()) const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
@ -221,43 +221,27 @@ const deleteTable = () => {
const isOnDuplicateLoading = ref<boolean>(false) const isOnDuplicateLoading = ref<boolean>(false)
async function onDuplicate() { async function onDuplicate() {
isOnDuplicateLoading.value = true
// Load views if not loaded // Load views if not loaded
if (!viewsByTable.value.get(table.value.id as string)) { if (!viewsByTable.value.get(table.value.id as string)) {
isOnDuplicateLoading.value = true
await _openTable(table.value, undefined, false) await _openTable(table.value, undefined, false)
isOnDuplicateLoading.value = false
} }
isOptionsOpen.value = false
const views = viewsByTable.value.get(table.value.id as string) const views = viewsByTable.value.get(table.value.id as string)
const defaultView = views?.find((v) => v.is_default) || views?.[0] const defaultView = views?.find((v) => v.is_default) || views?.[0]
const isOpen = ref(true) if (defaultView) {
const view = await duplicateView(defaultView)
const { close } = useDialog(resolveComponent('DlgViewCreate'), {
'modelValue': isOpen, refreshCommandPalette()
'title': defaultView!.title,
'type': defaultView!.type as ViewTypes,
'tableId': table.value!.id,
'selectedViewId': defaultView!.id,
'groupingFieldColumnId': defaultView!.view!.fk_grp_col_id,
'views': views,
'calendarRange': defaultView!.view!.calendar_range,
'coverImageColumnId': defaultView!.view!.fk_cover_image_col_id,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
refreshCommandPalette()
await _loadViews({
force: true,
tableId: table.value!.id!,
})
await _loadViews({
force: true,
tableId: table.value!.id!,
})
if (view) {
navigateToView({ navigateToView({
view, view,
tableId: table.value!.id!, tableId: table.value!.id!,
@ -266,14 +250,11 @@ async function onDuplicate() {
}) })
$e('a:view:create', { view: view.type, sidebar: true }) $e('a:view:create', { view: view.type, sidebar: true })
}, }
})
function closeDialog() {
isOpen.value = false
close(1000)
} }
isOnDuplicateLoading.value = false
isOptionsOpen.value = false
} }
// TODO: Should find a way to render the components without using the `nextTick` function // TODO: Should find a way to render the components without using the `nextTick` function

67
packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue

@ -28,8 +28,7 @@ const view = computed(() => props.view)
const table = computed(() => props.table) const table = computed(() => props.table)
const { viewsByTable } = storeToRefs(useViewsStore()) const { loadViews, navigateToView, duplicateView } = useViewsStore()
const { loadViews, navigateToView } = useViewsStore()
const { base } = storeToRefs(useBase()) const { base } = storeToRefs(useBase())
@ -37,8 +36,6 @@ const { refreshCommandPalette } = useCommandPalette()
const lockType = computed(() => (view.value?.lock_type as LockType) || LockType.Collaborative) const lockType = computed(() => (view.value?.lock_type as LockType) || LockType.Collaborative)
const views = computed(() => viewsByTable.value.get(table.value.id!))
const isViewIdCopied = ref(false) const isViewIdCopied = ref(false)
const currentSourceId = computed(() => table.value?.source_id) const currentSourceId = computed(() => table.value?.source_id)
@ -91,51 +88,34 @@ async function changeLockType(type: LockType) {
emits('closeModal') emits('closeModal')
} }
const isOnDuplicateLoading = ref<boolean>(false)
/** Duplicate a view */ /** Duplicate a view */
// todo: This is not really a duplication, maybe we need to implement a true duplication? // todo: This is not really a duplication, maybe we need to implement a true duplication?
function onDuplicate() { async function onDuplicate() {
emits('closeModal') isOnDuplicateLoading.value = true
const duplicatedView = await duplicateView(view.value)
refreshCommandPalette()
const isOpen = ref(true) await loadViews({
force: true,
const { close } = useDialog(resolveComponent('DlgViewCreate'), { tableId: table.value!.id!,
'modelValue': isOpen,
'title': view.value!.title,
'type': view.value!.type as ViewTypes,
'tableId': table.value!.id,
'selectedViewId': view.value!.id,
'groupingFieldColumnId': view.value!.view!.fk_grp_col_id,
'views': views,
'description': view.value!.description,
'calendarRange': view.value!.view!.calendar_range,
'coverImageColumnId': view.value!.view!.fk_cover_image_col_id,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
refreshCommandPalette()
await loadViews({
force: true,
tableId: table.value!.id!,
})
navigateToView({
view,
tableId: table.value!.id!,
baseId: base.value.id!,
hardReload: view.type === ViewTypes.FORM,
})
$e('a:view:create', { view: view.type, sidebar: props.inSidebar })
},
}) })
function closeDialog() { if (duplicatedView) {
isOpen.value = false navigateToView({
view: duplicatedView,
tableId: table.value!.id!,
baseId: base.value.id!,
hardReload: duplicatedView.type === ViewTypes.FORM,
})
close(1000) $e('a:view:create', { view: duplicatedView.type, sidebar: true })
} }
isOnDuplicateLoading.value = false
emits('closeModal')
} }
const { copy } = useCopy() const { copy } = useCopy()
@ -208,7 +188,8 @@ const onDelete = async () => {
</NcMenuItem> </NcMenuItem>
</template> </template>
<NcMenuItem @click="onDuplicate"> <NcMenuItem @click="onDuplicate">
<GeneralIcon class="nc-view-copy-icon" icon="duplicate" /> <GeneralLoader v-if="isOnDuplicateLoading" size="regular" />
<GeneralIcon v-else class="nc-view-copy-icon" icon="duplicate" />
{{ {{
$t('general.duplicateEntity', { $t('general.duplicateEntity', {
entity: view.type !== ViewTypes.FORM ? $t('objects.view').toLowerCase() : $t('objects.viewType.form').toLowerCase(), entity: view.type !== ViewTypes.FORM ? $t('objects.view').toLowerCase() : $t('objects.viewType.form').toLowerCase(),

74
packages/nc-gui/store/views.ts

@ -1,4 +1,4 @@
import type { FilterType, SortType, ViewType, ViewTypes } from 'nocodb-sdk' import type { CalendarType, FilterType, GalleryType, KanbanType, MapType, SortType, ViewType, ViewTypes } from 'nocodb-sdk'
import { ViewTypes as _ViewTypes } from 'nocodb-sdk' import { ViewTypes as _ViewTypes } from 'nocodb-sdk'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import { useTitle } from '@vueuse/core' import { useTitle } from '@vueuse/core'
@ -415,6 +415,77 @@ export const useViewsStore = defineStore('viewsStore', () => {
) )
} }
const duplicateView = async (view: ViewType) => {
if (!view?.id) return
const views = viewsByTable.value.get(view.fk_model_id) || []
const uniqueTitle = generateUniqueTitle(`${view.title} copy`, views, 'title', '_', true)
const payload: {
title: string
type: ViewTypes
description?: string
copy_from_id: string | null
// for kanban view only
fk_grp_col_id: string | null
fk_geo_data_col_id: string | null
// for calendar view only
calendar_range: Array<{
fk_from_column_id: string
fk_to_column_id: string | null // for ee only
}>
fk_cover_image_col_id: string | null
} = {
title: uniqueTitle,
type: view.type,
description: view.description || '',
copy_from_id: view.id!,
fk_grp_col_id: null,
fk_geo_data_col_id: null,
fk_cover_image_col_id: null,
calendar_range: [],
}
try {
switch (payload.type) {
case _ViewTypes.GRID:
return await $api.dbView.gridCreate(view.fk_model_id, payload)
case _ViewTypes.GALLERY:
payload.fk_cover_image_col_id = (view.view as GalleryType)?.fk_cover_image_col_id || null
return await $api.dbView.galleryCreate(view.fk_model_id, payload)
case _ViewTypes.FORM:
return await $api.dbView.formCreate(view.fk_model_id, payload)
case _ViewTypes.KANBAN:
payload.fk_cover_image_col_id = (view.view as KanbanType)?.fk_cover_image_col_id || null
payload.fk_grp_col_id = (view.view as KanbanType)?.fk_grp_col_id || null
return await $api.dbView.kanbanCreate(view.fk_model_id, payload)
case _ViewTypes.MAP:
payload.fk_geo_data_col_id = (view.view as MapType)?.fk_geo_data_col_id || null
return await $api.dbView.mapCreate(view.fk_model_id, payload)
case _ViewTypes.CALENDAR:
payload.calendar_range =
(view.view as CalendarType)?.calendar_range?.map((range) => ({
fk_from_column_id: range.fk_from_column_id as string,
fk_to_column_id: range.fk_to_column_id as string,
})) || []
return await $api.dbView.calendarCreate(view.fk_model_id, payload)
}
} catch (e: any) {
message.error(e.message)
}
}
refreshViewTabTitle.on(() => { refreshViewTabTitle.on(() => {
updateTabTitle() updateTabTitle()
}) })
@ -453,6 +524,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
preFillFormSearchParams, preFillFormSearchParams,
refreshViewTabTitle: refreshViewTabTitle.trigger, refreshViewTabTitle: refreshViewTabTitle.trigger,
updateViewCoverImageColumnId, updateViewCoverImageColumnId,
duplicateView,
} }
}) })

13
tests/playwright/pages/Dashboard/ViewSidebar/index.ts

@ -154,19 +154,18 @@ export class ViewSidebarPage extends BasePage {
.locator('.nc-sidebar-view-node-context-btn') .locator('.nc-sidebar-view-node-context-btn')
.click(); .click();
await this.rootPage const copyViewAction = () =>
.locator(`[data-testid="view-sidebar-view-actions-${title}"]`) this.rootPage.locator(`[data-testid="view-sidebar-view-actions-${title}"]`).locator('.nc-view-copy-icon').click({
.locator('.nc-view-copy-icon')
.click({
force: true, force: true,
}); });
const submitAction = () =>
this.rootPage.locator('.ant-modal-content').locator('button:has-text("Create View"):visible').click();
await this.waitForResponse({ await this.waitForResponse({
httpMethodsToMatch: ['POST'], httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/api/v1/db/meta/tables/', requestUrlPathToMatch: '/api/v1/db/meta/tables/',
uiAction: submitAction, uiAction: copyViewAction,
}); });
await this.rootPage.locator(`[data-testid="view-sidebar-view-actions-${title}"]`).waitFor({ state: 'hidden' });
// await this.verifyToast({ message: 'View created successfully' }); // await this.verifyToast({ message: 'View created successfully' });
} }

Loading…
Cancel
Save