Browse Source

fix(nc-gui): refactor calendar views

pull/7611/head
DarkPhoenix2704 8 months ago
parent
commit
affe4c4897
  1. 18
      packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue
  2. 304
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  3. 251
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  4. 39
      packages/nc-gui/components/smartsheet/calendar/RecordCard.vue
  5. 7
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  6. 326
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  7. 297
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  8. 30
      packages/nc-gui/components/smartsheet/calendar/index.vue
  9. 10
      packages/nc-gui/components/smartsheet/toolbar/CalendarRange.vue
  10. 3
      packages/nc-gui/lang/en.json
  11. 8
      packages/nc-gui/lib/types.ts
  12. 4
      packages/nc-gui/utils/viewUtils.ts
  13. 2
      packages/nocodb/src/services/calendars.service.ts

18
packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue

@ -15,6 +15,8 @@ const { isUIAllowed } = useRoles()
const { selectedDate, formattedData, formattedSideBarData, calendarRange, updateRowProperty, displayField } =
useCalendarViewStoreOrThrow()
// We loop through all the records and calculate the position of each record based on the range
// We only need to calculate the top, of the record since there is no overlap in the day view of date Field
const recordsAcrossAllRange = computed<Row[]>(() => {
let dayRecordCount = 0
const perRecordHeight = 40
@ -38,6 +40,7 @@ const recordsAcrossAllRange = computed<Row[]>(() => {
width: '100%',
}
// This property is used to determine which side the record should be rounded. It can be left, right, both or none
let position = 'none'
const isSelectedDay = (date: dayjs.Dayjs) => date.isSame(selectedDate.value, 'day')
const isBeforeSelectedDay = (date: dayjs.Dayjs) => date.isBefore(selectedDate.value, 'day')
@ -91,6 +94,7 @@ const dragElement = ref<HTMLElement | null>(null)
const hoverRecord = ref<string | null>(null)
// We support drag and drop from the sidebar to the day view of the date field
const dropEvent = (event: DragEvent) => {
if (!isUIAllowed('dataEdit')) return
event.preventDefault()
@ -142,28 +146,22 @@ const dropEvent = (event: DragEvent) => {
if (!newRow) return
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
return pk === newPk ? newRow : r
})
} else {
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk !== extractPkFromRow(newRow.row, meta.value!.columns!)
return extractPkFromRow(r.row, meta.value!.columns!) !== newPk
})
}
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)

304
packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue

