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 } = const { selectedDate, formattedData, formattedSideBarData, calendarRange, updateRowProperty, displayField } =
useCalendarViewStoreOrThrow() 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[]>(() => { const recordsAcrossAllRange = computed<Row[]>(() => {
let dayRecordCount = 0 let dayRecordCount = 0
const perRecordHeight = 40 const perRecordHeight = 40
@ -38,6 +40,7 @@ const recordsAcrossAllRange = computed<Row[]>(() => {
width: '100%', 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' let position = 'none'
const isSelectedDay = (date: dayjs.Dayjs) => date.isSame(selectedDate.value, 'day') const isSelectedDay = (date: dayjs.Dayjs) => date.isSame(selectedDate.value, 'day')
const isBeforeSelectedDay = (date: dayjs.Dayjs) => date.isBefore(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) 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) => { const dropEvent = (event: DragEvent) => {
if (!isUIAllowed('dataEdit')) return if (!isUIAllowed('dataEdit')) return
event.preventDefault() event.preventDefault()
@ -142,28 +146,22 @@ const dropEvent = (event: DragEvent) => {
if (!newRow) return if (!newRow) return
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
if (dragElement.value) { if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => { formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!) 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
}) })
} else { } else {
formattedData.value = [...formattedData.value, newRow] formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => { formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!) return extractPkFromRow(r.row, meta.value!.columns!) !== newPk
return pk !== extractPkFromRow(newRow.row, meta.value!.columns!)
}) })
} }
if (dragElement.value) { if (dragElement.value) {
dragElement.value.style.boxShadow = 'none' dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
dragElement.value = null dragElement.value = null
} }
updateRowProperty(newRow, updateProperty, false) 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 container = ref<null | HTMLElement>(null)
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const hours = computed(() => { const hours = computed(() => {
@ -48,6 +49,11 @@ const recordsAcrossAllRange = computed<{
const scheduleStart = dayjs(selectedDate.value).startOf('day') const scheduleStart = dayjs(selectedDate.value).startOf('day')
const scheduleEnd = dayjs(selectedDate.value).endOf('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: { const overlaps: {
[key: string]: { [key: string]: {
id: string[] id: string[]
@ -64,6 +70,8 @@ const recordsAcrossAllRange = computed<{
const fromCol = range.fk_from_col const fromCol = range.fk_from_col
const endCol = range.fk_to_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 sortedFormattedData = [...formattedData.value].filter((record) => {
const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null
@ -78,27 +86,39 @@ const recordsAcrossAllRange = computed<{
return false 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) { if (fromCol && endCol) {
for (const record of sortedFormattedData) { 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() const id = generateRandomNumber()
let startDate = dayjs(record.row[fromCol.title!]) let startDate = dayjs(record.row[fromCol.title!])
let endDate = dayjs(record.row[endCol.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 (!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()) { if (!endDate.isValid()) {
endDate = startDate.clone().add(30, 'minutes') 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')) { if (startDate.isBefore(scheduleStart, 'minutes')) {
startDate = scheduleStart 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')) { if (endDate.isAfter(scheduleEnd, 'minutes')) {
endDate = scheduleEnd endDate = scheduleEnd
} }
// The top of the record is calculated based on the start hour and minute
const topInPixels = (startDate.hour() + startDate.minute() / 60) * 80 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 heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 80, perRecordHeight)
const startHour = startDate.hour() const startHour = startDate.hour()
@ -110,6 +130,8 @@ const recordsAcrossAllRange = computed<{
top: `${topInPixels + 5 + startHour * 2}px`, 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)) { while (_startDate.isBefore(endDate)) {
const timeKey = _startDate.format('HH:mm') const timeKey = _startDate.format('HH:mm')
if (!overlaps[timeKey]) { if (!overlaps[timeKey]) {
@ -121,6 +143,7 @@ const recordsAcrossAllRange = computed<{
} }
overlaps[timeKey].id.push(id) 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) { if (overlaps[timeKey].id.length > 4) {
overlaps[timeKey].overflow = true overlaps[timeKey].overflow = true
style.display = 'none' style.display = 'none'
@ -129,6 +152,8 @@ const recordsAcrossAllRange = computed<{
_startDate = _startDate.add(15, 'minutes') _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' let position = 'none'
const isSelectedDay = (date: dayjs.Dayjs) => date.isSame(selectedDate.value, 'day') const isSelectedDay = (date: dayjs.Dayjs) => date.isSame(selectedDate.value, 'day')
const isBeforeSelectedDay = (date: dayjs.Dayjs) => date.isBefore(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)) { } else if (isBeforeSelectedDay(startDate) && isAfterSelectedDay(endDate)) {
position = 'none' position = 'none'
} else if (isSelectedDay(startDate) && isAfterSelectedDay(endDate)) { } else if (isSelectedDay(startDate) && isAfterSelectedDay(endDate)) {
position = 'leftRounded' position = 'topRounded'
} else if (isBeforeSelectedDay(startDate) && isSelectedDay(endDate)) { } else if (isBeforeSelectedDay(startDate) && isSelectedDay(endDate)) {
position = 'rightRounded' position = 'bottomRounded'
} else { } else {
position = 'none' position = 'none'
} }
@ -169,6 +194,7 @@ const recordsAcrossAllRange = computed<{
let style: Partial<CSSStyleDeclaration> = {} let style: Partial<CSSStyleDeclaration> = {}
let _startDate = startDate.clone() 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)) { while (_startDate.isBefore(endDate)) {
const timeKey = _startDate.startOf('hour').format('HH:mm') const timeKey = _startDate.startOf('hour').format('HH:mm')
@ -181,6 +207,7 @@ const recordsAcrossAllRange = computed<{
} }
overlaps[timeKey].id.push(id) 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) { if (overlaps[timeKey].id.length > 8) {
overlaps[timeKey].overflow = true overlaps[timeKey].overflow = true
overlaps[timeKey].overflowCount += 1 overlaps[timeKey].overflowCount += 1
@ -193,6 +220,8 @@ const recordsAcrossAllRange = computed<{
} }
const topInPixels = (startDate.hour() + startDate.startOf('hour').minute() / 60) * 80 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 heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 80, perRecordHeight)
const finalTopInPixels = topInPixels + startHour * 2 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) => { 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 maxOverlaps = 1
let overlapIndex = 0 let overlapIndex = 0
for (const minutes in overlaps) { for (const minutes in overlaps) {
@ -229,6 +262,7 @@ const recordsAcrossAllRange = computed<{
const spacing = 0.25 const spacing = 0.25
const widthPerRecord = (100 - spacing * (maxOverlaps - 1)) / maxOverlaps const widthPerRecord = (100 - spacing * (maxOverlaps - 1)) / maxOverlaps
const leftPerRecord = (widthPerRecord + spacing) * overlapIndex const leftPerRecord = (widthPerRecord + spacing) * overlapIndex
record.rowMeta.style = { record.rowMeta.style = {
@ -249,14 +283,10 @@ const dragRecord = ref<Row | null>(null)
const isDragging = ref(false) const isDragging = ref(false)
const draggingId = ref<string | null>(null)
const dragElement = ref<HTMLElement | null>(null) const dragElement = ref<HTMLElement | null>(null)
const resizeDirection = ref<'right' | 'left' | null>() const resizeDirection = ref<'right' | 'left' | null>()
const resizeInProgress = ref(false)
const resizeRecord = ref<Row | null>() const resizeRecord = ref<Row | null>()
const dragTimeout = ref<ReturnType<typeof setTimeout>>() const dragTimeout = ref<ReturnType<typeof setTimeout>>()
@ -267,6 +297,86 @@ const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[],
updateRowProperty(row, updateProperty, isDelete) updateRowProperty(row, updateProperty, isDelete)
}, 500) }, 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) => { const onResize = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return if (!isUIAllowed('dataEdit')) return
if (!container.value || !resizeRecord.value) return if (!container.value || !resizeRecord.value) return
@ -274,6 +384,7 @@ const onResize = (event: MouseEvent) => {
const { scrollHeight } = container.value 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) { if (event.clientY > bottom - 20) {
container.value.scrollTop += 10 container.value.scrollTop += 10
} else if (event.clientY < top + 20) { } else if (event.clientY < top + 20) {
@ -289,66 +400,65 @@ const onResize = (event: MouseEvent) => {
const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!]) const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
const ogStartDate = dayjs(resizeRecord.value.row[fromCol.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 (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') 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')) { if (dayjs(newEndDate).isBefore(ogStartDate, 'day')) {
newEndDate = ogStartDate.clone() newEndDate = ogStartDate.clone()
} }
if (!newEndDate.isValid()) return if (!newEndDate.isValid()) return
const newRow = { newRow = {
...resizeRecord.value, ...resizeRecord.value,
row: { row: {
...resizeRecord.value.row, ...resizeRecord.value.row,
[toCol.title!]: newEndDate.format('YYYY-MM-DD HH:mm:ssZ'), [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') { } else if (resizeDirection.value === 'left') {
let newStartDate = dayjs(selectedDate.value).add(hour, 'hour') 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)) { if (dayjs(newStartDate).isAfter(ogEndDate)) {
newStartDate = dayjs(dayjs(ogEndDate)).clone() newStartDate = dayjs(dayjs(ogEndDate)).clone()
} }
if (!newStartDate) return if (!newStartDate) return
const newRow = { newRow = {
...resizeRecord.value, ...resizeRecord.value,
row: { row: {
...resizeRecord.value.row, ...resizeRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'), [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 = () => { const onResizeEnd = () => {
resizeInProgress.value = false
resizeDirection.value = null resizeDirection.value = null
resizeRecord.value = null resizeRecord.value = null
document.removeEventListener('mousemove', onResize) document.removeEventListener('mousemove', onResize)
@ -357,7 +467,6 @@ const onResizeEnd = () => {
const onResizeStart = (direction: 'right' | 'left', _event: MouseEvent, record: Row) => { const onResizeStart = (direction: 'right' | 'left', _event: MouseEvent, record: Row) => {
if (!isUIAllowed('dataEdit')) return if (!isUIAllowed('dataEdit')) return
resizeInProgress.value = true
resizeDirection.value = direction resizeDirection.value = direction
resizeRecord.value = record resizeRecord.value = record
document.addEventListener('mousemove', onResize) document.addEventListener('mousemove', onResize)
@ -375,129 +484,16 @@ const onDrag = (event: MouseEvent) => {
} else if (event.clientY < top + 20) { } else if (event.clientY < top + 20) {
container.value.scrollTop -= 10 container.value.scrollTop -= 10
} }
calculateNewRow(event)
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
})
} }
const stopDrag = (event: MouseEvent) => { const stopDrag = (event: MouseEvent) => {
event.preventDefault() event.preventDefault()
clearTimeout(dragTimeout.value!) clearTimeout(dragTimeout.value!)
if (!isUIAllowed('dataEdit')) return if (!isUIAllowed('dataEdit') || !isDragging.value || !container.value || !dragRecord.value) 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)
const newStartDate = dayjs(selectedDate.value).startOf('day').add(hour, 'hour') const { newRow, updateProperty } = calculateNewRow(event)
if (!newStartDate || !fromCol) return if (!newRow && !updateProperty) 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 allRecords = document.querySelectorAll('.draggable-record') const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => { allRecords.forEach((el) => {
@ -507,9 +503,6 @@ const stopDrag = (event: MouseEvent) => {
if (dragElement.value) { if (dragElement.value) {
dragElement.value.style.boxShadow = 'none' dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
// isDragging.value = false
draggingId.value = null
dragElement.value = null dragElement.value = null
} }
@ -525,12 +518,14 @@ const dragStart = (event: MouseEvent, record: Row) => {
isDragging.value = false isDragging.value = false
// We use a timeout to determine if the user is dragging or clicking on the record
dragTimeout.value = setTimeout(() => { dragTimeout.value = setTimeout(() => {
isDragging.value = true isDragging.value = true
while (!target.classList.contains('draggable-record')) { while (!target.classList.contains('draggable-record')) {
target = target.parentElement as HTMLElement 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') const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => { allRecords.forEach((el) => {
if (!el.getAttribute('data-unique-id').includes(record.rowMeta.id!)) { if (!el.getAttribute('data-unique-id').includes(record.rowMeta.id!)) {
@ -541,9 +536,7 @@ const dragStart = (event: MouseEvent, record: Row) => {
dragRecord.value = record dragRecord.value = record
isDragging.value = true
dragElement.value = target dragElement.value = target
draggingId.value = record.rowMeta.id!
dragRecord.value = record dragRecord.value = record
document.addEventListener('mousemove', onDrag) document.addEventListener('mousemove', onDrag)
@ -566,7 +559,6 @@ const viewMore = (hour: dayjs.Dayjs) => {
selectedTime.value = hour selectedTime.value = hour
showSideMenu.value = true showSideMenu.value = true
} }
</script> </script>
<template> <template>

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

@ -98,6 +98,8 @@ const recordsToDisplay = computed<{
const spaceBetweenRecords = 35 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: { const recordsInDay: {
[key: string]: { [key: string]: {
overflow: boolean overflow: boolean
@ -113,6 +115,7 @@ const recordsToDisplay = computed<{
const startCol = range.fk_from_col const startCol = range.fk_from_col
const endCol = range.fk_to_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 sortedFormattedData = [...formattedData.value].filter((record) => {
const fromDate = record.row[startCol!.title!] ? dayjs(record.row[startCol!.title!]) : null const fromDate = record.row[startCol!.title!] ? dayjs(record.row[startCol!.title!]) : null
@ -128,6 +131,7 @@ const recordsToDisplay = computed<{
sortedFormattedData.forEach((record: Row) => { sortedFormattedData.forEach((record: Row) => {
if (!endCol && startCol) { 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 startDate = dayjs(record.row[startCol!.title!])
const dateKey = startDate.format('YYYY-MM-DD') const dateKey = startDate.format('YYYY-MM-DD')
@ -137,8 +141,10 @@ const recordsToDisplay = computed<{
recordsInDay[dateKey].count++ recordsInDay[dateKey].count++
const id = record.rowMeta.id ?? generateRandomNumber() 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'))) 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) => { const dayIndex = (dates.value[weekIndex] ?? []).findIndex((day) => {
return dayjs(day).isSame(startDate, 'day') return dayjs(day).isSame(startDate, 'day')
}) })
@ -148,9 +154,14 @@ const recordsToDisplay = computed<{
width: `${perWidth}px`, width: `${perWidth}px`,
} }
// Number of records in that day
const recordIndex = recordsInDay[dateKey].count 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 const top = weekIndex * perHeight + spaceBetweenRecords + (recordIndex - 1) * perRecordHeight
// The 25 is obtained from the trial and error
const heightRequired = perRecordHeight * recordIndex + spaceBetweenRecords + 25 const heightRequired = perRecordHeight * recordIndex + spaceBetweenRecords + 25
if (heightRequired > perHeight) { if (heightRequired > perHeight) {
@ -172,19 +183,27 @@ const recordsToDisplay = computed<{
}, },
}) })
} else if (startCol && endCol) { } else if (startCol && endCol) {
// If the range specifies fromCol and endCol
const startDate = dayjs(record.row[startCol!.title!]) const startDate = dayjs(record.row[startCol!.title!])
const endDate = dayjs(record.row[endCol!.title!]) const endDate = dayjs(record.row[endCol!.title!])
let currentWeekStart = startDate.startOf('week') let currentWeekStart = startDate.startOf('week')
const id = record.rowMeta.id ?? generateRandomNumber() 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 ( while (
currentWeekStart.isSameOrBefore(endDate, 'day') && 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]) 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 currentWeekEnd = currentWeekStart.endOf('week')
const recordStart = currentWeekStart.isBefore(startDate) ? startDate : currentWeekStart const recordStart = currentWeekStart.isBefore(startDate) ? startDate : currentWeekStart
const recordEnd = currentWeekEnd.isAfter(endDate) ? endDate : currentWeekEnd 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() let day = recordStart.clone()
while (day.isSameOrBefore(recordEnd)) { while (day.isSameOrBefore(recordEnd)) {
const dateKey = day.format('YYYY-MM-DD') const dateKey = day.format('YYYY-MM-DD')
@ -196,6 +215,7 @@ const recordsToDisplay = computed<{
day = day.add(1, 'day') day = day.add(1, 'day')
} }
// Find the index of the week from the dates array
const weekIndex = Math.max( const weekIndex = Math.max(
dates.value.findIndex((week) => { dates.value.findIndex((week) => {
return ( return (
@ -209,6 +229,7 @@ const recordsToDisplay = computed<{
let maxRecordCount = 0 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++) { for (let i = 0; i < (dates.value[weekIndex] ?? []).length; i++) {
const day = dates.value[weekIndex][i] const day = dates.value[weekIndex][i]
@ -221,7 +242,6 @@ const recordsToDisplay = computed<{
} }
} }
const recordIndex = recordsInDay[dateKey].count const recordIndex = recordsInDay[dateKey].count
maxRecordCount = Math.max(maxRecordCount, recordIndex) maxRecordCount = Math.max(maxRecordCount, recordIndex)
} }
@ -234,15 +254,20 @@ const recordsToDisplay = computed<{
0, 0,
) )
// The left and width of the record is calculated based on the start and end day index
const style: Partial<CSSStyleDeclaration> = { const style: Partial<CSSStyleDeclaration> = {
left: `${startDayIndex * perWidth}px`, left: `${startDayIndex * perWidth}px`,
width: `${(endDayIndex - startDayIndex + 1) * 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 top = weekIndex * perHeight + spaceBetweenRecords + Math.max(maxRecordCount - 1, 0) * perRecordHeight
const heightRequired = perRecordHeight * maxRecordCount + spaceBetweenRecords const heightRequired = perRecordHeight * maxRecordCount + spaceBetweenRecords
let position = 'rounded' 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] const isStartMonthBeforeCurrentWeek = dates.value[weekIndex - 1]
? dayjs(dates.value[weekIndex - 1][0]).isBefore(startDate, 'month') ? dayjs(dates.value[weekIndex - 1][0]).isBefore(startDate, 'month')
: false : false
@ -261,6 +286,8 @@ const recordsToDisplay = computed<{
position = 'none' 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) { if (heightRequired + 15 > perHeight) {
style.display = 'none' style.display = 'none'
for (let i = startDayIndex; i <= endDayIndex; i++) { for (let i = startDayIndex; i <= endDayIndex; i++) {
@ -298,8 +325,7 @@ const recordsToDisplay = computed<{
} }
}) })
const onDrag = (event: MouseEvent) => { const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
if (!isUIAllowed('dataEdit') || !dragRecord.value) return
const { top, height, width, left } = calendarGridContainer.value.getBoundingClientRect() const { top, height, width, left } = calendarGridContainer.value.getBoundingClientRect()
const percentY = (event.clientY - top - window.scrollY) / height 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 week = Math.floor(percentY * dates.value.length)
const day = Math.floor(percentX * 7) 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 const newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
if (!newStartDate) return if (!newStartDate) return
let endDate let endDate
@ -323,14 +345,16 @@ const onDrag = (event: MouseEvent) => {
const newRow = { const newRow = {
...dragRecord.value, ...dragRecord.value,
row: { row: {
...dragRecord.value!.row, ...dragRecord.value.row,
[fromCol!.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'), [fromCol!.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
}, },
} }
const updateProperty = [fromCol!.title!]
if (toCol) { if (toCol) {
const fromDate = dragRecord.value!.row[fromCol!.title!] ? dayjs(dragRecord!.value!.row[fromCol!.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 const toDate = dragRecord.value.row[toCol!.title!] ? dayjs(dragRecord.value.row[toCol!.title!]) : null
if (fromDate && toDate) { if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day') endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
@ -342,17 +366,37 @@ const onDrag = (event: MouseEvent) => {
endDate = newStartDate.clone() 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) => { if (!newRow) return
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) { const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
return newRow
} if (updateSideBar) {
return r 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) => { 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 week = Math.floor(percentY * dates.value.length)
const day = Math.floor(percentX * 7) const day = Math.floor(percentX * 7)
let updateProperty: string[] = []
let newRow: Row
if (resizeDirection.value === 'right') { if (resizeDirection.value === 'right') {
let newEndDate = dates.value[week] ? dayjs(dates.value[week][day]).endOf('day') : null let newEndDate = dates.value[week] ? dayjs(dates.value[week][day]).endOf('day') : null
updateProperty = [toCol!.title!]
const updateProperty = [toCol!.title!]
if (dayjs(newEndDate).isBefore(ogStartDate)) { if (dayjs(newEndDate).isBefore(ogStartDate)) {
newEndDate = dayjs(ogStartDate).clone().endOf('day') newEndDate = dayjs(ogStartDate).clone().endOf('day')
@ -387,51 +433,39 @@ const onResize = (event: MouseEvent) => {
if (!newEndDate) return if (!newEndDate) return
const newRow = { newRow = {
...resizeRecord.value, ...resizeRecord.value,
row: { row: {
...resizeRecord.value.row, ...resizeRecord.value.row,
[toCol!.title!]: dayjs(newEndDate).format('YYYY-MM-DD HH:mm:ssZ'), [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') { } else if (resizeDirection.value === 'left') {
let newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null let newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
const updateProperty = [fromCol!.title!] updateProperty = [fromCol!.title!]
if (dayjs(newStartDate).isAfter(ogEndDate)) { if (dayjs(newStartDate).isAfter(ogEndDate)) {
newStartDate = dayjs(ogEndDate).clone() newStartDate = dayjs(ogEndDate).clone()
} }
if (!newStartDate) return if (!newStartDate) return
const newRow = { newRow = {
...resizeRecord.value, ...resizeRecord.value,
row: { row: {
...resizeRecord.value.row, ...resizeRecord.value.row,
[fromCol!.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'), [fromCol!.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
}, },
} }
}
formattedData.value = formattedData.value.map((r) => { const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
const pk = extractPkFromRow(r.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 pk === newPk ? newRow : r
return newRow })
}
return r
})
useDebouncedRowUpdate(newRow, updateProperty, false) useDebouncedRowUpdate(newRow, updateProperty, false)
}
} }
const onResizeEnd = () => { const onResizeEnd = () => {
@ -463,71 +497,7 @@ const stopDrag = (event: MouseEvent) => {
dragElement.value!.style.boxShadow = 'none' dragElement.value!.style.boxShadow = 'none'
const { top, height, width, left } = calendarGridContainer.value.getBoundingClientRect() const { newRow, updateProperty } = calculateNewRow(event, false)
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 allRecords = document.querySelectorAll('.draggable-record') const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => { allRecords.forEach((el) => {
@ -537,7 +507,6 @@ const stopDrag = (event: MouseEvent) => {
if (dragElement.value) { if (dragElement.value) {
dragElement.value.style.boxShadow = 'none' dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
isDragging.value = false isDragging.value = false
draggingId.value = null draggingId.value = null
dragElement.value = null dragElement.value = null
@ -603,75 +572,13 @@ const dropEvent = (event: DragEvent) => {
}: { }: {
record: Row record: Row
} = JSON.parse(data) } = 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 = { dragRecord.value = record
...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!)
return pk !== extractPkFromRow(newRow.row, meta.value!.columns!) const { newRow, updateProperty } = calculateNewRow(event, true)
})
}
if (dragElement.value) { if (dragElement.value) {
dragElement.value.style.boxShadow = 'none' dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
dragElement.value = null dragElement.value = null
} }
updateRowProperty(newRow, updateProperty, false) 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']) const emit = defineEmits(['resize-start'])
</script> </script>
<template> <template>
@ -42,7 +41,7 @@ const emit = defineEmits(['resize-start'])
'group-hover:(border-brand-500 border-1)': resize, 'group-hover:(border-brand-500 border-1)': resize,
'!border-brand-500 border-1': selected || hover, '!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 <div
v-if="position === 'leftRounded' || position === 'rounded'" 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" class="block h-full min-h-5 w-1 rounded"
></div> ></div>
<div <div v-if="(position === 'leftRounded' || position === 'rounded') && resize" class="mt-0.1 h-7.1 absolute -left-4 resize">
v-if="(position === 'leftRounded' || position === 'rounded') && resize" <NcButton
:class="{ :class="{
'!block !border-1 !rounded-lg !border-brand-500': selected || hover, '!block z-1 !border-brand-500': selected || hover,
}" '!hidden': !selected && !hover,
class="mt-0.1 h-7.1 absolute hidden -left-4 resize" }"
> size="xsmall"
<NcButton size="xsmall" type="secondary" @mousedown.stop="emit('resize-start', 'left', $event, record)"> type="secondary"
@mousedown.stop="emit('resize-start', 'left', $event, record)"
>
<component :is="iconMap.drag" class="text-gray-400"></component> <component :is="iconMap.drag" class="text-gray-400"></component>
</NcButton> </NcButton>
</div> </div>
@ -77,14 +78,16 @@ const emit = defineEmits(['resize-start'])
<span v-if="position === 'leftRounded' || position === 'none'" class="absolute my-0 right-5"> .... </span> <span v-if="position === 'leftRounded' || position === 'none'" class="absolute my-0 right-5"> .... </span>
</div> </div>
<div <div v-if="(position === 'rightRounded' || position === 'rounded') && resize" class="absolute mt-0.1 z-1 -right-4 resize">
v-if="(position === 'rightRounded' || position === 'rounded') && resize" <NcButton
:class="{ :class="{
'!block border-1 rounded-lg border-brand-500': selected || hover, '!block !border-brand-500': selected || hover,
}" '!hidden': !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)" }"
> size="xsmall"
<NcButton size="xsmall" type="secondary" @mousedown.stop="emit('resize-start', 'right', $event, record)"> type="secondary"
@mousedown.stop="emit('resize-start', 'right', $event, record)"
>
<component :is="iconMap.drag" class="text-gray-400"></component> <component :is="iconMap.drag" class="text-gray-400"></component>
</NcButton> </NcButton>
</div> </div>

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

@ -8,7 +8,7 @@ const props = defineProps<{
visible: boolean visible: boolean
}>() }>()
const emit = defineEmits(['expand-record', 'new-record']) const emit = defineEmits(['expand-record', 'newRecord'])
const INFINITY_SCROLL_THRESHOLD = 100 const INFINITY_SCROLL_THRESHOLD = 100
@ -55,7 +55,6 @@ const dragElement = ref<HTMLElement | null>(null)
const dragStart = (event: DragEvent, record: Row) => { const dragStart = (event: DragEvent, record: Row) => {
dragElement.value = event.target as HTMLElement 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)' 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() 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') 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) const height = ref(0)
@ -390,7 +389,7 @@ onUnmounted(() => {
<template v-else-if="renderData.length > 0"> <template v-else-if="renderData.length > 0">
<LazySmartsheetRow v-for="(record, rowIndex) in renderData" :key="rowIndex" :row="record"> <LazySmartsheetRow v-for="(record, rowIndex) in renderData" :key="rowIndex" :row="record">
<LazySmartsheetCalendarSideRecordCard <LazySmartsheetCalendarSideRecordCard
:draggable="sideBarFilterOption === 'withoutDates'" :draggable="sideBarFilterOption === 'withoutDates' && activeCalendarView !== 'year'"
:from-date=" :from-date="
record.rowMeta.range?.fk_from_col record.rowMeta.range?.fk_from_col
? calDataType === UITypes.Date ? 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()) const meta = inject(MetaInj, ref())
// Calculate the dates of the week
const weekDates = computed(() => { const weekDates = computed(() => {
const startOfWeek = new Date(selectedDateRange.value.start!) const startOfWeek = new Date(selectedDateRange.value.start!)
const endOfWeek = new Date(selectedDateRange.value.end!) const endOfWeek = new Date(selectedDateRange.value.end!)
@ -29,30 +30,40 @@ const weekDates = computed(() => {
return datesArray return datesArray
}) })
const findFirstSuitableColumn = (recordsInDay: any, startDayIndex: number, spanDays: number) => { // This function is used to find the first suitable row for a record
let column = 0 // 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) { 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++) { for (let i = 0; i < spanDays; i++) {
const dayIndex = startDayIndex + i const dayIndex = startDayIndex + i
if (!recordsInDay[dayIndex]) { if (!recordsInDay[dayIndex]) {
recordsInDay[dayIndex] = {} recordsInDay[dayIndex] = {}
} }
if (recordsInDay[dayIndex][column]) { // If the row is occupied, the entire span is not suitable
isColumnSuitable = false if (recordsInDay[dayIndex][row]) {
isRowSuitable = false
break break
} }
} }
// If the row is suitable, return it
if (isColumnSuitable) { if (isRowSuitable) {
return column return row
} }
column++ row++
} }
} }
const calendarData = computed(() => { const calendarData = computed(() => {
if (!formattedData.value || !calendarRange.value) return [] 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: { const recordsInDay: {
[key: number]: { [key: number]: {
[key: number]: boolean [key: number]: boolean
@ -74,25 +85,32 @@ const calendarData = computed(() => {
const fromCol = range.fk_from_col const fromCol = range.fk_from_col
const toCol = range.fk_to_col const toCol = range.fk_to_col
if (fromCol && toCol) { 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) => { for (const record of [...formattedData.value].filter((r) => {
const startDate = dayjs(r.row[fromCol.title!]) const startDate = dayjs(r.row[fromCol.title!])
const endDate = dayjs(r.row[toCol.title!]) const endDate = dayjs(r.row[toCol.title!])
if (!startDate.isValid() || !endDate.isValid()) return false if (!startDate.isValid() || !endDate.isValid()) return false
return !endDate.isBefore(startDate) return !endDate.isBefore(startDate)
})) { })) {
// Generate a unique id for the record if it doesn't have one
const id = record.row.id ?? generateRandomNumber() const id = record.row.id ?? generateRandomNumber()
let startDate = dayjs(record.row[fromCol.title!]) let startDate = dayjs(record.row[fromCol.title!])
const ogStartDate = startDate.clone() const ogStartDate = startDate.clone()
const endDate = dayjs(record.row[toCol.title!]) 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)) { if (startDate.isBefore(selectedDateRange.value.start)) {
startDate = dayjs(selectedDateRange.value.start) startDate = dayjs(selectedDateRange.value.start)
} }
const startDaysDiff = startDate.diff(selectedDateRange.value.start, 'day') 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) 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')) { if (endDate.isAfter(startDate, 'month')) {
spanDays = 7 - startDaysDiff spanDays = 7 - startDaysDiff
} }
@ -102,7 +120,8 @@ const calendarData = computed(() => {
} }
const widthStyle = `calc(max(${spanDays} * ${perDayWidth}px, ${perDayWidth}px))` 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++) { for (let i = 0; i < spanDays; i++) {
const dayIndex = startDaysDiff + i const dayIndex = startDaysDiff + i
@ -110,15 +129,15 @@ const calendarData = computed(() => {
recordsInDay[dayIndex] = {} recordsInDay[dayIndex] = {}
} }
if (suitableColumn === -1) { if (suitableRow === -1) {
suitableColumn = findFirstSuitableColumn(recordsInDay, dayIndex, spanDays) suitableRow = findFirstSuitableRow(recordsInDay, dayIndex, spanDays)
} }
} }
// Mark the suitable column as occupied for the entire span // Mark the suitable column as occupied for the entire span
for (let i = 0; i < spanDays; i++) { for (let i = 0; i < spanDays; i++) {
const dayIndex = startDaysDiff + i const dayIndex = startDaysDiff + i
recordsInDay[dayIndex][suitableColumn] = true recordsInDay[dayIndex][suitableRow] = true
} }
let position = 'none' let position = 'none'
@ -127,6 +146,9 @@ const calendarData = computed(() => {
ogStartDate && ogStartDate.isBetween(selectedDateRange.value.start, selectedDateRange.value.end, 'day', '[]') ogStartDate && ogStartDate.isBetween(selectedDateRange.value.start, selectedDateRange.value.end, 'day', '[]')
const isEndInRange = endDate && endDate.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) { if (isStartInRange && isEndInRange) {
position = 'rounded' position = 'rounded'
} else if ( } else if (
@ -158,7 +180,7 @@ const calendarData = computed(() => {
style: { style: {
width: widthStyle, width: widthStyle,
left: `${startDaysDiff * perDayWidth}px`, 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 startDate = dayjs(record.row[fromCol.title!])
const startDaysDiff = Math.max(startDate.diff(selectedDateRange.value.start, 'day'), 0) 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({ recordsInRange.push({
...record, ...record,
@ -182,7 +206,7 @@ const calendarData = computed(() => {
style: { style: {
width: `calc(${perDayWidth}px)`, width: `calc(${perDayWidth}px)`,
left: `${startDaysDiff * 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 dragElement = ref<HTMLElement | null>(null)
const draggingId = ref<string | null>(null)
const resizeInProgress = ref(false) const resizeInProgress = ref(false)
const dragTimeout = ref<ReturnType<typeof setTimeout>>() const dragTimeout = ref<ReturnType<typeof setTimeout>>()
@ -215,11 +237,13 @@ const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[],
updateRowProperty(row, updateProperty, isDelete) updateRowProperty(row, updateProperty, isDelete)
}, 500) }, 500)
// This function is used to calculate the new start and end date of a record when resizing
const onResize = (event: MouseEvent) => { const onResize = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return if (!isUIAllowed('dataEdit') || !container.value || !resizeRecord.value) return
if (!container.value || !resizeRecord.value) return
const { width, left } = container.value.getBoundingClientRect() 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 percentX = (event.clientX - left - window.scrollX) / width
const fromCol = resizeRecord.value.rowMeta.range?.fk_from_col 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 ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
const ogStartDate = dayjs(resizeRecord.value.row[fromCol.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') { 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') 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')) { if (dayjs(newEndDate).isBefore(ogStartDate, 'day')) {
newEndDate = ogStartDate.clone() newEndDate = ogStartDate.clone()
} }
if (!newEndDate.isValid()) return if (!newEndDate.isValid()) return
const newRow = { updateRecord = {
...resizeRecord.value, ...resizeRecord.value,
row: { row: {
...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') { } 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') 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)) { if (dayjs(newStartDate).isAfter(ogEndDate)) {
newStartDate = dayjs(dayjs(ogEndDate)).clone() newStartDate = dayjs(dayjs(ogEndDate)).clone()
} }
if (!newStartDate) return if (!newStartDate) return
const newRow = { updateRecord = {
...resizeRecord.value, ...resizeRecord.value,
row: { row: {
...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) => { // Update the record in the store
const pk = extractPkFromRow(r.row, meta.value!.columns!) 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 pk === newPk ? updateRecord : r
return newRow })
} useDebouncedRowUpdate(updateRecord, updateProperty, false)
return r
})
useDebouncedRowUpdate(newRow, updateProperty, false)
}
} }
const onResizeEnd = () => { const onResizeEnd = () => {
@ -304,22 +324,26 @@ const onResizeStart = (direction: 'right' | 'left', event: MouseEvent, record: R
document.addEventListener('mouseup', onResizeEnd) document.addEventListener('mouseup', onResizeEnd)
} }
const onDrag = (event: MouseEvent) => { // This method is used to calculate the new start and end date of a record when dragging and dropping
if (!isUIAllowed('dataEdit')) return const calculateNewRow = (event: MouseEvent, updateSideBarData?: boolean) => {
if (!container.value || !dragRecord.value) return
const { width, left } = container.value.getBoundingClientRect() 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 percentX = (event.clientX - left - window.scrollX) / width
const fromCol = dragRecord.value.rowMeta.range?.fk_from_col const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
const toCol = dragRecord.value.rowMeta.range?.fk_to_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') const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
if (!newStartDate) return if (!newStartDate) return { updatedProperty: [], newRow: null }
let endDate let endDate
@ -327,14 +351,20 @@ const onDrag = (event: MouseEvent) => {
...dragRecord.value, ...dragRecord.value,
row: { row: {
...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) { if (toCol) {
const fromDate = dragRecord.value.row[fromCol.title!] ? dayjs(dragRecord.value.row[fromCol.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 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) { if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day') endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
} else if (fromDate && !toDate) { } else if (fromDate && !toDate) {
@ -345,17 +375,34 @@ const onDrag = (event: MouseEvent) => {
endDate = newStartDate.clone() 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 newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
const pk = extractPkFromRow(r.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 { updateProperty, newRow }
return newRow }
}
return r const onDrag = (event: MouseEvent) => {
}) if (!isUIAllowed('dataEdit')) return
if (!container.value || !dragRecord.value) return
calculateNewRow(event)
} }
const stopDrag = (event: MouseEvent) => { const stopDrag = (event: MouseEvent) => {
@ -365,69 +412,11 @@ const stopDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return if (!isUIAllowed('dataEdit')) return
if (!isDragging.value || !container.value || !dragRecord.value) return if (!isDragging.value || !container.value || !dragRecord.value) return
const { width, left } = container.value.getBoundingClientRect() const { updateProperty, newRow } = calculateNewRow(event)
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!)
}
if (!newRow) return if (!newRow) return
if (dragElement.value) { // Open drop the record, we reset the opacity of the other records
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 allRecords = document.querySelectorAll('.draggable-record') const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => { allRecords.forEach((el) => {
el.style.visibility = '' el.style.visibility = ''
@ -436,9 +425,6 @@ const stopDrag = (event: MouseEvent) => {
if (dragElement.value) { if (dragElement.value) {
dragElement.value.style.boxShadow = 'none' dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
// isDragging.value = false
draggingId.value = null
dragElement.value = null dragElement.value = null
} }
@ -464,7 +450,6 @@ const dragStart = (event: MouseEvent, record: Row) => {
const allRecords = document.querySelectorAll('.draggable-record') const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => { allRecords.forEach((el) => {
if (!el.getAttribute('data-unique-id').includes(record.rowMeta.id!)) { if (!el.getAttribute('data-unique-id').includes(record.rowMeta.id!)) {
// el.style.visibility = 'hidden'
el.style.opacity = '30%' el.style.opacity = '30%'
} }
}) })
@ -473,7 +458,6 @@ const dragStart = (event: MouseEvent, record: Row) => {
isDragging.value = true isDragging.value = true
dragElement.value = target dragElement.value = target
draggingId.value = record.rowMeta.id!
dragRecord.value = record dragRecord.value = record
document.addEventListener('mousemove', onDrag) document.addEventListener('mousemove', onDrag)
@ -502,75 +486,18 @@ const dropEvent = (event: DragEvent) => {
}: { }: {
record: Row record: Row
} = JSON.parse(data) } = 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!] dragRecord.value = record
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!)
return pk !== extractPkFromRow(newRow.row, meta.value!.columns!) const { updateProperty, newRow } = calculateNewRow(event, true)
})
}
if (dragElement.value) { if (dragElement.value) {
dragElement.value.style.boxShadow = 'none' dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
dragElement.value = null dragElement.value = null
} }
updateRowProperty(newRow, updateProperty, false) updateRowProperty(newRow, updateProperty, false)
dragRecord.value = null
} }
} }
</script> </script>
@ -613,7 +540,7 @@ const dropEvent = (event: DragEvent) => {
:style="{ :style="{
...record.rowMeta.style, ...record.rowMeta.style,
boxShadow: 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)' ? '0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.10)'
: 'none', : 'none',
}" }"
@ -627,6 +554,13 @@ const dropEvent = (event: DragEvent) => {
:hover="hoverRecord === record.rowMeta.id" :hover="hoverRecord === record.rowMeta.id"
:position="record.rowMeta.position" :position="record.rowMeta.position"
:record="record" :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')" :resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
color="blue" color="blue"
@dblclick="emits('expand-record', record)" @dblclick="emits('expand-record', record)"
@ -635,8 +569,8 @@ const dropEvent = (event: DragEvent) => {
<template v-if="!isRowEmpty(record, displayField)"> <template v-if="!isRowEmpty(record, displayField)">
<div <div
:class="{ :class="{
'!mt-2': displayField.uidt === UITypes.SingleLineText, 'mt-2': displayField.uidt === UITypes.SingleLineText,
'!mt-1': displayField.uidt === UITypes.MultiSelect || displayField.uidt === UITypes.SingleSelect, 'mt-1': displayField.uidt === UITypes.MultiSelect || displayField.uidt === UITypes.SingleSelect,
}" }"
> >
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
@ -663,13 +597,9 @@ const dropEvent = (event: DragEvent) => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.hide {
transition: 0.01s;
transform: translateX(-9999px);
}
.prevent-select { .prevent-select {
-webkit-user-select: none; /* Safari */ -webkit-user-select: none;
-ms-user-select: none; /* IE 10 and IE 11 */ -ms-user-select: none;
user-select: none; /* Standard syntax */ user-select: none;
} }
</style> </style>

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

@ -28,6 +28,7 @@ const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref()) 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 = computed(() => {
const datesHours: Array<Array<dayjs.Dayjs>> = [] const datesHours: Array<Array<dayjs.Dayjs>> = []
let startOfWeek = dayjs(selectedDateRange.value.start) ?? dayjs().startOf('week') 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 scheduleStart = dayjs(selectedDateRange.value.start).startOf('day')
const scheduleEnd = dayjs(selectedDateRange.value.end).endOf('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: { const overlaps: {
[key: string]: { [key: string]: {
[key: string]: { [key: string]: {
@ -91,6 +95,8 @@ const recordsAcrossAllRange = computed<{
const fromCol = range.fk_from_col const fromCol = range.fk_from_col
const toCol = range.fk_to_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 sortedFormattedData = [...formattedData.value].filter((record) => {
const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null
@ -107,14 +113,20 @@ const recordsAcrossAllRange = computed<{
sortedFormattedData.forEach((record: Row) => { sortedFormattedData.forEach((record: Row) => {
if (!toCol && fromCol) { if (!toCol && fromCol) {
// If there is no toColumn chosen in the range
const startDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null const startDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
if (!startDate) return 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 dateKey = startDate?.format('YYYY-MM-DD')
const hourKey = startDate?.startOf('hour').format('HH:mm') const hourKey = startDate?.startOf('hour').format('HH:mm')
const id = record.rowMeta.id ?? generateRandomNumber() const id = record.rowMeta.id ?? generateRandomNumber()
let style: Partial<CSSStyleDeclaration> = {} let style: Partial<CSSStyleDeclaration> = {}
// If the dateKey and hourKey are valid, we add the id to the overlaps object
if (dateKey && hourKey) { if (dateKey && hourKey) {
if (!overlaps[dateKey]) { if (!overlaps[dateKey]) {
overlaps[dateKey] = {} overlaps[dateKey] = {}
@ -128,18 +140,23 @@ const recordsAcrossAllRange = computed<{
} }
overlaps[dateKey][hourKey].id.push(id) 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) { if (overlaps[dateKey][hourKey].id.length > 4) {
overlaps[dateKey][hourKey].overflow = true overlaps[dateKey][hourKey].overflow = true
style.display = 'none' style.display = 'none'
overlaps[dateKey][hourKey].overflowCount += 1 overlaps[dateKey][hourKey].overflowCount += 1
} }
// TODO: dayIndex is not calculated perfectly
// Should revisit this part in next iteration
let dayIndex = dayjs(dateKey).day() - 1 let dayIndex = dayjs(dateKey).day() - 1
if (dayIndex === -1) { if (dayIndex === -1) {
dayIndex = 6 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( const hourIndex = Math.max(
datesHours.value[dayIndex].findIndex((h) => h.startOf('hour').format('HH:mm') === hourKey), datesHours.value[dayIndex].findIndex((h) => h.startOf('hour').format('HH:mm') === hourKey),
0, 0,
@ -168,12 +185,17 @@ const recordsAcrossAllRange = computed<{
let startDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null let startDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
let endDate = record.row[toCol.title!] ? dayjs(record.row[toCol.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 (!startDate?.isValid()) return
// If the end date is not valid, we set it to 30 minutes after the start date
if (!endDate?.isValid()) { if (!endDate?.isValid()) {
endDate = startDate.clone().add(30, 'minutes') 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')) { if (startDate.isBefore(scheduleStart, 'minutes')) {
startDate = scheduleStart startDate = scheduleStart
} }
@ -181,8 +203,10 @@ const recordsAcrossAllRange = computed<{
endDate = scheduleEnd endDate = scheduleEnd
} }
// Setting the current start date to the start date of the record
let currentStartDate: dayjs.Dayjs = startDate.clone() 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')) { while (currentStartDate.isSameOrBefore(endDate!, 'day')) {
const currentEndDate = currentStartDate.clone().endOf('day') const currentEndDate = currentStartDate.clone().endOf('day')
const recordStart: dayjs.Dayjs = currentEndDate.isSame(startDate, 'day') ? startDate : currentStartDate const recordStart: dayjs.Dayjs = currentEndDate.isSame(startDate, 'day') ? startDate : currentStartDate
@ -190,12 +214,14 @@ const recordsAcrossAllRange = computed<{
const dateKey = recordStart.format('YYYY-MM-DD') 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 let dayIndex = recordStart.day() - 1
if (dayIndex === -1) { if (dayIndex === -1) {
dayIndex = 6 dayIndex = 6
} }
// We calculate the index of the start and end hour in the day
const startHourIndex = Math.max( const startHourIndex = Math.max(
(datesHours.value[dayIndex] ?? []).findIndex((h) => h.format('HH:mm') === recordStart.format('HH:mm')), (datesHours.value[dayIndex] ?? []).findIndex((h) => h.format('HH:mm') === recordStart.format('HH:mm')),
0, 0,
@ -226,6 +252,7 @@ const recordsAcrossAllRange = computed<{
let style: Partial<CSSStyleDeclaration> = {} 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) { while (_startHourIndex <= endHourIndex) {
const hourKey = datesHours.value[dayIndex][_startHourIndex].format('HH:mm') const hourKey = datesHours.value[dayIndex][_startHourIndex].format('HH:mm')
if (!overlaps[dateKey]) { if (!overlaps[dateKey]) {
@ -240,6 +267,8 @@ const recordsAcrossAllRange = computed<{
} }
overlaps[dateKey][hourKey].id.push(id) 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) { if (overlaps[dateKey][hourKey].id.length > 4) {
overlaps[dateKey][hourKey].overflow = true overlaps[dateKey][hourKey].overflow = true
style.display = 'none' style.display = 'none'
@ -272,20 +301,27 @@ const recordsAcrossAllRange = computed<{
dayIndex, dayIndex,
}, },
}) })
// We set the current start date to the next day
currentStartDate = currentStartDate.add(1, 'day').hour(0).minute(0) 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) => { 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 maxOverlaps = 1
let overlapIndex = 0 let overlapIndex = 0
const dayIndex = record.rowMeta.dayIndex as number const dayIndex = record.rowMeta.dayIndex as number
const dateKey = dayjs(selectedDateRange.value.start).add(dayIndex, 'day').format('YYYY-MM-DD') const dateKey = dayjs(selectedDateRange.value.start).add(dayIndex, 'day').format('YYYY-MM-DD')
for (const hours in overlaps[dateKey]) { 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!)) { if (overlaps[dateKey][hours].id.includes(record.rowMeta.id!)) {
maxOverlaps = Math.max(maxOverlaps, overlaps[dateKey][hours].id.length - overlaps[dateKey][hours].overflowCount) 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!)) 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 dragElement = ref<HTMLElement | null>(null)
const draggingId = ref<string | null>(null)
const resizeInProgress = ref(false) const resizeInProgress = ref(false)
const dragTimeout = ref<ReturnType<typeof setTimeout>>() const dragTimeout = ref<ReturnType<typeof setTimeout>>()
@ -333,85 +367,83 @@ const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[],
}, 500) }, 500)
const onResize = (event: MouseEvent) => { const onResize = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return if (!isUIAllowed('dataEdit') || !container.value || !resizeRecord.value) return
if (!container.value || !resizeRecord.value) return
const { width, left, top, bottom } = container.value.getBoundingClientRect() const { width, left, top, bottom } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value 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) { if (event.clientY > bottom - 20) {
container.value.scrollTop += 10 container.value.scrollTop += 10
} else if (event.clientY < top + 20) { } else if (event.clientY < top + 20) {
container.value.scrollTop -= 10 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 percentX = (event.clientX - left - window.scrollX) / width
const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight
const fromCol = resizeRecord.value.rowMeta.range?.fk_from_col const fromCol = resizeRecord.value.rowMeta.range?.fk_from_col
const toCol = resizeRecord.value.rowMeta.range?.fk_to_col const toCol = resizeRecord.value.rowMeta.range?.fk_to_col
if (!fromCol || !toCol) return if (!fromCol || !toCol) return
const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!]) const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
const ogStartDate = dayjs(resizeRecord.value.row[fromCol.title!]) const ogStartDate = dayjs(resizeRecord.value.row[fromCol.title!])
const day = Math.floor(percentX * 7) 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') { if (resizeDirection.value === 'right') {
let newEndDate = dayjs(selectedDateRange.value.start).add(day, 'day').add(hour, 'hour') 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')) { if (dayjs(newEndDate).isBefore(ogStartDate, 'day')) {
newEndDate = ogStartDate.clone() newEndDate = ogStartDate.clone()
} }
if (!newEndDate.isValid()) return if (!newEndDate.isValid()) return
const newRow = { newRow = {
...resizeRecord.value, ...resizeRecord.value,
row: { row: {
...resizeRecord.value.row, ...resizeRecord.value.row,
[toCol.title!]: newEndDate.format('YYYY-MM-DD HH:mm:ssZ'), [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') { } else if (resizeDirection.value === 'left') {
let newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day').add(hour, 'hour') 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)) { if (dayjs(newStartDate).isAfter(ogEndDate)) {
newStartDate = dayjs(dayjs(ogEndDate)).clone() newStartDate = dayjs(dayjs(ogEndDate)).clone()
} }
if (!newStartDate) return if (!newStartDate) return
const newRow = { newRow = {
...resizeRecord.value, ...resizeRecord.value,
row: { row: {
...resizeRecord.value.row, ...resizeRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'), [fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
}, },
} }
}
formattedData.value = formattedData.value.map((r) => { const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) { formattedData.value = formattedData.value.map((r) => {
return newRow const pk = extractPkFromRow(r.row, meta.value!.columns!)
} return pk === newPk ? newRow : r
return r })
}) useDebouncedRowUpdate(newRow, updateProperty, false)
useDebouncedRowUpdate(newRow, updateProperty, false)
}
} }
const onResizeEnd = () => { const onResizeEnd = () => {
@ -430,16 +462,10 @@ const onResizeStart = (direction: 'right' | 'left', event: MouseEvent, record: R
document.addEventListener('mouseup', onResizeEnd) document.addEventListener('mouseup', onResizeEnd)
} }
const onDrag = (event: MouseEvent) => { // We calculate the new row based on the mouse position and update the record
if (!isUIAllowed('dataEdit')) return // We also update the sidebar data if the dropped from the sidebar
if (!container.value || !dragRecord.value) return const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
const { width, left, top, bottom } = container.value.getBoundingClientRect() const { width, left, top } = container.value.getBoundingClientRect()
if (event.clientY > bottom - 20) {
container.value.scrollTop += 10
} else if (event.clientY < top + 20) {
container.value.scrollTop -= 10
}
const { scrollHeight } = container.value const { scrollHeight } = container.value
@ -452,12 +478,13 @@ const onDrag = (event: MouseEvent) => {
if (!fromCol) return if (!fromCol) return
const day = Math.floor(percentX * 7) 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') const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day').add(hour, 'hour')
if (!newStartDate) return if (!newStartDate) return
let endDate let endDate
const updatedProperty = [fromCol.title!]
const newRow = { const newRow = {
...dragRecord.value, ...dragRecord.value,
@ -482,92 +509,53 @@ const onDrag = (event: MouseEvent) => {
} }
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ') newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
updatedProperty.push(toCol.title!)
} }
formattedData.value = formattedData.value.map((r) => { if (!newRow) return { newRow: null, updatedProperty }
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
event.preventDefault() const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
clearTimeout(dragTimeout.value!)
const { width, left, top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight if (updateSideBar) {
const percentX = (event.clientX - left - window.scrollX) / width formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const fromCol = dragRecord.value.rowMeta.range?.fk_from_col const pk = extractPkFromRow(r.row, meta.value!.columns!)
const toCol = dragRecord.value.rowMeta.range?.fk_to_col return pk !== newPk
})
const day = Math.floor(percentX * 7) } else {
const hour = Math.max(Math.min(Math.floor(percentY * 24), 24), 0) formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day').add(hour, 'hour') return pk === newPk ? newRow : r
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!] return { newRow, updatedProperty }
}
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 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) { const stopDrag = (event: MouseEvent) => {
formattedData.value = formattedData.value.map((r) => { if (!isUIAllowed('dataEdit') || !isDragging.value || !container.value || !dragRecord.value) return
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) { event.preventDefault()
return newRow clearTimeout(dragTimeout.value!)
}
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, updatedProperty } = calculateNewRow(event, false)
})
}
// We set the visibility and opacity of the records back to normal
const allRecords = document.querySelectorAll('.draggable-record') const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => { allRecords.forEach((el) => {
el.style.visibility = '' el.style.visibility = ''
@ -576,13 +564,10 @@ const stopDrag = (event: MouseEvent) => {
if (dragElement.value) { if (dragElement.value) {
dragElement.value.style.boxShadow = 'none' dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
// isDragging.value = false
draggingId.value = null
dragElement.value = null dragElement.value = null
} }
updateRowProperty(newRow, updateProperty, false) updateRowProperty(newRow, updatedProperty, false)
document.removeEventListener('mousemove', onDrag) document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag) document.removeEventListener('mouseup', stopDrag)
@ -613,7 +598,6 @@ const dragStart = (event: MouseEvent, record: Row) => {
isDragging.value = true isDragging.value = true
dragElement.value = target dragElement.value = target
draggingId.value = record.rowMeta.id!
dragRecord.value = record dragRecord.value = record
document.addEventListener('mousemove', onDrag) document.addEventListener('mousemove', onDrag)
@ -642,79 +626,12 @@ const dropEvent = (event: DragEvent) => {
}: { }: {
record: Row record: Row
} = JSON.parse(data) } = 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) dragRecord.value = record
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!)
})
}
if (dragElement.value) { const { newRow, updatedProperty } = calculateNewRow(event, true)
dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
dragElement.value = null updateRowProperty(newRow, updatedProperty, false)
}
updateRowProperty(newRow, updateProperty, false)
} }
} }

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

@ -131,6 +131,16 @@ reloadViewDataHook?.on(async () => {
await Promise.all([loadCalendarData(), loadSidebarData()]) 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(() => { const headerText = computed(() => {
switch (activeCalendarView.value) { switch (activeCalendarView.value) {
case 'day': 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-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"> <div class="flex justify-start gap-3 items-center">
<NcTooltip> <NcTooltip>
<template #title> Previous </template> <template #title> {{ $t('labels.previous') }}</template>
<NcButton <NcButton
v-e="`['c:calendar:calendar-${activeCalendarView}-prev-btn']`" v-e="`['c:calendar:calendar-${activeCalendarView}-prev-btn']`"
data-testid="nc-calendar-prev-btn" data-testid="nc-calendar-prev-btn"
@ -207,7 +217,7 @@ const headerText = computed(() => {
</template> </template>
</NcDropdown> </NcDropdown>
<NcTooltip> <NcTooltip>
<template #title> Next </template> <template #title> {{ $t('labels.next') }}</template>
<NcButton <NcButton
v-e="`['c:calendar:calendar-${activeCalendarView}-next-btn']`" v-e="`['c:calendar:calendar-${activeCalendarView}-next-btn']`"
data-testid="nc-calendar-next-btn" data-testid="nc-calendar-next-btn"
@ -224,26 +234,16 @@ const headerText = computed(() => {
data-testid="nc-calendar-today-btn" data-testid="nc-calendar-today-btn"
size="small" size="small"
type="secondary" type="secondary"
@click=" @click="goToToday"
() => {
selectedDate = dayjs()
pageDate = dayjs()
selectedMonth = dayjs()
selectedDateRange = {
start: dayjs().startOf('week'),
end: dayjs().endOf('week'),
}
}
"
> >
Go to Today {{ $t('activity.goToToday') }}
</NcButton> </NcButton>
<span class="opacity-0" data-testid="nc-active-calendar-view"> <span class="opacity-0" data-testid="nc-active-calendar-view">
{{ activeCalendarView }} {{ activeCalendarView }}
</span> </span>
</div> </div>
<NcTooltip> <NcTooltip>
<template #title> Toggle Sidebar </template> <template #title> {{ $t('activity.toggleSidebar') }}</template>
<NcButton <NcButton
v-if="!isMobileMode" v-if="!isMobileMode"
v-e="`['c:calendar:calendar-${activeCalendarView}-toggle-sidebar']`" 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> <script lang="ts" setup>
import { UITypes, isSystemColumn } from 'nocodb-sdk' import { UITypes, isSystemColumn } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue' import type { SelectProps } from 'ant-design-vue'
import { type CalendarRangeType } from '~/lib'
import { import {
ActiveViewInj, ActiveViewInj,
IsLockedInj, IsLockedInj,
@ -40,8 +42,8 @@ watch(
{ immediate: true }, { immediate: true },
) )
const calendarRange = computed<{ fk_from_column_id: string; fk_to_column_id: string | null }[]>(() => { const calendarRange = computed<CalendarRangeType[]>(() => {
const tempCalendarRange: { fk_from_column_id: string; fk_to_column_id: string | null }[] = [] const tempCalendarRange: CalendarRangeType[] = []
if (!activeView.value || !activeView.value.view) return tempCalendarRange if (!activeView.value || !activeView.value.view) return tempCalendarRange
activeView.value.view.calendar_range?.forEach((range) => { 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 // 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 () => { const saveCalendarRanges = async () => {
if (activeView.value) { if (activeView.value) {
@ -66,7 +68,7 @@ const saveCalendarRanges = async () => {
fk_to_column_id: range.fk_to_column_id, fk_to_column_id: range.fk_to_column_id,
})) }))
await $api.dbView.calendarUpdate(activeView.value?.id as string, { 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 loadCalendarMeta()
await Promise.all([loadCalendarData(), loadSidebarData()]) await Promise.all([loadCalendarData(), loadSidebarData()])

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

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

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

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

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

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

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

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

Loading…
Cancel
Save