Browse Source

fix: Added column drag and drop (WIP, without auto scrolling)

pull/6810/head
Muhammed Mustafa 11 months ago
parent
commit
b8efaa2fe9
  1. 157
      packages/nc-gui/components/smartsheet/grid/Table.vue
  2. 84
      packages/nc-gui/components/smartsheet/grid/useColumnDrag.ts

157
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -3,6 +3,8 @@ import axios from 'axios'
import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, ViewTypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { useColumnDrag } from './useColumnDrag'
import type { CellRange, Row } from '#imports'
import {
ActiveViewInj,
CellUrlDisableOverlayInj,
@ -39,7 +41,6 @@ import {
useViewsStore,
watch,
} from '#imports'
import type { CellRange, Row } from '#imports'
const props = defineProps<{
data: Row[]
@ -179,6 +180,11 @@ const { isUIAllowed } = useRoles()
const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
const isAddingColumnAllowed = computed(() => !readOnly.value && !isLocked.value && isUIAllowed('fieldAdd') && !isSqlView.value)
const { onDrag, onDragStart, draggedCol, dragColPlaceholderDomRef, toBeDroppedColId } = useColumnDrag({
fields,
tableBodyEl,
})
// #Variables
const addColumnDropdown = ref(false)
@ -1199,8 +1205,27 @@ const loaderText = computed(() => {
</script>
<template>
<div class="flex flex-col" :class="`${headerOnly !== true ? 'h-full w-full' : ''}`">
<div ref="gridWrapper" class="nc-grid-wrapper min-h-0 flex-1 relative" :class="gridWrapperClass">
<div :class="`${headerOnly !== true ? 'h-full w-full' : ''}`" class="flex flex-col">
<div data-testid="drag-icon-placeholder" class="absolute w-1 h-1 pointer-events-none"></div>
<div
ref="dragColPlaceholderDomRef"
:class="{
'hidden w-0 !h-0 left-0 !max-h-0 !max-w-0': !draggedCol,
}"
class="absolute flex items-center z-40 top-0 h-full bg-gray-50 pointer-events-none opacity-60"
>
<div
v-if="draggedCol"
:style="{
'min-width': gridViewCols[draggedCol.id!]?.width || '200px',
'max-width': gridViewCols[draggedCol.id!]?.width || '200px',
'width': gridViewCols[draggedCol.id!]?.width || '200px',
}"
class="border-r-1 border-l-1 border-gray-200 h-full"
></div>
</div>
<div ref="gridWrapper" :class="gridWrapperClass" class="nc-grid-wrapper min-h-0 flex-1 relative">
<div
v-show="showSkeleton && !isPaginationLoading && showLoaderAfterDelay"
class="flex items-center justify-center absolute l-0 t-0 w-full h-full z-10 pb-10"
@ -1215,14 +1240,14 @@ const loaderText = computed(() => {
:trigger="isSqlView ? [] : ['contextmenu']"
overlay-class-name="nc-dropdown-grid-context-menu"
>
<div class="table-overlay" :class="{ 'nc-grid-skelton-loader': showSkeleton }">
<div :class="{ 'nc-grid-skelton-loader': showSkeleton }" class="table-overlay">
<table
ref="smartTable"
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white relative"
:class="{
mobile: isMobileMode,
desktop: !isMobileMode,
}"
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white relative pr-60 pb-12"
@contextmenu="showContextMenu"
>
<thead v-show="hideHeader !== true" ref="tableHeadEl">
@ -1230,15 +1255,15 @@ const loaderText = computed(() => {
<td
v-for="(col, colIndex) of dummyColumnDataForLoading"
:key="colIndex"
class="!bg-gray-50 h-full border-b-1 border-r-1"
:class="{ 'min-w-50': colIndex !== 0, 'min-w-21.25': colIndex === 0 }"
class="!bg-gray-50 h-full border-b-1 border-r-1"
>
<a-skeleton
:active="true"
:title="true"
:class="{ 'max-w-32': colIndex !== 0, 'max-w-5 !ml-3.5': colIndex === 0 }"
:paragraph="false"
:title="true"
class="ml-2 -mt-2"
:class="{ 'max-w-32': colIndex !== 0, 'max-w-5 !ml-3.5': colIndex === 0 }"
/>
</td>
</tr>
@ -1246,7 +1271,7 @@ const loaderText = computed(() => {
<th class="w-[85px] min-w-[85px]" data-testid="grid-id-column" @dblclick="() => {}">
<div class="w-full h-full flex pl-5 pr-1 items-center" data-testid="nc-check-all">
<template v-if="!readOnly">
<div class="nc-no-label text-gray-500" :class="{ hidden: vSelectedAllRecords }">#</div>
<div :class="{ hidden: vSelectedAllRecords }" class="nc-no-label text-gray-500">#</div>
<div
:class="{ hidden: !vSelectedAllRecords, flex: vSelectedAllRecords }"
class="nc-check-all w-full items-center"
@ -1272,10 +1297,14 @@ const loaderText = computed(() => {
'max-width': gridViewCols[col.id]?.width || '200px',
'width': gridViewCols[col.id]?.width || '200px',
}"
@xcstartresizing="onXcStartResizing(col.id, $event)"
class="nc-grid-column-header relative"
draggable="true"
@click="selectColumn(col.id!)"
@xcresize="onresize(col.id, $event)"
@xcresizing="onXcResizing(col.id, $event)"
@click="selectColumn(col.id!)"
@xcstartresizing="onXcStartResizing(col.id, $event)"
@dragstart.stop="onDragStart(col.id!, $event)"
@drag.stop="onDrag($event)"
>
<div class="w-full h-full flex items-center">
<LazySmartsheetHeaderVirtualCell
@ -1285,14 +1314,24 @@ const loaderText = computed(() => {
/>
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly || isMobileMode" />
</div>
<div
:class="{
'w-3': toBeDroppedColId === col.id,
'hidden': toBeDroppedColId !== col.id,
}"
:style="{
'background-color': 'rgba(54, 191, 255, 0.5)',
}"
class="absolute -right-1.5 -bottom-2 h-2 z-10 rounded-b-lg"
></div>
</th>
<th
v-if="isAddingColumnAllowed"
v-e="['c:column:add']"
class="cursor-pointer !border-0 relative !xs:hidden"
:style="{
borderWidth: '0px !important',
}"
class="cursor-pointer !border-0 relative !xs:hidden"
@click.stop="addColumnDropdown = true"
>
<div class="absolute top-0 left-0 h-10.25 border-b-1 border-r-1 border-gray-200 nc-grid-add-edit-column group">
@ -1303,7 +1342,7 @@ const loaderText = computed(() => {
@visible-change="persistMenu = altModifier"
>
<div class="h-full w-[60px] flex items-center justify-center">
<GeneralIcon v-if="isEeUI && (altModifier || persistMenu)" icon="magic" class="text-sm text-orange-400" />
<GeneralIcon v-if="isEeUI && (altModifier || persistMenu)" class="text-sm text-orange-400" icon="magic" />
<component :is="iconMap.plus" class="text-base nc-column-add text-gray-500 !group-hover:text-black" />
</div>
@ -1380,14 +1419,14 @@ const loaderText = computed(() => {
<template v-else #overlay>
<SmartsheetColumnEditOrAddProvider
v-if="addColumnDropdown"
:preload="preloadColumn"
:column-position="columnOrder"
:class="{ hidden: isJsonExpand }"
@submit="closeAddColumnDropdown(true)"
:column-position="columnOrder"
:preload="preloadColumn"
@cancel="closeAddColumnDropdown()"
@mounted="preloadColumn = undefined"
@submit="closeAddColumnDropdown(true)"
@click.stop
@keydown.stop
@mounted="preloadColumn = undefined"
/>
</template>
</a-dropdown>
@ -1401,35 +1440,35 @@ const loaderText = computed(() => {
<td
v-for="(col, colIndex) of dummyColumnDataForLoading"
:key="colIndex"
class="border-b-1 border-r-1"
:class="{ 'min-w-50': colIndex !== 0, 'min-w-21.25': colIndex === 0 }"
class="border-b-1 border-r-1"
></td>
</tr>
</template>
<LazySmartsheetRow
v-for="(row, rowIndex) of dataRef"
v-show="!showSkeleton"
ref="rowRefs"
:key="rowIndex"
ref="rowRefs"
:row="row"
>
<template #default="{ state }">
<tr
class="nc-grid-row !xs:h-14"
:style="{ height: rowHeight ? `${rowHeight * 1.8}rem` : `1.8rem` }"
:data-testid="`grid-row-${rowIndex}`"
:style="{ height: rowHeight ? `${rowHeight * 1.8}rem` : `1.8rem` }"
class="nc-grid-row !xs:h-14"
>
<td
key="row-index"
class="caption nc-grid-cell pl-5 pr-1"
:data-testid="`cell-Id-${rowIndex}`"
class="caption nc-grid-cell pl-5 pr-1"
@contextmenu="contextMenuTarget = null"
>
<div class="items-center flex gap-1 min-w-[60px]">
<div
v-if="!readOnly || !isLocked || isMobileMode"
class="nc-row-no sm:min-w-4 text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
class="nc-row-no sm:min-w-4 text-xs text-gray-500"
>
{{ ((paginationDataRef?.page ?? 1) - 1) * (paginationDataRef?.pageSize ?? 25) + rowIndex + 1 }}
</div>
@ -1444,21 +1483,21 @@ const loaderText = computed(() => {
<div
v-if="isUIAllowed('expandedForm')"
class="nc-expand"
:data-testid="`nc-expand-${rowIndex}`"
:class="{ 'nc-comment': row.rowMeta?.commentCount }"
:data-testid="`nc-expand-${rowIndex}`"
class="nc-expand"
>
<a-spin
v-if="row.rowMeta.saving"
class="!flex items-center"
:data-testid="`row-save-spinner-${rowIndex}`"
class="!flex items-center"
/>
<template v-else-if="!isLocked">
<span
v-if="row.rowMeta?.commentCount && expandForm"
v-e="['c:expanded-form:open']"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
@click="expandAndLooseFocus(row, state)"
>
{{ row.rowMeta.commentCount }}
@ -1482,7 +1521,6 @@ const loaderText = computed(() => {
<SmartsheetTableDataCell
v-for="(columnObj, colIndex) of fields"
:key="columnObj.id"
class="cell relative nc-grid-cell"
:class="{
'cursor-pointer': hasEditPermission,
'active': hasEditPermission && isCellSelected(rowIndex, colIndex),
@ -1502,31 +1540,32 @@ const loaderText = computed(() => {
hasEditPermission &&
isCellSelected(rowIndex, colIndex),
}"
:data-col="columnObj.id"
:data-col-index="colIndex"
:data-key="`data-key-${rowIndex}-${columnObj.id}`"
:data-row-index="rowIndex"
:data-testid="`cell-${columnObj.title}-${rowIndex}`"
:data-title="columnObj.title"
:style="{
'min-width': gridViewCols[columnObj.id]?.width || '200px',
'max-width': gridViewCols[columnObj.id]?.width || '200px',
'width': gridViewCols[columnObj.id]?.width || '200px',
}"
:data-testid="`cell-${columnObj.title}-${rowIndex}`"
:data-key="`data-key-${rowIndex}-${columnObj.id}`"
:data-col="columnObj.id"
:data-title="columnObj.title"
:data-row-index="rowIndex"
:data-col-index="colIndex"
@mousedown="handleMouseDown($event, rowIndex, colIndex)"
@mouseover="handleMouseOver($event, rowIndex, colIndex)"
class="cell relative nc-grid-cell"
@click="handleCellClick($event, rowIndex, colIndex)"
@dblclick="makeEditable(row, columnObj)"
@contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })"
@dblclick="makeEditable(row, columnObj)"
@mousedown="handleMouseDown($event, rowIndex, colIndex)"
@mouseover="handleMouseOver($event, rowIndex, colIndex)"
>
<div v-if="!switchingTab" class="w-full h-full">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(columnObj) && columnObj.title"
v-model="row.row[columnObj.title]"
:column="columnObj"
:active="activeCell.col === colIndex && activeCell.row === rowIndex"
:row="row"
:column="columnObj"
:read-only="readOnly"
:row="row"
@navigate="onNavigate"
@save="updateOrSaveRow?.(row, '', state)"
/>
@ -1534,17 +1573,17 @@ const loaderText = computed(() => {
<LazySmartsheetCell
v-else-if="columnObj.title"
v-model="row.row[columnObj.title]"
:active="activeCell.col === colIndex && activeCell.row === rowIndex"
:column="columnObj"
:edit-enabled="
!!hasEditPermission && !!editEnabled && activeCell.col === colIndex && activeCell.row === rowIndex
"
:row-index="rowIndex"
:active="activeCell.col === colIndex && activeCell.row === rowIndex"
:read-only="readOnly"
@update:edit-enabled="editEnabled = $event"
@save="updateOrSaveRow?.(row, columnObj.title, state)"
@navigate="onNavigate"
:row-index="rowIndex"
@cancel="editEnabled = false"
@navigate="onNavigate"
@save="updateOrSaveRow?.(row, columnObj.title, state)"
@update:edit-enabled="editEnabled = $event"
/>
</div>
</SmartsheetTableDataCell>
@ -1555,12 +1594,12 @@ const loaderText = computed(() => {
<tr
v-if="isAddingEmptyRowAllowed && !isGroupBy"
v-e="['c:row:add:grid-bottom']"
class="text-left nc-grid-add-new-cell cursor-pointer group relative z-3 xs:hidden"
:class="{
'!border-r-2 !border-r-gray-100': visibleColLength === 1,
}"
@mouseup.stop
class="text-left nc-grid-add-new-cell cursor-pointer group relative z-3 xs:hidden"
@click="addEmptyRow()"
@mouseup.stop
>
<div
class="h-10.5 border-b-1 border-gray-100 bg-white group-hover:bg-gray-50 absolute left-0 bottom-0 px-2 sticky z-40 w-full flex items-center text-gray-500"
@ -1571,7 +1610,7 @@ const loaderText = computed(() => {
class="text-pint-500 text-base ml-2 mt-0 text-gray-600 group-hover:text-black"
/>
</div>
<td class="!border-gray-100" :colspan="visibleColLength"></td>
<td :colspan="visibleColLength" class="!border-gray-100"></td>
</tr>
</tbody>
</table>
@ -1580,13 +1619,13 @@ const loaderText = computed(() => {
<div
v-show="showFillHandle"
ref="fillHandle"
class="nc-fill-handle"
:class="
(!selectedRange.isEmpty() && selectedRange.end.col !== 0) || (selectedRange.isEmpty() && activeCell.col !== 0)
? 'z-3'
: 'z-4'
"
:style="{ top: `${fillHandleTop}px`, left: `${fillHandleLeft}px`, cursor: 'crosshair' }"
class="nc-fill-handle"
/>
</div>
@ -1659,7 +1698,7 @@ const loaderText = computed(() => {
class="nc-base-menu-item"
@click="clearSelectedRangeOfCells()"
>
<GeneralIcon icon="closeBox" class="text-gray-500" />
<GeneralIcon class="text-gray-500" icon="closeBox" />
{{ $t('general.clear') }}
</NcMenuItem>
@ -1680,7 +1719,7 @@ const loaderText = computed(() => {
class="nc-base-menu-item !text-red-600 !hover:bg-red-50"
@click="deleteSelectedRangeOfRows"
>
<GeneralIcon icon="delete" class="text-gray-500 text-red-600" />
<GeneralIcon class="text-gray-500 text-red-600" icon="delete" />
<!-- Delete Rows -->
{{ $t('activity.deleteRows') }}
</NcMenuItem>
@ -1694,13 +1733,13 @@ const loaderText = computed(() => {
v-if="headerOnly !== true"
:key="isMobileMode"
v-model:pagination-data="paginationDataRef"
:show-api-timing="!isGroupBy"
align-count-on-right
:align-left="isGroupBy"
:change-page="changePage"
:hide-sidebars="paginationStyleRef?.hideSidebars === true"
:fixed-size="paginationStyleRef?.fixedSize"
:extra-style="paginationStyleRef?.extraStyle"
:fixed-size="paginationStyleRef?.fixedSize"
:hide-sidebars="paginationStyleRef?.hideSidebars === true"
:show-api-timing="!isGroupBy"
align-count-on-right
>
<template #add-record>
<div v-if="isAddingEmptyRowAllowed" class="flex ml-1">
@ -1720,7 +1759,7 @@ const loaderText = computed(() => {
placement="top"
@click="isAddNewRecordGridMode ? addEmptyRow() : onNewRecordToFormClick()"
>
<div data-testid="nc-pagination-add-record" class="flex items-center px-2 text-gray-600 hover:text-black">
<div class="flex items-center px-2 text-gray-600 hover:text-black" data-testid="nc-pagination-add-record">
<span>
<template v-if="isAddNewRecordGridMode"> {{ $t('activity.newRecord') }} </template>
<template v-else> {{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }} </template>
@ -1730,12 +1769,12 @@ const loaderText = computed(() => {
<template #overlay>
<div class="relative overflow-visible min-h-17 w-10">
<div
class="absolute -top-19 flex flex-col h-34.5 w-70 bg-white rounded-lg justify-start overflow-hidden"
style="box-shadow: 0px 4px 6px -2px rgba(0, 0, 0, 0.06), 0px -12px 16px -4px rgba(0, 0, 0, 0.1)"
:class="{
'-left-44': !isAddNewRecordGridMode,
'-left-32': isAddNewRecordGridMode,
}"
class="absolute -top-19 flex flex-col h-34.5 w-70 bg-white rounded-lg justify-start overflow-hidden"
style="box-shadow: 0px 4px 6px -2px rgba(0, 0, 0, 0.06), 0px -12px 16px -4px rgba(0, 0, 0, 0.1)"
>
<div
v-e="['c:row:add:grid']"
@ -1800,13 +1839,14 @@ const loaderText = computed(() => {
}
</style>
<style scoped lang="scss">
<style lang="scss" scoped>
.nc-grid-wrapper {
@apply h-full w-full;
.nc-grid-add-edit-column {
@apply bg-gray-50;
}
.nc-grid-add-new-cell:hover td {
@apply text-black !bg-gray-50;
}
@ -1927,6 +1967,7 @@ const loaderText = computed(() => {
thead th:nth-child(2) {
@apply border-r-1 !border-r-gray-50;
}
tbody td:nth-child(2) {
@apply border-r-1 !border-r-gray-50;
}

84
packages/nc-gui/components/smartsheet/grid/useColumnDrag.ts

@ -0,0 +1,84 @@
import type { ColumnType } from 'nocodb-sdk'
export const useColumnDrag = ({
fields,
tableBodyEl,
}: {
fields: Ref<ColumnType[]>
tableBodyEl: Ref<HTMLElement | undefined>
}) => {
const { updateGridViewColumn, gridViewCols } = useViewColumnsOrThrow()
const { leftSidebarWidth } = storeToRefs(useSidebarStore())
const draggedCol = ref<ColumnType | null>(null)
const dragColPlaceholderDomRef = ref<HTMLElement | null>(null)
const toBeDroppedColId = ref<string | null>(null)
const reorderColumn = async (colId: string, toColId: string) => {
const col = gridViewCols.value[colId]
const toCol = gridViewCols.value[toColId]!
const toColIndex = fields.value.findIndex((f) => f.id === toColId)
const nextToColField = toColIndex < fields.value.length - 1 ? fields.value[toColIndex + 1] : null
const nextToCol = nextToColField ? gridViewCols.value[nextToColField.id!] : null
const newOrder = nextToCol ? toCol.order! + (nextToCol.order! - toCol.order!) / 2 : toCol.order! + 1
col.order = newOrder
await updateGridViewColumn(colId, { order: newOrder } as any)
}
const onDragStart = (colId: string, e: DragEvent) => {
if (!e.dataTransfer) return
const dom = document.querySelector('[data-testid="drag-icon-placeholder"]')
e.dataTransfer.dropEffect = 'none'
e.dataTransfer.effectAllowed = 'none'
e.dataTransfer.setDragImage(dom!, 10, 10)
e.dataTransfer.clearData()
e.dataTransfer.setData('text/plain', colId)
draggedCol.value = fields.value.find((f) => f.id === colId) ?? null
dragColPlaceholderDomRef.value!.style.height = `${tableBodyEl.value?.getBoundingClientRect().height}px`
}
const onDrag = (e: DragEvent) => {
if (!e.dataTransfer) return
e.preventDefault()
if (!dragColPlaceholderDomRef.value) return
if (e.clientX === 0) {
dragColPlaceholderDomRef.value!.style.left = `0px`
dragColPlaceholderDomRef.value!.style.height = '0px'
reorderColumn(draggedCol.value!.id!, toBeDroppedColId.value!)
draggedCol.value = null
toBeDroppedColId.value = null
return
}
const y = dragColPlaceholderDomRef.value!.getBoundingClientRect().top
const domsUnderMouse = document.elementsFromPoint(e.clientX, y)
const columnDom = domsUnderMouse.find((dom) => dom.classList.contains('nc-grid-column-header'))
if (columnDom) {
toBeDroppedColId.value = columnDom?.getAttribute('data-col') ?? null
}
const x = e.clientX - leftSidebarWidth.value
if (x >= 0) {
dragColPlaceholderDomRef.value.style.left = `${x.toString()}px`
}
}
return {
onDrag,
onDragStart,
draggedCol,
dragColPlaceholderDomRef,
toBeDroppedColId,
}
}
Loading…
Cancel
Save