@ -21,6 +21,7 @@ const {
const container = ref<null | HTMLElement>(null)
const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref())
const hours = computed(() => {
@ -48,6 +49,11 @@ const recordsAcrossAllRange = computed<{
const scheduleStart = dayjs(selectedDate.value).startOf('day')
const scheduleEnd = dayjs(selectedDate.value).endOf('day')
// We use this object to keep track of the number of records that overlap at a given time, and if the number of records exceeds 4, we hide the record
// and show a button to view more records
// The key is the time in HH:mm format
// id is the id of the record generated below
const overlaps: {
[key: string]: {
id: string[]
@ -64,6 +70,8 @@ const recordsAcrossAllRange = computed<{
const fromCol = range.fk_from_col
const endCol = range.fk_to_col
// We fetch all the records that match the calendar ranges in a single time.
// But not all fetched records are valid for the certain range, so we filter them out & sort them
const sortedFormattedData = [...formattedData.value].filter((record) => {
const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null
@ -78,27 +86,39 @@ const recordsAcrossAllRange = computed<{
return false
})
// If there is a start and end column, we calculate the top and height of the record based on the start and end date
if (fromCol && endCol) {
for (const record of sortedFormattedData) {
// We use this id during the drag and drop operation and to keep track of the number of records that overlap at a given time
const id = generateRandomNumber()
let startDate = dayjs(record.row[fromCol.title!])
let endDate = dayjs(record.row[endCol.title!])
// If no start date is provided or startDate is after the endDate, we skip the record
if (!startDate.isValid() || startDate.isAfter(endDate)) continue
// If there is no end date, we add 30 minutes to the start date and use that as the end date
if (!endDate.isValid()) {
endDate = startDate.clone().add(30, 'minutes')
}
// If the start date is before the opened date, we use the schedule start as the start date
// This is to ensure the generated style of the record is not outside the bounds of the calendar
if (startDate.isBefore(scheduleStart, 'minutes')) {
startDate = scheduleStart
}
// If the end date is after the schedule end, we use the schedule end as the end date
// This is to ensure the generated style of the record is not outside the bounds of the calendar
if (endDate.isAfter(scheduleEnd, 'minutes')) {
endDate = scheduleEnd
}
// The top of the record is calculated based on the start hour and minute
const topInPixels = (startDate.hour() + startDate.minute() / 60) * 80
// A minimum height of 80px is set for each record
// 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') / 60) * 80, perRecordHeight)
const startHour = startDate.hour()
@ -110,6 +130,8 @@ const recordsAcrossAllRange = computed<{
top: `${topInPixels + 5 + startHour * 2}px`,
}
// We loop through every 15 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]) {
@ -121,6 +143,7 @@ const recordsAcrossAllRange = computed<{
}
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'
@ -129,6 +152,8 @@ const recordsAcrossAllRange = computed<{
_startDate = _startDate.add(15, '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'
const isSelectedDay = (date: dayjs.Dayjs) => date.isSame(selectedDate.value, 'day')
const isBeforeSelectedDay = (date: dayjs.Dayjs) => date.isBefore(selectedDate.value, 'day')
@ -139,9 +164,9 @@ const recordsAcrossAllRange = computed<{
} else if (isBeforeSelectedDay(startDate) && isAfterSelectedDay(endDate)) {
position = 'none'
} else if (isSelectedDay(startDate) && isAfterSelectedDay(endDate)) {
position = 'leftRounded'
position = 'topRounded'
} else if (isBeforeSelectedDay(startDate) && isSelectedDay(endDate)) {
position = 'rightRounded'
position = 'bottomRounded'
} else {
position = 'none'
}
@ -169,6 +194,7 @@ const recordsAcrossAllRange = computed<{
let style: Partial<CSSStyleDeclaration> = {}
let _startDate = startDate.clone()
// We loop through every 15 minutes between the start and end date and keep track of the number of records that overlap at a given time
while (_startDate.isBefore(endDate)) {
const timeKey = _startDate.startOf('hour').format('HH:mm')
@ -181,6 +207,7 @@ const recordsAcrossAllRange = computed<{
}
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
@ -193,6 +220,8 @@ const recordsAcrossAllRange = computed<{
}
const topInPixels = (startDate.hour() + startDate.startOf('hour').minute() / 60) * 80
// A minimum height of 80px is set for each record
const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 80, perRecordHeight)
const finalTopInPixels = topInPixels + startHour * 2
@ -217,7 +246,11 @@ const recordsAcrossAllRange = computed<{
}
})
// 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) {
@ -229,6 +262,7 @@ const recordsAcrossAllRange = computed<{
const spacing = 0.25
const widthPerRecord = (100 - spacing * (maxOverlaps - 1)) / maxOverlaps
const leftPerRecord = (widthPerRecord + spacing) * overlapIndex
record.rowMeta.style = {
@ -249,14 +283,10 @@ const dragRecord = ref<Row | null>(null)
const isDragging = ref(false)
const draggingId = ref<string | null>(null)
const dragElement = ref<HTMLElement | null>(null)
const resizeDirection = ref<'right' | 'left' | null>()
const resizeInProgress = ref(false)
const resizeRecord = ref<Row | null>()
const dragTimeout = ref<ReturnType<typeof setTimeout>>()
@ -267,6 +297,86 @@ const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[],
updateRowProperty(row, updateProperty, isDelete)
}, 500)
// When the user is dragging a record, we calculate the new start and end date based on the mouse position
const calculateNewRow = (event: MouseEvent) => {
const { top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
// We calculate the percentage of the mouse position in the scroll container
const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight
const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
const toCol = dragRecord.value.rowMeta.range?.fk_to_col
// We calculate the hour based on the percentage of the mouse position in the scroll container
// It can be between 0 and 23 (inclusive)
const hour = Math.max(Math.floor(percentY * 23), 0)
// We calculate the new startDate by adding the hour to the start of the selected date
const newStartDate = dayjs(selectedDate.value).startOf('day').add(hour, 'hour')
if (!newStartDate || !fromCol) return { newRow: null, updateProperty: [] }
let endDate
const newRow = {
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
const updateProperty = [fromCol.title!]
if (toCol) {
const fromDate = dragRecord.value.row[fromCol.title!] ? dayjs(dragRecord.value.row[fromCol.title!]) : null
const toDate = dragRecord.value.row[toCol.title!] ? dayjs(dragRecord.value.row[toCol.title!]) : null
// If there is an end date, we calculate the new end date based on the new start date and add the difference between the start and end date to the new start date
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'hour'), 'hour')
} else if (fromDate && !toDate) {
// If there is no end date, we set the end date to the end of the day
endDate = dayjs(newStartDate).endOf('hour')
} else if (!fromDate && toDate) {
// If there is no start date, we set the end date to the end of the day
endDate = dayjs(newStartDate).endOf('hour')
} else {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
updateProperty.push(toCol.title!)
}
if (!newRow) {
return { newRow: null, updateProperty: [] }
}
// We use the primary key of the new row to find the old row in the formattedData array
// If the old row is found, we replace it with the new row in the formattedData array
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === newPk) {
return newRow
}
return r
})
} else {
// If the old row is not found, we add the new row to the formattedData array and remove the old row from the formattedSideBarData array
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk !== newPk
})
}
return { newRow, updateProperty }
}
const onResize = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return
if (!container.value || !resizeRecord.value) return
@ -274,6 +384,7 @@ const onResize = (event: MouseEvent) => {
const { scrollHeight } = container.value
// If the mouse position is near the top or bottom of the scroll container, we scroll the container
if (event.clientY > bottom - 20) {
container.value.scrollTop += 10
} else if (event.clientY < top + 20) {
@ -289,66 +400,65 @@ const onResize = (event: MouseEvent) => {
const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
const ogStartDate = dayjs(resizeRecord.value.row[fromCol.title!])
const hour = Math.max(Math.min(Math.floor(percentY * 24), 23), 0)
const hour = Math.max(Math.floor(percentY * 23), 0)
let newRow
let updateProperty: string[] = []
if (resizeDirection.value === 'right') {
// If the user is resizing the record to the right, we calculate the new end date based on the mouse position
let newEndDate = dayjs(selectedDate.value).add(hour, 'hour')
const updateProperty = [toCol.title!]
updateProperty = [toCol.title!]
// If the new end date is before the start date, we set the new end date to the start date
// This is to ensure the end date is always same or after the start date
if (dayjs(newEndDate).isBefore(ogStartDate, 'day')) {
newEndDate = ogStartDate.clone()
}
if (!newEndDate.isValid()) return
const newRow = {
newRow = {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[toCol.title!]: newEndDate.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
useDebouncedRowUpdate(newRow, updateProperty, false)
} else if (resizeDirection.value === 'left') {
let newStartDate = dayjs(selectedDate.value).add(hour, 'hour')
const updateProperty = [fromCol.title!]
updateProperty = [fromCol.title!]
// If the new start date is after the end date, we set the new start date to the end date
// This is to ensure the start date is always before or same the end date
if (dayjs(newStartDate).isAfter(ogEndDate)) {
newStartDate = dayjs(dayjs(ogEndDate)).clone()
}
if (!newStartDate) return
const newRow = {
newRow = {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
useDebouncedRowUpdate(newRow, updateProperty, false)
}
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === newPk) {
return newRow
}
return r
})
useDebouncedRowUpdate(newRow, updateProperty, false)
}
const onResizeEnd = () => {
resizeInProgress.value = false
resizeDirection.value = null
resizeRecord.value = null
document.removeEventListener('mousemove', onResize)
@ -357,7 +467,6 @@ const onResizeEnd = () => {
const onResizeStart = (direction: 'right' | 'left', _event: MouseEvent, record: Row) => {
if (!isUIAllowed('dataEdit')) return
resizeInProgress.value = true
resizeDirection.value = direction
resizeRecord.value = record
document.addEventListener('mousemove', onResize)
@ -375,129 +484,16 @@ const onDrag = (event: MouseEvent) => {
} else if (event.clientY < top + 20) {
container.value.scrollTop -= 10
}
const { scrollHeight } = container.value
const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight
const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
const toCol = dragRecord.value.rowMeta.range?.fk_to_col
if (!fromCol) return
const hour = Math.max(Math.min(Math.floor(percentY * 24), 23), 0)
const newStartDate = dayjs(selectedDate.value).startOf('day').add(hour, 'hour')
if (!newStartDate) return
let endDate
const newRow = {
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
if (toCol) {
const fromDate = dragRecord.value.row[fromCol.title!] ? dayjs(dragRecord.value.row[fromCol.title!]) : null
const toDate = dragRecord.value.row[toCol.title!] ? dayjs(dragRecord.value.row[toCol.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'hour'), 'hour')
} else if (fromDate && !toDate) {
endDate = dayjs(newStartDate).endOf('hour')
} else if (!fromDate && toDate) {
endDate = dayjs(newStartDate).endOf('hour')
} else {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
calculateNewRow(event)
}
const stopDrag = (event: MouseEvent) => {
event.preventDefault()
clearTimeout(dragTimeout.value!)
if (!isUIAllowed('dataEdit')) return
if (!isDragging.value || !container.value || !dragRecord.value) return
const { top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight
const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
const toCol = dragRecord.value.rowMeta.range?.fk_to_col
const hour = Math.max(Math.min(Math.floor(percentY * 24), 23), 0)
if (!isUIAllowed('dataEdit') || !isDragging.value || !container.value || !dragRecord.value) return
const newStartDate = dayjs(selectedDate.value).startOf('day').add(hour, 'hour')
if (!newStartDate || !fromCol) return
let endDate
const newRow = {
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
const updateProperty = [fromCol.title!]
if (toCol) {
const fromDate = dragRecord.value.row[fromCol.title!] ? dayjs(dragRecord.value.row[fromCol.title!]) : null
const toDate = dragRecord.value.row[toCol.title!] ? dayjs(dragRecord.value.row[toCol.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'hour'), 'hour')
} else if (fromDate && !toDate) {
endDate = dayjs(newStartDate).endOf('hour')
} else if (!fromDate && toDate) {
endDate = dayjs(newStartDate).endOf('hour')
} else {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
updateProperty.push(toCol.title!)
}
if (!newRow) return
if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
} else {
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk !== extractPkFromRow(newRow.row, meta.value!.columns!)
})
}
const { newRow, updateProperty } = calculateNewRow(event)
if (!newRow && !updateProperty) return
const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => {
@ -507,9 +503,6 @@ const stopDrag = (event: MouseEvent) => {
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
// isDragging.value = false
draggingId.value = null
dragElement.value = null
}
@ -525,12 +518,14 @@ const dragStart = (event: MouseEvent, record: Row) => {
isDragging.value = false
// We use a timeout to determine if the user is dragging or clicking on the record
dragTimeout.value = setTimeout(() => {
isDragging.value = true
while (!target.classList.contains('draggable-record')) {
target = target.parentElement as HTMLElement
}
// When the user starts dragging a record, we reduce opacity of all other records
const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => {
if (!el.getAttribute('data-unique-id').includes(record.rowMeta.id!)) {
@ -541,9 +536,7 @@ const dragStart = (event: MouseEvent, record: Row) => {
dragRecord.value = record
isDragging.value = true
dragElement.value = target
draggingId.value = record.rowMeta.id!
dragRecord.value = record
document.addEventListener('mousemove', onDrag)
@ -566,7 +559,6 @@ const viewMore = (hour: dayjs.Dayjs) => {
selectedTime.value = hour
showSideMenu.value = true
}
</script>
<template>

251
packages/nc-gui/components/smartsheet/calendar/MonthView.vue

@ -98,6 +98,8 @@ const recordsToDisplay = computed<{
const spaceBetweenRecords = 35
// This object is used to keep track of the number of records in a day
// The key is the date in the format YYYY-MM-DD
const recordsInDay: {
[key: string]: {
overflow: boolean
@ -113,6 +115,7 @@ const recordsToDisplay = computed<{
const startCol = range.fk_from_col
const endCol = range.fk_to_col
// Filter out records that don't satisfy the range and sort them by start date
const sortedFormattedData = [...formattedData.value].filter((record) => {
const fromDate = record.row[startCol!.title!] ? dayjs(record.row[startCol!.title!]) : null
@ -128,6 +131,7 @@ const recordsToDisplay = computed<{
sortedFormattedData.forEach((record: Row) => {
if (!endCol && startCol) {
// If there is no end date, we just display the record on the start date
const startDate = dayjs(record.row[startCol!.title!])
const dateKey = startDate.format('YYYY-MM-DD')
@ -137,8 +141,10 @@ const recordsToDisplay = computed<{
recordsInDay[dateKey].count++
const id = record.rowMeta.id ?? generateRandomNumber()
// Find the index of the week from the dates array
const weekIndex = dates.value.findIndex((week) => week.some((day) => dayjs(day).isSame(startDate, 'day')))
// Find the index of the day from the dates array
const dayIndex = (dates.value[weekIndex] ?? []).findIndex((day) => {
return dayjs(day).isSame(startDate, 'day')
})
@ -148,9 +154,14 @@ const recordsToDisplay = computed<{
width: `${perWidth}px`,
}
// Number of records in that day
const recordIndex = recordsInDay[dateKey].count
// The top is calculated from the week index and the record index
// If the record in 1st week and no record in that date them the top will be 0
const top = weekIndex * perHeight + spaceBetweenRecords + (recordIndex - 1) * perRecordHeight
// The 25 is obtained from the trial and error
const heightRequired = perRecordHeight * recordIndex + spaceBetweenRecords + 25
if (heightRequired > perHeight) {
@ -172,19 +183,27 @@ const recordsToDisplay = computed<{
},
})
} else if (startCol && endCol) {
// If the range specifies fromCol and endCol
const startDate = dayjs(record.row[startCol!.title!])
const endDate = dayjs(record.row[endCol!.title!])
let currentWeekStart = startDate.startOf('week')
const id = record.rowMeta.id ?? generateRandomNumber()
// Since the records can span multiple weeks, to display, we render multiple records
// for each week the record spans. The id is used to identify the elements that belong to the same record
while (
currentWeekStart.isSameOrBefore(endDate, 'day') &&
// If the current week start is before the last day of the last week
currentWeekStart.isBefore(dates.value[dates.value.length - 1][6])
) {
// We update the record start to currentWeekStart if it is before the start date
// and record end to currentWeekEnd if it is after the end date
const currentWeekEnd = currentWeekStart.endOf('week')
const recordStart = currentWeekStart.isBefore(startDate) ? startDate : currentWeekStart
const recordEnd = currentWeekEnd.isAfter(endDate) ? endDate : currentWeekEnd
// Update the recordsInDay object to keep track of the number of records in a day
let day = recordStart.clone()
while (day.isSameOrBefore(recordEnd)) {
const dateKey = day.format('YYYY-MM-DD')
@ -196,6 +215,7 @@ const recordsToDisplay = computed<{
day = day.add(1, 'day')
}
// Find the index of the week from the dates array
const weekIndex = Math.max(
dates.value.findIndex((week) => {
return (
@ -209,6 +229,7 @@ const recordsToDisplay = computed<{
let maxRecordCount = 0
// Find the maximum number of records in a day in that week
for (let i = 0; i < (dates.value[weekIndex] ?? []).length; i++) {
const day = dates.value[weekIndex][i]
@ -221,7 +242,6 @@ const recordsToDisplay = computed<{
}
}
const recordIndex = recordsInDay[dateKey].count
maxRecordCount = Math.max(maxRecordCount, recordIndex)
}
@ -234,15 +254,20 @@ const recordsToDisplay = computed<{
0,
)
// The left and width of the record is calculated based on the start and end day index
const style: Partial<CSSStyleDeclaration> = {
left: `${startDayIndex * perWidth}px`,
width: `${(endDayIndex - startDayIndex + 1) * perWidth}px`,
}
// The top is calculated from the week index and the record index
// If the record in 1st week and no record in that date them the top will be 0
// If the record in 1st week and 1 record in that date then the top will be perRecordHeight + spaceBetweenRecords
const top = weekIndex * perHeight + spaceBetweenRecords + Math.max(maxRecordCount - 1, 0) * perRecordHeight
const heightRequired = perRecordHeight * maxRecordCount + spaceBetweenRecords
let position = 'rounded'
// Here we are checking if the startDay is before all the dates shown in UI rather that the current month
const isStartMonthBeforeCurrentWeek = dates.value[weekIndex - 1]
? dayjs(dates.value[weekIndex - 1][0]).isBefore(startDate, 'month')
: false
@ -261,6 +286,8 @@ const recordsToDisplay = computed<{
position = 'none'
}
// If the height required is more than the height of the week, we hide the record
// and update the recordsInDay object for all the spanned days
if (heightRequired + 15 > perHeight) {
style.display = 'none'
for (let i = startDayIndex; i <= endDayIndex; i++) {
@ -298,8 +325,7 @@ const recordsToDisplay = computed<{
}
})
const onDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit') || !dragRecord.value) return
const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
const { top, height, width, left } = calendarGridContainer.value.getBoundingClientRect()
const percentY = (event.clientY - top - window.scrollY) / height
@ -311,11 +337,7 @@ const onDrag = (event: MouseEvent) => {
const week = Math.floor(percentY * dates.value.length)
const day = Math.floor(percentX * 7)
focusedDate.value = dates.value[week] ? dates.value[week][day] : null
selectedDate.value = null
const newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
if (!newStartDate) return
let endDate
@ -323,14 +345,16 @@ const onDrag = (event: MouseEvent) => {
const newRow = {
...dragRecord.value,
row: {
...dragRecord.value!.row,
...dragRecord.value.row,
[fromCol!.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
const updateProperty = [fromCol!.title!]
if (toCol) {
const fromDate = dragRecord.value!.row[fromCol!.title!] ? dayjs(dragRecord!.value!.row[fromCol!.title!]) : null
const toDate = dragRecord.value!.row[toCol!.title!] ? dayjs(dragRecord.value!.row[toCol!.title!]) : null
const fromDate = dragRecord.value.row[fromCol!.title!] ? dayjs(dragRecord.value.row[fromCol!.title!]) : null
const toDate = dragRecord.value.row[toCol!.title!] ? dayjs(dragRecord.value.row[toCol!.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
@ -342,17 +366,37 @@ const onDrag = (event: MouseEvent) => {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
dragRecord.value = undefined
newRow.row[toCol!.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
updateProperty.push(toCol!.title!)
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (!newRow) return
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
if (updateSideBar) {
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk !== newPk
})
} else {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk === newPk ? newRow : r
})
}
return {
newRow,
updateProperty,
}
}
const onDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit') || !dragRecord.value) return
calculateNewRow(event, false)
}
const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[], isDelete: boolean) => {
@ -376,10 +420,12 @@ const onResize = (event: MouseEvent) => {
const week = Math.floor(percentY * dates.value.length)
const day = Math.floor(percentX * 7)
let updateProperty: string[] = []
let newRow: Row
if (resizeDirection.value === 'right') {
let newEndDate = dates.value[week] ? dayjs(dates.value[week][day]).endOf('day') : null
const updateProperty = [toCol!.title!]
updateProperty = [toCol!.title!]
if (dayjs(newEndDate).isBefore(ogStartDate)) {
newEndDate = dayjs(ogStartDate).clone().endOf('day')
@ -387,51 +433,39 @@ const onResize = (event: MouseEvent) => {
if (!newEndDate) return
const newRow = {
newRow = {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[toCol!.title!]: dayjs(newEndDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
useDebouncedRowUpdate(newRow, updateProperty, false)
} else if (resizeDirection.value === 'left') {
let newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
const updateProperty = [fromCol!.title!]
updateProperty = [fromCol!.title!]
if (dayjs(newStartDate).isAfter(ogEndDate)) {
newStartDate = dayjs(ogEndDate).clone()
}
if (!newStartDate) return
const newRow = {
newRow = {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[fromCol!.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
return pk === newPk ? newRow : r
})
useDebouncedRowUpdate(newRow, updateProperty, false)
}
useDebouncedRowUpdate(newRow, updateProperty, false)
}
const onResizeEnd = () => {
@ -463,71 +497,7 @@ const stopDrag = (event: MouseEvent) => {
dragElement.value!.style.boxShadow = 'none'
const { top, height, width, left } = calendarGridContainer.value.getBoundingClientRect()
const percentY = (event.clientY - top - window.scrollY) / height
const percentX = (event.clientX - left - window.scrollX) / width
const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
const toCol = dragRecord.value.rowMeta.range?.fk_to_col
const week = Math.floor(percentY * dates.value.length)
const day = Math.floor(percentX * 7)
const newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
if (!newStartDate) return
let endDate
const newRow = {
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol!.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
const updateProperty = [fromCol!.title!]
if (toCol) {
const fromDate = dragRecord.value.row[fromCol!.title!] ? dayjs(dragRecord.value.row[fromCol!.title!]) : null
const toDate = dragRecord.value.row[toCol!.title!] ? dayjs(dragRecord.value.row[toCol!.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
} else if (fromDate && !toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else if (!fromDate && toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else {
endDate = newStartDate.clone()
}
dragRecord.value = undefined
newRow.row[toCol!.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
updateProperty.push(toCol!.title!)
}
if (!newRow) return
if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
} else {
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk !== extractPkFromRow(newRow.row, meta.value!.columns!)
})
}
const { newRow, updateProperty } = calculateNewRow(event, false)
const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => {
@ -537,7 +507,6 @@ const stopDrag = (event: MouseEvent) => {
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
isDragging.value = false
draggingId.value = null
dragElement.value = null
@ -603,75 +572,13 @@ const dropEvent = (event: DragEvent) => {
}: {
record: Row
} = JSON.parse(data)
const { width, left, top, height } = calendarGridContainer.value.getBoundingClientRect()
const percentY = (event.clientY - top - window.scrollY) / height
const percentX = (event.clientX - left - window.scrollX) / width
const fromCol = record.rowMeta.range?.fk_from_col
const toCol = record.rowMeta.range?.fk_to_col
if (!fromCol) return
const week = Math.floor(percentY * dates.value.length)
const day = Math.floor(percentX * 7)
const newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
if (!newStartDate) return
let endDate
const newRow = {
...record,
row: {
...record.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
const updateProperty = [fromCol.title!]
if (toCol) {
const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
const toDate = record.row[toCol.title!] ? dayjs(record.row[toCol.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
} else if (fromDate && !toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else if (!fromDate && toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
updateProperty.push(toCol.title!)
}
if (!newRow) return
if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
} else {
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
dragRecord.value = record
return pk !== extractPkFromRow(newRow.row, meta.value!.columns!)
})
}
const { newRow, updateProperty } = calculateNewRow(event, true)
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)

39
packages/nc-gui/components/smartsheet/calendar/RecordCard.vue

@ -21,7 +21,6 @@ withDefaults(defineProps<Props>(), {
})
const emit = defineEmits(['resize-start'])
</script>
<template>
@ -42,7 +41,7 @@ const emit = defineEmits(['resize-start'])
'group-hover:(border-brand-500 border-1)': resize,
'!border-brand-500 border-1': selected || hover,
}"
class="relative flex items-center px-1 group border-1 border-transparent"
class="relative transition-all flex items-center px-1 group border-1 border-transparent"
>
<div
v-if="position === 'leftRounded' || position === 'rounded'"
@ -57,14 +56,16 @@ const emit = defineEmits(['resize-start'])
class="block h-full min-h-5 w-1 rounded"
></div>
<div
v-if="(position === 'leftRounded' || position === 'rounded') && resize"
:class="{
'!block !border-1 !rounded-lg !border-brand-500': selected || hover,
}"
class="mt-0.1 h-7.1 absolute hidden -left-4 resize"
>
<NcButton size="xsmall" type="secondary" @mousedown.stop="emit('resize-start', 'left', $event, record)">
<div v-if="(position === 'leftRounded' || position === 'rounded') && resize" class="mt-0.1 h-7.1 absolute -left-4 resize">
<NcButton
:class="{
'!block z-1 !border-brand-500': selected || hover,
'!hidden': !selected && !hover,
}"
size="xsmall"
type="secondary"
@mousedown.stop="emit('resize-start', 'left', $event, record)"
>
<component :is="iconMap.drag" class="text-gray-400"></component>
</NcButton>
</div>
@ -77,14 +78,16 @@ const emit = defineEmits(['resize-start'])
<span v-if="position === 'leftRounded' || position === 'none'" class="absolute my-0 right-5"> .... </span>
</div>
<div
v-if="(position === 'rightRounded' || position === 'rounded') && resize"
:class="{
'!block border-1 rounded-lg border-brand-500': selected || hover,
}"
class="absolute mt-0.1 hidden h-7.1 -right-4 border-1 resize !group-hover:(border-brand-500 border-1 block rounded-lg)"
>
<NcButton size="xsmall" type="secondary" @mousedown.stop="emit('resize-start', 'right', $event, record)">
<div v-if="(position === 'rightRounded' || position === 'rounded') && resize" class="absolute mt-0.1 z-1 -right-4 resize">
<NcButton
:class="{
'!block !border-brand-500': selected || hover,
'!hidden': !selected && !hover,
}"
size="xsmall"
type="secondary"
@mousedown.stop="emit('resize-start', 'right', $event, record)"
>
<component :is="iconMap.drag" class="text-gray-400"></component>
</NcButton>
</div>

7
packages/nc-gui/components/smartsheet/calendar/SideMenu.vue

@ -8,7 +8,7 @@ const props = defineProps<{
visible: boolean
}>()
const emit = defineEmits(['expand-record', 'new-record'])
const emit = defineEmits(['expand-record', 'newRecord'])
const INFINITY_SCROLL_THRESHOLD = 100
@ -55,7 +55,6 @@ const dragElement = ref<HTMLElement | null>(null)
const dragStart = (event: DragEvent, record: Row) => {
dragElement.value = event.target as HTMLElement
dragElement.value.classList.add('hide')
dragElement.value.style.boxShadow = '0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.10)'
const eventRect = dragElement.value.getBoundingClientRect()
@ -267,7 +266,7 @@ const newRecord = () => {
row[calendarRange.value[0]!.fk_from_col!.title!] = selectedDate.value.format('YYYY-MM-DD HH:mm:ssZ')
}
emit('new-record', { row, oldRow: {}, rowMeta: { new: true } })
emit('newRecord', { row, oldRow: {}, rowMeta: { new: true } })
}
const height = ref(0)
@ -390,7 +389,7 @@ onUnmounted(() => {
<template v-else-if="renderData.length > 0">
<LazySmartsheetRow v-for="(record, rowIndex) in renderData" :key="rowIndex" :row="record">
<LazySmartsheetCalendarSideRecordCard
:draggable="sideBarFilterOption === 'withoutDates'"
:draggable="sideBarFilterOption === 'withoutDates' && activeCalendarView !== 'year'"
:from-date="
record.rowMeta.range?.fk_from_col
? calDataType === UITypes.Date

326
packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue

@ -18,6 +18,7 @@ const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref())
// Calculate the dates of the week
const weekDates = computed(() => {
const startOfWeek = new Date(selectedDateRange.value.start!)
const endOfWeek = new Date(selectedDateRange.value.end!)
@ -29,30 +30,40 @@ const weekDates = computed(() => {
return datesArray
})
const findFirstSuitableColumn = (recordsInDay: any, startDayIndex: number, spanDays: number) => {
let column = 0
// This function is used to find the first suitable row for a record
// It takes the recordsInDay object, the start day index and the span of the record in days
// It returns the first suitable row for the entire span of the record
const findFirstSuitableRow = (recordsInDay: any, startDayIndex: number, spanDays: number) => {
let row = 0
while (true) {
let isColumnSuitable = true
let isRowSuitable = true
// Check if the row is suitable for the entire span
for (let i = 0; i < spanDays; i++) {
const dayIndex = startDayIndex + i
if (!recordsInDay[dayIndex]) {
recordsInDay[dayIndex] = {}
}
if (recordsInDay[dayIndex][column]) {
isColumnSuitable = false
// If the row is occupied, the entire span is not suitable
if (recordsInDay[dayIndex][row]) {
isRowSuitable = false
break
}
}
if (isColumnSuitable) {
return column
// If the row is suitable, return it
if (isRowSuitable) {
return row
}
column++
row++
}
}
const calendarData = computed(() => {
if (!formattedData.value || !calendarRange.value) return []
// We use the recordsInDay object to keep track of which columns are occupied for each day
// This is used to calculate the position of the records in the calendar
// The key is the day index (0-6) and the value is an object with the row index as the key and a boolean as the value
// Since no hours are considered, the rowIndex will be sufficient to calculate the position
const recordsInDay: {
[key: number]: {
[key: number]: boolean
@ -74,25 +85,32 @@ const calendarData = computed(() => {
const fromCol = range.fk_from_col
const toCol = range.fk_to_col
if (fromCol && toCol) {
// Filter out records that have an invalid date range
// i.e. the end date is before the start date
for (const record of [...formattedData.value].filter((r) => {
const startDate = dayjs(r.row[fromCol.title!])
const endDate = dayjs(r.row[toCol.title!])
if (!startDate.isValid() || !endDate.isValid()) return false
return !endDate.isBefore(startDate)
})) {
// Generate a unique id for the record if it doesn't have one
const id = record.row.id ?? generateRandomNumber()
let startDate = dayjs(record.row[fromCol.title!])
const ogStartDate = startDate.clone()
const endDate = dayjs(record.row[toCol.title!])
// If the start date is before the selected date range, we need to adjust the start date
if (startDate.isBefore(selectedDateRange.value.start)) {
startDate = dayjs(selectedDateRange.value.start)
}
const startDaysDiff = startDate.diff(selectedDateRange.value.start, 'day')
// Calculate the span of the record in days
let spanDays = Math.max(Math.min(endDate.diff(startDate, 'day'), 6) + 1, 1)
// If the end date is after the month of the selected date range, we need to adjust the span
if (endDate.isAfter(startDate, 'month')) {
spanDays = 7 - startDaysDiff
}
@ -102,7 +120,8 @@ const calendarData = computed(() => {
}
const widthStyle = `calc(max(${spanDays} * ${perDayWidth}px, ${perDayWidth}px))`
let suitableColumn = -1
let suitableRow = -1
// Find the first suitable row for the entire span
for (let i = 0; i < spanDays; i++) {
const dayIndex = startDaysDiff + i
@ -110,15 +129,15 @@ const calendarData = computed(() => {
recordsInDay[dayIndex] = {}
}
if (suitableColumn === -1) {
suitableColumn = findFirstSuitableColumn(recordsInDay, dayIndex, spanDays)
if (suitableRow === -1) {
suitableRow = findFirstSuitableRow(recordsInDay, dayIndex, spanDays)
}
}
// Mark the suitable column as occupied for the entire span
for (let i = 0; i < spanDays; i++) {
const dayIndex = startDaysDiff + i
recordsInDay[dayIndex][suitableColumn] = true
recordsInDay[dayIndex][suitableRow] = true
}
let position = 'none'
@ -127,6 +146,9 @@ const calendarData = computed(() => {
ogStartDate && ogStartDate.isBetween(selectedDateRange.value.start, selectedDateRange.value.end, 'day', '[]')
const isEndInRange = endDate && endDate.isBetween(selectedDateRange.value.start, selectedDateRange.value.end, 'day', '[]')
// Calculate the position of the record in the calendar based on the start and end date
// The position can be 'none', 'leftRounded', 'rightRounded', 'rounded'
// This is used to assign the rounded corners to the records
if (isStartInRange && isEndInRange) {
position = 'rounded'
} else if (
@ -158,7 +180,7 @@ const calendarData = computed(() => {
style: {
width: widthStyle,
left: `${startDaysDiff * perDayWidth}px`,
top: `${suitableColumn * 40}px`,
top: `${suitableRow * 40}px`,
},
},
})
@ -169,8 +191,10 @@ const calendarData = computed(() => {
const startDate = dayjs(record.row[fromCol.title!])
const startDaysDiff = Math.max(startDate.diff(selectedDateRange.value.start, 'day'), 0)
const suitableColumn = findFirstSuitableColumn(recordsInDay, startDaysDiff, 1)
recordsInDay[startDaysDiff][suitableColumn] = true
// Find the first suitable row for record. Here since the span is 1, we can use the findFirstSuitableRow function
const suitableRow = findFirstSuitableRow(recordsInDay, startDaysDiff, 1)
recordsInDay[startDaysDiff][suitableRow] = true
recordsInRange.push({
...record,
@ -182,7 +206,7 @@ const calendarData = computed(() => {
style: {
width: `calc(${perDayWidth}px)`,
left: `${startDaysDiff * perDayWidth}px`,
top: `${suitableColumn * 40}px`,
top: `${suitableRow * 40}px`,
},
},
})
@ -195,8 +219,6 @@ const calendarData = computed(() => {
const dragElement = ref<HTMLElement | null>(null)
const draggingId = ref<string | null>(null)
const resizeInProgress = ref(false)
const dragTimeout = ref<ReturnType<typeof setTimeout>>()
@ -215,11 +237,13 @@ const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[],
updateRowProperty(row, updateProperty, isDelete)
}, 500)
// This function is used to calculate the new start and end date of a record when resizing
const onResize = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return
if (!container.value || !resizeRecord.value) return
if (!isUIAllowed('dataEdit') || !container.value || !resizeRecord.value) return
const { width, left } = container.value.getBoundingClientRect()
// Calculate the percentage of the width based on the mouse position
const percentX = (event.clientX - left - window.scrollX) / width
const fromCol = resizeRecord.value.rowMeta.range?.fk_from_col
@ -229,62 +253,58 @@ const onResize = (event: MouseEvent) => {
const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
const ogStartDate = dayjs(resizeRecord.value.row[fromCol.title!])
const day = Math.min(Math.floor(percentX * 7), 6)
const day = Math.floor(percentX * 7)
let updateProperty: string[] = []
let updateRecord: Row
if (resizeDirection.value === 'right') {
// Calculate the new end date based on the day index by adding the day index to the start date of the selected date range
let newEndDate = dayjs(selectedDateRange.value.start).add(day, 'day')
const updateProperty = [toCol.title!]
updateProperty = [toCol.title!]
// If the new end date is before the start date, we need to adjust the end date to the start date
if (dayjs(newEndDate).isBefore(ogStartDate, 'day')) {
newEndDate = ogStartDate.clone()
}
if (!newEndDate.isValid()) return
const newRow = {
updateRecord = {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[toCol.title!]: newEndDate.format('YYYY-MM-DD'),
[toCol.title!]: newEndDate.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
useDebouncedRowUpdate(newRow, updateProperty, false)
} else if (resizeDirection.value === 'left') {
// Calculate the new start date based on the day index by adding the day index to the start date of the selected date range
let newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
const updateProperty = [fromCol.title!]
updateProperty = [fromCol.title!]
// If the new start date is after the end date, we need to adjust the start date to the end date
if (dayjs(newStartDate).isAfter(ogEndDate)) {
newStartDate = dayjs(dayjs(ogEndDate)).clone()
}
if (!newStartDate) return
const newRow = {
updateRecord = {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD'),
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
// Update the record in the store
const newPk = extractPkFromRow(updateRecord.row, meta.value!.columns!)
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
useDebouncedRowUpdate(newRow, updateProperty, false)
}
return pk === newPk ? updateRecord : r
})
useDebouncedRowUpdate(updateRecord, updateProperty, false)
}
const onResizeEnd = () => {
@ -304,22 +324,26 @@ const onResizeStart = (direction: 'right' | 'left', event: MouseEvent, record: R
document.addEventListener('mouseup', onResizeEnd)
}
const onDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return
if (!container.value || !dragRecord.value) return
// This method is used to calculate the new start and end date of a record when dragging and dropping
const calculateNewRow = (event: MouseEvent, updateSideBarData?: boolean) => {
const { width, left } = container.value.getBoundingClientRect()
// Calculate the percentage of the width based on the mouse position
// This is used to calculate the day index
const percentX = (event.clientX - left - window.scrollX) / width
const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
const toCol = dragRecord.value.rowMeta.range?.fk_to_col
if (!fromCol) return
if (!fromCol) return { updatedProperty: [], newRow: null }
const day = Math.min(Math.floor(percentX * 7), 6)
// Calculate the day index based on the percentage of the width
// The day index is a number between 0 and 6
const day = Math.floor(percentX * 7)
// Calculate the new start date based on the day index by adding the day index to the start date of the selected date range
const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
if (!newStartDate) return
if (!newStartDate) return { updatedProperty: [], newRow: null }
let endDate
@ -327,14 +351,20 @@ const onDrag = (event: MouseEvent) => {
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD'),
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
const updateProperty = [fromCol.title!]
if (toCol) {
const fromDate = dragRecord.value.row[fromCol.title!] ? dayjs(dragRecord.value.row[fromCol.title!]) : null
const toDate = dragRecord.value.row[toCol.title!] ? dayjs(dragRecord.value.row[toCol.title!]) : null
// Calculate the new end date based on the day index by adding the day index to the start date of the selected date range
// If the record has an end date, we need to calculate the new end date based on the difference between the start and end date
// If the record doesn't have an end date, we need to calculate the new end date based on the start date
// If the record has an end date and no start Date, we set the end date to the start date
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
} else if (fromDate && !toDate) {
@ -345,17 +375,34 @@ const onDrag = (event: MouseEvent) => {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD')
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
updateProperty.push(toCol.title!)
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
if (updateSideBarData) {
// If the record is being dragged from the sidebar, we need to remove the record from the sidebar data
// and add the new record to the calendar data
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk !== newPk
})
} else {
// If the record is being dragged within the calendar, we need to update the record in the calendar data
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk === newPk ? newRow : r
})
}
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
return { updateProperty, newRow }
}
const onDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return
if (!container.value || !dragRecord.value) return
calculateNewRow(event)
}
const stopDrag = (event: MouseEvent) => {
@ -365,69 +412,11 @@ const stopDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return
if (!isDragging.value || !container.value || !dragRecord.value) return
const { width, left } = container.value.getBoundingClientRect()
const percentX = (event.clientX - left - window.scrollX) / width
const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
const toCol = dragRecord.value.rowMeta.range?.fk_to_col
const day = Math.floor(percentX * 7)
const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
if (!newStartDate || !fromCol) return
let endDate
const newRow = {
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD'),
},
}
const updateProperty = [fromCol.title!]
if (toCol) {
const fromDate = dragRecord.value.row[fromCol.title!] ? dayjs(dragRecord.value.row[fromCol.title!]) : null
const toDate = dragRecord.value.row[toCol.title!] ? dayjs(dragRecord.value.row[toCol.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
} else if (fromDate && !toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else if (!fromDate && toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD')
updateProperty.push(toCol.title!)
}
const { updateProperty, newRow } = calculateNewRow(event)
if (!newRow) return
if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
} else {
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk !== extractPkFromRow(newRow.row, meta.value!.columns!)
})
}
// Open drop the record, we reset the opacity of the other records
const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => {
el.style.visibility = ''
@ -436,9 +425,6 @@ const stopDrag = (event: MouseEvent) => {
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
// isDragging.value = false
draggingId.value = null
dragElement.value = null
}
@ -464,7 +450,6 @@ const dragStart = (event: MouseEvent, record: Row) => {
const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => {
if (!el.getAttribute('data-unique-id').includes(record.rowMeta.id!)) {
// el.style.visibility = 'hidden'
el.style.opacity = '30%'
}
})
@ -473,7 +458,6 @@ const dragStart = (event: MouseEvent, record: Row) => {
isDragging.value = true
dragElement.value = target
draggingId.value = record.rowMeta.id!
dragRecord.value = record
document.addEventListener('mousemove', onDrag)
@ -502,75 +486,18 @@ const dropEvent = (event: DragEvent) => {
}: {
record: Row
} = JSON.parse(data)
const { width, left } = container.value.getBoundingClientRect()
const percentX = (event.clientX - left - window.scrollX) / width
const fromCol = record.rowMeta.range?.fk_from_col
const toCol = record.rowMeta.range?.fk_to_col
if (!fromCol) return
const day = Math.floor(percentX * 7)
const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
let endDate
const newRow = {
...record,
row: {
...record.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD'),
},
}
const updateProperty = [fromCol.title!]
if (toCol) {
const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
const toDate = record.row[toCol.title!] ? dayjs(record.row[toCol.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
} else if (fromDate && !toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else if (!fromDate && toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD')
updateProperty.push(toCol.title!)
}
if (!newRow) return
if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
} else {
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
dragRecord.value = record
return pk !== extractPkFromRow(newRow.row, meta.value!.columns!)
})
}
const { updateProperty, newRow } = calculateNewRow(event, true)
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)
dragRecord.value = null
}
}
</script>
@ -613,7 +540,7 @@ const dropEvent = (event: DragEvent) => {
:style="{
...record.rowMeta.style,
boxShadow:
record.rowMeta.id === draggingId
record.rowMeta.id === dragElement?.getAttribute('data-unique-id')
? '0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.10)'
: 'none',
}"
@ -627,6 +554,13 @@ const dropEvent = (event: DragEvent) => {
:hover="hoverRecord === record.rowMeta.id"
:position="record.rowMeta.position"
:record="record"
:selected="
dragRecord
? dragRecord.rowMeta.id === record.rowMeta.id
: resizeRecord
? resizeRecord.rowMeta.id === record.rowMeta.id
: false
"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
color="blue"
@dblclick="emits('expand-record', record)"
@ -635,8 +569,8 @@ const dropEvent = (event: DragEvent) => {
<template v-if="!isRowEmpty(record, displayField)">
<div
:class="{
'!mt-2': displayField.uidt === UITypes.SingleLineText,
'!mt-1': displayField.uidt === UITypes.MultiSelect || displayField.uidt === UITypes.SingleSelect,
'mt-2': displayField.uidt === UITypes.SingleLineText,
'mt-1': displayField.uidt === UITypes.MultiSelect || displayField.uidt === UITypes.SingleSelect,
}"
>
<LazySmartsheetVirtualCell
@ -663,13 +597,9 @@ const dropEvent = (event: DragEvent) => {
</template>
<style lang="scss" scoped>
.hide {
transition: 0.01s;
transform: translateX(-9999px);
}
.prevent-select {
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>

297
packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue

@ -28,6 +28,7 @@ const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref())
// Since it is a datetime Week view, we need to create a 2D array of dayjs objects to represent the hours in a day for each day in the week
const datesHours = computed(() => {
const datesHours: Array<Array<dayjs.Dayjs>> = []
let startOfWeek = dayjs(selectedDateRange.value.start) ?? dayjs().startOf('week')
@ -75,6 +76,9 @@ const recordsAcrossAllRange = computed<{
const scheduleStart = dayjs(selectedDateRange.value.start).startOf('day')
const scheduleEnd = dayjs(selectedDateRange.value.end).endOf('day')
// We need to keep track of the overlaps for each day and hour in the week to calculate the width and left position of each record
// The first key is the date, the second key is the hour, and the value is an object containing the ids of the records that overlap
// The key is in the format YYYY-MM-DD and the hour is in the format HH:mm
const overlaps: {
[key: string]: {
[key: string]: {
@ -91,6 +95,8 @@ const recordsAcrossAllRange = computed<{
const fromCol = range.fk_from_col
const toCol = range.fk_to_col
// We fetch all the records that match the calendar ranges in a single time.
// But not all fetched records are valid for the certain range, so we filter them out & sort them
const sortedFormattedData = [...formattedData.value].filter((record) => {
const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null
@ -107,14 +113,20 @@ const recordsAcrossAllRange = computed<{
sortedFormattedData.forEach((record: Row) => {
if (!toCol && fromCol) {
// If there is no toColumn chosen in the range
const startDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
if (!startDate) return
// Hour Key currently is set as start of the hour
// TODO: Need to work on the granularity of the hour
const dateKey = startDate?.format('YYYY-MM-DD')
const hourKey = startDate?.startOf('hour').format('HH:mm')
const id = record.rowMeta.id ?? generateRandomNumber()
let style: Partial<CSSStyleDeclaration> = {}
// If the dateKey and hourKey are valid, we add the id to the overlaps object
if (dateKey && hourKey) {
if (!overlaps[dateKey]) {
overlaps[dateKey] = {}
@ -128,18 +140,23 @@ const recordsAcrossAllRange = computed<{
}
overlaps[dateKey][hourKey].id.push(id)
}
// If the number of records that overlap in a single hour is more than 4, we hide the record and set the overflow flag to true
// We also keep track of the number of records that overflow
if (overlaps[dateKey][hourKey].id.length > 4) {
overlaps[dateKey][hourKey].overflow = true
style.display = 'none'
overlaps[dateKey][hourKey].overflowCount += 1
}
// TODO: dayIndex is not calculated perfectly
// Should revisit this part in next iteration
let dayIndex = dayjs(dateKey).day() - 1
if (dayIndex === -1) {
dayIndex = 6
}
// We calculate the index of the hour in the day and set the top and height of the record
const hourIndex = Math.max(
datesHours.value[dayIndex].findIndex((h) => h.startOf('hour').format('HH:mm') === hourKey),
0,
@ -168,12 +185,17 @@ const recordsAcrossAllRange = computed<{
let startDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
let endDate = record.row[toCol.title!] ? dayjs(record.row[toCol.title!]) : null
// If the start date is not valid, we skip the record
if (!startDate?.isValid()) return
// If the end date is not valid, we set it to 30 minutes after the start date
if (!endDate?.isValid()) {
endDate = startDate.clone().add(30, 'minutes')
}
// If the start date is before the start of the schedule, we set it to the start of the schedule
// If the end date is after the end of the schedule, we set it to the end of the schedule
// This is to ensure that the records are within the bounds of the schedule and do not overflow
if (startDate.isBefore(scheduleStart, 'minutes')) {
startDate = scheduleStart
}
@ -181,8 +203,10 @@ const recordsAcrossAllRange = computed<{
endDate = scheduleEnd
}
// Setting the current start date to the start date of the record
let currentStartDate: dayjs.Dayjs = startDate.clone()
// We loop through the start date to the end date and create a record for each day as it spans bottom to top
while (currentStartDate.isSameOrBefore(endDate!, 'day')) {
const currentEndDate = currentStartDate.clone().endOf('day')
const recordStart: dayjs.Dayjs = currentEndDate.isSame(startDate, 'day') ? startDate : currentStartDate
@ -190,12 +214,14 @@ const recordsAcrossAllRange = computed<{
const dateKey = recordStart.format('YYYY-MM-DD')
// TODO: dayIndex is not calculated perfectly
// Should revisit this part in next iteration
let dayIndex = recordStart.day() - 1
if (dayIndex === -1) {
dayIndex = 6
}
// We calculate the index of the start and end hour in the day
const startHourIndex = Math.max(
(datesHours.value[dayIndex] ?? []).findIndex((h) => h.format('HH:mm') === recordStart.format('HH:mm')),
0,
@ -226,6 +252,7 @@ const recordsAcrossAllRange = computed<{
let style: Partial<CSSStyleDeclaration> = {}
// We loop through the start hour index to the end hour index and add the id to the overlaps object
while (_startHourIndex <= endHourIndex) {
const hourKey = datesHours.value[dayIndex][_startHourIndex].format('HH:mm')
if (!overlaps[dateKey]) {
@ -240,6 +267,8 @@ const recordsAcrossAllRange = computed<{
}
overlaps[dateKey][hourKey].id.push(id)
// If the number of records that overlap in a single hour is more than 4, we hide the record and set the overflow flag to true
// We also keep track of the number of records that overflow
if (overlaps[dateKey][hourKey].id.length > 4) {
overlaps[dateKey][hourKey].overflow = true
style.display = 'none'
@ -272,20 +301,27 @@ const recordsAcrossAllRange = computed<{
dayIndex,
},
})
// We set the current start date to the next day
currentStartDate = currentStartDate.add(1, 'day').hour(0).minute(0)
}
}
})
// With can't find the left and width of the record without knowing the overlaps
// Hence the first iteration is to find the overlaps, top, height and then the second iteration is to find the left and width
// This is because the left and width of the record depends on the overlaps
recordsToDisplay = recordsToDisplay.map((record) => {
// maxOverlaps is the maximum number of records that overlap in a single hour
// overlapIndex is the index of the record in the overlaps object
let maxOverlaps = 1
let overlapIndex = 0
const dayIndex = record.rowMeta.dayIndex as number
const dateKey = dayjs(selectedDateRange.value.start).add(dayIndex, 'day').format('YYYY-MM-DD')
for (const hours in overlaps[dateKey]) {
// We are checking if the overlaps object contains the id of the record
// If it does, we set the maxOverlaps and overlapIndex
if (overlaps[dateKey][hours].id.includes(record.rowMeta.id!)) {
maxOverlaps = Math.max(maxOverlaps, overlaps[dateKey][hours].id.length - overlaps[dateKey][hours].overflowCount)
overlapIndex = Math.max(overlapIndex, overlaps[dateKey][hours].id.indexOf(record.rowMeta.id!))
@ -312,8 +348,6 @@ const recordsAcrossAllRange = computed<{
const dragElement = ref<HTMLElement | null>(null)
const draggingId = ref<string | null>(null)
const resizeInProgress = ref(false)
const dragTimeout = ref<ReturnType<typeof setTimeout>>()
@ -333,85 +367,83 @@ const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[],
}, 500)
const onResize = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return
if (!container.value || !resizeRecord.value) return
if (!isUIAllowed('dataEdit') || !container.value || !resizeRecord.value) return
const { width, left, top, bottom } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
// If the mouse is near the bottom of the container, we scroll down
// If the mouse is near the top of the container, we scroll up
if (event.clientY > bottom - 20) {
container.value.scrollTop += 10
} else if (event.clientY < top + 20) {
container.value.scrollTop -= 10
}
// We calculate the percentage of the mouse position in the container
// percentX is used for the day and percentY is used for the hour
const percentX = (event.clientX - left - window.scrollX) / width
const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight
const fromCol = resizeRecord.value.rowMeta.range?.fk_from_col
const toCol = resizeRecord.value.rowMeta.range?.fk_to_col
if (!fromCol || !toCol) return
const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
const ogStartDate = dayjs(resizeRecord.value.row[fromCol.title!])
const day = Math.floor(percentX * 7)
const hour = Math.floor(percentY * 24)
const hour = Math.floor(percentY * 23)
let updateProperty: string[] = []
let newRow: Row
if (resizeDirection.value === 'right') {
let newEndDate = dayjs(selectedDateRange.value.start).add(day, 'day').add(hour, 'hour')
const updateProperty = [toCol.title!]
updateProperty = [toCol.title!]
// If the new end date is before the start date, we set the new end date to the start date
if (dayjs(newEndDate).isBefore(ogStartDate, 'day')) {
newEndDate = ogStartDate.clone()
}
if (!newEndDate.isValid()) return
const newRow = {
newRow = {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[toCol.title!]: newEndDate.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
useDebouncedRowUpdate(newRow, updateProperty, false)
} else if (resizeDirection.value === 'left') {
let newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day').add(hour, 'hour')
const updateProperty = [fromCol.title!]
updateProperty = [fromCol.title!]
// If the new start date is after the end date, we set the new start date to the end date
if (dayjs(newStartDate).isAfter(ogEndDate)) {
newStartDate = dayjs(dayjs(ogEndDate)).clone()
}
if (!newStartDate) return
const newRow = {
newRow = {
...resizeRecord.value,
row: {
...resizeRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
useDebouncedRowUpdate(newRow, updateProperty, false)
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk === newPk ? newRow : r
})
useDebouncedRowUpdate(newRow, updateProperty, false)
}
const onResizeEnd = () => {
@ -430,16 +462,10 @@ const onResizeStart = (direction: 'right' | 'left', event: MouseEvent, record: R
document.addEventListener('mouseup', onResizeEnd)
}
const onDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return
if (!container.value || !dragRecord.value) return
const { width, left, top, bottom } = container.value.getBoundingClientRect()
if (event.clientY > bottom - 20) {
container.value.scrollTop += 10
} else if (event.clientY < top + 20) {
container.value.scrollTop -= 10
}
// We calculate the new row based on the mouse position and update the record
// We also update the sidebar data if the dropped from the sidebar
const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
const { width, left, top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
@ -452,12 +478,13 @@ const onDrag = (event: MouseEvent) => {
if (!fromCol) return
const day = Math.floor(percentX * 7)
const hour = Math.max(Math.min(Math.floor(percentY * 24), 23), 0)
const hour = Math.floor(percentY * 23)
const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day').add(hour, 'hour')
if (!newStartDate) return
let endDate
const updatedProperty = [fromCol.title!]
const newRow = {
...dragRecord.value,
@ -482,92 +509,53 @@ const onDrag = (event: MouseEvent) => {
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
updatedProperty.push(toCol.title!)
}
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
}
const stopDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return
if (!isDragging.value || !container.value || !dragRecord.value) return
if (!newRow) return { newRow: null, updatedProperty }
event.preventDefault()
clearTimeout(dragTimeout.value!)
const { width, left, top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight
const percentX = (event.clientX - left - window.scrollX) / width
const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
const toCol = dragRecord.value.rowMeta.range?.fk_to_col
const day = Math.floor(percentX * 7)
const hour = Math.max(Math.min(Math.floor(percentY * 24), 24), 0)
const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day').add(hour, 'hour')
if (!newStartDate || !fromCol) return
let endDate
const newRow = {
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
if (updateSideBar) {
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk !== newPk
})
} else {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk === newPk ? newRow : r
})
}
const updateProperty = [fromCol.title!]
if (toCol) {
const fromDate = dragRecord.value.row[fromCol.title!] ? dayjs(dragRecord.value.row[fromCol.title!]) : null
const toDate = dragRecord.value.row[toCol.title!] ? dayjs(dragRecord.value.row[toCol.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
} else if (fromDate && !toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else if (!fromDate && toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else {
endDate = newStartDate.clone()
}
return { newRow, updatedProperty }
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
const onDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit') || !container.value || !dragRecord.value) return
const { top, bottom } = container.value.getBoundingClientRect()
updateProperty.push(toCol.title!)
// If the mouse is near the bottom of the container, we scroll down
// If the mouse is near the top of the container, we scroll up
if (event.clientY > bottom - 20) {
container.value.scrollTop += 10
} else if (event.clientY < top + 20) {
container.value.scrollTop -= 10
}
if (!newRow) return
calculateNewRow(event)
}
if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
const stopDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit') || !isDragging.value || !container.value || !dragRecord.value) return
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
} else {
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
event.preventDefault()
clearTimeout(dragTimeout.value!)
return pk !== extractPkFromRow(newRow.row, meta.value!.columns!)
})
}
const { newRow, updatedProperty } = calculateNewRow(event, false)
// We set the visibility and opacity of the records back to normal
const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => {
el.style.visibility = ''
@ -576,13 +564,10 @@ const stopDrag = (event: MouseEvent) => {
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
// isDragging.value = false
draggingId.value = null
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)
updateRowProperty(newRow, updatedProperty, false)
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
@ -613,7 +598,6 @@ const dragStart = (event: MouseEvent, record: Row) => {
isDragging.value = true
dragElement.value = target
draggingId.value = record.rowMeta.id!
dragRecord.value = record
document.addEventListener('mousemove', onDrag)
@ -642,79 +626,12 @@ const dropEvent = (event: DragEvent) => {
}: {
record: Row
} = JSON.parse(data)
const { width, left, top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
const percentX = (event.clientX - left - window.scrollX) / width
const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight
const fromCol = record.rowMeta.range?.fk_from_col
const toCol = record.rowMeta.range?.fk_to_col
if (!fromCol) return
const day = Math.floor(percentX * 7)
const hour = Math.floor(percentY * 24)
const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day').add(hour, 'hour')
let endDate
const newRow = {
...record,
row: {
...record.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
const updateProperty = [fromCol.title!]
if (toCol) {
const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
const toDate = record.row[toCol.title!] ? dayjs(record.row[toCol.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day').add(toDate.diff(fromDate, 'hour'), 'hour')
} else if (fromDate && !toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else if (!fromDate && toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
updateProperty.push(toCol.title!)
}
if (!newRow) return
if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
} else {
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk !== extractPkFromRow(newRow.row, meta.value!.columns!)
})
}
dragRecord.value = record
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
const { newRow, updatedProperty } = calculateNewRow(event, true)
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)
updateRowProperty(newRow, updatedProperty, false)
}
}

30
packages/nc-gui/components/smartsheet/calendar/index.vue

@ -131,6 +131,16 @@ reloadViewDataHook?.on(async () => {
await Promise.all([loadCalendarData(), loadSidebarData()])
})
const goToToday = () => {
selectedDate.value = dayjs()
pageDate.value = dayjs()
selectedMonth.value = dayjs()
selectedDateRange.value = {
start: dayjs().startOf('week'),
end: dayjs().endOf('week'),
}
}
const headerText = computed(() => {
switch (activeCalendarView.value) {
case 'day':
@ -157,7 +167,7 @@ const headerText = computed(() => {
<div class="flex justify-between p-3 items-center border-b-1 border-gray-200" data-testid="nc-calendar-topbar">
<div class="flex justify-start gap-3 items-center">
<NcTooltip>
<template #title> Previous </template>
<template #title> {{ $t('labels.previous') }}</template>
<NcButton
v-e="`['c:calendar:calendar-${activeCalendarView}-prev-btn']`"
data-testid="nc-calendar-prev-btn"
@ -207,7 +217,7 @@ const headerText = computed(() => {
</template>
</NcDropdown>
<NcTooltip>
<template #title> Next </template>
<template #title> {{ $t('labels.next') }}</template>
<NcButton
v-e="`['c:calendar:calendar-${activeCalendarView}-next-btn']`"
data-testid="nc-calendar-next-btn"
@ -224,26 +234,16 @@ const headerText = computed(() => {
data-testid="nc-calendar-today-btn"
size="small"
type="secondary"
@click="
() => {
selectedDate = dayjs()
pageDate = dayjs()
selectedMonth = dayjs()
selectedDateRange = {
start: dayjs().startOf('week'),
end: dayjs().endOf('week'),
}
}
"
@click="goToToday"
>
Go to Today
{{ $t('activity.goToToday') }}
</NcButton>
<span class="opacity-0" data-testid="nc-active-calendar-view">
{{ activeCalendarView }}
</span>
</div>
<NcTooltip>
<template #title> Toggle Sidebar </template>
<template #title> {{ $t('activity.toggleSidebar') }}</template>
<NcButton
v-if="!isMobileMode"
v-e="`['c:calendar:calendar-${activeCalendarView}-toggle-sidebar']`"

10
packages/nc-gui/components/smartsheet/toolbar/CalendarRange.vue

@ -1,6 +1,8 @@
<script lang="ts" setup>
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
import { type CalendarRangeType } from '~/lib'
import {
ActiveViewInj,
IsLockedInj,
@ -40,8 +42,8 @@ watch(
{ immediate: true },
)
const calendarRange = computed<{ fk_from_column_id: string; fk_to_column_id: string | null }[]>(() => {
const tempCalendarRange: { fk_from_column_id: string; fk_to_column_id: string | null }[] = []
const calendarRange = computed<CalendarRangeType[]>(() => {
const tempCalendarRange: CalendarRangeType[] = []
if (!activeView.value || !activeView.value.view) return tempCalendarRange
activeView.value.view.calendar_range?.forEach((range) => {
@ -54,7 +56,7 @@ const calendarRange = computed<{ fk_from_column_id: string; fk_to_column_id: str
})
// We keep the calendar range here and update it when the user selects a new range
const _calendar_ranges = ref<{ fk_from_column_id: string; fk_to_column_id: string | null }[]>(calendarRange.value)
const _calendar_ranges = ref<CalendarRangeType[]>(calendarRange.value)
const saveCalendarRanges = async () => {
if (activeView.value) {
@ -66,7 +68,7 @@ const saveCalendarRanges = async () => {
fk_to_column_id: range.fk_to_column_id,
}))
await $api.dbView.calendarUpdate(activeView.value?.id as string, {
calendar_range: calRanges as { fk_from_column_id: string; fk_to_column_id: string | null }[],
calendar_range: calRanges as CalendarRangeType[],
})
await loadCalendarMeta()
await Promise.all([loadCalendarData(), loadSidebarData()])

3
packages/nc-gui/lang/en.json

@ -212,7 +212,6 @@
"page": "Page",
"pages": "Pages",
"record": "record",
"Records": "Records",
"records": "records",
"webhook": "Webhook",
"webhooks": "Webhooks",
@ -688,6 +687,8 @@
}
},
"activity": {
"goToToday": "Go to Today",
"toggleSidebar": "Toggle Sidebar",
"addEndDate": "Add end date",
"withEndDate": "with end date",
"calendar": "Calendar",

8
packages/nc-gui/lib/types.ts

@ -70,11 +70,16 @@ interface Row {
fk_to_col: ColumnType | null
}
id?: string
position?: 'leftRounded' | 'rightRounded' | 'rounded' | 'none' | 'topRounded' | 'bottomRounded' | string
position?: string
dayIndex?: number
}
}
interface CalendarRangeType {
fk_from_column_id: string
fk_to_column_id: string | null
}
type RolePermissions = Omit<typeof rolePermissions, 'guest' | 'admin' | 'super'>
type GetKeys<T> = T extends Record<any, Record<infer Key, boolean>> ? Key : never
@ -220,4 +225,5 @@ export type {
SidebarTableNode,
UsersSortType,
CommandPaletteType,
CalendarRangeType,
}

4
packages/nc-gui/utils/viewUtils.ts

@ -1,11 +1,11 @@
import { ViewTypes } from 'nocodb-sdk'
import type { Language } from '../lib'
import { iconMap } from './iconUtils'
import type { Language } from '~/lib'
export const viewIcons: Record<number | string, { icon: any; color: string }> = {
[ViewTypes.GRID]: { icon: iconMap.grid, color: '#36BFFF' },
[ViewTypes.FORM]: { icon: iconMap.form, color: '#7D26CD' },
[ViewTypes.CALENDAR]: { icon: iconMap.calendar, color: 'purple' },
[ViewTypes.CALENDAR]: { icon: iconMap.calendar, color: '#B33771' },
[ViewTypes.GALLERY]: { icon: iconMap.gallery, color: '#FC3AC6' },
[ViewTypes.MAP]: { icon: iconMap.map, color: 'blue' },
[ViewTypes.KANBAN]: { icon: iconMap.kanban, color: '#FF9052' },

2
packages/nocodb/src/services/calendars.service.ts

@ -27,7 +27,7 @@ export class CalendarsService {
user: UserType;
req: NcRequest;
}) {
-validatePayload(
validatePayload(
'swagger.json#/components/schemas/ViewCreateReq',
param.calendar,
);

Loading…
Cancel
Save