Browse Source

Merge pull request #5807 from nocodb/feat/drag-n-drop-ltar-lookup-creation

feat: Drag n drop LTAR and Lookup creation
pull/5856/head
mertmit 1 year ago committed by GitHub
parent
commit
f0a2d09369
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 20
      packages/nc-gui/components/dashboard/TreeView.vue
  2. 2
      packages/nc-gui/components/smartsheet/Gallery.vue
  3. 10
      packages/nc-gui/components/smartsheet/Grid.vue
  4. 15
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  5. 10
      packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue
  6. 73
      packages/nc-gui/components/tabs/Smartsheet.vue
  7. 2
      packages/nc-gui/components/virtual-cell/HasMany.vue
  8. 116
      tests/playwright/tests/db/columnLtarDragdrop.spec.ts

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

@ -97,6 +97,8 @@ const initSortable = (el: Element) => {
onEnd: async (evt) => {
const { newIndex = 0, oldIndex = 0 } = evt
if(newIndex === oldIndex) return
const itemEl = evt.item as HTMLLIElement
const item = tablesById[itemEl.dataset.id as string]
@ -185,6 +187,18 @@ const initSortable = (el: Element) => {
})
},
animation: 150,
setData(dataTransfer, dragEl) {
dataTransfer.setData(
'text/json',
JSON.stringify({
id: dragEl.dataset.id,
title: dragEl.dataset.title,
type: dragEl.dataset.type,
baseId: dragEl.dataset.baseId,
}),
)
},
revertOnSpill: true,
})
}
@ -718,6 +732,9 @@ const duplicateTable = async (table: TableType) => {
class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order"
:data-id="table.id"
:data-base-id="bases[0].id"
:data-type="table.type"
:data-title="table.title"
:data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)"
>
@ -1042,6 +1059,9 @@ const duplicateTable = async (table: TableType) => {
class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order"
:data-id="table.id"
:data-title="table.title"
:data-base-id="base.id"
:data-type="table.type"
:data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)"
>

2
packages/nc-gui/components/smartsheet/Gallery.vue

