Browse Source

Merge pull request #4917 from nocodb/fix/4916-filter-value-key-events-issue

Fix: Grid view - keydown event handler issues
pull/4922/head
Raju Udava 1 year ago committed by GitHub
parent
commit
917c15713e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      packages/nc-gui/components/account/License.vue
  2. 546
      packages/nc-gui/components/smartsheet/Grid.vue
  3. 11
      packages/nc-gui/composables/useGlobal/actions.ts
  4. 1
      packages/nc-gui/composables/useGlobal/types.ts
  5. 7
      packages/nc-gui/composables/useMultiSelect/index.ts
  6. 2
      packages/nc-gui/pages/account/index.vue

7
packages/nc-gui/components/account/License.vue

@ -1,11 +1,13 @@
<script lang="ts" setup>
import { useNuxtApp } from '#app'
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg, useApi } from '#imports'
import { extractSdkResponseErrorMsg, useApi,useGlobal } from '#imports'
const { api, isLoading } = useApi()
const {$e} = useNuxtApp()
const { $e } = useNuxtApp()
const { loadAppInfo } = useGlobal()
let key = $ref('')
@ -22,6 +24,7 @@ const setLicense = async () => {
try {
await api.orgLicense.set({ key: key })
message.success('License key updated')
await loadAppInfo();
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}

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

@ -156,8 +156,8 @@ const getContainerScrollForElement = (
relativePos.right + (offset?.right || 0) > 0
? container.scrollLeft + relativePos.right + (offset?.right || 0)
: relativePos.left - (offset?.left || 0) < 0
? container.scrollLeft + relativePos.left - (offset?.left || 0)
: container.scrollLeft
? container.scrollLeft + relativePos.left - (offset?.left || 0)
: container.scrollLeft
/*
* If the element is below the container, scroll down (positive)
@ -167,134 +167,144 @@ const getContainerScrollForElement = (
relativePos.bottom + (offset?.bottom || 0) > 0
? container.scrollTop + relativePos.bottom + (offset?.bottom || 0)
: relativePos.top - (offset?.top || 0) < 0
? container.scrollTop + relativePos.top - (offset?.top || 0)
: container.scrollTop
? container.scrollTop + relativePos.top - (offset?.top || 0)
: container.scrollTop
return scroll
}
const { isCellSelected, activeCell, handleMouseDown, handleMouseOver, handleCellClick, clearSelectedRange, copyValue } =
useMultiSelect(
meta,
fields,
data,
$$(editEnabled),
isPkAvail,
clearCell,
makeEditable,
scrollToCell,
(e: KeyboardEvent) => {
// ignore navigating if picker(Date, Time, DateTime, Year)
// or single/multi select options is open
const activePickerOrDropdownEl = document.querySelector(
'.nc-picker-datetime.active,.nc-dropdown-single-select-cell.active,.nc-dropdown-multi-select-cell.active,.nc-picker-date.active,.nc-picker-year.active,.nc-picker-time.active',
)
if (activePickerOrDropdownEl) {
const {
isCellSelected,
activeCell,
handleMouseDown,
handleMouseOver,
handleCellClick,
clearSelectedRange,
copyValue,
isCellActive,
} = useMultiSelect(
meta,
fields,
data,
$$(editEnabled),
isPkAvail,
clearCell,
makeEditable,
scrollToCell,
(e: KeyboardEvent) => {
// ignore navigating if picker(Date, Time, DateTime, Year)
// or single/multi select options is open
const activePickerOrDropdownEl = document.querySelector(
'.nc-picker-datetime.active,.nc-dropdown-single-select-cell.active,.nc-dropdown-multi-select-cell.active,.nc-picker-date.active,.nc-picker-year.active,.nc-picker-time.active',
)
if (activePickerOrDropdownEl) {
e.preventDefault()
return true
}
// skip keyboard event handling if there is a drawer / modal
if (isDrawerOrModalExist()) {
return true
}
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
const altOrOptionKey = e.altKey
if (e.key === ' ') {
if (isCellActive.value && !editEnabled && hasEditPermission) {
e.preventDefault()
clearSelectedRange()
const row = data.value[activeCell.row]
expandForm(row)
return true
}
// skip keyboard event handling if there is a drawer / modal
if (isDrawerOrModalExist()) {
} else if (e.key === 'Escape') {
if (editEnabled) {
editEnabled = false
return true
}
} else if (e.key === 'Enter') {
if (e.shiftKey) {
// add a line break for types like LongText / JSON
return true
}
if (editEnabled) {
editEnabled = false
return true
}
}
if (cmdOrCtrl) {
if (!isCellActive.value) return
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
const altOrOptionKey = e.altKey
if (e.key === ' ') {
if (activeCell.row != null && !editEnabled && hasEditPermission) {
switch (e.key) {
case 'ArrowUp':
e.preventDefault()
clearSelectedRange()
const row = data.value[activeCell.row]
expandForm(row)
activeCell.row = 0
activeCell.col = activeCell.col ?? 0
scrollToCell?.()
editEnabled = false
return true
}
} else if (e.key === 'Escape') {
if (editEnabled) {
case 'ArrowDown':
e.preventDefault()
clearSelectedRange()
activeCell.row = data.value.length - 1
activeCell.col = activeCell.col ?? 0
scrollToCell?.()
editEnabled = false
return true
}
} else if (e.key === 'Enter') {
if (e.shiftKey) {
// add a line break for types like LongText / JSON
case 'ArrowRight':
e.preventDefault()
clearSelectedRange()
activeCell.row = activeCell.row ?? 0
activeCell.col = fields.value?.length - 1
scrollToCell?.()
editEnabled = false
return true
}
if (editEnabled) {
case 'ArrowLeft':
e.preventDefault()
clearSelectedRange()
activeCell.row = activeCell.row ?? 0
activeCell.col = 0
scrollToCell?.()
editEnabled = false
return true
}
}
if (cmdOrCtrl) {
switch (e.key) {
case 'ArrowUp':
e.preventDefault()
clearSelectedRange()
activeCell.row = 0
activeCell.col = activeCell.col ?? 0
scrollToCell?.()
editEnabled = false
return true
case 'ArrowDown':
e.preventDefault()
clearSelectedRange()
activeCell.row = data.value.length - 1
activeCell.col = activeCell.col ?? 0
scrollToCell?.()
editEnabled = false
return true
case 'ArrowRight':
e.preventDefault()
clearSelectedRange()
activeCell.row = activeCell.row ?? 0
activeCell.col = fields.value?.length - 1
scrollToCell?.()
editEnabled = false
return true
case 'ArrowLeft':
e.preventDefault()
clearSelectedRange()
activeCell.row = activeCell.row ?? 0
activeCell.col = 0
scrollToCell?.()
editEnabled = false
return true
}
}
}
if (altOrOptionKey) {
switch (e.keyCode) {
case 82: {
// ALT + R
if (isAddingEmptyRowAllowed) {
$e('c:shortcut', { key: 'ALT + R' })
addEmptyRow()
}
break
if (altOrOptionKey) {
switch (e.keyCode) {
case 82: {
// ALT + R
if (isAddingEmptyRowAllowed) {
$e('c:shortcut', { key: 'ALT + R' })
addEmptyRow()
}
case 67: {
// ALT + C
if (isAddingColumnAllowed) {
$e('c:shortcut', { key: 'ALT + C' })
addColumnDropdown.value = true
}
break
break
}
case 67: {
// ALT + C
if (isAddingColumnAllowed) {
$e('c:shortcut', { key: 'ALT + C' })
addColumnDropdown.value = true
}
break
}
}
},
async (ctx: { row: number; col?: number; updatedColumnTitle?: string }) => {
const rowObj = data.value[ctx.row]
const columnObj = ctx.col !== undefined ? fields.value[ctx.col] : null
}
},
async (ctx: { row: number; col?: number; updatedColumnTitle?: string }) => {
const rowObj = data.value[ctx.row]
const columnObj = ctx.col !== undefined ? fields.value[ctx.col] : null
if (!ctx.updatedColumnTitle && isVirtualCol(columnObj)) {
return
}
if (!ctx.updatedColumnTitle && isVirtualCol(columnObj)) {
return
}
// update/save cell value
await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title)
},
)
// update/save cell value
await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title)
},
)
function scrollToCell(row?: number | null, col?: number | null) {
row = row ?? activeCell.row
@ -672,105 +682,104 @@ const closeAddColumnDropdown = () => {
@contextmenu="showContextMenu"
>
<thead ref="tableHead">
<tr class="nc-grid-header border-1 bg-gray-100 sticky top[-1px]">
<th data-testid="grid-id-column">
<div class="w-full h-full bg-gray-100 flex min-w-[70px] 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: selectedAllRecords }">#</div>
<div
:class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }"
class="nc-check-all w-full items-center"
>
<a-checkbox v-model:checked="selectedAllRecords" />
<span class="flex-1" />
</div>
</template>
<template v-else>
<div class="text-gray-500">#</div>
</template>
</div>
</th>
<th
v-for="col in fields"
:key="col.title"
v-xc-ver-resize
:data-col="col.id"
:data-title="col.title"
@xcresize="onresize(col.id, $event)"
@xcresizing="onXcResizing(col.title, $event)"
@xcresized="resizingCol = null"
>
<div class="w-full h-full bg-gray-100 flex items-center">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readOnly" />
<tr class="nc-grid-header border-1 bg-gray-100 sticky top[-1px]">
<th data-testid="grid-id-column">
<div class="w-full h-full bg-gray-100 flex min-w-[70px] 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: selectedAllRecords }">#</div>
<div
:class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }"
class="nc-check-all w-full items-center"
>
<a-checkbox v-model:checked="selectedAllRecords" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly" />
</div>
</th>
<th
v-if="isAddingColumnAllowed"
v-e="['c:column:add']"
class="cursor-pointer"
@click.stop="addColumnDropdown = true"
>
<a-dropdown
v-model:visible="addColumnDropdown"
:trigger="['click']"
overlay-class-name="nc-dropdown-grid-add-column"
<span class="flex-1" />
</div>
</template>
<template v-else>
<div class="text-gray-500">#</div>
</template>
</div>
</th>
<th
v-for="col in fields"
:key="col.title"
v-xc-ver-resize
:data-col="col.id"
:data-title="col.title"
@xcresize="onresize(col.id, $event)"
@xcresizing="onXcResizing(col.title, $event)"
@xcresized="resizingCol = null"
>
<div class="h-full w-[60px] flex items-center justify-center">
<MdiPlus class="text-sm nc-column-add" />
<div class="w-full h-full bg-gray-100 flex items-center">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readOnly" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly" />
</div>
</th>
<th
v-if="isAddingColumnAllowed"
v-e="['c:column:add']"
class="cursor-pointer"
@click.stop="addColumnDropdown = true"
>
<a-dropdown
v-model:visible="addColumnDropdown"
:trigger="['click']"
overlay-class-name="nc-dropdown-grid-add-column"
>
<div class="h-full w-[60px] flex items-center justify-center">
<MdiPlus class="text-sm nc-column-add" />
</div>
<template #overlay>
<SmartsheetColumnEditOrAddProvider
v-if="addColumnDropdown"
:column-position="columnOrder"
@submit="closeAddColumnDropdown"
@cancel="closeAddColumnDropdown"
@click.stop
@keydown.stop
/>
</template>
</a-dropdown>
</th>
</tr>
<template #overlay>
<SmartsheetColumnEditOrAddProvider
v-if="addColumnDropdown"
:column-position="columnOrder"
@submit="closeAddColumnDropdown"
@cancel="closeAddColumnDropdown"
@click.stop
@keydown.stop
/>
</template>
</a-dropdown>
</th>
</tr>
</thead>
<tbody ref="tbodyEl">
<LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }">
<tr class="nc-grid-row" :data-testid="`grid-row-${rowIndex}`">
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1" :data-testid="`cell-Id-${rowIndex}`">
<div class="items-center flex gap-1 min-w-[55px]">
<div
v-if="!readOnly || !isLocked"
class="nc-row-no text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
>
{{ rowIndex + 1 }}
</div>
<div
v-if="!readOnly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
</div>
<span class="flex-1" />
<div
v-if="!readOnly || hasRole('commenter', true) || hasRole('viewer', true)"
class="nc-expand"
:data-testid="`nc-expand-${rowIndex}`"
:class="{ 'nc-comment': row.rowMeta?.commentCount }"
>
<a-spin
v-if="row.rowMeta.saving"
class="!flex items-center"
:data-testid="`row-save-spinner-${rowIndex}`"
/>
<template v-else>
<LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }">
<tr class="nc-grid-row" :data-testid="`grid-row-${rowIndex}`">
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1" :data-testid="`cell-Id-${rowIndex}`">
<div class="items-center flex gap-1 min-w-[55px]">
<div
v-if="!readOnly || !isLocked"
class="nc-row-no text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
>
{{ rowIndex + 1 }}
</div>
<div
v-if="!readOnly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
</div>
<span class="flex-1" />
<div
v-if="!readOnly || hasRole('commenter', true) || hasRole('viewer', true)"
class="nc-expand"
:data-testid="`nc-expand-${rowIndex}`"
:class="{ 'nc-comment': row.rowMeta?.commentCount }"
>
<a-spin
v-if="row.rowMeta.saving"
class="!flex items-center"
:data-testid="`row-save-spinner-${rowIndex}`"
/>
<template v-else>
<span
v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
@ -779,86 +788,86 @@ const closeAddColumnDropdown = () => {
>
{{ row.rowMeta.commentCount }}
</span>
<div
v-else
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)"
>
<MdiArrowExpand
v-e="['c:row-expand']"
class="select-none transform hover:(text-accent scale-120) nc-row-expand"
@click="expandForm(row, state)"
/>
</div>
</template>
<div
v-else
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)"
>
<MdiArrowExpand
v-e="['c:row-expand']"
class="select-none transform hover:(text-accent scale-120) nc-row-expand"
@click="expandForm(row, state)"
/>
</div>
</template>
</div>
</div>
</div>
</td>
<SmartsheetTableDataCell
v-for="(columnObj, colIndex) of fields"
:key="columnObj.id"
class="cell relative cursor-pointer nc-grid-cell"
:class="{
</td>
<SmartsheetTableDataCell
v-for="(columnObj, colIndex) of fields"
:key="columnObj.id"
class="cell relative cursor-pointer nc-grid-cell"
:class="{
'active': hasEditPermission && isCellSelected(rowIndex, colIndex),
'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row),
}"
:data-testid="`cell-${columnObj.title}-${rowIndex}`"
:data-key="rowIndex + columnObj.id"
:data-col="columnObj.id"
:data-title="columnObj.title"
@mousedown="handleMouseDown($event, rowIndex, colIndex)"
@mouseover="handleMouseOver(rowIndex, colIndex)"
@click="handleCellClick($event, rowIndex, colIndex)"
@dblclick="makeEditable(row, columnObj)"
@contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })"
>
<div v-if="!switchingTab" class="w-full h-full">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(columnObj)"
v-model="row.row[columnObj.title]"
:column="columnObj"
:active="activeCell.col === colIndex && activeCell.row === rowIndex"
:row="row"
:read-only="readOnly"
@navigate="onNavigate"
/>
:data-testid="`cell-${columnObj.title}-${rowIndex}`"
:data-key="rowIndex + columnObj.id"
:data-col="columnObj.id"
:data-title="columnObj.title"
@mousedown="handleMouseDown($event, rowIndex, colIndex)"
@mouseover="handleMouseOver(rowIndex, colIndex)"
@click="handleCellClick($event, rowIndex, colIndex)"
@dblclick="makeEditable(row, columnObj)"
@contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })"
>
<div v-if="!switchingTab" class="w-full h-full">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(columnObj)"
v-model="row.row[columnObj.title]"
:column="columnObj"
:active="activeCell.col === colIndex && activeCell.row === rowIndex"
:row="row"
:read-only="readOnly"
@navigate="onNavigate"
/>
<LazySmartsheetCell
v-else
v-model="row.row[columnObj.title]"
:column="columnObj"
:edit-enabled="
<LazySmartsheetCell
v-else
v-model="row.row[columnObj.title]"
: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"
@cancel="editEnabled = false"
/>
</div>
</SmartsheetTableDataCell>
</tr>
</template>
</LazySmartsheetRow>
<tr v-if="isAddingEmptyRowAllowed">
<td
v-e="['c:row:add:grid-bottom']"
:colspan="visibleColLength + 1"
class="text-left pointer nc-grid-add-new-cell cursor-pointer"
@click="addEmptyRow()"
>
<div class="px-2 w-full flex items-center text-gray-500">
<MdiPlus class="text-pint-500 text-xs ml-2 text-primary" />
: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"
@cancel="editEnabled = false"
/>
</div>
</SmartsheetTableDataCell>
</tr>
</template>
</LazySmartsheetRow>
<tr v-if="isAddingEmptyRowAllowed">
<td
v-e="['c:row:add:grid-bottom']"
:colspan="visibleColLength + 1"
class="text-left pointer nc-grid-add-new-cell cursor-pointer"
@click="addEmptyRow()"
>
<div class="px-2 w-full flex items-center text-gray-500">
<MdiPlus class="text-pint-500 text-xs ml-2 text-primary" />
<span class="ml-1">
<span class="ml-1">
{{ $t('activity.addRow') }}
</span>
</div>
</td>
</tr>
</div>
</td>
</tr>
</tbody>
</table>
@ -897,8 +906,7 @@ const closeAddColumnDropdown = () => {
</div>
</a-menu-item>
<a-menu-item v-if="contextMenuTarget" data-testid="context-menu-item-copy"
@click="copyValue(contextMenuTarget)">
<a-menu-item v-if="contextMenuTarget" data-testid="context-menu-item-copy" @click="copyValue(contextMenuTarget)">
<div v-e="['a:row:copy']" class="nc-project-menu-item">
<!-- Copy -->
{{ $t('general.copy') }}

11
packages/nc-gui/composables/useGlobal/actions.ts

@ -43,5 +43,14 @@ export function useGlobalActions(state: State): Actions {
})
}
return { signIn, signOut, refreshToken }
const loadAppInfo = async () => {
try {
const nuxtApp = useNuxtApp()
state.appInfo.value = await nuxtApp.$api.utils.appInfo()
} catch (e) {
console.error(e)
}
}
return { signIn, signOut, refreshToken, loadAppInfo }
}

1
packages/nc-gui/composables/useGlobal/types.ts

@ -61,6 +61,7 @@ export interface Actions {
signOut: () => void
signIn: (token: string) => void
refreshToken: () => void
loadAppInfo: () => void
}
export type ReadonlyState = Readonly<Pick<State, 'token' | 'user'>> & Omit<State, 'token' | 'user'>

7
packages/nc-gui/composables/useMultiSelect/index.ts

@ -61,6 +61,10 @@ export function useMultiSelect(
const columnLength = $computed(() => unref(fields)?.length)
const isCellActive = computed(
() => !(activeCell.row === null || activeCell.col === null || isNaN(activeCell.row) || isNaN(activeCell.col)),
)
function makeActive(row: number, col: number) {
if (activeCell.row === row && activeCell.col === col) {
return
@ -171,7 +175,7 @@ export function useMultiSelect(
return true
}
if (activeCell.row === null || activeCell.col === null) {
if (!isCellActive.value) {
return
}
@ -367,6 +371,7 @@ export function useMultiSelect(
useEventListener(document, 'mouseup', handleMouseUp)
return {
isCellActive,
handleMouseDown,
handleMouseOver,
clearSelectedRange,

2
packages/nc-gui/pages/account/index.vue

@ -81,7 +81,7 @@ const openKeys = ref([/^\/account\/users/.test($route.fullPath) && 'users'])
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('license')"
key="apps"
key="license"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
@click="navigateTo('/account/license')"
>

Loading…
Cancel
Save