From fdd2884da6a68497e467997f962610ac920cfd2d Mon Sep 17 00:00:00 2001 From: Anbarasu Date: Thu, 11 Apr 2024 16:44:04 +0530 Subject: [PATCH] fix: calendar datetime overlap fix (#8247) * fix: some perf fixes * fix: date time overlap issue * fix: show hidden records count --- .../calendar/DayView/DateTimeField.vue | 298 +++++++----------- 1 file changed, 120 insertions(+), 178 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue b/packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue index d8e1da4423..ef01c0a05e 100644 --- a/packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue +++ b/packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue @@ -7,7 +7,6 @@ import { generateRandomNumber, isRowEmpty } from '~/utils' const emit = defineEmits(['expandRecord', 'newRecord']) const { - // activeCalendarView, selectedDate, selectedTime, formattedData, @@ -81,7 +80,7 @@ const calculateNewDates = ({ return { endDate, startDate } } -/* const getGridTime = (date: dayjs.Dayjs, round = false) => { +const getGridTime = (date: dayjs.Dayjs, round = false) => { const gridCalc = date.hour() * 60 + date.minute() if (round) { return Math.ceil(gridCalc) @@ -95,10 +94,9 @@ const getGridTimeSlots = (from: dayjs.Dayjs, to: dayjs.Dayjs) => { from: getGridTime(from, false), to: getGridTime(to, true) - 1, } -} */ +} -/* const hasSlotForRecord = ( - record: Row, +const hasSlotForRecord = ( columnArray: Row[], dates: { fromDate: dayjs.Dayjs @@ -117,7 +115,9 @@ const getGridTimeSlots = (from: dayjs.Dayjs, to: dayjs.Dayjs) => { const { startDate: columnFromDate, endDate: columnToDate } = calculateNewDates({ startDate: dayjs(column.row[columnFromCol.title!]), - endDate: columnToCol ? dayjs(column.row[columnToCol.title!]) : dayjs(column.row[columnFromCol.title!]).add(1, 'hour'), + endDate: columnToCol + ? dayjs(column.row[columnToCol.title!]) + : dayjs(column.row[columnFromCol.title!]).add(1, 'hour').subtract(1, 'minute'), scheduleStart: dayjs(selectedDate.value).startOf('day'), scheduleEnd: dayjs(selectedDate.value).endOf('day'), }) @@ -130,65 +130,60 @@ const getGridTimeSlots = (from: dayjs.Dayjs, to: dayjs.Dayjs) => { } } return true -} */ - -/* const getMaxOfGrid = ( - { - fromDate, - toDate, - }: { - fromDate: dayjs.Dayjs - toDate: dayjs.Dayjs - }, - gridTimeMap: Map, -) => { - let max = 0 - const gridTimes = getGridTimeSlots(fromDate, toDate) - - for (let gridCounter = gridTimes.from; gridCounter <= gridTimes.to; gridCounter++) { - if (gridTimeMap.has(gridCounter) && gridTimeMap.get(gridCounter) > max) { - max = gridTimeMap.get(gridCounter) +} +const getMaxOverlaps = ({ + row, + gridTimeMap, + columnArray, +}: { + row: Row + gridTimeMap: Map< + number, + { + count: number + id: string[] + } + > + columnArray: Array> +}) => { + const visited: Set = new Set() + const graph: Map> = new Map() + + // Build the graph + for (const [_gridTime, { id: ids }] of gridTimeMap) { + for (const id1 of ids) { + if (!graph.has(id1)) { + graph.set(id1, new Set()) + } + for (const id2 of ids) { + if (id1 !== id2) { + graph.get(id1)!.add(id2) + } + } } } - return max -} */ -/* const isOverlaps = (row1: Row, row2: Row) => { - const fromCol1 = row1.rowMeta.range?.fk_from_col - const toCol1 = row1.rowMeta.range?.fk_to_col - const fromCol2 = row2.rowMeta.range?.fk_from_col - const toCol2 = row2.rowMeta.range?.fk_to_col - - if (!fromCol1 || !fromCol2) return false - - const { startDate: startDate1, endDate: endDate1 } = calculateNewDates({ - endDate: toCol1 ? dayjs(row1.row[toCol1.title!]) : dayjs(row1.row[fromCol1.title!]).add(1, 'hour'), - startDate: dayjs(row1.row[fromCol1.title!]), - scheduleStart: dayjs(selectedDate.value).startOf('day'), - scheduleEnd: dayjs(selectedDate.value).endOf('day'), - }) - - const { startDate: startDate2, endDate: endDate2 } = calculateNewDates({ - endDate: toCol2 ? dayjs(row2.row[toCol2.title!]) : dayjs(row2.row[fromCol2.title!]).add(1, 'hour'), - startDate: dayjs(row2.row[fromCol2.title!]), - scheduleStart: dayjs(selectedDate.value).startOf('day'), - scheduleEnd: dayjs(selectedDate.value).endOf('day'), - }) - return startDate1.isBetween(startDate2, endDate2, null, '[]') || endDate1.isBetween(startDate2, endDate2, null, '[]') -} */ - -/* const getMaxOverlaps = ({ row, rowArray }: { row: Row; rowArray: Row[] }) => { - let maxOverlaps = row.rowMeta.numberOfOverlaps - for (const record of rowArray) { - if (isOverlaps(row, record)) { - if (!record.rowMeta.numberOfOverlaps || !row.rowMeta.numberOfOverlaps) continue - if (record.rowMeta.numberOfOverlaps > row.rowMeta.numberOfOverlaps) { - maxOverlaps = record.rowMeta.numberOfOverlaps + const dfs = (id: string): number => { + visited.add(id) + let maxOverlaps = 1 + const neighbors = graph.get(id) + if (neighbors) { + for (const neighbor of neighbors) { + if (!visited.has(neighbor)) { + maxOverlaps = Math.min(Math.max(maxOverlaps, dfs(neighbor) + 1), columnArray.length) + } } } + return maxOverlaps + } + + let maxOverlaps = 1 + const id = row.rowMeta.id as string + if (graph.has(id)) { + maxOverlaps = dfs(id) } return maxOverlaps -} */ +} const recordsAcrossAllRange = computed<{ record: Row[] @@ -220,10 +215,16 @@ const recordsAcrossAllRange = computed<{ const perRecordHeight = 60 - /* const columnArray: Array> = [[]] - const gridTimeMap = new Map() */ + const columnArray: Array> = [[]] + const gridTimeMap = new Map< + number, + { + count: number + id: string[] + } + >() - let recordsByRange: Array = [] + const recordsByRange: Array = [] calendarRange.value.forEach((range) => { const fromCol = range.fk_from_col @@ -268,36 +269,11 @@ const recordsAcrossAllRange = computed<{ // The height of the record is calculated based on the difference between the start and end date const heightInPixels = Math.max(endDate.diff(startDate, 'minute'), perRecordHeight) - const startHour = startDate.hour() - let _startDate = startDate.clone() - const style: Partial = { height: `${heightInPixels}px`, top: `${topInPixels}px`, } - // We loop through every 1 minutes between the start and end date and keep track of the number of records that overlap at a given time - // If the number of records exceeds 4, we hide the record and show a button to view more records - while (_startDate.isBefore(endDate)) { - const timeKey = _startDate.format('HH:mm') - if (!overlaps[timeKey]) { - overlaps[timeKey] = { - id: [], - overflow: false, - overflowCount: 0, - } - } - overlaps[timeKey].id.push(id) - - // If the number of records exceeds 4, we hide the record mark the time as overflow - if (overlaps[timeKey].id.length > 4) { - overlaps[timeKey].overflow = true - style.display = 'none' - overlaps[timeKey].overflowCount += 1 - } - _startDate = _startDate.add(1, 'minutes') - } - // This property is used to determine which side the record should be rounded. It can be top, bottom, both or none // We use the start and end date to determine the position of the record let position = 'none' @@ -330,41 +306,12 @@ const recordsAcrossAllRange = computed<{ } else if (fromCol) { const { startDate, endDate } = calculateNewDates({ startDate: dayjs(record.row[fromCol.title!]), - endDate: dayjs(record.row[fromCol.title!]).add(1, 'hour'), + endDate: dayjs(record.row[fromCol.title!]).add(1, 'hour').subtract(1, 'minute'), scheduleStart, scheduleEnd, }) - const startHour = startDate.hour() - let style: Partial = {} - let _startDate = startDate.clone() - - // We loop through every minute between the start and end date and keep track of the number of records that overlap at a given time - while (_startDate.isBefore(endDate, 'minute')) { - const timeKey = _startDate.format('HH:mm') - - if (!overlaps[timeKey]) { - overlaps[timeKey] = { - id: [], - overflow: false, - overflowCount: 0, - } - } - overlaps[timeKey].id.push(id) - - // If the number of records exceeds 8, we hide the record and mark it as overflow - if (overlaps[timeKey].id.length > 8) { - overlaps[timeKey].overflow = true - overlaps[timeKey].overflowCount += 1 - style = { - ...style, - display: 'none', - } - } - - _startDate = _startDate.add(1, 'minute') - } // The top of the record is calculated based on the start hour // Update such that it is also based on Minutes @@ -392,41 +339,15 @@ const recordsAcrossAllRange = computed<{ } } }) - /* + recordsByRange.sort((a, b) => { const fromColA = a.rowMeta.range?.fk_from_col const fromColB = b.rowMeta.range?.fk_from_col if (!fromColA || !fromColB) return 0 return dayjs(a.row[fromColA.title!]).isBefore(dayjs(b.row[fromColB.title!])) ? -1 : 1 - }) */ - - // We can't calculate the width & left of the records without knowing the number of records that overlap at a given time - // So we loop through the records again and calculate the width & left of the records based on the number of records that overlap at a given time - recordsByRange = recordsByRange.map((record) => { - // MaxOverlaps is the number of records that overlap at a given time - // overlapIndex is the index of the record in the list of records that overlap at a given time - let maxOverlaps = 1 - let overlapIndex = 0 - for (const minutes in overlaps) { - if (overlaps[minutes].id.includes(record.rowMeta.id!)) { - maxOverlaps = Math.max(maxOverlaps, overlaps[minutes].id.length - overlaps[minutes].overflowCount) - overlapIndex = Math.max(overlaps[minutes].id.indexOf(record.rowMeta.id!), overlapIndex) - } - } - const spacing = 0.25 - const widthPerRecord = (100 - spacing * (maxOverlaps - 1)) / maxOverlaps - const leftPerRecord = (widthPerRecord + spacing) * overlapIndex - record.rowMeta.style = { - ...record.rowMeta.style, - left: `${leftPerRecord - 0.08}%`, - width: `calc(${widthPerRecord}%)`, - } - return record }) - // TODO: Rewrite the calculations for the style of the records - - /* for (const record of recordsByRange) { + for (const record of recordsByRange) { const fromCol = record.rowMeta.range?.fk_from_col const toCol = record.rowMeta.range?.fk_to_col @@ -434,7 +355,7 @@ const recordsAcrossAllRange = computed<{ const { startDate, endDate } = calculateNewDates({ startDate: dayjs(record.row[fromCol.title!]), - endDate: toCol ? dayjs(record.row[toCol.title!]) : dayjs(record.row[fromCol.title!]).add(1, 'hour'), + endDate: toCol ? dayjs(record.row[toCol.title!]) : dayjs(record.row[fromCol.title!]).add(1, 'hour').subtract(1, 'minute'), scheduleStart, scheduleEnd, }) @@ -442,10 +363,16 @@ const recordsAcrossAllRange = computed<{ const gridTimes = getGridTimeSlots(startDate, endDate) for (let gridCounter = gridTimes.from; gridCounter <= gridTimes.to; gridCounter++) { - if (gridTimeMap.has(gridCounter)) { - gridTimeMap.set(gridCounter, gridTimeMap.get(gridCounter) + 1) + if (!gridTimeMap.has(gridCounter)) { + gridTimeMap.set(gridCounter, { + count: 1, + id: [record.rowMeta.id!], + }) } else { - gridTimeMap.set(gridCounter, 1) + gridTimeMap.set(gridCounter, { + count: gridTimeMap.get(gridCounter)!.count + 1, + id: [...gridTimeMap.get(gridCounter)!.id, record.rowMeta.id!], + }) } } @@ -453,7 +380,7 @@ const recordsAcrossAllRange = computed<{ for (const column in columnArray) { if ( - hasSlotForRecord(record, columnArray[column], { + hasSlotForRecord(columnArray[column], { fromDate: startDate, toDate: endDate, }) @@ -468,47 +395,62 @@ const recordsAcrossAllRange = computed<{ columnArray.push([record]) } } - for (const columnIndex in columnArray) { + for (const columnIndex in columnArray) { for (const record of columnArray[columnIndex]) { - const recordRange = record.rowMeta.range - const fromCol = recordRange?.fk_from_col - const toCol = recordRange?.fk_to_col - - if (!fromCol) continue - - const { startDate, endDate } = calculateNewDates({ - startDate: dayjs(record.row[fromCol.title!]), - endDate: toCol ? dayjs(record.row[toCol.title!]) : dayjs(record.row[fromCol.title!]).add(1, 'hour'), - scheduleStart: dayjs(selectedDate.value).startOf('day'), - scheduleEnd: dayjs(selectedDate.value).endOf('day'), - }) - - record.rowMeta.numberOfOverlaps = - getMaxOfGrid( - { - fromDate: startDate, - toDate: endDate, - }, - gridTimeMap, - ) - 1 record.rowMeta.overLapIteration = parseInt(columnIndex) + 1 } } for (const record of recordsByRange) { - record.rowMeta.numberOfOverlaps = getMaxOverlaps({ + const numberOfOverlaps = getMaxOverlaps({ row: record, - rowArray: recordsByRange, + gridTimeMap, + columnArray, }) - const width = 100 / columnArray.length - const left = width * (record.rowMeta.overLapIteration - 1) + record.rowMeta.numberOfOverlaps = numberOfOverlaps + + let width + let left = 100 + let display = 'block' + + if (numberOfOverlaps && numberOfOverlaps > 0) { + width = 100 / Math.min(numberOfOverlaps, 8) + + if (record.rowMeta.overLapIteration! - 1 > 7) { + display = 'none' + gridTimeMap.forEach((value, key) => { + if (value.id.includes(record.rowMeta.id!)) { + if (!overlaps[key]) { + overlaps[key] = { + id: value.id, + overflow: true, + overflowCount: value.id.length, + } + } else { + overlaps[key].overflow = true + value.id.forEach((id) => { + if (!overlaps[key].id.includes(id)) { + overlaps[key].id.push(id) + } + }) + } + } + }) + } else { + left = width * (record.rowMeta.overLapIteration! - 1) + } + } else { + width = 100 + left = 0 + } record.rowMeta.style = { ...record.rowMeta.style, + display, width: `${width.toFixed(2)}%`, - left: `${left}%`, + left: `${left.toFixed(2)}%`, } - } */ + } return { count: overlaps, @@ -802,7 +744,7 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => { let overflowCount = 0 while (startOfHour.isBefore(endOfHour, 'minute')) { - const hourKey = startOfHour.format('HH:mm') + const hourKey = startOfHour.hour() * 60 + startOfHour.minute() if (recordsAcrossAllRange.value?.count?.[hourKey]?.overflow) { isOverflow = true