@ -1,5 +1,7 @@
< script lang = "ts" setup >
< script lang = "ts" setup >
import type { VNodeRef } from '@vue/runtime-core'
import Draggable from 'vuedraggable'
import Draggable from 'vuedraggable'
import tinycolor from 'tinycolor2'
import { ViewTypes , isVirtualCol } from 'nocodb-sdk'
import { ViewTypes , isVirtualCol } from 'nocodb-sdk'
import type { Row as RowType } from '#imports'
import type { Row as RowType } from '#imports'
@ -9,8 +11,6 @@ interface Attachment {
const INFINITY _SCROLL _THRESHOLD = 100
const INFINITY _SCROLL _THRESHOLD = 100
const emptyPagination = ref ( )
const meta = inject ( MetaInj , ref ( ) )
const meta = inject ( MetaInj , ref ( ) )
const view = inject ( ActiveViewInj , ref ( ) )
const view = inject ( ActiveViewInj , ref ( ) )
@ -47,6 +47,8 @@ const route = router.currentRoute
const { getPossibleAttachmentSrc } = useAttachment ( )
const { getPossibleAttachmentSrc } = useAttachment ( )
const { metaColumnById } = useViewColumnsOrThrow ( view , meta )
const {
const {
loadKanbanData ,
loadKanbanData ,
loadMoreKanbanData ,
loadMoreKanbanData ,
@ -59,11 +61,14 @@ const {
groupingFieldColOptions ,
groupingFieldColOptions ,
updateKanbanStackMeta ,
updateKanbanStackMeta ,
groupingField ,
groupingField ,
groupingFieldColumn ,
countByStack ,
countByStack ,
deleteStack ,
deleteStack ,
shouldScrollToRight ,
shouldScrollToRight ,
deleteRow ,
deleteRow ,
moveHistory ,
moveHistory ,
addNewStackId ,
removeRowFromUncategorizedStack ,
} = useKanbanViewStoreOrThrow ( )
} = useKanbanViewStoreOrThrow ( )
const { isViewDataLoading } = storeToRefs ( useViewsStore ( ) )
const { isViewDataLoading } = storeToRefs ( useViewsStore ( ) )
@ -108,6 +113,7 @@ const kanbanContainerRef = ref()
const selectedStackTitle = ref ( '' )
const selectedStackTitle = ref ( '' )
reloadViewDataHook ? . on ( async ( ) => {
reloadViewDataHook ? . on ( async ( ) => {
console . log ( 'load' )
await loadKanbanMeta ( )
await loadKanbanMeta ( )
await loadKanbanData ( )
await loadKanbanData ( )
} )
} )
@ -271,15 +277,21 @@ const kanbanListScrollHandler = useDebounceFn(async (e: any) => {
if ( stack && ( countByStack . value . get ( stackTitle ) === undefined || stack . length < countByStack . value . get ( stackTitle ) ! ) ) {
if ( stack && ( countByStack . value . get ( stackTitle ) === undefined || stack . length < countByStack . value . get ( stackTitle ) ! ) ) {
const page = Math . ceil ( stack . length / pageSize )
const page = Math . ceil ( stack . length / pageSize )
await loadMoreKanbanData ( stackTitle , { offset : page * pageSize } )
await loadMoreKanbanData ( stackTitle , {
offset :
page * pageSize > countByStack . value . get ( stackTitle ) ! || page * pageSize > stack . length
? ( page - 1 ) * pageSize
: page * pageSize ,
} )
}
}
}
}
} )
} )
const kanbanListRef = ( kanbanListElement : HTMLElement ) => {
const kanbanListRef : VNodeRef = ( kanbanListElement ) => {
if ( kanbanListElement ) {
if ( kanbanListElement ) {
kanbanListElement . removeEventListener ( 'scroll' , kanbanListScrollHandler )
; ( kanbanListElement as HTMLElement ) . removeEventListener ( 'scroll' , kanbanListScrollHandler )
kanbanListElement . addEventListener ( 'scroll' , kanbanListScrollHandler )
; ( kanbanListElement as HTMLElement ) . addEventListener ( 'scroll' , kanbanListScrollHandler )
}
}
}
}
@ -300,6 +312,28 @@ const handleCollapseStack = async (stackIdx: number) => {
await updateKanbanStackMeta ( )
await updateKanbanStackMeta ( )
}
}
}
}
const handleCollapseAllStack = async ( ) => {
groupingFieldColOptions . value . forEach ( ( stack ) => {
if ( stack . id !== addNewStackId && ! stack . collapsed ) {
stack . collapsed = true
}
} )
if ( ! isPublic . value ) {
await updateKanbanStackMeta ( )
}
}
const handleExpandAllStack = async ( ) => {
groupingFieldColOptions . value . forEach ( ( stack ) => {
if ( stack . id !== addNewStackId && stack . collapsed ) {
stack . collapsed = false
}
} )
if ( ! isPublic . value ) {
await updateKanbanStackMeta ( )
}
}
const openNewRecordFormHookHandler = async ( ) => {
const openNewRecordFormHookHandler = async ( ) => {
const newRow = await addEmptyRow ( )
const newRow = await addEmptyRow ( )
@ -368,11 +402,39 @@ const getRowId = (row: RowType) => {
const pk = extractPkFromRow ( row . row , meta . value ! . columns ! )
const pk = extractPkFromRow ( row . row , meta . value ! . columns ! )
return pk ? ` row- ${ pk } ` : ''
return pk ? ` row- ${ pk } ` : ''
}
}
const hideEmptyStack = computed < boolean > ( ( ) => parseProp ( kanbanMetaData . value ? . meta ) . hide _empty _stack || false )
const addNewStackObj = {
id : addNewStackId ,
}
const isRenameOrNewStack = ref ( null )
const compareStack = ( stack : any , stack2 ? : any ) => stack ? . id && stack2 ? . id && stack . id === stack2 . id
const isSavingStack = ref ( null )
const handleSubmitRenameOrNewStack = async ( loadMeta : boolean , stack ? : any , stackIdx ? : number ) => {
isSavingStack . value = isRenameOrNewStack . value
isRenameOrNewStack . value = null
if ( stack && stack ? . title && stack ? . color && stackIdx !== undefined ) {
groupingFieldColOptions . value [ stackIdx ] . title = stack . title
groupingFieldColOptions . value [ stackIdx ] . color = stack . color
}
if ( loadMeta ) {
await loadKanbanMeta ( )
}
isSavingStack . value = null
}
< / script >
< / script >
< template >
< template >
< div
< div
class = "flex flex-col w-full bg-white h-full"
class = "flex flex-col w-full bg-gray-50 h-full"
data - testid = "nc-kanban-wrapper"
data - testid = "nc-kanban-wrapper"
: style = " {
: style = " {
minHeight : 'calc(100% - var(--topbar-height))' ,
minHeight : 'calc(100% - var(--topbar-height))' ,
@ -380,28 +442,30 @@ const getRowId = (row: RowType) => {
>
>
< div
< div
ref = "kanbanContainerRef"
ref = "kanbanContainerRef"
class = "nc-kanban-container flex mt-4 pb-4 px-4 overflow-y-hidden w-full nc-scrollbar-x-lg"
class = "nc-kanban-container flex p-3 overflow-y-hidden w-full nc-scrollbar-x-lg"
: style = " {
: style = " {
minHeight : isMobileMode ? 'calc(100% - 2rem)' : 'calc(100vh - var(--topbar-height) - 4.1 rem)' ,
minHeight : isMobileMode ? 'calc(100% - 2rem)' : 'calc(100vh - var(--topbar-height) - var(--toolbar-height) - 0.4 rem)' ,
maxHeight : isMobileMode ? 'calc(100% - 2rem)' : 'calc(100vh - var(--topbar-height) - 4.1 rem)' ,
maxHeight : isMobileMode ? 'calc(100% - 2rem)' : 'calc(100vh - var(--topbar-height) - var(--toolbar-height) - 0.4 rem)' ,
} "
} "
>
>
< div v-if ="isViewDataLoading" class="flex flex-row min-h-full gap-x-2" >
< div v-if ="isViewDataLoading" class="flex flex-row min-h-full gap-x-2" >
< a -skeleton -input v -for = " index of Array ( 20 ) " :key ="index" class = "!min-w-80 !min-h-full !rounded-xl overflow-hidden" / >
< a -skeleton -input v -for = " index of Array ( 20 ) " :key ="index" class = "!min-w-80 !min-h-full !rounded-xl overflow-hidden" / >
< / div >
< / div >
< a -d ropdown
< NcD ropdown
v - else
v - else
v - model : visible = "contextMenu"
v - model : visible = "contextMenu"
: trigger = "['contextmenu']"
: trigger = "['contextmenu']"
overlay - class - name = "nc-dropdown-kanban-context-menu"
overlay - class - name = "nc-dropdown-kanban-context-menu"
>
>
< div class = "flex gap-3" >
<!-- Draggable Stack -- >
<!-- Draggable Stack -- >
< Draggable
< Draggable
v - model = "groupingFieldColOptions"
v - model = "groupingFieldColOptions"
class = "flex gap-4 "
class = "flex gap-3 "
item - key = "id"
item - key = "id"
group = "kanban-stack"
group = "kanban-stack"
draggable = ".nc-kanban-stack"
draggable = ".nc-kanban-stack"
handle = ".nc-kanban-stack-drag-handler"
filter = ".not-draggable"
filter = ".not-draggable"
: move = "onMoveCallback"
: move = "onMoveCallback"
@ start = "(e) => e.target.classList.add('grabbing')"
@ start = "(e) => e.target.classList.add('grabbing')"
@ -409,53 +473,143 @@ const getRowId = (row: RowType) => {
@ change = "onMoveStack($event)"
@ change = "onMoveStack($event)"
>
>
< template # item = "{ element: stack, index: stackIdx }" >
< template # item = "{ element: stack, index: stackIdx }" >
< div class = "nc-kanban-stack" : class = "{ 'w-[50px]': stack.collapsed }" >
< div
class = "nc-kanban-stack"
: class = " {
'w-[44px]' : stack . collapsed ,
'hidden' : hideEmptyStack && ! formattedData . get ( stack . title ) ? . length ,
} "
: data - testid = "`nc-kanban-stack-${stack.title}`"
>
<!-- Non Collapsed Stacks -- >
<!-- Non Collapsed Stacks -- >
< a -card
< a -card
v - if = "!stack.collapsed"
v - if = "!stack.collapsed"
: key = "`${stack.id}-${stackIdx}`"
: key = "`${stack.id}-${stackIdx}`"
class = "mx-4 !bg-gray-100 flex flex-col w-80 h-full !rounded-xl overflow-y-hidden"
class = "flex flex-col w-68.5 h-full !rounded-xl overflow-y-hidden !shadow-none !hover:shadow-none !border-gray-200 "
: class = " {
: class = " {
'not-draggable' : stack . title === null || isLocked || isPublic || ! hasEditPermission ,
'not-draggable' : stack . title === null || isLocked || isPublic || ! hasEditPermission ,
'!cursor-default' : isLocked || ! hasEditPermission ,
'!cursor-default' : isLocked || ! hasEditPermission ,
} "
} "
: head - style = "{ paddingBottom: '0px' }"
: head - style = "{ paddingBottom: '0px' }"
: body - style = "{ padding: '0px', height: '100%', borderRadius: '0.75rem !important', paddingBottom: '0rem' }"
: body - style = " {
padding : '0px !important' ,
height : '100%' ,
borderRadius : '0.75rem !important' ,
paddingBottom : '0rem !important' ,
} "
>
>
<!-- Header Color Bar -- >
< div
: style = "`background-color: ${stack.color}`"
class = "nc-kanban-stack-head-color h-[10px] mt-3 mx-3 rounded-full"
> < / div >
<!-- Skeleton -- >
<!-- Skeleton -- >
< div v-if ="!formattedData.get(stack.title) || !countByStack" class="mt-2.5 px-3 !w-full" >
< div v-if ="!formattedData.get(stack.title) || !countByStack" class="mt-2.5 px-3 !w-full" >
< a -skeleton -input :active ="true" class = "!w-full !h-9.75 !rounded-lg overflow-hidden" / >
< a -skeleton -input :active ="true" class = "!w-full !h-9.75 !rounded-lg overflow-hidden" / >
< / div >
< / div >
<!-- Stack -- >
<!-- Stack -- >
< a -layout v -else class = "!bg-gray-100" >
< a -layout v-else >
< a -layout -header >
< a -layout -header class = "border-b-1 border-gray-100 min-h-[49px]" >
< div class = "nc-kanban-stack-head font-medium flex items-center" >
< div
< a -dropdown
class = "nc-kanban-stack-head w-full flex gap-1"
: trigger = "['click']"
: class = " {
overlay - class - name = "nc-dropdown-kanban-stack-context-menu"
'items-start' : compareStack ( stack , isRenameOrNewStack ) ,
class = "bg-white !rounded-lg"
'items-center' : ! compareStack ( stack , isRenameOrNewStack ) ,
} "
>
>
< div
< div
class = "flex items-center w-full mx-2 px-3 py-1"
class = "flex-1 flex gap-1 max-w-[calc(100%_-_32px)]"
: class = "{ 'capitalize': stack.title === null, 'cursor-pointer': !isLocked }"
: class = " {
'items-start' : compareStack ( stack , isRenameOrNewStack ) ,
'items-center' : ! compareStack ( stack , isRenameOrNewStack ) ,
} "
>
>
< LazyGeneralTruncateText > { { stack . title ? ? 'uncategorized' } } < / LazyGeneralTruncateText >
< NcButton
< span v-if ="!isLocked" class="w-full flex w-[15px]" >
v - if = "!(isLocked || isPublic || !hasEditPermission)"
< component :is ="iconMap.arrowDown" class = "text-grey text-lg ml-auto" / >
: disabled = "
! stack . title || compareStack ( stack , isSavingStack ) || compareStack ( stack , isRenameOrNewStack )
"
type = "text"
size = "xs"
class = "nc-kanban-stack-drag-handler !px-1.5 !cursor-move !:disabled:cursor-not-allowed mt-0.5"
>
< GeneralLoader v -if = " compareStack ( stack , isSavingStack ) " size = "regular" class = "stack-rename-loader" / >
< GeneralIcon v -else icon = "ncDrag" class = "!font-weight-800 flex-none" / >
< / NcButton >
< div
class = "flex-1 flex max-w-[calc(100%_-_28px)]"
: class = " {
'-ml-1' : compareStack ( stack , isRenameOrNewStack ) ,
} "
>
< template
v - if = "compareStack(stack, isRenameOrNewStack) && metaColumnById[isRenameOrNewStack?.fk_column_id]"
>
< SmartsheetKanbanEditOrAddStack
: column = "metaColumnById[isRenameOrNewStack?.fk_column_id]"
: option - id = "isRenameOrNewStack.id"
@ submit = "(loadMeta, payload) => handleSubmitRenameOrNewStack(loadMeta, payload, stackIdx)"
/ >
< / template >
< a -tag
v - else
class = "max-w-full !rounded-full !px-2 !py-1 h-7 !m-0 !border-none !mt-0.5"
: color = "stack.color"
@ dblclick = "
( ) => {
if ( stack . title !== null && hasEditPermission && ! isPublic && ! isLocked ) {
isRenameOrNewStack = stack
}
}
"
>
< span
: style = " {
color : tinycolor . isReadable ( stack . color || '#ccc' , '#fff' , { level : 'AA' , size : 'large' } )
? '#fff'
: tinycolor . mostReadable ( stack . color || '#ccc' , [ '#0b1d05' , '#fff' ] ) . toHex8String ( ) ,
} "
class = "text-sm font-semibold"
>
< NcTooltip class = "truncate max-w-full" placement = "bottom" show -on -truncate -only >
< template # title >
{ { stack . title ? ? 'Uncategorized' } }
< / template >
< span
data - testid = "nc-kanban-stack-title"
class = "text-ellipsis overflow-hidden"
: style = " {
wordBreak : 'keep-all' ,
whiteSpace : 'nowrap' ,
display : 'inline' ,
} "
>
{ { stack . title ? ? 'Uncategorized' } }
< / span >
< / NcTooltip >
< / span >
< / span >
< / a - t a g >
< / div >
< / div >
< template v -if = " ! isLocked " # overlay >
< / div >
< a -menu class = "ml-6 !text-sm !px-0 !py-2 !rounded" >
< NcDropdown
< a -menu -item
v - if = "!isLocked"
placement = "bottomRight"
overlay - class - name = "nc-dropdown-kanban-stack-context-menu"
class = "bg-white !rounded-lg"
>
< NcButton
: disabled = "compareStack(stack, isSavingStack)"
type = "text"
size = "xs"
class = "!px-1.5 mt-0.5"
data - testid = "nc-kanban-stack-context-menu"
>
< GeneralIcon icon = "threeDotVertical" / >
< / NcButton >
< template # overlay >
< NcMenu class = "!text-sm" >
< NcMenuItem
v - if = "hasEditPermission && !isPublic && !isLocked"
v - if = "hasEditPermission && !isPublic && !isLocked"
v - e = "['c:kanban:add-new-record']"
v - e = "['c:kanban:add-new-record']"
data - testid = "nc-kanban-context-menu-add-new-record"
@ click = "
@ click = "
( ) => {
( ) => {
selectedStackTitle = stack . title
selectedStackTitle = stack . title
@ -463,56 +617,116 @@ const getRowId = (row: RowType) => {
}
}
"
"
>
>
< div class = "py-2 flex gap-2 items-center" >
< div class = "flex gap-2 items-center" >
< component :is ="iconMap.plus" class = "text-gray-500 " / >
< component :is ="iconMap.plus" class = "flex-none w-4 h-4 " / >
{ { $t ( 'activity.addNewRecord' ) } }
{ { $t ( 'activity.addNewRecord' ) } }
< / div >
< / div >
< / a - m e n u - i t e m >
< / NcMenuItem >
< a -menu -item v-e ="['c:kanban:collapse-stack']" @click="handleCollapseStack(stackIdx)" >
< NcMenuItem
< div class = "py-2 flex gap-1.8 items-center" >
v - if = "stack.title !== null && hasEditPermission && !isPublic && !isLocked"
< component :is ="iconMap.arrowCollapse" class = "text-gray-500" / >
v - e = "['c:kanban:rename-stack']"
data - testid = "nc-kanban-context-menu-rename-stack"
@ click = "
( ) => {
isRenameOrNewStack = stack
}
"
>
< div class = "flex gap-2 items-center" >
< component :is ="iconMap.ncEdit" class = "flex-none w-4 h-4" / >
{ { $t ( 'activity.kanban.renameStack' ) } }
< / div >
< / NcMenuItem >
< NcMenuItem
v - e = "['c:kanban:collapse-stack']"
data - testid = "nc-kanban-context-menu-collapse-stack"
@ click = "handleCollapseStack(stackIdx)"
>
< div class = "flex gap-2 items-center" >
< component :is ="iconMap.minimize" class = "flex-none w-4 h-4" / >
{ { $t ( 'activity.kanban.collapseStack' ) } }
{ { $t ( 'activity.kanban.collapseStack' ) } }
< / div >
< / div >
< / a - m e n u - i t e m >
< / NcMenuItem >
< a -menu -item
v - if = "stack.title !== null && !isPublic && hasEditPermission"
< NcMenuItem
v - e = "['c:kanban:collapse-all-stack']"
data - testid = "nc-kanban-context-menu-collapse-all-stack"
@ click = "handleCollapseAllStack"
>
< div class = "flex gap-2 items-center" >
< component :is ="iconMap.minimizeAll" class = "flex-none w-4 h-4" / >
{ { $t ( 'activity.kanban.collapseAll' ) } }
< / div >
< / NcMenuItem >
< NcMenuItem
v - e = "['c:kanban:expand-all-stack']"
data - testid = "nc-kanban-context-menu-expand-all-stack"
@ click = "handleExpandAllStack"
>
< div class = "flex gap-2 items-center" >
< component :is ="iconMap.maximizeAll" class = "flex-none w-4 h-4" / >
{ { $t ( 'activity.kanban.expandAll' ) } }
< / div >
< / NcMenuItem >
< template v-if ="stack.title !== null && !isPublic && hasEditPermission" >
< NcDivider / >
< NcMenuItem
v - e = "['c:kanban:delete-stack']"
v - e = "['c:kanban:delete-stack']"
class = "!text-red-600 !hover:bg-red-50"
data - testid = "nc-kanban-context-menu-delete-stack"
@ click = "handleDeleteStackClick(stack.title, stackIdx)"
@ click = "handleDeleteStackClick(stack.title, stackIdx)"
>
>
< div class = "py-2 flex gap-2 items-center" >
< div class = "flex gap-2 items-center" >
< component :is ="iconMap.delete" class = "text-gray-500" / >
< component :is ="iconMap.delete" class = "flex-none w-4 h-4 " / >
{ { $t ( 'activity.kanban.deleteStack' ) } }
{ { $t ( 'activity.kanban.deleteStack' ) } }
< / div >
< / div >
< / a - m e n u - i t e m >
< / NcMenuItem >
< / a - m e n u >
< / template >
< / NcMenu >
< / template >
< / template >
< / a - d r o p d o w n >
< / NcDropdown >
< / div >
< / div >
< / a - l a y o u t - h e a d e r >
< / a - l a y o u t - h e a d e r >
< a -layout -content
< a -layout -content
class = "overflow-y-hidden mt-1"
class = "overflow-y-hidden"
: style = "{ maxHeight: isUIAllowed('dataInsert') ? 'calc(100% - 11rem)' : 'calc(100% - 8rem)' }"
: style = " {
backgroundColor : tinycolor
. mix (
stack . color || '#ccc' ,
'#ffffff' ,
tinycolor ( stack . color || '#ccc' ) . isLight ( )
? 70
: tinycolor ( stack . color || '#ccc' ) . getBrightness ( ) <= 100
? 80
: 90 ,
)
. toString ( ) ,
} "
>
< div
: ref = "kanbanListRef"
class = "nc-kanban-list h-full px-2 nc-scrollbar-thin"
: data - stack - title = "stack.title"
>
>
< div :ref ="kanbanListRef" class = "nc-kanban-list h-full nc-scrollbar-dark-md" :data-stack-title ="stack.title" >
<!-- Draggable Record Card -- >
<!-- Draggable Record Card -- >
< Draggable
< Draggable
: list = "formattedData.get(stack.title)"
: list = "formattedData.get(stack.title)"
item - key = "row.Id"
item - key = "row.Id"
draggable = ".nc-kanban-item"
draggable = ".nc-kanban-item"
group = "kanban-card"
group = "kanban-card"
class = "h-full"
class = "flex flex-col h-full mb-2 "
filter = ".not-draggable"
filter = ".not-draggable"
@ start = "(e) => e.target.classList.add('grabbing')"
@ start = "(e) => e.target.classList.add('grabbing')"
@ end = "(e) => e.target.classList.remove('grabbing')"
@ end = "(e) => e.target.classList.remove('grabbing')"
@ change = "onMove($event, stack.title)"
@ change = "onMove($event, stack.title)"
>
>
< template # item = "{ element: record, index }" >
< template # item = "{ element: record, index }" >
< div class = "nc-kanban-item py-2 pl-3 pr-2" >
< div class = "nc-kanban-item py-1 first:pt-2 last:pb -2" >
< LazySmartsheetRow :row ="record" >
< LazySmartsheetRow :row ="record" >
< a -card
< a -card
: key = "`${getRowId(record)}-${index}`"
: key = "`${getRowId(record)}-${index}`"
class = "!rounded-x l h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] cursor-pointer"
class = "!rounded-lg h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] cursor-pointer"
: body - style = "{ padding: '16px !important' }"
: body - style = "{ padding: '16px !important' }"
: data - stack = "stack.title"
: data - stack = "stack.title"
: data - testid = "`nc-gallery-card-${record.row.id}`"
: data - testid = "`nc-gallery-card-${record.row.id}`"
@ -606,7 +820,7 @@ const getRowId = (row: RowType) => {
< div v-for ="col in fieldsWithoutDisplay" :key="`record-${record.row.id}-${col.id}`" >
< div v-for ="col in fieldsWithoutDisplay" :key="`record-${record.row.id}-${col.id}`" >
< div class = "flex flex-col rounded-lg w-full" >
< div class = "flex flex-col rounded-lg w-full" >
< div class = "flex flex-row w-full justify-start" >
< div class = "flex flex-row w-full justify-start" >
< div class = "nc-card-col-header w-full text-gray-500 uppercase " >
< div class = "nc-card-col-header w-full !children: text-gray-500" >
< LazySmartsheetHeaderVirtualCell
< LazySmartsheetHeaderVirtualCell
v - if = "isVirtualCol(col)"
v - if = "isVirtualCol(col)"
: column = "col"
: column = "col"
@ -646,23 +860,44 @@ const getRowId = (row: RowType) => {
< / LazySmartsheetRow >
< / LazySmartsheetRow >
< / div >
< / div >
< / template >
< / template >
< / Draggable >
< template v -if = " ! formattedData.get ( stack.title ) ? .length " # footer >
< div class = "h-full w-full flex flex-col gap-4 items-center justify-center" >
< div class = "flex flex-col items-center gap-2 text-gray-600 text-center" >
< span class = "text-sm font-semibold" >
{ { $t ( 'general.empty' ) } } { { $t ( 'general.stack' ) . toLowerCase ( ) } }
< / span >
< span class = "text-xs font-weight-500" >
{ { $t ( 'title.looksLikeThisStackIsEmpty' ) } }
< / span >
< / div >
< / div >
< / a - l a y o u t - c o n t e n t >
< NcButton
v - if = "isUIAllowed('dataInsert')"
< div class = "!rounded-lg !px-3 pt-3" >
size = "xs"
< div v-if ="formattedData.get(stack.title)" class="text-center" >
type = "secondary"
<!-- Stack Title -- >
@ click = "
( ) => {
selectedStackTitle = stack . title
openNewRecordFormHook . trigger ( stack . title )
}
"
>
< div class = "flex items-center gap-2" >
< component :is ="iconMap.plus" v -if = " ! isPublic & & ! isLocked " / >
<!-- Record Count -- >
{ { $t ( 'activity.newRecord' ) } }
< div class = "nc-kanban-data-count text-gray-500" >
{ { formattedData . get ( stack . title ) ! . length } } / { { countByStack . get ( stack . title ) ? ? 0 } }
{ { countByStack . get ( stack . title ) !== 1 ? $t ( 'objects.records' ) : $t ( 'objects.record' ) } }
< / div >
< / div >
< / NcButton >
< div
< / div >
< / template >
< / Draggable >
< / div >
< / a - l a y o u t - c o n t e n t >
< a -layout -footer v-if ="formattedData.get(stack.title)" class="border-t-1 border-gray-100" >
< div class = "flex items-center justify-between" >
< NcButton
v - if = "isUIAllowed('dataInsert')"
v - if = "isUIAllowed('dataInsert')"
class = "flex flex-row w-full mt-3 justify-between items-center cursor-pointer bg-white px-4 py-2 rounded-lg border-gray-100 border-1 shadow-sm shadow-gray-100"
size = "xs"
type = "secondary"
@ click = "
@ click = "
( ) => {
( ) => {
selectedStackTitle = stack . title
selectedStackTitle = stack . title
@ -670,11 +905,21 @@ const getRowId = (row: RowType) => {
}
}
"
"
>
>
Add Record
< div class = "flex items-center gap-2" >
< component :is ="iconMap.plus" v -if = " ! isPublic & & ! isLocked " class = "" / >
< component :is ="iconMap.plus" v -if = " ! isPublic & & ! isLocked " class = "" / >
{ { $t ( 'activity.newRecord' ) } }
< / div >
< / div >
< / NcButton >
< div v-else > & nbsp ; < / div >
<!-- Record Count -- >
< div class = "nc-kanban-data-count text-gray-500 font-weight-500 px-1" >
{ { formattedData . get ( stack . title ) ! . length } } / { { countByStack . get ( stack . title ) ? ? 0 } }
{ { countByStack . get ( stack . title ) !== 1 ? $t ( 'objects.records' ) : $t ( 'objects.record' ) } }
< / div >
< / div >
< / div >
< / div >
< / a - l a y o u t - f o o t e r >
< / a - l a y o u t >
< / a - l a y o u t >
< / a - c a r d >
< / a - c a r d >
@ -682,64 +927,190 @@ const getRowId = (row: RowType) => {
< a -card
< a -card
v - else
v - else
: key = "`${stack.id}-collapsed`"
: key = "`${stack.id}-collapsed`"
: style = "`background-color: ${stack.color} !important`"
class = "nc-kanban-collapsed-stack flex items-center w-68.5 h-[44px] !rounded-xl cursor-pointer h-full !p-2 overflow-hidden !shadow-none !hover:shadow-none !border-gray-200"
class = "nc-kanban-collapsed-stack mx-4 flex items-center w-[300px] h-[50px] !rounded-xl cursor-pointer h-full !pr-[10px] overflow-hidden"
: class = " {
: class = " {
'not-draggable' : stack . title === null || isLocked || isPublic || ! hasEditPermission ,
'not-draggable' : stack . title === null || isLocked || isPublic || ! hasEditPermission ,
} "
} "
: body - style = "{ padding: '0px', height: '100%', width: '100%', background: '#f0f2f5 !important' }"
: body - style = " {
padding : '0px !important' ,
height : '100%' ,
width : '100%' ,
borderRadius : '0.75rem !important' ,
paddingBottom : '0rem !important' ,
} "
>
< div class = "h-full flex items-center justify-between" @click ="handleCollapseStack(stackIdx)" >
< div
v - if = "!formattedData.get(stack.title) || !countByStack"
class = "!w-full !h-full flex items-center justify-center"
>
>
< div class = "items-center justify-between" @click ="handleCollapseStack(stackIdx)" >
< div v-if ="!formattedData.get(stack.title) || !countByStack" class="mt-4 px-3 !w-full" >
< a -skeleton -input :active ="true" class = "!w-full !h-4 !rounded-lg overflow-hidden" / >
< a -skeleton -input :active ="true" class = "!w-full !h-4 !rounded-lg overflow-hidden" / >
< / div >
< / div >
< div v -else class = "nc-kanban-data-count mt-[12px] mx-[10px]" >
< div v -else class = "nc-kanban-stack-head w-full flex items-center justify-between gap-2" >
<!-- Stack title -- >
< div class = "flex items-center gap-1" >
< div
< NcButton
class = "float-right flex gap-2 items-center cursor-pointer font-bold"
v - if = "!(isLocked || isPublic || !hasEditPermission)"
: class = "{ capitalize: stack.title === null }"
: disabled = "!stack.title"
type = "text"
size = "xs"
class = "nc-kanban-stack-drag-handler !px-1.5 !cursor-move"
@ click . stop
>
>
< LazyGeneralTruncateText > { { stack . title ? ? 'uncategorized' } } < / LazyGeneralTruncateText >
< GeneralIcon icon = "ncDrag" class = "font-weight-800 flex-none" / >
< component :is ="iconMap.arrowDown" class = "text-grey text-lg" / >
< / NcButton >
< div class = "flex-1 flex max-w-[115px]" >
< a -tag class = "max-w-full !rounded-full !px-2 !py-1 h-7 !m-0 !border-none" :color ="stack.color" >
< span
: style = " {
color : tinycolor . isReadable ( stack . color || '#ccc' , '#fff' , { level : 'AA' , size : 'large' } )
? '#fff'
: tinycolor . mostReadable ( stack . color || '#ccc' , [ '#0b1d05' , '#fff' ] ) . toHex8String ( ) ,
} "
class = "text-sm font-semibold"
>
< NcTooltip class = "truncate max-w-full" placement = "left" show -on -truncate -only >
< template # title >
{ { stack . title ? ? 'Uncategorized' } }
< / template >
< span
data - testid = "nc-kanban-stack-title"
class = "text-ellipsis overflow-hidden"
: style = " {
wordBreak : 'keep-all' ,
whiteSpace : 'nowrap' ,
display : 'inline' ,
} "
>
{ { stack . title ? ? 'Uncategorized' } }
< / span >
< / NcTooltip >
< / span >
< / a - t a g >
< / div >
< / div >
< / div >
< div class = "flex items-center gap-3" >
< div
class = "nc-kanban-data-count px-1 rounded bg-gray-200 text-gray-800 text-sm font-weight-500"
: style = "{ 'word-break': 'keep-all', 'white-space': 'nowrap' }"
>
<!-- Record Count -- >
<!-- Record Count -- >
{ { formattedData . get ( stack . title ) ! . length } } / { { countByStack . get ( stack . title ) } }
{ { formattedData . get ( stack . title ) ! . length } }
{ { countByStack . get ( stack . title ) !== 1 ? $t ( 'objects.records' ) : $t ( 'objects.record' ) } }
{ { countByStack . get ( stack . title ) !== 1 ? $t ( 'objects.records' ) : $t ( 'objects.record' ) } }
< / div >
< / div >
< NcButton type = "text" size = "xs" class = "!px-1.5" >
< component :is ="iconMap.arrowDown" class = "text-grey h-4 w-4 flex-none" / >
< / NcButton >
< / div >
< / div >
< / div >
< / div >
< / a - c a r d >
< / a - c a r d >
< / div >
< / div >
< / template >
< / template >
< / Draggable >
< / Draggable >
< div v-if ="hasEditPermission && !isPublic && !isLocked && groupingFieldColumn?.id" class="nc-kanban-add-new-stack" >
<!-- Add New Stack -- >
< a -card
class = "flex flex-col w-68.5 !rounded-xl overflow-y-hidden !shadow-none !hover:shadow-none border-gray-200"
: class = " {
'!cursor-default' : isLocked || ! hasEditPermission ,
'!border-none' : ! compareStack ( addNewStackObj , isRenameOrNewStack ) ,
} "
: head - style = "{ paddingBottom: '0px' }"
: body - style = " {
padding : '0px !important' ,
height : '100%' ,
borderRadius : '0.75rem !important' ,
paddingBottom : '0rem !important' ,
} "
>
<!-- Skeleton -- >
< div v-if ="!formattedData.get(null) || !countByStack" class="mt-2.5 px-3 !w-full" >
< a -skeleton -input :active ="true" class = "!w-full !h-9.75 !rounded-lg overflow-hidden" / >
< / div >
<!-- Stack -- >
< a -layout v-else >
< a -layout -header
: class = " {
'!p-0 overflow-hidden' : ! compareStack ( addNewStackObj , isRenameOrNewStack ) ,
} "
>
< div
class = "w-full flex"
: class = " {
'items-start' : compareStack ( addNewStackObj , isRenameOrNewStack ) ,
'cursor-pointer' : ! compareStack ( addNewStackObj , isRenameOrNewStack ) ,
} "
@ click = "
( ) => {
if ( ! compareStack ( addNewStackObj , isRenameOrNewStack ) ) {
isRenameOrNewStack = addNewStackObj
}
}
"
>
< NcButton
v - if = "!compareStack(addNewStackObj, isRenameOrNewStack)"
type = "secondary"
class = "add-new-stack-btn w-full !rounded-xl min-h-11"
>
< div class = "flex items-center gap-2" >
< component :is ="iconMap.plus" v -if = " ! isPublic & & ! isLocked " class = "" / >
{ { $t ( 'general.new' ) } } { { $t ( 'general.stack' ) . toLowerCase ( ) } }
< / div >
< / NcButton >
< div
v - else
class = "flex-1 flex"
: class = " {
'-ml-1' : compareStack ( addNewStackObj , isRenameOrNewStack ) ,
} "
@ click . stop
>
< template
v - if = "compareStack(addNewStackObj, isRenameOrNewStack) && metaColumnById[groupingFieldColumn?.id]"
>
< SmartsheetKanbanEditOrAddStack
: column = "metaColumnById[groupingFieldColumn?.id]"
is - new - stack
@ submit = "(loadMeta) => handleSubmitRenameOrNewStack(loadMeta, undefined)"
/ >
< / template >
< / div >
< / div >
< / a - l a y o u t - h e a d e r >
< / a - l a y o u t >
< / a - c a r d >
< / div >
< / div >
<!-- Drop down Menu -- >
<!-- Drop down Menu -- >
< template v -if = " ! isLocked & & ! isPublic & & hasEditPermission " # overlay >
< template v -if = " ! isLocked & & ! isPublic & & hasEditPermission " # overlay >
< a -menu class = "shadow !rounded !py-0" @ click = "contextMenu = false" >
< NcMenu @ click = "contextMenu = false" >
< a -menu -item v-if ="contextMenuTarget" @click="expandForm(contextMenuTarget)" >
< NcMenuI tem v-if ="contextMenuTarget" @click="expandForm(contextMenuTarget)" >
< div v-e ="['a:kanban:expand-record']" class="nc-base-menu-item nc-kanban-context-menu-item" >
< div v-e ="['a:kanban:expand-record']" class="flex items-center gap-2 nc-kanban-context-menu-item" >
< component :is ="iconMap.expand" class = "flex" / >
< component :is ="iconMap.expand" class = "flex" / >
<!-- Expand Record -- >
<!-- Expand Record -- >
{ { $t ( 'activity.expandRecord' ) } }
{ { $t ( 'activity.expandRecord' ) } }
< / div >
< / div >
< / a - m e n u - i t e m >
< / NcMenuItem >
< a -divider class = "!m-0 !p-0" / >
< NcDivider / >
< a -menu -item v-if ="contextMenuTarget" @click="deleteRow(contextMenuTarget)" >
< NcMenuI tem v-if ="contextMenuTarget" class="!text-red-600 !hover:bg-red-50 " @click="deleteRow(contextMenuTarget)" >
< div v-e ="['a:kanban:delete-record']" class="nc-base-menu-item nc-kanban-context-menu-item" >
< div v-e ="['a:kanban:delete-record']" class="flex items-center gap-2 nc-kanban-context-menu-item" >
< component :is ="iconMap.delete" class = "flex" / >
< component :is ="iconMap.delete" class = "flex" / >
<!-- Delete Record -- >
<!-- Delete Record -- >
{ { $t ( 'activity.deleteRecord' ) } }
{ { $t ( 'activity.deleteRecord' ) } }
< / div >
< / div >
< / a - m e n u - i t e m >
< / NcMenuItem >
< / a - m e n u >
< / NcMenu >
< / template >
< / template >
< / a - d r o p d o w n >
< / NcDropdown >
< / div >
< / div >
< LazySmartsheetPagination
v - model : pagination - data = "emptyPagination"
align - count - on - right
hide - pagination
class = "!py-4 h-10 !xs:py-0"
>
< / LazySmartsheetPagination >
< / div >
< / div >
< Suspense >
< Suspense >
@ -751,6 +1122,7 @@ const getRowId = (row: RowType) => {
: meta = "meta"
: meta = "meta"
: load - row = "!isPublic"
: load - row = "!isPublic"
: view = "view"
: view = "view"
@ cancel = "removeRowFromUncategorizedStack"
/ >
/ >
< / Suspense >
< / Suspense >
@ -785,12 +1157,16 @@ const getRowId = (row: RowType) => {
/ / o v e r r i d e a n t d e s i g n s t y l e
/ / o v e r r i d e a n t d e s i g n s t y l e
. a - layout ,
. a - layout ,
. ant - layout - header ,
. ant - layout - header ,
. ant - layout - footer {
@ apply ! bg - white ;
}
. ant - layout - content {
. ant - layout - content {
@ apply ! bg - gray - 100 ;
background - color : unset ;
}
}
. ant - layout - header ,
. ant - layout - header {
. ant - layout - footer {
@ apply ! h - [ 30 px ] ! leading - [ 30 px ] ! px - [ 5 px ] ! my - [ 10 px ] ;
@ apply p - 2 text - sm ;
height : unset ! important ;
}
}
. nc - kanban - collapsed - stack {
. nc - kanban - collapsed - stack {
@ -846,17 +1222,17 @@ const getRowId = (row: RowType) => {
}
}
. nc - card - display - value - wrapper {
. nc - card - display - value - wrapper {
@ apply my - 0 text - xl leading - 8 text - gray - 6 00;
@ apply my - 0 text - base leading - 8 text - gray - 8 00;
. nc - cell ,
. nc - cell ,
. nc - virtual - cell {
. nc - virtual - cell {
@ apply text - xl leading - 8 ;
@ apply text - base leading - 6 ;
: deep ( . nc - cell - field ) ,
: deep ( . nc - cell - field ) ,
: deep ( input ) ,
: deep ( input ) ,
: deep ( textarea ) ,
: deep ( textarea ) ,
: deep ( . nc - cell - field - link ) {
: deep ( . nc - cell - field - link ) {
@ apply ! text - xl leading - 8 text - gray - 6 00;
@ apply ! text - base leading - 6 text - gray - 8 00;
}
}
}
}
}
}
@ -864,7 +1240,7 @@ const getRowId = (row: RowType) => {
. nc - card - col - header {
. nc - card - col - header {
: deep ( . nc - cell - icon ) ,
: deep ( . nc - cell - icon ) ,
: deep ( . nc - virtual - cell - icon ) {
: deep ( . nc - virtual - cell - icon ) {
@ apply ml - 0 ;
@ apply ml - 0 ! w - 3.5 ! h - 3.5 ;
}
}
}
}