@ -1,5 +1,5 @@
< script lang = "ts" setup >
< script lang = "ts" setup >
import type { ColumnType , TableType , ViewType } from 'nocodb-sdk'
import type { ColumnReqType , Column Type , TableType , ViewType } from 'nocodb-sdk'
import { UITypes , isVirtualCol } from 'nocodb-sdk'
import { UITypes , isVirtualCol } from 'nocodb-sdk'
import {
import {
ActiveViewInj ,
ActiveViewInj ,
@ -11,17 +11,20 @@ import {
IsGridInj ,
IsGridInj ,
IsLockedInj ,
IsLockedInj ,
MetaInj ,
MetaInj ,
NavigateDir ,
OpenNewRecordFormHookInj ,
OpenNewRecordFormHookInj ,
PaginationDataInj ,
PaginationDataInj ,
ReadonlyInj ,
ReadonlyInj ,
ReloadRowDataHookInj ,
ReloadRowDataHookInj ,
ReloadViewDataHookInj ,
ReloadViewDataHookInj ,
SmartsheetStoreEvents ,
computed ,
computed ,
createEventHook ,
createEventHook ,
enumColor ,
enumColor ,
extractPkFromRow ,
extractPkFromRow ,
inject ,
inject ,
isColumnRequiredAndNull ,
isColumnRequiredAndNull ,
isDrawerOrModalExist ,
isMac ,
isMac ,
message ,
message ,
onBeforeUnmount ,
onBeforeUnmount ,
@ -34,6 +37,7 @@ import {
useI18n ,
useI18n ,
useMetas ,
useMetas ,
useMultiSelect ,
useMultiSelect ,
useNuxtApp ,
useRoles ,
useRoles ,
useRoute ,
useRoute ,
useSmartsheetStoreOrThrow ,
useSmartsheetStoreOrThrow ,
@ -42,7 +46,6 @@ import {
watch ,
watch ,
} from '#imports'
} from '#imports'
import type { Row } from '~/lib'
import type { Row } from '~/lib'
import { NavigateDir } from '~/lib'
const { t } = useI18n ( )
const { t } = useI18n ( )
@ -50,6 +53,8 @@ const meta = inject(MetaInj, ref())
const view = inject ( ActiveViewInj , ref ( ) )
const view = inject ( ActiveViewInj , ref ( ) )
const { $e } = useNuxtApp ( )
/ / k e e p a r o o t f i e l d s v a r i a b l e a n d w i l l g e t m o d i f i e d f r o m
/ / k e e p a r o o t f i e l d s v a r i a b l e a n d w i l l g e t m o d i f i e d f r o m
/ / f i e l d s m e n u a n d g e t u s e d i n g r i d a n d g a l l e r y
/ / f i e l d s m e n u a n d g e t u s e d i n g r i d a n d g a l l e r y
const fields = inject ( FieldsInj , ref ( [ ] ) )
const fields = inject ( FieldsInj , ref ( [ ] ) )
@ -71,7 +76,7 @@ const isView = false
let editEnabled = $ref ( false )
let editEnabled = $ref ( false )
const { xWhere , isPkAvail , isSqlView } = useSmartsheetStoreOrThrow ( )
const { xWhere , isPkAvail , isSqlView , eventBus } = useSmartsheetStoreOrThrow ( )
const visibleColLength = $computed ( ( ) => fields . value ? . length )
const visibleColLength = $computed ( ( ) => fields . value ? . length )
@ -95,6 +100,10 @@ const tbodyEl = ref<HTMLElement>()
const gridWrapper = ref < HTMLElement > ( )
const gridWrapper = ref < HTMLElement > ( )
const tableHead = ref < HTMLElement > ( )
const tableHead = ref < HTMLElement > ( )
const isAddingColumnAllowed = ! readOnly . value && ! isLocked . value && isUIAllowed ( 'add-column' ) && ! isSqlView . value
const isAddingEmptyRowAllowed = ! isView && ! isLocked . value && hasEditPermission && ! isSqlView . value
const {
const {
isLoading ,
isLoading ,
loadData ,
loadData ,
@ -162,83 +171,132 @@ const getContainerScrollForElement = (
return scroll
return scroll
}
}
const { selectCell , selectBlock , selectedRange , clearRangeRows , startSelectRange , selected } = useMultiSelect (
const { selectCell , startSelectRange , endSelectRange , clearSelectedRange , copyValue , isCellSelected , selectedCell } =
fields ,
useMultiSelect (
data ,
meta ,
$$ ( editEnabled ) ,
fields ,
isPkAvail ,
data ,
clearCell ,
$$ ( editEnabled ) ,
makeEditable ,
isPkAvail ,
scrollToCell ,
clearCell ,
( e : KeyboardEvent ) => {
makeEditable ,
/ / i g n o r e n a v i g a t i n g i f p i c k e r ( D a t e , T i m e , D a t e T i m e , Y e a r )
scrollToCell ,
/ / o r s i n g l e / m u l t i s e l e c t o p t i o n s i s o p e n
( e : KeyboardEvent ) => {
const activePickerOrDropdownEl = document . querySelector (
/ / i g n o r e n a v i g a t i n g i f p i c k e r ( D a t e , T i m e , D a t e T i m e , Y e a r )
'.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' ,
/ / o r s i n g l e / m u l t i s e l e c t o p t i o n s i s o p e n
)
const activePickerOrDropdownEl = document . querySelector (
if ( activePickerOrDropdownEl ) {
'.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' ,
e . preventDefault ( )
)
return true
if ( activePickerOrDropdownEl ) {
}
/ / i f e x p a n d e d f o r m i s a c t i v e s k i p k e y b o a r d e v e n t h a n d l i n g
if ( document . querySelector ( '.nc-drawer-expanded-form.active' ) ) {
return true
}
const cmdOrCtrl = isMac ( ) ? e . metaKey : e . ctrlKey
if ( e . key === ' ' ) {
if ( selected . row !== null && ! editEnabled ) {
e . preventDefault ( )
e . preventDefault ( )
const row = data . value [ selected . row ]
expandForm ( row )
return true
}
} else if ( e . key === 'Escape' ) {
if ( editEnabled ) {
editEnabled = false
return true
return true
}
}
} else if ( e . key === 'Enter' ) {
if ( editEnabled ) {
/ / s k i p k e y b o a r d e v e n t h a n d l i n g i f t h e r e i s a d r a w e r / m o d a l
editEnabled = false
if ( isDrawerOrModalExist ( ) ) {
return true
return true
}
}
}
if ( cmdOrCtrl ) {
const cmdOrCtrl = isMac ( ) ? e . metaKey : e . ctrlKey
switch ( e . key ) {
const altOrOptionKey = e . altKey
case 'ArrowUp' :
if ( e . key === ' ' ) {
selected . row = 0
if ( selectedCell . row !== null && ! editEnabled ) {
selected . col = selected . col ? ? 0
e . preventDefault ( )
scrollToCell ? . ( )
const row = data . value [ selectedCell . row ]
editEnabled = false
expandForm ( row )
return true
return true
case 'ArrowDown' :
}
selected . row = data . value . length - 1
} else if ( e . key === 'Escape' ) {
selected . col = selected . col ? ? 0
if ( editEnabled ) {
scrollToCell ? . ( )
editEnabled = false
editEnabled = false
return true
return true
case 'ArrowRight' :
}
selected . row = selected . row ? ? 0
} else if ( e . key === 'Enter' ) {
selected . col = fields . value ? . length - 1
if ( e . shiftKey ) {
scrollToCell ? . ( )
/ / a d d a l i n e b r e a k f o r t y p e s l i k e L o n g T e x t / J S O N
editEnabled = false
return true
return true
case 'ArrowLeft' :
}
selected . row = selected . row ? ? 0
if ( editEnabled ) {
selected . col = 0
scrollToCell ? . ( )
editEnabled = false
editEnabled = false
return true
return true
}
}
if ( cmdOrCtrl ) {
switch ( e . key ) {
case 'ArrowUp' :
e . preventDefault ( )
$e ( 'c:shortcut' , { key : 'CTRL + ArrowUp' } )
selectedCell . row = 0
selectedCell . col = selectedCell . col ? ? 0
scrollToCell ? . ( )
editEnabled = false
return true
case 'ArrowDown' :
e . preventDefault ( )
$e ( 'c:shortcut' , { key : 'CTRL + ArrowDown' } )
selectedCell . row = data . value . length - 1
selectedCell . col = selectedCell . col ? ? 0
scrollToCell ? . ( )
editEnabled = false
return true
case 'ArrowRight' :
e . preventDefault ( )
$e ( 'c:shortcut' , { key : 'CTRL + ArrowRight' } )
selectedCell . row = selectedCell . row ? ? 0
selectedCell . col = fields . value ? . length - 1
scrollToCell ? . ( )
editEnabled = false
return true
case 'ArrowLeft' :
e . preventDefault ( )
$e ( 'c:shortcut' , { key : 'CTRL + ArrowLeft' } )
selectedCell . row = selectedCell . row ? ? 0
selectedCell . col = 0
scrollToCell ? . ( )
editEnabled = false
return true
}
}
}
}
} ,
if ( altOrOptionKey ) {
)
switch ( e . keyCode ) {
case 82 : {
/ / A L T + R
if ( isAddingEmptyRowAllowed ) {
$e ( 'c:shortcut' , { key : 'ALT + R' } )
addEmptyRow ( )
}
break
}
case 67 : {
/ / A L T + 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 !== null && ctx . col !== undefined ? fields . value [ ctx . col ] : null
if ( ! ctx . updatedColumnTitle && isVirtualCol ( columnObj ) ) {
return
}
/ / u p d a t e / s a v e c e l l v a l u e
await updateOrSaveRow ( rowObj , ctx . updatedColumnTitle || columnObj . title )
} ,
)
function scrollToCell ( row ? : number | null , col ? : number | null ) {
function scrollToCell ( row ? : number | null , col ? : number | null ) {
row = row ? ? selected . row
row = row ? ? selectedCell . row
col = col ? ? selected . col
col = col ? ? selectedCell . col
if ( row !== undefined && col !== undefined && row !== null && col !== null ) {
if ( row !== undefined && col !== undefined && row !== null && col !== null ) {
/ / g e t a c t i v e c e l l
/ / g e t a c t i v e c e l l
const rows = tbodyEl . value ? . querySelectorAll ( 'tr' )
const rows = tbodyEl . value ? . querySelectorAll ( 'tr' )
@ -341,8 +399,13 @@ watch(contextMenu, () => {
const rowRefs = $ref < any [ ] > ( )
const rowRefs = $ref < any [ ] > ( )
async function clearCell ( ctx : { row : number ; col : number } | null ) {
async function clearCell ( ctx : { row : number ; col : number } | null , skipUpdate = false ) {
if ( ! ctx ) return
if (
! ctx ||
! hasEditPermission ||
( fields . value [ ctx . col ] . uidt !== UITypes . LinkToAnotherRecord && isVirtualCol ( fields . value [ ctx . col ] ) )
)
return
const rowObj = data . value [ ctx . row ]
const rowObj = data . value [ ctx . row ]
const columnObj = fields . value [ ctx . col ]
const columnObj = fields . value [ ctx . col ]
@ -353,8 +416,11 @@ async function clearCell(ctx: { row: number; col: number } | null) {
}
}
rowObj . row [ columnObj . title ] = null
rowObj . row [ columnObj . title ] = null
/ / u p d a t e / s a v e c e l l v a l u e
await updateOrSaveRow ( rowObj , columnObj . title )
if ( ! skipUpdate ) {
/ / u p d a t e / s a v e c e l l v a l u e
await updateOrSaveRow ( rowObj , columnObj . title )
}
}
}
function makeEditable ( row : Row , col : ColumnType ) {
function makeEditable ( row : Row , col : ColumnType ) {
@ -393,10 +459,12 @@ useEventListener(document, 'keyup', async (e: KeyboardEvent) => {
/** On clicking outside of table reset active cell */
/** On clicking outside of table reset active cell */
const smartTable = ref ( null )
const smartTable = ref ( null )
onClickOutside ( smartTable , ( e ) => {
onClickOutside ( smartTable , ( e ) => {
clearRangeRows ( )
/ / d o n o t h i n g i f c o n t e x t m e n u w a s o p e n
if ( selected . col === null ) return
if ( contextMenu . value ) return
clearSelectedRange ( )
if ( selectedCell . col === null ) return
const activeCol = fields . value [ selected . col ]
const activeCol = fields . value [ selectedCell . col ]
if ( editEnabled && ( isVirtualCol ( activeCol ) || activeCol . uidt === UITypes . JSON ) ) return
if ( editEnabled && ( isVirtualCol ( activeCol ) || activeCol . uidt === UITypes . JSON ) ) return
@ -412,30 +480,30 @@ onClickOutside(smartTable, (e) => {
)
)
return
return
/ / i f e x p a n d e d f o r m i s a c t i v e s k i p r e s e t t i n g t h e a c t i v e c e l l
/ / s k i p i f d r a w e r / m o d a l i s a c t i v e
if ( document . querySelector ( '.nc-drawer-expanded-form.active' ) ) {
if ( isDrawerOrModalExist ( ) ) {
return
return
}
}
selected . row = null
selectedCell . row = null
selected . col = null
selectedCell . col = null
} )
} )
const onNavigate = ( dir : NavigateDir ) => {
const onNavigate = ( dir : NavigateDir ) => {
if ( selected . row === null || selected . col === null ) return
if ( selectedCell . row === null || selectedCell . col === null ) return
editEnabled = false
editEnabled = false
switch ( dir ) {
switch ( dir ) {
case NavigateDir . NEXT :
case NavigateDir . NEXT :
if ( selected . row < data . value . length - 1 ) {
if ( selectedCell . row < data . value . length - 1 ) {
selected . row ++
selectedCell . row ++
} else {
} else {
addEmptyRow ( )
addEmptyRow ( )
selected . row ++
selectedCell . row ++
}
}
break
break
case NavigateDir . PREV :
case NavigateDir . PREV :
if ( selected . row > 0 ) {
if ( selectedCell . row > 0 ) {
selected . row --
selectedCell . row --
}
}
break
break
}
}
@ -549,6 +617,20 @@ watch(
} ,
} ,
{ immediate : true } ,
{ immediate : true } ,
)
)
const columnOrder = ref < Pick < ColumnReqType , ' column_order ' > | null > ( null )
eventBus . on ( async ( event , payload ) => {
if ( event === SmartsheetStoreEvents . FIELD _ADD ) {
columnOrder . value = payload
addColumnDropdown . value = true
}
} )
const closeAddColumnDropdown = ( ) => {
columnOrder . value = null
addColumnDropdown . value = false
}
< / script >
< / script >
< template >
< template >
@ -607,7 +689,7 @@ watch(
< / div >
< / div >
< / th >
< / th >
< th
< th
v - if = "!readOnly && !isLocked && isUIAllowed('add-column') && !isSqlView "
v - if = "isAddingColumnAllowed "
v - e = "['c:column:add']"
v - e = "['c:column:add']"
class = "cursor-pointer"
class = "cursor-pointer"
@ click . stop = "addColumnDropdown = true"
@ click . stop = "addColumnDropdown = true"
@ -624,8 +706,9 @@ watch(
< template # overlay >
< template # overlay >
< SmartsheetColumnEditOrAddProvider
< SmartsheetColumnEditOrAddProvider
v - if = "addColumnDropdown"
v - if = "addColumnDropdown"
@ submit = "addColumnDropdown = false"
: column - position = "columnOrder"
@ cancel = "addColumnDropdown = false"
@ submit = "closeAddColumnDropdown"
@ cancel = "closeAddColumnDropdown"
@ click . stop
@ click . stop
@ keydown . stop
@ keydown . stop
/ >
/ >
@ -634,8 +717,7 @@ watch(
< / th >
< / th >
< / tr >
< / tr >
< / thead >
< / thead >
<!-- this prevent select text from field if not in edit mode -- >
< tbody ref = "tbodyEl" >
< tbody ref = "tbodyEl" @ selectstart.prevent >
< LazySmartsheetRow v-for ="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row ="row" >
< LazySmartsheetRow v-for ="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row ="row" >
< template # default = "{ state }" >
< template # default = "{ state }" >
< tr class = "nc-grid-row" :data-testid ="`grid-row-${rowIndex}`" >
< tr class = "nc-grid-row" :data-testid ="`grid-row-${rowIndex}`" >
@ -696,9 +778,7 @@ watch(
: key = "columnObj.id"
: key = "columnObj.id"
class = "cell relative cursor-pointer nc-grid-cell"
class = "cell relative cursor-pointer nc-grid-cell"
: class = " {
: class = " {
'active' :
'active' : hasEditPermission && isCellSelected ( rowIndex , colIndex ) ,
( hasEditPermission && selected . col === colIndex && selected . row === rowIndex ) ||
( hasEditPermission && selectedRange ( rowIndex , colIndex ) ) ,
'nc-required-cell' : isColumnRequiredAndNull ( columnObj , row . row ) ,
'nc-required-cell' : isColumnRequiredAndNull ( columnObj , row . row ) ,
} "
} "
: data - testid = "`cell-${columnObj.title}-${rowIndex}`"
: data - testid = "`cell-${columnObj.title}-${rowIndex}`"
@ -708,7 +788,7 @@ watch(
@ click = "selectCell(rowIndex, colIndex)"
@ click = "selectCell(rowIndex, colIndex)"
@ dblclick = "makeEditable(row, columnObj)"
@ dblclick = "makeEditable(row, columnObj)"
@ mousedown = "startSelectRange($event, rowIndex, colIndex)"
@ mousedown = "startSelectRange($event, rowIndex, colIndex)"
@ mouseover = "selectBlock (rowIndex, colIndex)"
@ mouseover = "endSelectRange (rowIndex, colIndex)"
@ contextmenu = "showContextMenu($event, { row: rowIndex, col: colIndex })"
@ contextmenu = "showContextMenu($event, { row: rowIndex, col: colIndex })"
>
>
< div v-if ="!switchingTab" class="w-full h-full" >
< div v-if ="!switchingTab" class="w-full h-full" >
@ -716,7 +796,7 @@ watch(
v - if = "isVirtualCol(columnObj)"
v - if = "isVirtualCol(columnObj)"
v - model = "row.row[columnObj.title]"
v - model = "row.row[columnObj.title]"
: column = "columnObj"
: column = "columnObj"
: active = "selected.col === colIndex && selected.row === rowIndex"
: active = "selectedCell .col === colIndex && selectedCell .row === rowIndex"
: row = "row"
: row = "row"
@ navigate = "onNavigate"
@ navigate = "onNavigate"
/ >
/ >
@ -726,10 +806,10 @@ watch(
v - model = "row.row[columnObj.title]"
v - model = "row.row[columnObj.title]"
: column = "columnObj"
: column = "columnObj"
: edit - enabled = "
: edit - enabled = "
! ! hasEditPermission && ! ! editEnabled && selected . col === colIndex && selected . row === rowIndex
! ! hasEditPermission && ! ! editEnabled && selectedCell . col === colIndex && selectedCell . row === rowIndex
"
"
: row - index = "rowIndex"
: row - index = "rowIndex"
: active = "selected.col === colIndex && selected.row === rowIndex"
: active = "selectedCell .col === colIndex && selectedCell .row === rowIndex"
@ update : edit - enabled = "editEnabled = $event"
@ update : edit - enabled = "editEnabled = $event"
@ save = "updateOrSaveRow(row, columnObj.title, state)"
@ save = "updateOrSaveRow(row, columnObj.title, state)"
@ navigate = "onNavigate"
@ navigate = "onNavigate"
@ -741,7 +821,7 @@ watch(
< / template >
< / template >
< / LazySmartsheetRow >
< / LazySmartsheetRow >
< tr v-if ="!isView && !isLocked && hasEditPermission && !isSqlView " >
< tr v-if ="isAddingEmptyRowAllowed " >
< td
< td
v - e = "['c:row:add:grid-bottom']"
v - e = "['c:row:add:grid-bottom']"
: colspan = "visibleColLength + 1"
: colspan = "visibleColLength + 1"
@ -794,6 +874,13 @@ watch(
{ { $t ( 'activity.insertRow' ) } }
{ { $t ( 'activity.insertRow' ) } }
< / div >
< / div >
< / a - m e n u - i t e m >
< / a - m e n u - i t e m >
< a -menu -item v-if ="contextMenuTarget" @click="copyValue(contextMenuTarget)" >
< div v-e ="['a:row:copy']" class="nc-project-menu-item" >
<!-- Copy -- >
{ { $t ( 'general.copy' ) } }
< / div >
< / a - m e n u - i t e m >
< / a - m e n u >
< / a - m e n u >
< / template >
< / template >
< / a - d r o p d o w n >
< / a - d r o p d o w n >