@ -239,9 +239,9 @@ watch(view, async (nextView) => {
hoverable
class="!rounded-lg h-full overflow-hidden break-all max-w-[450px]"
:data-testid="`nc-gallery-card-${record.row.id}`"
:style="isPublic ? { cursor: 'default' } : { cursor: 'pointer' }"
@click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, { row: rowIndex })"
:style="isPublic ? { cursor: 'default' } : { cursor: 'pointer' }"
>
<template v-if="galleryData?.fk_cover_image_col_id" #cover>
<a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows>

10
packages/nc-gui/components/smartsheet/Grid.vue

@ -430,6 +430,8 @@ const showLoading = ref(true)
const skipRowRemovalOnCancel = ref(false)
const preloadColumn = ref<Partial<any>>()
function expandForm(row: Row, state?: Record<string, any>, fromToolbar = false) {
const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
@ -459,6 +461,13 @@ const onXcResizing = (cn: string, event: any) => {
defineExpose({
loadData,
openColumnCreate: (data) => {
tableHead.value?.querySelector('th:last-child')?.scrollIntoView({ behavior: 'smooth' })
setTimeout(() => {
addColumnDropdown.value = true
preloadColumn.value = data
}, 500)
},
})
// reset context menu target on hide
@ -896,6 +905,7 @@ function addEmptyRow(row?: number) {
<template #overlay>
<SmartsheetColumnEditOrAddProvider
v-if="addColumnDropdown"
:preload="preloadColumn"
:column-position="columnOrder"
@submit="closeAddColumnDropdown(true)"
@cancel="closeAddColumnDropdown()"

15
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { ColumnReqType } from 'nocodb-sdk'
import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import {
IsFormInj,
@ -25,6 +25,7 @@ import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
import MdiIdentifierIcon from '~icons/mdi/identifier'
const props = defineProps<{
preload?: Partial<ColumnType>
columnPosition?: Pick<ColumnReqType, 'column_order'>
}>()
@ -125,6 +126,18 @@ watchEffect(() => {
onMounted(() => {
if (!isEdit.value) {
generateNewColumnMeta()
const { colOptions, ...others } = props.preload || {}
formState.value = {
...formState.value,
...others,
}
if (colOptions) {
onUidtOrIdTypeChange()
formState.value = {
...formState.value,
...colOptions,
}
}
} else {
if (formState.value.pk) {
message.info(t('msg.info.editingPKnotSupported'))

10
packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue

@ -6,6 +6,7 @@ import { MetaInj, inject, ref, toRef, useProvideColumnCreateStore } from '#impor
interface Props {
column?: ColumnType
columnPosition?: Pick<ColumnReqType, 'column_order'>
preload?: Partial<ColumnType>
}
const props = defineProps<Props>()
@ -16,9 +17,16 @@ const meta = inject(MetaInj, ref())
const column = toRef(props, 'column')
const preload = toRef(props, 'preload')
useProvideColumnCreateStore(meta, column)
</script>
<template>
<SmartsheetColumnEditOrAdd :column-position="props.columnPosition" @submit="emit('submit')" @cancel="emit('cancel')" />
<SmartsheetColumnEditOrAdd
:preload="preload"
:column-position="props.columnPosition"
@submit="emit('submit')"
@cancel="emit('cancel')"
/>
</template>

73
packages/nc-gui/components/tabs/Smartsheet.vue

@ -1,5 +1,6 @@
<script setup lang="ts">
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import {
ActiveViewInj,
FieldsInj,
@ -29,7 +30,7 @@ const props = defineProps<{
const { isUIAllowed } = useUIPermission()
const { metas } = useMetas()
const { metas, getMeta } = useMetas()
const activeTab = toRef(props, 'activeTab')
@ -64,10 +65,74 @@ provide(
ReadonlyInj,
computed(() => !isUIAllowed('xcDatatableEditable')),
)
const grid = ref()
const onDrop = async (event: DragEvent) => {
event.preventDefault()
try {
// Access the dropped data
const data = JSON.parse(event.dataTransfer?.getData('text/json')!)
// Do something with the received data
// if dragged item is not from the same base, return
if (data.baseId !== meta.value?.base_id) return
// if dragged item or opened view is not a table, return
if (data.type !== 'table' || meta.value?.type !== 'table') return
const childMeta = await getMeta(data.id)
const parentMeta = metas.value[meta.value.id!]
if (!childMeta || !parentMeta) return
const parentPkCol = parentMeta.columns?.find((c) => c.pk)
const childPkCol = childMeta.columns?.find((c) => c.pk)
// if already a link column exists, create a new Lookup column
const relationCol = parentMeta.columns?.find((c: ColumnType) => {
if (c.uidt !== UITypes.LinkToAnotherRecord) return false
const ltarOptions = c.colOptions as LinkToAnotherRecordType
if (ltarOptions.type !== 'mm') {
return false
}
if (ltarOptions.fk_related_model_id === childMeta.id) {
return true
}
return false
})
if (relationCol) {
const lookupCol = childMeta.columns?.find((c) => c.pv) ?? childMeta.columns?.[0]
grid.value?.openColumnCreate({
uidt: UITypes.Lookup,
title: `${data.title}Lookup`,
fk_relation_column_id: relationCol.id,
fk_lookup_column_id: lookupCol?.id,
})
} else {
grid.value?.openColumnCreate({
uidt: UITypes.LinkToAnotherRecord,
title: `${data.title}List`,
parentId: parentMeta.id,
childId: childMeta.id,
parentTable: parentMeta.title,
parentColumn: parentPkCol.title,
childTable: childMeta.title,
childColumn: childPkCol?.title,
})
}
} catch (e) {
console.log('error', e)
}
}
</script>
<template>
<div class="nc-container flex h-full">
<div class="nc-container flex h-full" @drop="onDrop" @dragover.prevent>
<div class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar />
@ -75,7 +140,7 @@ provide(
<template v-if="meta">
<div class="flex flex-1 min-h-0">
<div v-if="activeView" class="h-full flex-1 min-w-0 min-h-0 bg-gray-50">
<LazySmartsheetGrid v-if="isGrid" />
<LazySmartsheetGrid v-if="isGrid" ref="grid" />
<LazySmartsheetGallery v-else-if="isGallery" />

2
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -6,6 +6,7 @@ import {
ColumnInj,
IsFormInj,
IsLockedInj,
IsUnderLookupInj,
ReadonlyInj,
ReloadRowDataHookInj,
RowInj,
@ -16,7 +17,6 @@ import {
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
useUIPermission,
IsUnderLookupInj
} from '#imports'
const column = inject(ColumnInj)!

116
tests/playwright/tests/db/columnLtarDragdrop.spec.ts

@ -0,0 +1,116 @@
import { expect, Locator, test } from '@playwright/test';
import setup from '../../setup';
import { Api, UITypes } from 'nocodb-sdk';
import { DashboardPage } from '../../pages/Dashboard';
import { GridPage } from '../../pages/Dashboard/Grid';
import { getTextExcludeIconText } from '../utils/general';
let api: Api<any>;
const recordCount = 10;
test.describe('Test table', () => {
let context: any;
let dashboard: DashboardPage;
let grid: GridPage;
const tables = [];
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: true });
dashboard = new DashboardPage(page, context.project);
grid = dashboard.grid;
api = new Api({
baseURL: `http://localhost:8080/`,
headers: {
'xc-auth': context.token,
},
});
const columns = [
{
column_name: 'Id',
title: 'Id',
uidt: UITypes.ID,
},
{
column_name: 'Title',
title: 'Title',
uidt: UITypes.SingleLineText,
pv: true,
},
];
const rows = [];
for (let i = 0; i < recordCount; i++) {
rows.push({
Id: i + 1,
Title: `${i + 1}`,
});
}
// Create tables
const project = await api.project.read(context.project.id);
for (let i = 0; i < 2; i++) {
const table = await api.base.tableCreate(context.project.id, project.bases?.[0].id, {
table_name: `Table${i}`,
title: `Table${i}`,
columns: columns,
});
tables.push(table);
await api.dbTableRow.bulkCreate('noco', context.project.id, tables[i].id, rows);
}
// refresh page
await page.reload();
});
test('drag drop for LTAR, lookup creation', async () => {
await dashboard.treeView.openTable({ title: 'Table0' });
const src = await dashboard.rootPage.locator(`[data-testid="tree-view-table-draggable-handle-Table1"]`);
const dst = await dashboard.rootPage.locator(`[data-testid="grid-row-0"]`);
// drag drop for LTAR column creation
//
await src.dragTo(dst);
const columnAddModal = await dashboard.rootPage.locator(`.nc-dropdown-grid-add-column`);
{
const columnType = await getTextExcludeIconText(await columnAddModal.locator(`.nc-column-type-input`));
const linkTable = await getTextExcludeIconText(
await columnAddModal.locator(`.ant-form-item-control-input`).nth(3)
);
expect(columnType).toContain('LinkToAnotherRecord');
expect(linkTable).toContain('Table1');
// save
await columnAddModal.locator(`.ant-btn-primary`).click();
// verify if column is created
await grid.column.verify({ title: 'Table1List', isVisible: true });
}
// drag drop for lookup column creation
//
await src.dragTo(dst);
{
// const columnAddModal = await dashboard.rootPage.locator(`.nc-dropdown-grid-add-column`);
const columnType = await getTextExcludeIconText(await columnAddModal.locator(`.nc-column-type-input`));
const linkField = await getTextExcludeIconText(
await columnAddModal.locator(`.ant-form-item-control-input`).nth(2)
);
const childColumn = await getTextExcludeIconText(
await columnAddModal.locator(`.ant-form-item-control-input`).nth(3)
);
// validate
expect(columnType).toContain('Lookup');
expect(linkField).toContain('Table1List');
expect(childColumn).toContain('Title');
// save
await columnAddModal.locator(`.ant-btn-primary`).click();
// verify if column is created
await grid.column.verify({ title: 'Table1Lookup', isVisible: true });
}
});
});
Loading…
Cancel
Save