Browse Source

feat(nc-gui): split date time for week view

pull/7611/head
DarkPhoenix2704 10 months ago
parent
commit
fd3c3d0045
  1. 2
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  2. 101
      packages/nc-gui/components/smartsheet/calendar/DayView/RecordCard.vue
  3. 4
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  4. 27
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  5. 545
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  6. 9
      packages/nc-gui/components/smartsheet/calendar/index.vue

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

@ -92,7 +92,7 @@ const recordsAcrossAllRange = computed<Row[]>(() => {
const style: Partial<CSSStyleDeclaration> = {
top: `${finalTopInPixels}px`,
height: `${heightInPixels - 5}px`,
height: `${heightInPixels}px`,
}
let position = 'none'

101
packages/nc-gui/components/smartsheet/calendar/DayView/RecordCard.vue

@ -0,0 +1,101 @@
<script lang="ts" setup>
interface Props {
record: Record<string, string>
name: string
date?: string
color?: string
resize?: boolean
selected?: boolean
size?: 'small' | 'medium' | 'large' | 'auto'
showDate?: boolean
position?: 'leftRounded' | 'rightRounded' | 'rounded' | 'none'
}
withDefaults(defineProps<Props>(), {
name: '',
date: '',
resize: true,
selected: false,
color: 'blue',
size: 'small',
showDate: true,
position: 'rounded',
})
const emit = defineEmits(['resize-start'])
</script>
<template>
<div
:class="{
'min-h-9': size === 'small',
'min-h-10': size === 'medium',
'min-h-12': size === 'large',
'h-full': size === 'auto',
'rounded-l-lg ml-3': position === 'leftRounded',
'rounded-r-lg mr-3': position === 'rightRounded',
'rounded-lg mx-3': position === 'rounded',
'rounded-none': position === 'none',
'bg-maroon-50': color === 'maroon',
'bg-blue-50': color === 'blue',
'bg-green-50': color === 'green',
'bg-yellow-50': color === 'yellow',
'bg-pink-50': color === 'pink',
'bg-purple-50': color === 'purple',
'group-hover:(border-brand-500 border-2)': resize,
'border-brand-500 border-2': selected,
}"
class="relative group"
>
<div class="h-full absolute py-2">
<div
v-if="position === 'leftRounded' || position === 'rounded'"
:class="{
'bg-maroon-500': color === 'maroon',
'bg-blue-500': color === 'blue',
'bg-green-500': color === 'green',
'bg-yellow-500': color === 'yellow',
'bg-pink-500': color === 'pink',
'bg-purple-500': color === 'purple',
}"
class="block h-full min-h-5 ml-1 w-1 rounded mr-2"
></div>
</div>
<div
v-if="(position === 'leftRounded' || position === 'rounded') && resize"
:class="{
'!block border-2 rounded-lg border-brand-500': selected,
}"
class="absolute mt-0.6 h-7.1 hidden -left-3 resize !group-hover:(border-brand-500 block border-2 rounded-lg)"
>
<NcButton size="xsmall" type="secondary" @mousedown.stop="emit('resize-start', 'left', $event, record)">
<component :is="iconMap.drag" class="text-gray-400"></component>
</NcButton>
</div>
<div class="ml-3 mt-2 pr-3 text-ellipsis overflow-hidden w-full h-6 absolute">
<span v-if="position === 'rightRounded' || position === 'none'"> .... </span>
<span class="text-sm text-gray-800">{{ name }}</span>
<span v-if="showDate" class="text-xs ml-1 text-gray-600">{{ date }}</span>
<span v-if="position === 'leftRounded' || position === 'none'" class="absolute my-0 right-5"> .... </span>
</div>
<div
v-if="(position === 'rightRounded' || position === 'rounded') && resize"
:class="{
'!block border-2 rounded-lg border-brand-500': selected,
}"
class="absolute mt-0.6 hidden h-7.1 -right-3 border-1 resize !group-hover:(border-brand-500 border-2 block rounded-lg)"
>
<NcButton size="xsmall" type="secondary" @mousedown.stop="emit('resize-start', 'right', $event, record)">
<component :is="iconMap.drag" class="text-gray-400"></component>
</NcButton>
</div>
</div>
</template>
<style lang="scss" scoped>
.resize {
cursor: ew-resize;
}
</style>

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

