|
|
|
@ -90,6 +90,8 @@ const props = defineProps<{
|
|
|
|
|
extraStyle?: string |
|
|
|
|
} |
|
|
|
|
disableSkeleton?: boolean |
|
|
|
|
disableVirtualX?: boolean |
|
|
|
|
disableVirtualY?: boolean |
|
|
|
|
}>() |
|
|
|
|
|
|
|
|
|
const emits = defineEmits(['update:selectedAllRecords', 'bulkUpdateDlg', 'toggleOptimisedQuery']) |
|
|
|
@ -102,6 +104,16 @@ const dataRef = toRef(props, 'data')
|
|
|
|
|
|
|
|
|
|
const paginationStyleRef = toRef(props, 'pagination') |
|
|
|
|
|
|
|
|
|
const headerOnly = toRef(props, 'headerOnly') |
|
|
|
|
|
|
|
|
|
const hideHeader = toRef(props, 'hideHeader') |
|
|
|
|
|
|
|
|
|
const disableSkeleton = toRef(props, 'disableSkeleton') |
|
|
|
|
|
|
|
|
|
const disableVirtualX = toRef(props, 'disableVirtualX') |
|
|
|
|
|
|
|
|
|
const disableVirtualY = toRef(props, 'disableVirtualY') |
|
|
|
|
|
|
|
|
|
const { api } = useApi() |
|
|
|
|
|
|
|
|
|
const { |
|
|
|
@ -115,9 +127,6 @@ const {
|
|
|
|
|
deleteRangeOfRows, |
|
|
|
|
removeRowIfNew, |
|
|
|
|
bulkUpdateRows, |
|
|
|
|
headerOnly, |
|
|
|
|
hideHeader, |
|
|
|
|
disableSkeleton, |
|
|
|
|
} = props |
|
|
|
|
|
|
|
|
|
// #Injections |
|
|
|
@ -171,23 +180,15 @@ const {
|
|
|
|
|
predictedNextColumn, |
|
|
|
|
predictingNextFormulas, |
|
|
|
|
predictedNextFormulas, |
|
|
|
|
predictNextColumn: _predictNextColumn, |
|
|
|
|
predictNextFormulas: _predictNextFormulas, |
|
|
|
|
predictNextColumn, |
|
|
|
|
predictNextFormulas, |
|
|
|
|
} = useNocoEe().table |
|
|
|
|
|
|
|
|
|
const predictNextColumn = async () => { |
|
|
|
|
await _predictNextColumn(meta) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const predictNextFormulas = async () => { |
|
|
|
|
await _predictNextFormulas(meta) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const { paste } = usePaste() |
|
|
|
|
|
|
|
|
|
// #Refs |
|
|
|
|
const { addLTARRef, syncLTARRefs, clearLTARCell, cleaMMCell } = useSmartsheetLtarHelpersOrThrow() |
|
|
|
|
|
|
|
|
|
const rowRefs = ref<any[]>() |
|
|
|
|
// #Refs |
|
|
|
|
|
|
|
|
|
const smartTable = ref(null) |
|
|
|
|
|
|
|
|
@ -199,7 +200,7 @@ const tableBodyEl = ref<HTMLElement>()
|
|
|
|
|
|
|
|
|
|
const fillHandle = ref<HTMLElement>() |
|
|
|
|
|
|
|
|
|
const gridRect = useElementBounding(gridWrapper) |
|
|
|
|
const { height: tableHeadHeight, width: _tableHeadWidth } = useElementBounding(tableHeadEl) |
|
|
|
|
|
|
|
|
|
const isViewColumnsLoading = computed(() => _isViewColumnsLoading.value || !meta.value) |
|
|
|
|
|
|
|
|
@ -279,8 +280,8 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
|
|
|
|
|
if (isVirtualCol(columnObj)) { |
|
|
|
|
let mmClearResult |
|
|
|
|
|
|
|
|
|
if (isMm(columnObj) && rowRefs.value) { |
|
|
|
|
mmClearResult = await rowRefs.value[ctx.row]!.cleaMMCell(columnObj) |
|
|
|
|
if (isMm(columnObj) && rowObj) { |
|
|
|
|
mmClearResult = await cleaMMCell(rowObj, columnObj) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
addUndo({ |
|
|
|
@ -298,12 +299,11 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
|
|
|
|
|
rowId === extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) && |
|
|
|
|
columnObj.id === col.id |
|
|
|
|
) { |
|
|
|
|
if (rowRefs.value) { |
|
|
|
|
if (isBt(columnObj) || isOo(columnObj)) { |
|
|
|
|
rowObj.row[columnObj.title] = row.row[columnObj.title] |
|
|
|
|
|
|
|
|
|
await rowRefs.value[ctx.row]!.addLTARRef(rowObj.row[columnObj.title], columnObj) |
|
|
|
|
await rowRefs.value[ctx.row]!.syncLTARRefs(rowObj.row) |
|
|
|
|
await addLTARRef(rowObj, rowObj.row[columnObj.title], columnObj) |
|
|
|
|
await syncLTARRefs(rowObj, rowObj.row) |
|
|
|
|
} else if (isMm(columnObj)) { |
|
|
|
|
await api.dbDataTableRow.nestedLink( |
|
|
|
|
meta.value?.id as string, |
|
|
|
@ -313,7 +313,6 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
|
|
|
|
|
) |
|
|
|
|
rowObj.row[columnObj.title] = mmClearResult?.length ? mmClearResult?.length : null |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define |
|
|
|
|
activeCell.col = ctx.col |
|
|
|
@ -339,12 +338,10 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
|
|
|
|
|
const rowObj = dataRef.value[ctx.row] |
|
|
|
|
const columnObj = fields.value[ctx.col] |
|
|
|
|
if (rowId === extractPkFromRow(rowObj.row, meta.value?.columns as ColumnType[]) && columnObj.id === col.id) { |
|
|
|
|
if (rowRefs.value) { |
|
|
|
|
if (isBt(columnObj) || isOo(columnObj)) { |
|
|
|
|
await rowRefs.value[ctx.row]!.clearLTARCell(columnObj) |
|
|
|
|
await clearLTARCell(rowObj, columnObj) |
|
|
|
|
} else if (isMm(columnObj)) { |
|
|
|
|
await rowRefs.value[ctx.row]!.cleaMMCell(columnObj) |
|
|
|
|
} |
|
|
|
|
await cleaMMCell(rowObj, columnObj) |
|
|
|
|
} |
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define |
|
|
|
|
activeCell.col = ctx.col |
|
|
|
@ -362,7 +359,7 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
|
|
|
|
|
}, |
|
|
|
|
scope: defineViewScope({ view: view.value }), |
|
|
|
|
}) |
|
|
|
|
if ((isBt(columnObj) || isOo(columnObj)) && rowRefs.value) await rowRefs.value[ctx.row]!.clearLTARCell(columnObj) |
|
|
|
|
if (isBt(columnObj) || isOo(columnObj)) await clearLTARCell(rowObj, columnObj) |
|
|
|
|
|
|
|
|
|
return |
|
|
|
|
} |
|
|
|
@ -426,7 +423,7 @@ const visibleColLength = computed(() => fields.value?.length)
|
|
|
|
|
|
|
|
|
|
const gridWrapperClass = computed<string>(() => { |
|
|
|
|
const classes = [] |
|
|
|
|
if (headerOnly !== true) { |
|
|
|
|
if (headerOnly.value !== true) { |
|
|
|
|
if (!scrollParent.value) { |
|
|
|
|
classes.push('nc-scrollbar-x-lg !overflow-auto') |
|
|
|
|
} |
|
|
|
@ -453,10 +450,33 @@ const dummyRowDataForLoading = computed(() => {
|
|
|
|
|
|
|
|
|
|
const showSkeleton = computed( |
|
|
|
|
() => |
|
|
|
|
(disableSkeleton !== true && (isViewDataLoading.value || isPaginationLoading.value || isViewColumnsLoading.value)) || |
|
|
|
|
(disableSkeleton.value !== true && (isViewDataLoading.value || isPaginationLoading.value || isViewColumnsLoading.value)) || |
|
|
|
|
!meta.value, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
const cellMeta = computed(() => { |
|
|
|
|
return dataRef.value.map((row) => { |
|
|
|
|
return fields.value.map((col) => { |
|
|
|
|
return { |
|
|
|
|
isColumnRequiredAndNull: isColumnRequiredAndNull(col, row.row), |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
}) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const colMeta = computed(() => { |
|
|
|
|
return fields.value.map((col) => { |
|
|
|
|
return { |
|
|
|
|
isLookup: isLinksOrLTAR(col), |
|
|
|
|
isRollup: isBt(col), |
|
|
|
|
isFormula: isFormula(col), |
|
|
|
|
isCreatedOrLastModifiedTimeCol: isCreatedOrLastModifiedTimeCol(col), |
|
|
|
|
isCreatedOrLastModifiedByCol: isCreatedOrLastModifiedByCol(col), |
|
|
|
|
isVirtualCol: isVirtualCol(col), |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// #Grid |
|
|
|
|
|
|
|
|
|
function openColumnCreate(data: any) { |
|
|
|
@ -503,7 +523,12 @@ const onNewRecordToFormClick = () => {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const getContainerScrollForElement = ( |
|
|
|
|
el: HTMLElement, |
|
|
|
|
childPos: { |
|
|
|
|
top: number |
|
|
|
|
right: number |
|
|
|
|
bottom: number |
|
|
|
|
left: number |
|
|
|
|
}, |
|
|
|
|
container: HTMLElement, |
|
|
|
|
offset?: { |
|
|
|
|
top?: number |
|
|
|
@ -512,7 +537,6 @@ const getContainerScrollForElement = (
|
|
|
|
|
right?: number |
|
|
|
|
}, |
|
|
|
|
) => { |
|
|
|
|
const childPos = el.getBoundingClientRect() |
|
|
|
|
const parentPos = container.getBoundingClientRect() |
|
|
|
|
|
|
|
|
|
// provide an extra offset to show the prev/next/up/bottom cell |
|
|
|
@ -524,10 +548,10 @@ const getContainerScrollForElement = (
|
|
|
|
|
const stickyColsWidth = numColWidth + primaryColWidth |
|
|
|
|
|
|
|
|
|
const relativePos = { |
|
|
|
|
top: childPos.top - parentPos.top, |
|
|
|
|
right: childPos.right - parentPos.right, |
|
|
|
|
bottom: childPos.bottom - parentPos.bottom, |
|
|
|
|
left: childPos.left - parentPos.left - stickyColsWidth, |
|
|
|
|
right: childPos.right + numColWidth - parentPos.width - container.scrollLeft, |
|
|
|
|
left: childPos.left + numColWidth - container.scrollLeft - stickyColsWidth, |
|
|
|
|
bottom: childPos.bottom - parentPos.height - container.scrollTop, |
|
|
|
|
top: childPos.top - container.scrollTop, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const scroll = { |
|
|
|
@ -561,7 +585,8 @@ const getContainerScrollForElement = (
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const { |
|
|
|
|
isCellSelected, |
|
|
|
|
selectRangeMap, |
|
|
|
|
fillRangeMap, |
|
|
|
|
activeCell, |
|
|
|
|
handleMouseDown, |
|
|
|
|
handleMouseOver, |
|
|
|
@ -572,7 +597,6 @@ const {
|
|
|
|
|
resetSelectedRange, |
|
|
|
|
makeActive, |
|
|
|
|
selectedRange, |
|
|
|
|
isCellInFillRange, |
|
|
|
|
isFillMode, |
|
|
|
|
} = useMultiSelect( |
|
|
|
|
meta, |
|
|
|
@ -947,22 +971,46 @@ async function clearSelectedRangeOfCells() {
|
|
|
|
|
await bulkUpdateRows?.(rows, props) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const colPositions = computed(() => { |
|
|
|
|
return fields.value |
|
|
|
|
.filter((col) => col.id && gridViewCols.value[col.id] && gridViewCols.value[col.id].width && gridViewCols.value[col.id].show) |
|
|
|
|
.map((col) => { |
|
|
|
|
return +gridViewCols.value[col.id!]!.width!.replace('px', '') || 200 |
|
|
|
|
}) |
|
|
|
|
.reduce( |
|
|
|
|
(acc, width, i) => { |
|
|
|
|
acc.push(acc[i] + width) |
|
|
|
|
return acc |
|
|
|
|
}, |
|
|
|
|
[0], |
|
|
|
|
) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const scrollWrapper = computed(() => scrollParent.value || gridWrapper.value) |
|
|
|
|
|
|
|
|
|
function scrollToCell(row?: number | null, col?: number | null) { |
|
|
|
|
row = row ?? activeCell.row |
|
|
|
|
col = col ?? activeCell.col |
|
|
|
|
|
|
|
|
|
if (row !== null && col !== null) { |
|
|
|
|
// get active cell |
|
|
|
|
const rows = tableBodyEl.value?.querySelectorAll('tr') |
|
|
|
|
const cols = rows?.[row]?.querySelectorAll('td') |
|
|
|
|
const td = cols?.[col === 0 ? 0 : col + 1] |
|
|
|
|
if (row !== null && col !== null && scrollWrapper.value) { |
|
|
|
|
// calculate cell position |
|
|
|
|
const td = { |
|
|
|
|
top: row * rowHeightInPx[`${props.rowHeight}`], |
|
|
|
|
left: colPositions.value[col], |
|
|
|
|
right: |
|
|
|
|
col === fields.value.length - 1 ? colPositions.value[colPositions.value.length - 1] + 200 : colPositions.value[col + 1], |
|
|
|
|
bottom: (row + 1) * rowHeightInPx[`${props.rowHeight}`], |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!td || !scrollWrapper.value) return |
|
|
|
|
const tdScroll = getContainerScrollForElement(td, scrollWrapper.value, { |
|
|
|
|
top: 9, |
|
|
|
|
bottom: (tableHeadHeight.value || 40) + 9, |
|
|
|
|
right: 9, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const { height: headerHeight } = tableHeadEl.value!.getBoundingClientRect() |
|
|
|
|
const tdScroll = getContainerScrollForElement(td, scrollWrapper.value, { top: headerHeight || 40, bottom: 9, right: 9 }) |
|
|
|
|
if (isGroupBy.value) { |
|
|
|
|
tdScroll.top = scrollWrapper.value.scrollTop |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// if first column set left to 0 since it's sticky it will be visible and calculated value will be wrong |
|
|
|
|
// setting left to 0 will make it scroll to the left |
|
|
|
@ -970,20 +1018,11 @@ function scrollToCell(row?: number | null, col?: number | null) {
|
|
|
|
|
tdScroll.left = 0 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (rows && row === rows.length - 2) { |
|
|
|
|
// if last row make 'Add New Row' visible |
|
|
|
|
const lastRow = rows[rows.length - 1] || rows[rows.length - 2] |
|
|
|
|
|
|
|
|
|
const lastRowScroll = getContainerScrollForElement(lastRow, scrollWrapper.value, { |
|
|
|
|
top: headerHeight || 40, |
|
|
|
|
bottom: 9, |
|
|
|
|
right: 9, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
if (row === dataRef.value.length - 1) { |
|
|
|
|
scrollWrapper.value.scrollTo({ |
|
|
|
|
top: lastRowScroll.top, |
|
|
|
|
top: isGroupBy.value ? scrollWrapper.value.scrollTop : scrollWrapper.value.scrollHeight, |
|
|
|
|
left: |
|
|
|
|
cols && col === cols.length - 2 // if corner cell |
|
|
|
|
col === fields.value.length - 1 // if corner cell |
|
|
|
|
? scrollWrapper.value.scrollWidth |
|
|
|
|
: tdScroll.left, |
|
|
|
|
behavior: 'smooth', |
|
|
|
@ -991,7 +1030,7 @@ function scrollToCell(row?: number | null, col?: number | null) {
|
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (cols && col === cols.length - 2) { |
|
|
|
|
if (col === fields.value.length - 1) { |
|
|
|
|
// if last column make 'Add New Column' visible |
|
|
|
|
scrollWrapper.value.scrollTo({ |
|
|
|
|
top: tdScroll.top, |
|
|
|
@ -1026,14 +1065,11 @@ async function resetAndChangePage(row: number, col: number, pageChange?: number)
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const saveOrUpdateRecords = async (args: { metaValue?: TableType; viewMetaValue?: ViewType; data?: any } = {}) => { |
|
|
|
|
let index = -1 |
|
|
|
|
for (const currentRow of args.data || dataRef.value) { |
|
|
|
|
index++ |
|
|
|
|
/** if new record save row and save the LTAR cells */ |
|
|
|
|
if (currentRow.rowMeta.new) { |
|
|
|
|
const syncLTARRefs = rowRefs.value?.[index]?.syncLTARRefs |
|
|
|
|
const savedRow = await updateOrSaveRow?.(currentRow, '', {}, args) |
|
|
|
|
await syncLTARRefs?.(savedRow, args) |
|
|
|
|
await syncLTARRefs?.(currentRow, savedRow, args) |
|
|
|
|
currentRow.rowMeta.changed = false |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
@ -1078,6 +1114,133 @@ const loadColumn = (title: string, tp: string, colOptions?: any) => {
|
|
|
|
|
persistMenu.value = false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Virtual scroll |
|
|
|
|
|
|
|
|
|
const maxGridWidth = computed(() => { |
|
|
|
|
// 64 for the row number column |
|
|
|
|
// count first column twice because it's sticky |
|
|
|
|
// 100 for add new column |
|
|
|
|
return colPositions.value[colPositions.value.length - 1] + colPositions.value[1] + 64 + 100 |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const maxGridHeight = computed(() => { |
|
|
|
|
// 2 extra rows for the add new row and the sticky header |
|
|
|
|
return dataRef.value.length * rowHeightInPx[`${props.rowHeight}`] + 2 * rowHeightInPx[`${props.rowHeight}`] |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const colSlice = ref({ |
|
|
|
|
start: 0, |
|
|
|
|
end: 0, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const rowSlice = ref({ |
|
|
|
|
start: 0, |
|
|
|
|
end: 0, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const VIRTUAL_MARGIN = 10 |
|
|
|
|
|
|
|
|
|
const calculateSlices = () => { |
|
|
|
|
// if the grid is not rendered yet |
|
|
|
|
if (!scrollWrapper.value || !gridWrapper.value) { |
|
|
|
|
colSlice.value = { |
|
|
|
|
start: 0, |
|
|
|
|
end: 0, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
rowSlice.value = { |
|
|
|
|
start: 0, |
|
|
|
|
end: 0, |
|
|
|
|
} |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let renderStart = 0 |
|
|
|
|
|
|
|
|
|
if (disableVirtualX.value !== true) { |
|
|
|
|
// use binary search to find the start and end columns |
|
|
|
|
let startRange = 0 |
|
|
|
|
let endRange = colPositions.value.length - 1 |
|
|
|
|
|
|
|
|
|
while (endRange !== startRange) { |
|
|
|
|
const middle = Math.floor((endRange - startRange) / 2 + startRange) |
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
colPositions.value[middle] <= scrollWrapper.value.scrollLeft && |
|
|
|
|
colPositions.value[middle + 1] > scrollWrapper.value.scrollLeft |
|
|
|
|
) { |
|
|
|
|
renderStart = middle |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (middle === startRange) { |
|
|
|
|
renderStart = endRange |
|
|
|
|
break |
|
|
|
|
} else { |
|
|
|
|
if (colPositions.value[middle] <= scrollWrapper.value.scrollLeft) { |
|
|
|
|
startRange = middle |
|
|
|
|
} else { |
|
|
|
|
endRange = middle |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let renderEnd = 0 |
|
|
|
|
let renderEndFound = false |
|
|
|
|
|
|
|
|
|
for (let i = renderStart; i < colPositions.value.length; i++) { |
|
|
|
|
if (colPositions.value[i] > gridWrapper.value.clientWidth + scrollWrapper.value.scrollLeft) { |
|
|
|
|
renderEnd = i |
|
|
|
|
renderEndFound = true |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
colSlice.value = { |
|
|
|
|
start: Math.max(0, renderStart - VIRTUAL_MARGIN), |
|
|
|
|
end: renderEndFound ? Math.min(fields.value.length, renderEnd + VIRTUAL_MARGIN) : fields.value.length, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (disableVirtualY.value !== true) { |
|
|
|
|
const rowHeight = rowHeightInPx[`${props.rowHeight}`] |
|
|
|
|
const rowRenderStart = Math.max(0, Math.floor(scrollWrapper.value.scrollTop / rowHeight) - VIRTUAL_MARGIN) |
|
|
|
|
const rowRenderEnd = Math.min( |
|
|
|
|
dataRef.value.length, |
|
|
|
|
rowRenderStart + Math.ceil(gridWrapper.value.clientHeight / rowHeight) + VIRTUAL_MARGIN, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
rowSlice.value = { |
|
|
|
|
start: rowRenderStart, |
|
|
|
|
end: rowRenderEnd, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const visibleFields = computed(() => { |
|
|
|
|
if (disableVirtualX.value) return fields.value.map((field, index) => ({ field, index })).filter((f) => f.index !== 0) |
|
|
|
|
// return data as { field, index } to keep track of the index |
|
|
|
|
const vFields = fields.value.slice(colSlice.value.start, colSlice.value.end) |
|
|
|
|
return vFields.map((field, index) => ({ field, index: index + colSlice.value.start })).filter((f) => f.index !== 0) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const visibleData = computed(() => { |
|
|
|
|
if (disableVirtualY.value) return dataRef.value.map((row, index) => ({ row, index })) |
|
|
|
|
// return data as { row, index } to keep track of the index |
|
|
|
|
return dataRef.value.slice(rowSlice.value.start, rowSlice.value.end).map((row, index) => ({ |
|
|
|
|
row, |
|
|
|
|
index: index + rowSlice.value.start, |
|
|
|
|
})) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const leftOffset = computed(() => { |
|
|
|
|
return colSlice.value.start > 0 ? colPositions.value[colSlice.value.start] - colPositions.value[1] : 0 |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const topOffset = computed(() => { |
|
|
|
|
return rowHeightInPx[`${props.rowHeight}`] * rowSlice.value.start |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// #Fill Handle |
|
|
|
|
|
|
|
|
|
const fillHandleTop = ref() |
|
|
|
@ -1085,12 +1248,18 @@ const fillHandleLeft = ref()
|
|
|
|
|
|
|
|
|
|
const refreshFillHandle = () => { |
|
|
|
|
nextTick(() => { |
|
|
|
|
const cellRef = document.querySelector('.last-cell') |
|
|
|
|
if (cellRef) { |
|
|
|
|
const cellRect = cellRef.getBoundingClientRect() |
|
|
|
|
if (!cellRect || !gridWrapper.value) return |
|
|
|
|
fillHandleTop.value = cellRect.top + cellRect.height - gridRect.top.value + gridWrapper.value.scrollTop |
|
|
|
|
fillHandleLeft.value = cellRect.left + cellRect.width - gridRect.left.value + gridWrapper.value.scrollLeft |
|
|
|
|
const rowIndex = isNaN(selectedRange.end.row) ? activeCell.row : selectedRange.end.row |
|
|
|
|
const colIndex = isNaN(selectedRange.end.col) ? activeCell.col : selectedRange.end.col |
|
|
|
|
if (rowIndex !== null && colIndex !== null) { |
|
|
|
|
if (!scrollWrapper.value || !gridWrapper.value) return |
|
|
|
|
|
|
|
|
|
// 32 for the header |
|
|
|
|
fillHandleTop.value = (rowIndex + 1) * rowHeightInPx[`${props.rowHeight}`] + (hideHeader.value ? 0 : 32) |
|
|
|
|
// 64 for the row number column |
|
|
|
|
fillHandleLeft.value = |
|
|
|
|
64 + |
|
|
|
|
colPositions.value[colIndex + 1] + |
|
|
|
|
(colIndex === 0 ? Math.max(0, scrollWrapper.value.scrollLeft - gridWrapper.value.offsetLeft) : 0) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
@ -1115,6 +1284,7 @@ watch(
|
|
|
|
|
[() => selectedRange.end.row, () => selectedRange.end.col, () => activeCell.row, () => activeCell.col], |
|
|
|
|
([sr, sc, ar, ac], [osr, osc, oar, oac]) => { |
|
|
|
|
if (sr !== osr || sc !== osc || ar !== oar || ac !== oac) { |
|
|
|
|
calculateSlices() |
|
|
|
|
refreshFillHandle() |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
@ -1130,6 +1300,9 @@ onMounted(() => {
|
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
if (smartTable.value) resizeObserver.observe(smartTable.value) |
|
|
|
|
until(scrollWrapper) |
|
|
|
|
.toBeTruthy() |
|
|
|
|
.then(() => calculateSlices()) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// #Listeners |
|
|
|
@ -1160,12 +1333,22 @@ async function reloadViewDataHandler(params: void | { shouldShowLoading?: boolea
|
|
|
|
|
|
|
|
|
|
await loadData?.({ ...(params?.offset !== undefined ? { offset: params.offset } : {}) }) |
|
|
|
|
|
|
|
|
|
calculateSlices() |
|
|
|
|
|
|
|
|
|
isViewDataLoading.value = false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let frame: number | null = null |
|
|
|
|
|
|
|
|
|
useEventListener(scrollWrapper, 'scroll', () => { |
|
|
|
|
if (frame) { |
|
|
|
|
cancelAnimationFrame(frame) |
|
|
|
|
} |
|
|
|
|
frame = requestAnimationFrame(() => { |
|
|
|
|
calculateSlices() |
|
|
|
|
refreshFillHandle() |
|
|
|
|
}) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
useEventListener(document, 'mousedown', (e) => { |
|
|
|
|
if (e.offsetX > (e.target as HTMLElement)?.clientWidth || e.offsetY > (e.target as HTMLElement)?.clientHeight) { |
|
|
|
@ -1310,6 +1493,7 @@ watch(
|
|
|
|
|
isViewDataLoading.value = true |
|
|
|
|
try { |
|
|
|
|
await loadData?.() |
|
|
|
|
calculateSlices() |
|
|
|
|
} catch (e) { |
|
|
|
|
if (!axios.isCancel(e)) { |
|
|
|
|
console.log(e) |
|
|
|
@ -1328,6 +1512,10 @@ watch(
|
|
|
|
|
{ immediate: true }, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
watch([() => fields.value.length, () => dataRef.value.length], () => { |
|
|
|
|
calculateSlices() |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// #Providers |
|
|
|
|
|
|
|
|
|
provide(CellUrlDisableOverlayInj, disableUrlOverlay) |
|
|
|
@ -1414,16 +1602,16 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
:trigger="isSqlView ? [] : ['contextmenu']" |
|
|
|
|
overlay-class-name="nc-dropdown-grid-context-menu" |
|
|
|
|
> |
|
|
|
|
<div class="table-overlay" :class="{ 'nc-grid-skeleton-loader': showSkeleton }"> |
|
|
|
|
<div> |
|
|
|
|
<table |
|
|
|
|
ref="smartTable" |
|
|
|
|
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white relative" |
|
|
|
|
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white sticky top-0 z-5 bg-white" |
|
|
|
|
:class="{ |
|
|
|
|
'mobile': isMobileMode, |
|
|
|
|
'desktop': !isMobileMode, |
|
|
|
|
'pr-60 pb-12': !headerOnly, |
|
|
|
|
mobile: isMobileMode, |
|
|
|
|
desktop: !isMobileMode, |
|
|
|
|
}" |
|
|
|
|
:style="{ |
|
|
|
|
transform: `translateX(${leftOffset}px)`, |
|
|
|
|
}" |
|
|
|
|
@contextmenu="showContextMenu" |
|
|
|
|
> |
|
|
|
|
<thead v-show="hideHeader !== true" ref="tableHeadEl"> |
|
|
|
|
<tr v-if="isViewColumnsLoading"> |
|
|
|
@ -1446,7 +1634,13 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
</td> |
|
|
|
|
</tr> |
|
|
|
|
<tr v-show="!isViewColumnsLoading" class="nc-grid-header"> |
|
|
|
|
<th class="w-[64px] min-w-[64px]" data-testid="grid-id-column" @dblclick="() => {}"> |
|
|
|
|
<th |
|
|
|
|
class="w-[64px] min-w-[64px]" |
|
|
|
|
data-testid="grid-id-column" |
|
|
|
|
:style="{ |
|
|
|
|
left: `-${leftOffset}px`, |
|
|
|
|
}" |
|
|
|
|
> |
|
|
|
|
<div class="w-full h-full flex pl-2 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> |
|
|
|
@ -1468,8 +1662,47 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
</div> |
|
|
|
|
</th> |
|
|
|
|
<th |
|
|
|
|
v-for="(col, index) in fields" |
|
|
|
|
:key="col.title" |
|
|
|
|
v-if="fields[0]" |
|
|
|
|
v-xc-ver-resize |
|
|
|
|
:data-col="fields[0].id" |
|
|
|
|
:data-title="fields[0].title" |
|
|
|
|
:style="{ |
|
|
|
|
'min-width': gridViewCols[fields[0].id]?.width || '180px', |
|
|
|
|
'max-width': gridViewCols[fields[0].id]?.width || '180px', |
|
|
|
|
'width': gridViewCols[fields[0].id]?.width || '180px', |
|
|
|
|
...(leftOffset > 0 |
|
|
|
|
? { |
|
|
|
|
left: `-${leftOffset - 64}px`, |
|
|
|
|
} |
|
|
|
|
: {}), |
|
|
|
|
}" |
|
|
|
|
class="nc-grid-column-header" |
|
|
|
|
:class="{ |
|
|
|
|
'!border-r-blue-400 !border-r-3': toBeDroppedColId === fields[0].id, |
|
|
|
|
}" |
|
|
|
|
@xcstartresizing="onXcStartResizing(fields[0].id, $event)" |
|
|
|
|
@xcresize="onresize(fields[0].id, $event)" |
|
|
|
|
@xcresizing="onXcResizing(fields[0].id, $event)" |
|
|
|
|
@click="selectColumn(fields[0].id!)" |
|
|
|
|
> |
|
|
|
|
<div |
|
|
|
|
class="w-full h-full flex items-center text-gray-500 pl-2 pr-1" |
|
|
|
|
draggable="false" |
|
|
|
|
@dragstart.stop="onDragStart(fields[0].id!, $event)" |
|
|
|
|
@drag.stop="onDrag($event)" |
|
|
|
|
@dragend.stop="onDragEnd($event)" |
|
|
|
|
> |
|
|
|
|
<LazySmartsheetHeaderVirtualCell |
|
|
|
|
v-if="fields[0] && colMeta[0].isVirtualCol" |
|
|
|
|
:column="fields[0]" |
|
|
|
|
:hide-menu="readOnly || isMobileMode" |
|
|
|
|
/> |
|
|
|
|
<LazySmartsheetHeaderCell v-else :column="fields[0]" :hide-menu="readOnly || isMobileMode" /> |
|
|
|
|
</div> |
|
|
|
|
</th> |
|
|
|
|
<th |
|
|
|
|
v-for="{ field: col, index } in visibleFields" |
|
|
|
|
:key="col.id" |
|
|
|
|
v-xc-ver-resize |
|
|
|
|
:data-col="col.id" |
|
|
|
|
:data-title="col.title" |
|
|
|
@ -1495,7 +1728,7 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
@dragend.stop="onDragEnd($event)" |
|
|
|
|
> |
|
|
|
|
<LazySmartsheetHeaderVirtualCell |
|
|
|
|
v-if="isVirtualCol(col)" |
|
|
|
|
v-if="colMeta[index].isVirtualCol" |
|
|
|
|
:column="col" |
|
|
|
|
:hide-menu="readOnly || isMobileMode" |
|
|
|
|
/> |
|
|
|
@ -1623,6 +1856,29 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
</th> |
|
|
|
|
</tr> |
|
|
|
|
</thead> |
|
|
|
|
</table> |
|
|
|
|
<div |
|
|
|
|
class="table-overlay" |
|
|
|
|
:class="{ 'nc-grid-skeleton-loader': showSkeleton }" |
|
|
|
|
:style="{ |
|
|
|
|
height: `${maxGridHeight}px`, |
|
|
|
|
width: `${maxGridWidth}px`, |
|
|
|
|
}" |
|
|
|
|
> |
|
|
|
|
<table |
|
|
|
|
ref="smartTable" |
|
|
|
|
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white relative" |
|
|
|
|
:class="{ |
|
|
|
|
'mobile': isMobileMode, |
|
|
|
|
'desktop': !isMobileMode, |
|
|
|
|
'pr-60 pb-12': !headerOnly, |
|
|
|
|
'w-full': dataRef.length === 0, |
|
|
|
|
}" |
|
|
|
|
:style="{ |
|
|
|
|
transform: `translateY(${topOffset}px) translateX(${leftOffset}px)`, |
|
|
|
|
}" |
|
|
|
|
@contextmenu="showContextMenu" |
|
|
|
|
> |
|
|
|
|
<tbody v-if="headerOnly !== true" ref="tableBodyEl"> |
|
|
|
|
<template v-if="showSkeleton"> |
|
|
|
|
<tr v-for="(row, rowIndex) of dummyRowDataForLoading" :key="rowIndex"> |
|
|
|
@ -1634,7 +1890,7 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
></td> |
|
|
|
|
</tr> |
|
|
|
|
</template> |
|
|
|
|
<LazySmartsheetRow v-for="(row, rowIndex) of dataRef" ref="rowRefs" :key="rowIndex" :row="row"> |
|
|
|
|
<LazySmartsheetRow v-for="{ row, index: rowIndex } in visibleData" :key="rowIndex" :row="row"> |
|
|
|
|
<template #default="{ state }"> |
|
|
|
|
<tr |
|
|
|
|
v-show="!showSkeleton" |
|
|
|
@ -1649,6 +1905,9 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
<td |
|
|
|
|
key="row-index" |
|
|
|
|
class="caption nc-grid-cell w-[64px] min-w-[64px]" |
|
|
|
|
:style="{ |
|
|
|
|
left: `-${leftOffset}px`, |
|
|
|
|
}" |
|
|
|
|
:data-testid="`cell-Id-${rowIndex}`" |
|
|
|
|
@contextmenu="contextMenuTarget = null" |
|
|
|
|
> |
|
|
|
@ -1710,29 +1969,100 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
</div> |
|
|
|
|
</td> |
|
|
|
|
<SmartsheetTableDataCell |
|
|
|
|
v-for="(columnObj, colIndex) of fields" |
|
|
|
|
:key="columnObj.id" |
|
|
|
|
v-if="fields[0]" |
|
|
|
|
:key="fields[0].id" |
|
|
|
|
class="cell relative nc-grid-cell cursor-pointer" |
|
|
|
|
:class="{ |
|
|
|
|
'active': isCellSelected(rowIndex, colIndex), |
|
|
|
|
'active': selectRangeMap[`${rowIndex}-0`], |
|
|
|
|
'active-cell': |
|
|
|
|
(activeCell.row === rowIndex && activeCell.col === 0) || |
|
|
|
|
(selectedRange._start?.row === rowIndex && selectedRange._start?.col === 0), |
|
|
|
|
'nc-required-cell': cellMeta[rowIndex][0].isColumnRequiredAndNull && !isPublicView, |
|
|
|
|
'align-middle': !rowHeight || rowHeight === 1, |
|
|
|
|
'align-top': rowHeight && rowHeight !== 1, |
|
|
|
|
'filling': fillRangeMap[`${rowIndex}-0`], |
|
|
|
|
'readonly': |
|
|
|
|
(colMeta[0].isLookup || |
|
|
|
|
colMeta[0].isRollup || |
|
|
|
|
colMeta[0].isFormula || |
|
|
|
|
colMeta[0].isCreatedOrLastModifiedTimeCol || |
|
|
|
|
colMeta[0].isCreatedOrLastModifiedByCol) && |
|
|
|
|
hasEditPermission && |
|
|
|
|
selectRangeMap[`${rowIndex}-0`], |
|
|
|
|
'!border-r-blue-400 !border-r-3': toBeDroppedColId === fields[0].id, |
|
|
|
|
}" |
|
|
|
|
:style="{ |
|
|
|
|
'min-width': gridViewCols[fields[0].id]?.width || '180px', |
|
|
|
|
'max-width': gridViewCols[fields[0].id]?.width || '180px', |
|
|
|
|
'width': gridViewCols[fields[0].id]?.width || '180px', |
|
|
|
|
...(leftOffset > 0 |
|
|
|
|
? { |
|
|
|
|
left: `-${leftOffset - 64}px`, |
|
|
|
|
} |
|
|
|
|
: {}), |
|
|
|
|
}" |
|
|
|
|
:data-testid="`cell-${fields[0].title}-${rowIndex}`" |
|
|
|
|
:data-key="`data-key-${rowIndex}-${fields[0].id}`" |
|
|
|
|
:data-col="fields[0].id" |
|
|
|
|
:data-title="fields[0].title" |
|
|
|
|
:data-row-index="rowIndex" |
|
|
|
|
:data-col-index="0" |
|
|
|
|
@mousedown="handleMouseDown($event, rowIndex, 0)" |
|
|
|
|
@mouseover="handleMouseOver($event, rowIndex, 0)" |
|
|
|
|
@click="handleCellClick($event, rowIndex, 0)" |
|
|
|
|
@dblclick="makeEditable(row, fields[0])" |
|
|
|
|
@contextmenu="showContextMenu($event, { row: rowIndex, col: 0 })" |
|
|
|
|
> |
|
|
|
|
<div v-if="!switchingTab" class="w-full"> |
|
|
|
|
<LazySmartsheetVirtualCell |
|
|
|
|
v-if="fields[0] && colMeta[0].isVirtualCol && fields[0].title" |
|
|
|
|
v-model="row.row[fields[0].title]" |
|
|
|
|
:column="fields[0]" |
|
|
|
|
:active="activeCell.col === 0 && activeCell.row === rowIndex" |
|
|
|
|
:row="row" |
|
|
|
|
:read-only="!hasEditPermission" |
|
|
|
|
@navigate="onNavigate" |
|
|
|
|
@save="updateOrSaveRow?.(row, '', state)" |
|
|
|
|
/> |
|
|
|
|
|
|
|
|
|
<LazySmartsheetCell |
|
|
|
|
v-else-if="fields[0] && fields[0].title" |
|
|
|
|
v-model="row.row[fields[0].title]" |
|
|
|
|
:column="fields[0]" |
|
|
|
|
:edit-enabled=" |
|
|
|
|
!!hasEditPermission && !!editEnabled && activeCell.col === 0 && activeCell.row === rowIndex |
|
|
|
|
" |
|
|
|
|
:row-index="rowIndex" |
|
|
|
|
:active="activeCell.col === 0 && activeCell.row === rowIndex" |
|
|
|
|
:read-only="!hasEditPermission" |
|
|
|
|
@update:edit-enabled="editEnabled = $event" |
|
|
|
|
@save="updateOrSaveRow?.(row, fields[0].title, state)" |
|
|
|
|
@navigate="onNavigate" |
|
|
|
|
@cancel="editEnabled = false" |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
</SmartsheetTableDataCell> |
|
|
|
|
<SmartsheetTableDataCell |
|
|
|
|
v-for="{ field: columnObj, index: colIndex } of visibleFields" |
|
|
|
|
:key="`cell-${colIndex}-${rowIndex}`" |
|
|
|
|
class="cell relative nc-grid-cell cursor-pointer" |
|
|
|
|
:class="{ |
|
|
|
|
'active': selectRangeMap[`${rowIndex}-${colIndex}`], |
|
|
|
|
'active-cell': |
|
|
|
|
(activeCell.row === rowIndex && activeCell.col === colIndex) || |
|
|
|
|
(selectedRange._start?.row === rowIndex && selectedRange._start?.col === colIndex), |
|
|
|
|
'last-cell': |
|
|
|
|
rowIndex === (isNaN(selectedRange.end.row) ? activeCell.row : selectedRange.end.row) && |
|
|
|
|
colIndex === (isNaN(selectedRange.end.col) ? activeCell.col : selectedRange.end.col), |
|
|
|
|
'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row) && !isPublicView, |
|
|
|
|
'nc-required-cell': cellMeta[rowIndex][colIndex].isColumnRequiredAndNull && !isPublicView, |
|
|
|
|
'align-middle': !rowHeight || rowHeight === 1, |
|
|
|
|
'align-top': rowHeight && rowHeight !== 1, |
|
|
|
|
'filling': isCellInFillRange(rowIndex, colIndex), |
|
|
|
|
'filling': fillRangeMap[`${rowIndex}-${colIndex}`], |
|
|
|
|
'readonly': |
|
|
|
|
(isLookup(columnObj) || |
|
|
|
|
isRollup(columnObj) || |
|
|
|
|
isFormula(columnObj) || |
|
|
|
|
isCreatedOrLastModifiedTimeCol(columnObj) || |
|
|
|
|
isCreatedOrLastModifiedByCol(columnObj)) && |
|
|
|
|
(colMeta[colIndex].isLookup || |
|
|
|
|
colMeta[colIndex].isRollup || |
|
|
|
|
colMeta[colIndex].isFormula || |
|
|
|
|
colMeta[colIndex].isCreatedOrLastModifiedTimeCol || |
|
|
|
|
colMeta[colIndex].isCreatedOrLastModifiedByCol) && |
|
|
|
|
hasEditPermission && |
|
|
|
|
isCellSelected(rowIndex, colIndex), |
|
|
|
|
selectRangeMap[`${rowIndex}-${colIndex}`], |
|
|
|
|
'!border-r-blue-400 !border-r-3': toBeDroppedColId === columnObj.id, |
|
|
|
|
}" |
|
|
|
|
:style="{ |
|
|
|
@ -1754,7 +2084,7 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
> |
|
|
|
|
<div v-if="!switchingTab" class="w-full"> |
|
|
|
|
<LazySmartsheetVirtualCell |
|
|
|
|
v-if="isVirtualCol(columnObj) && columnObj.title" |
|
|
|
|
v-if="colMeta[colIndex].isVirtualCol && columnObj.title" |
|
|
|
|
v-model="row.row[columnObj.title]" |
|
|
|
|
:column="columnObj" |
|
|
|
|
:active="activeCell.col === colIndex && activeCell.row === rowIndex" |
|
|
|
@ -1797,6 +2127,9 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
> |
|
|
|
|
<div |
|
|
|
|
class="h-8 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" |
|
|
|
|
:style="{ |
|
|
|
|
left: `-${leftOffset}px`, |
|
|
|
|
}" |
|
|
|
|
> |
|
|
|
|
<component |
|
|
|
|
:is="iconMap.plus" |
|
|
|
@ -1826,6 +2159,7 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
}" |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<template #overlay> |
|
|
|
|
<NcMenu class="!rounded !py-0" @click="contextMenu = false"> |
|
|
|
@ -1906,7 +2240,7 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
contextMenuTarget && |
|
|
|
|
hasEditPermission && |
|
|
|
|
selectedRange.isSingleCell() && |
|
|
|
|
(isLinksOrLTAR(fields[contextMenuTarget.col]) || !isVirtualCol(fields[contextMenuTarget.col])) |
|
|
|
|
(isLinksOrLTAR(fields[contextMenuTarget.col]) || !cellMeta[0]?.[contextMenuTarget.col].isVirtualCol) |
|
|
|
|
" |
|
|
|
|
class="nc-base-menu-item" |
|
|
|
|
:disabled="isSystemColumn(fields[contextMenuTarget.col])" |
|
|
|
@ -2365,11 +2699,6 @@ onKeyStroke('ArrowDown', onDown)
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.nc-grid-header { |
|
|
|
|
position: sticky; |
|
|
|
|
top: -1px; |
|
|
|
|
|
|
|
|
|
@apply z-5 bg-white; |
|
|
|
|
|
|
|
|
|
&:hover { |
|
|
|
|
.nc-no-label { |
|
|
|
|
@apply hidden; |
|
|
|
|