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) => { onEnd: async (evt) => {
const { newIndex = 0, oldIndex = 0 } = evt const { newIndex = 0, oldIndex = 0 } = evt
if(newIndex === oldIndex) return
const itemEl = evt.item as HTMLLIElement const itemEl = evt.item as HTMLLIElement
const item = tablesById[itemEl.dataset.id as string] const item = tablesById[itemEl.dataset.id as string]
@ -185,6 +187,18 @@ const initSortable = (el: Element) => {
}) })
}, },
animation: 150, 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" class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order" :data-order="table.order"
:data-id="table.id" :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}`" :data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)" @click="addTableTab(table)"
> >
@ -1042,6 +1059,9 @@ const duplicateTable = async (table: TableType) => {
class="nc-tree-item text-sm cursor-pointer group" class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order" :data-order="table.order"
:data-id="table.id" :data-id="table.id"
:data-title="table.title"
:data-base-id="base.id"
:data-type="table.type"
:data-testid="`tree-view-table-${table.title}`" :data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)" @click="addTableTab(table)"
> >

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

@ -239,9 +239,9 @@ watch(view, async (nextView) => {
hoverable hoverable
class="!rounded-lg h-full overflow-hidden break-all max-w-[450px]" class="!rounded-lg h-full overflow-hidden break-all max-w-[450px]"
:data-testid="`nc-gallery-card-${record.row.id}`" :data-testid="`nc-gallery-card-${record.row.id}`"
:style="isPublic ? { cursor: 'default' } : { cursor: 'pointer' }"
@click="expandFormClick($event, record)" @click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, { row: rowIndex })" @contextmenu="showContextMenu($event, { row: rowIndex })"
:style="isPublic ? { cursor: 'default' } : { cursor: 'pointer' }"
> >
<template v-if="galleryData?.fk_cover_image_col_id" #cover> <template v-if="galleryData?.fk_cover_image_col_id" #cover>
<a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows> <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 skipRowRemovalOnCancel = ref(false)
const preloadColumn = ref<Partial<any>>()
function expandForm(row: Row, state?: Record<string, any>, fromToolbar = false) { function expandForm(row: Row, state?: Record<string, any>, fromToolbar = false) {
const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[]) const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
@ -459,6 +461,13 @@ const onXcResizing = (cn: string, event: any) => {
defineExpose({ defineExpose({
loadData, 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 // reset context menu target on hide
@ -896,6 +905,7 @@ function addEmptyRow(row?: number) {
<template #overlay> <template #overlay>
<SmartsheetColumnEditOrAddProvider <SmartsheetColumnEditOrAddProvider
v-if="addColumnDropdown" v-if="addColumnDropdown"
:preload="preloadColumn"
:column-position="columnOrder" :column-position="columnOrder"
@submit="closeAddColumnDropdown(true)" @submit="closeAddColumnDropdown(true)"
@cancel="closeAddColumnDropdown()" @cancel="closeAddColumnDropdown()"

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

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

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

@ -1,5 +1,6 @@
<script setup lang="ts"> <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 { import {
ActiveViewInj, ActiveViewInj,
FieldsInj, FieldsInj,
@ -29,7 +30,7 @@ const props = defineProps<{
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { metas } = useMetas() const { metas, getMeta } = useMetas()
const activeTab = toRef(props, 'activeTab') const activeTab = toRef(props, 'activeTab')
@ -64,10 +65,74 @@ provide(
ReadonlyInj, ReadonlyInj,
computed(() => !isUIAllowed('xcDatatableEditable')), 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> </script>
<template> <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"> <div class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar /> <LazySmartsheetToolbar />
@ -75,7 +140,7 @@ provide(
<template v-if="meta"> <template v-if="meta">
<div class="flex flex-1 min-h-0"> <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"> <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" /> <LazySmartsheetGallery v-else-if="isGallery" />

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

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