@ -376,12 +376,12 @@ const onResize = (event: MouseEvent) => {
const day = Math.floor(percentX * 7)
if (resizeDirection.value === 'right') {
let newEndDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
let newEndDate = dates.value[week] ? dayjs(dates.value[week][day]).endOf('day') : null
const updateProperty = [toCol.title]
if (dayjs(newEndDate).isBefore(ogStartDate)) {
newEndDate = dayjs(ogStartDate).clone()
newEndDate = dayjs(ogStartDate).clone().endOf('day')
}
if (!newEndDate) return

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

@ -1,21 +1,12 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { UITypes } from 'nocodb-sdk'
import type { Row } from '~/lib'
import { ref } from '#imports'
const emits = defineEmits(['expand-record'])
const {
selectedDateRange,
formattedData,
formattedSideBarData,
calendarRange,
selectedDate,
displayField,
calDataType,
updateRowProperty,
} = useCalendarViewStoreOrThrow()
const { selectedDateRange, formattedData, formattedSideBarData, calendarRange, selectedDate, displayField, updateRowProperty } =
useCalendarViewStoreOrThrow()
const container = ref<null | HTMLElement>(null)
@ -212,7 +203,7 @@ const draggingId = ref<string | null>(null)
const resizeInProgress = ref(false)
const dragTimeout = ref(null)
const dragTimeout = ref<string | number | null | NodeJS.Timeout>(null)
const isDragging = ref(false)
const dragRecord = ref<Row>()
@ -369,7 +360,7 @@ const onDrag = (event: MouseEvent) => {
const stopDrag = (event: MouseEvent) => {
event.preventDefault()
clearTimeout(dragTimeout.value)
clearTimeout(dragTimeout.value!)
if (!isUIAllowed('dataEdit')) return
if (!isDragging.value || !container.value || !dragRecord.value) return
@ -490,10 +481,10 @@ const dragStart = (event: MouseEvent, record: Row) => {
}, 200)
const onMouseUp = () => {
clearTimeout(dragTimeout.value)
clearTimeout(dragTimeout.value!)
document.removeEventListener('mouseup', onMouseUp)
if (!isDragging.value) {
emit('expand-record', record)
emits('expand-record', record)
}
}
@ -623,11 +614,7 @@ const dropEvent = (event: DragEvent) => {
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarRecordCard
:date="
calDataType === UITypes.DateTime
? dayjs(record.row[record.rowMeta.range!.fk_from_col.title!]).format('DD-MM-YYYY HH:MM')
: dayjs(record.row[record.rowMeta.range!.fk_from_col.title!]).format('DD-MM-YYYY')
"
:date="dayjs(record.row[record.rowMeta.range!.fk_from_col.title!]).format('DD-MM-YYYY')"
:name="record.row[displayField!.title!]"
:position="record.rowMeta.position"
:record="record"

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

@ -0,0 +1,545 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import type { Row } from '~/lib'
import { ref } from '#imports'
const emits = defineEmits(['expand-record'])
const { selectedDateRange, formattedData, formattedSideBarData, calendarRange, selectedDate, displayField, updateRowProperty } =
useCalendarViewStoreOrThrow()
const container = ref<null | HTMLElement>(null)
const { width: containerWidth } = useElementSize(container)
const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref())
const weekDates = computed(() => {
const startOfWeek = new Date(selectedDateRange.value.start!)
const endOfWeek = new Date(selectedDateRange.value.end!)
const datesArray = []
while (startOfWeek.getTime() <= endOfWeek.getTime()) {
datesArray.push(new Date(startOfWeek))
startOfWeek.setDate(startOfWeek.getDate() + 1)
}
return datesArray
})
function getRandomNumbers() {
const typedArray = new Uint8Array(10)
const randomValues = window.crypto.getRandomValues(typedArray)
return randomValues.join('')
}
const findFirstSuitableColumn = (recordsInDay: any, startDayIndex: number, spanDays: number) => {
let column = 0
while (true) {
let isColumnSuitable = true
for (let i = 0; i < spanDays; i++) {
const dayIndex = startDayIndex + i
if (!recordsInDay[dayIndex]) {
recordsInDay[dayIndex] = {}
}
if (recordsInDay[dayIndex][column]) {
isColumnSuitable = false
break
}
}
if (isColumnSuitable) {
return column
}
column++
}
}
const calendarData = computed(() => {
if (!formattedData.value || !calendarRange.value) return []
const recordsInDay: {
[key: number]: {
[key: number]: boolean
}
} = {
0: {},
1: {},
2: {},
3: {},
4: {},
5: {},
6: {},
}
const recordsInRange: Array<Row> = []
const perDayWidth = containerWidth.value / 7
calendarRange.value.forEach((range) => {
const fromCol = range.fk_from_col
const toCol = range.fk_to_col
if (fromCol && toCol) {
for (const record of [...formattedData.value].filter((r) => {
const startDate = dayjs(r.row[fromCol.title!])
const endDate = dayjs(r.row[toCol.title!])
if (!startDate.isValid() || !endDate.isValid()) return false
return !endDate.isBefore(startDate)
})) {
const id = record.row.id ?? getRandomNumbers()
let startDate = dayjs(record.row[fromCol.title!])
const ogStartDate = startDate.clone()
const endDate = dayjs(record.row[toCol.title!])
if (startDate.isBefore(selectedDateRange.value.start)) {
startDate = dayjs(selectedDateRange.value.start)
}
const startDaysDiff = startDate.diff(selectedDateRange.value.start, 'day')
let spanDays = Math.max(Math.min(endDate.diff(startDate, 'day'), 6) + 1, 1)
if (endDate.isAfter(startDate, 'month')) {
spanDays = 7 - startDaysDiff
}
if (startDaysDiff > 0) {
spanDays = Math.min(spanDays, 7 - startDaysDiff)
}
const widthStyle = `calc(max(${spanDays} * ${perDayWidth}px, ${perDayWidth}px))`
let suitableColumn = -1
for (let i = 0; i < spanDays; i++) {
const dayIndex = startDaysDiff + i
if (!recordsInDay[dayIndex]) {
recordsInDay[dayIndex] = {}
}
if (suitableColumn === -1) {
suitableColumn = findFirstSuitableColumn(recordsInDay, dayIndex, spanDays)
}
}
// Mark the suitable column as occupied for the entire span
for (let i = 0; i < spanDays; i++) {
const dayIndex = startDaysDiff + i
recordsInDay[dayIndex][suitableColumn] = true
}
let position = 'none'
const isStartInRange =
ogStartDate && ogStartDate.isBetween(selectedDateRange.value.start, selectedDateRange.value.end, 'day', '[]')
const isEndInRange = endDate && endDate.isBetween(selectedDateRange.value.start, selectedDateRange.value.end, 'day', '[]')
if (isStartInRange && isEndInRange) {
position = 'rounded'
} else if (
startDate &&
endDate &&
startDate.isBefore(selectedDateRange.value.start) &&
endDate.isAfter(selectedDateRange.value.end)
) {
position = 'none'
} else if (
startDate &&
endDate &&
(startDate.isAfter(selectedDateRange.value.end) || endDate.isBefore(selectedDateRange.value.start))
) {
position = 'none'
} else if (isStartInRange) {
position = 'leftRounded'
} else if (isEndInRange) {
position = 'rightRounded'
}
recordsInRange.push({
...record,
rowMeta: {
...record.rowMeta,
range: range as any,
position,
id,
style: {
width: widthStyle,
left: `${startDaysDiff * perDayWidth}px`,
top: `${suitableColumn * 40}px`,
},
},
})
}
} else if (fromCol) {
for (const record of formattedData.value) {
const id = record.row.id ?? getRandomNumbers()
const startDate = dayjs(record.row[fromCol.title!])
const startDaysDiff = Math.max(startDate.diff(selectedDateRange.value.start, 'day'), 0)
const suitableColumn = findFirstSuitableColumn(recordsInDay, startDaysDiff, 1)
recordsInDay[startDaysDiff][suitableColumn] = true
recordsInRange.push({
...record,
rowMeta: {
...record.rowMeta,
range: range as any,
id,
position: 'rounded',
style: {
width: `calc(${perDayWidth}px)`,
left: `${startDaysDiff * perDayWidth}px`,
top: `${suitableColumn * 40}px`,
},
},
})
}
}
})
return recordsInRange
})
const dragElement = ref<HTMLElement | null>(null)
const draggingId = ref<string | null>(null)
const resizeInProgress = ref(false)
const dragTimeout = ref<string | number | null | NodeJS.Timeout>(null)
const isDragging = ref(false)
const dragRecord = ref<Row>()
const onDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return
if (!container.value || !dragRecord.value) return
const { width, left } = container.value.getBoundingClientRect()
const percentX = (event.clientX - left - window.scrollX) / width
const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
const toCol = dragRecord.value.rowMeta.range?.fk_to_col
if (!fromCol) return
const day = Math.floor(percentX * 7)
const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
if (!newStartDate) return
let endDate
const newRow = {
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD'),
},
}
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')
}
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) => {
event.preventDefault()
clearTimeout(dragTimeout.value!)
if (!isUIAllowed('dataEdit')) return
if (!isDragging.value || !container.value || !dragRecord.value) return
const { width, left } = container.value.getBoundingClientRect()
const percentX = (event.clientX - left - window.scrollX) / width
const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
const toCol = dragRecord.value.rowMeta.range?.fk_to_col
const day = Math.floor(percentX * 7)
const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
if (!newStartDate || !fromCol) return
let endDate
const newRow = {
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD'),
},
}
const updateProperty = [fromCol.title!]
if (toCol) {
const fromDate = dragRecord.value.row[fromCol.title!] ? dayjs(dragRecord.value.row[fromCol.title!]) : null
const toDate = dragRecord.value.row[toCol.title!] ? dayjs(dragRecord.value.row[toCol.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
} else if (fromDate && !toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else if (!fromDate && toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD')
updateProperty.push(toCol.title!)
}
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')
allRecords.forEach((el) => {
el.style.visibility = ''
el.style.opacity = '100%'
})
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
// isDragging.value = false
draggingId.value = null
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
}
const dragStart = (event: MouseEvent, record: Row) => {
if (!isUIAllowed('dataEdit')) return
if (resizeInProgress.value) return
let target = event.target as HTMLElement
isDragging.value = false
dragTimeout.value = setTimeout(() => {
isDragging.value = true
while (!target.classList.contains('draggable-record')) {
target = target.parentElement as HTMLElement
}
const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => {
if (!el.getAttribute('data-unique-id').includes(record.rowMeta.id!)) {
// el.style.visibility = 'hidden'
el.style.opacity = '30%'
}
})
dragRecord.value = record
isDragging.value = true
dragElement.value = target
draggingId.value = record.rowMeta.id!
dragRecord.value = record
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
}, 200)
const onMouseUp = () => {
clearTimeout(dragTimeout.value!)
document.removeEventListener('mouseup', onMouseUp)
if (!isDragging.value) {
emits('expand-record', record)
}
}
document.addEventListener('mouseup', onMouseUp)
}
const dropEvent = (event: DragEvent) => {
if (!isUIAllowed('dataEdit')) return
event.preventDefault()
const data = event.dataTransfer?.getData('text/plain')
if (data) {
const {
record,
}: {
record: Row
} = JSON.parse(data)
const { width, left } = container.value.getBoundingClientRect()
const percentX = (event.clientX - left - window.scrollX) / width
const fromCol = record.rowMeta.range?.fk_from_col
const toCol = record.rowMeta.range?.fk_to_col
if (!fromCol) return
const day = Math.floor(percentX * 7)
const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
let endDate
const newRow = {
...record,
row: {
...record.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD'),
},
}
const updateProperty = [fromCol.title!]
if (toCol) {
const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
const toDate = record.row[toCol.title!] ? dayjs(record.row[toCol.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
} else if (fromDate && !toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else if (!fromDate && toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD')
updateProperty.push(toCol.title!)
}
if (!newRow) return
if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
return newRow
}
return r
})
} else {
formattedData.value = [...formattedData.value, newRow]
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk !== extractPkFromRow(newRow.row, meta.value!.columns!)
})
}
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value.classList.remove('hide')
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)
}
}
</script>
<template>
<div class="flex relative flex-col prevent-select" @drop="dropEvent">
<div class="flex">
<div
v-for="date in weekDates"
:key="date.toISOString()"
class="w-1/7 text-center text-sm text-gray-500 w-full py-1 border-gray-200 border-b-1 border-r-1 bg-gray-50"
>
{{ dayjs(date).format('DD ddd') }}
</div>
</div>
<div ref="container" class="flex h-[calc(100vh-11.6rem)]">
<div
v-for="date in weekDates"
:key="date.toISOString()"
:class="{
'!border-2 border-brand-500': dayjs(date).isSame(selectedDate, 'day'),
}"
class="flex flex-col border-r-1 min-h-[100vh] last:border-r-0 items-center w-1/7"
@click="selectedDate = date"
></div>
</div>
<div class="absolute nc-scrollbar-md overflow-y-auto mt-9 pointer-events-none inset-0">
<div
v-for="(record, id) in calendarData"
:key="id"
:data-unique-id="record.rowMeta.id"
:style="{
...record.rowMeta.style,
boxShadow:
record.rowMeta.id === draggingId
? '0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.10)'
: 'none',
}"
class="absolute group draggable-record pointer-events-auto"
@mousedown="dragStart($event, record)"
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarRecordCard
:date="dayjs(record.row[record.rowMeta.range!.fk_from_col.title!]).format('DD-MM-YYYY HH:mm')"
:name="record.row[displayField!.title!]"
:position="record.rowMeta.position"
:record="record"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
color="blue"
@dblclick="emits('expand-record', record)"
/>
</LazySmartsheetRow>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.hide {
transition: 0.01s;
transform: translateX(-9999px);
}
.prevent-select {
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
}
</style>

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

@ -233,8 +233,13 @@ const headerText = computed(() => {
@expand-record="expandRecord"
@new-record="newRecord"
/>
<LazySmartsheetCalendarWeekView
v-else-if="activeCalendarView === 'week'"
<LazySmartsheetCalendarWeekViewDateField
v-else-if="activeCalendarView === 'week' && calDataType === UITypes.Date"
@expand-record="expandRecord"
@new-record="newRecord"
/>
<LazySmartsheetCalendarWeekViewDateField
v-else-if="activeCalendarView === 'week' && calDataType === UITypes.DateTime"
@expand-record="expandRecord"
@new-record="newRecord"
/>

Loading…
Cancel
Save