|
|
|
<script lang="ts" setup>
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
import type { ColumnType } from 'nocodb-sdk'
|
|
|
|
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
|
|
|
|
import { isRowEmpty } from '~/utils'
|
|
|
|
|
|
|
|
const emit = defineEmits(['expand-record', 'new-record'])
|
|
|
|
|
|
|
|
const meta = inject(MetaInj, ref())
|
|
|
|
|
|
|
|
const container = ref()
|
|
|
|
|
|
|
|
const { isUIAllowed } = useRoles()
|
|
|
|
|
|
|
|
const { selectedDate, formattedData, formattedSideBarData, calendarRange, updateRowProperty, displayField } =
|
|
|
|
useCalendarViewStoreOrThrow()
|
|
|
|
|
|
|
|
const fields = inject(FieldsInj, ref())
|
|
|
|
|
|
|
|
const { fields: _fields } = useViewColumnsOrThrow()
|
|
|
|
|
|
|
|
const getFieldStyle = (field: ColumnType) => {
|
|
|
|
const fi = _fields.value.find((f) => f.title === field.title)
|
|
|
|
|
|
|
|
return {
|
|
|
|
underline: fi.underline,
|
|
|
|
bold: fi.bold,
|
|
|
|
italic: fi.italic,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const fieldsWithoutDisplay = computed(() => fields.value.filter((f) => !isPrimary(f)))
|
|
|
|
|
|
|
|
// We loop through all the records and calculate the position of each record based on the range
|
|
|
|
// We only need to calculate the top, of the record since there is no overlap in the day view of date Field
|
|
|
|
const recordsAcrossAllRange = computed<Row[]>(() => {
|
|
|
|
let dayRecordCount = 0
|
|
|
|
const perRecordHeight = 40
|
|
|
|
|
|
|
|
if (!calendarRange.value) return []
|
|
|
|
|
|
|
|
const recordsByRange: Array<Row> = []
|
|
|
|
|
|
|
|
calendarRange.value.forEach((range) => {
|
|
|
|
const fromCol = range.fk_from_col
|
|
|
|
const endCol = range.fk_to_col
|
|
|
|
if (fromCol && endCol) {
|
|
|
|
for (const record of formattedData.value) {
|
|
|
|
const startDate = dayjs(record.row[fromCol.title!])
|
|
|
|
const endDate = dayjs(record.row[endCol.title!])
|
|
|
|
|
|
|
|
dayRecordCount++
|
|
|
|
|
|
|
|
const style: Partial<CSSStyleDeclaration> = {
|
|
|
|
top: `${(dayRecordCount - 1) * perRecordHeight}px`,
|
|
|
|
width: '100%',
|
|
|
|
}
|
|
|
|
|
|
|
|
// This property is used to determine which side the record should be rounded. It can be left, right, both or none
|
|
|
|
let position = 'none'
|
|
|
|
const isSelectedDay = (date: dayjs.Dayjs) => date.isSame(selectedDate.value, 'day')
|
|
|
|
const isBeforeSelectedDay = (date: dayjs.Dayjs) => date.isBefore(selectedDate.value, 'day')
|
|
|
|
const isAfterSelectedDay = (date: dayjs.Dayjs) => date.isAfter(selectedDate.value, 'day')
|
|
|
|
|
|
|
|
if (isSelectedDay(startDate) && isSelectedDay(endDate)) {
|
|
|
|
position = 'rounded'
|
|
|
|
} else if (isBeforeSelectedDay(startDate) && isAfterSelectedDay(endDate)) {
|
|
|
|
position = 'none'
|
|
|
|
} else if (isSelectedDay(startDate) && isAfterSelectedDay(endDate)) {
|
|
|
|
position = 'leftRounded'
|
|
|
|
} else if (isBeforeSelectedDay(startDate) && isSelectedDay(endDate)) {
|
|
|
|
position = 'rightRounded'
|
|
|
|
} else {
|
|
|
|
position = 'none'
|
|
|
|
}
|
|
|
|
|
|
|
|
recordsByRange.push({
|
|
|
|
...record,
|
|
|
|
rowMeta: {
|
|
|
|
...record.rowMeta,
|
|
|
|
position,
|
|
|
|
style,
|
|
|
|
range: range as any,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else if (fromCol) {
|
|
|
|
for (const record of formattedData.value) {
|
|
|
|
dayRecordCount++
|
|
|
|
recordsByRange.push({
|
|
|
|
...record,
|
|
|
|
rowMeta: {
|
|
|
|
...record.rowMeta,
|
|
|
|
range: range as any,
|
|
|
|
style: {
|
|
|
|
width: '100%',
|
|
|
|
left: '0',
|
|
|
|
top: `${(dayRecordCount - 1) * perRecordHeight}px`,
|
|
|
|
},
|
|
|
|
position: 'rounded',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return recordsByRange
|
|
|
|
})
|
|
|
|
|
|
|
|
const dragElement = ref<HTMLElement | null>(null)
|
|
|
|
|
|
|
|
const hoverRecord = ref<string | null>(null)
|
|
|
|
|
|
|
|
// We support drag and drop from the sidebar to the day view of the date field
|
|
|
|
const dropEvent = (event: DragEvent) => {
|
|
|
|
if (!isUIAllowed('dataEdit')) return
|
|
|
|
event.preventDefault()
|
|
|
|
const data = event.dataTransfer?.getData('text/plain')
|
|
|
|
if (data) {
|
|
|
|
const {
|
|
|
|
record,
|
|
|
|
}: {
|
|
|
|
record: Row
|
|
|
|
initialClickOffsetY: number
|
|
|
|
initialClickOffsetX: number
|
|
|
|
} = JSON.parse(data)
|
|
|
|
|
|
|
|
const fromCol = record.rowMeta.range?.fk_from_col
|
|
|
|
const toCol = record.rowMeta.range?.fk_to_col
|
|
|
|
|
|
|
|
if (!fromCol) return
|
|
|
|
|
|
|
|
const newStartDate = dayjs(selectedDate.value).startOf('day')
|
|
|
|
|
|
|
|
let endDate
|
|
|
|
|
|
|
|
const newRow = {
|
|
|
|
...record,
|
|
|
|
row: {
|
|
|
|
...record.row,
|
|
|
|
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
const updateProperty = [fromCol.title!]
|
|
|
|
|
|
|
|
if (toCol) {
|
|
|
|
const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
|
|
|
|
const toDate = record.row[toCol.title!] ? dayjs(record.row[toCol.title!]) : null
|
|
|
|
|
|
|
|
if (fromDate && toDate) {
|
|
|
|
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
|
|
|
|
} else if (fromDate && !toDate) {
|
|
|
|
endDate = dayjs(newStartDate).endOf('day')
|
|
|
|
} else if (!fromDate && toDate) {
|
|
|
|
endDate = dayjs(newStartDate).endOf('day')
|
|
|
|
} else {
|
|
|
|
endDate = newStartDate.clone()
|
|
|
|
}
|
|
|
|
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
|
|
|
|
updateProperty.push(toCol.title!)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!newRow) return
|
|
|
|
|
|
|
|
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!)
|
|
|
|
return pk === newPk ? newRow : r
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
formattedData.value = [...formattedData.value, newRow]
|
|
|
|
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
|
|
|
|
return extractPkFromRow(r.row, meta.value!.columns!) !== newPk
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dragElement.value) {
|
|
|
|
dragElement.value.style.boxShadow = 'none'
|
|
|
|
dragElement.value = null
|
|
|
|
}
|
|
|
|
updateRowProperty(newRow, updateProperty, false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<div
|
|
|
|
v-if="recordsAcrossAllRange.length"
|
|
|
|
ref="container"
|
|
|
|
class="w-full relative h-[calc(100vh-10.8rem)] overflow-y-auto nc-scrollbar-md"
|
|
|
|
data-testid="nc-calendar-day-view"
|
|
|
|
@drop="dropEvent"
|
|
|
|
>
|
|
|
|
<div
|
|
|
|
v-for="(record, rowIndex) in recordsAcrossAllRange"
|
|
|
|
:key="rowIndex"
|
|
|
|
:style="record.rowMeta.style"
|
|
|
|
class="absolute mt-2"
|
|
|
|
data-testid="nc-calendar-day-record-card"
|
|
|
|
@mouseleave="hoverRecord = null"
|
|
|
|
@mouseover="hoverRecord = record.rowMeta.id as string"
|
|
|
|
>
|
|
|
|
<LazySmartsheetRow :row="record">
|
|
|
|
<LazySmartsheetCalendarRecordCard
|
|
|
|
:position="record.rowMeta.position"
|
|
|
|
:record="record"
|
|
|
|
:resize="false"
|
|
|
|
color="blue"
|
|
|
|
size="small"
|
|
|
|
@click="emit('expand-record', record)"
|
|
|
|
>
|
|
|
|
<template v-if="!isRowEmpty(record, displayField)">
|
|
|
|
<LazySmartsheetCalendarCell
|
|
|
|
v-if="!isRowEmpty(record, displayField!)"
|
|
|
|
v-model="record.row[displayField!.title!]"
|
|
|
|
:bold="getFieldStyle(displayField!).bold"
|
|
|
|
:column="displayField!"
|
|
|
|
:italic="getFieldStyle(displayField!).italic"
|
|
|
|
:underline="getFieldStyle(displayField!).underline"
|
|
|
|
/>
|
|
|
|
</template>
|
|
|
|
<template v-for="(field, id) in fieldsWithoutDisplay">
|
|
|
|
<LazySmartsheetCalendarCell
|
|
|
|
v-if="!isRowEmpty(record, field)"
|
|
|
|
:key="id"
|
|
|
|
v-model="record.row[field!.title!]"
|
|
|
|
:bold="getFieldStyle(field).bold"
|
|
|
|
:column="field"
|
|
|
|
:italic="getFieldStyle(field).italic"
|
|
|
|
:underline="getFieldStyle(field).underline"
|
|
|
|
/>
|
|
|
|
</template>
|
|
|
|
</LazySmartsheetCalendarRecordCard>
|
|
|
|
</LazySmartsheetRow>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div
|
|
|
|
v-else
|
|
|
|
ref="container"
|
|
|
|
class="w-full h-full flex text-md font-bold text-gray-500 items-center justify-center"
|
|
|
|
@drop="dropEvent"
|
|
|
|
>
|
|
|
|
No records in this day
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<style lang="scss" scoped></style>
|