Browse Source

Merge pull request #7753 from nocodb/nc-feat/cal-refine

feat(nc-gui): refined ranges support & end Date for Date Field
pull/7805/head
Raju Udava 9 months ago committed by GitHub
parent
commit
15fb48d0e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      packages/nc-gui/assets/style.scss
  2. 74
      packages/nc-gui/components/dlg/ViewCreate.vue
  3. 6
      packages/nc-gui/components/nc/DateWeekSelector.vue
  4. 6
      packages/nc-gui/components/nc/MonthYearSelector.vue
  5. 14
      packages/nc-gui/components/smartsheet/calendar/Cell.vue
  6. 26
      packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue
  7. 386
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  8. 88
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  9. 23
      packages/nc-gui/components/smartsheet/calendar/RecordCard.vue
  10. 23
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  11. 4
      packages/nc-gui/components/smartsheet/calendar/SideRecordCard.vue
  12. 15
      packages/nc-gui/components/smartsheet/calendar/VRecordCard.vue
  13. 56
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  14. 40
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  15. 8
      packages/nc-gui/components/smartsheet/calendar/index.vue
  16. 121
      packages/nc-gui/components/smartsheet/toolbar/CalendarRange.vue
  17. 21
      packages/nc-gui/composables/useCalendarViewStore.ts
  18. 4
      packages/nc-gui/lib/types.ts
  19. 12
      packages/nocodb/src/db/conditionV2.ts
  20. 1
      packages/nocodb/src/services/calendar-datas.service.ts

3
packages/nc-gui/assets/style.scss

@ -784,4 +784,5 @@ svg.nc-cell-icon, svg.nc-virtual-cell-icon {
@apply !flex !pl-4 max-w-[calc(100%_-_16px)]; @apply !flex !pl-4 max-w-[calc(100%_-_16px)];
} }
} }
} }

74
packages/nc-gui/components/dlg/ViewCreate.vue

@ -218,7 +218,6 @@ async function onSubmit() {
} }
/* /*
TODO: Add support for end date and multiple range in future
const addCalendarRange = async () => { const addCalendarRange = async () => {
form.calendar_range.push({ form.calendar_range.push({
fk_from_column_id: viewSelectFieldOptions.value[0].value as string, fk_from_column_id: viewSelectFieldOptions.value[0].value as string,
@ -318,7 +317,7 @@ onMounted(async () => {
<template> <template>
<NcModal <NcModal
v-model:visible="vModel" v-model:visible="vModel"
:size="[ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(form.type) ? 'medium' : 'small'" :size="[ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(form.type) ? 'small' : 'small'"
> >
<template #header> <template #header>
<div class="flex w-full flex-row justify-between items-center"> <div class="flex w-full flex-row justify-between items-center">
@ -453,23 +452,20 @@ onMounted(async () => {
</div> </div>
</a-select-option> </a-select-option>
</NcSelect> </NcSelect>
<!-- <!-- <div
<div v-if="range.fk_to_column_id === null && isEeUI"
v-if="range.fk_to_column_id === null && isEeUI" class="cursor-pointer flex items-center text-gray-800 gap-1"
class="cursor-pointer flex items-center text-gray-800 gap-1" @click="range.fk_to_column_id = undefined"
@click="range.fk_to_column_id = undefined" >
> <component :is="iconMap.plus" class="h-4 w-4" />
<component :is="iconMap.plus" class="h-4 w-4" /> {{ $t('activity.addEndDate') }}
{{ $t('activity.addEndDate') }} </div>
</div> <template v-else-if="isEeUI">
<template v-else-if="isEeUI"> <span>
<span> {{ $t('activity.withEndDate') }}
{{ $t('activity.withEndDate') }} </span>
</span>
<div class="flex">
TODO: Add support for end date and multiple range in future
<div class="flex">
<NcSelect <NcSelect
v-model:value="range.fk_to_column_id" v-model:value="range.fk_to_column_id"
:disabled="isMetaLoading" :disabled="isMetaLoading"
@ -499,27 +495,29 @@ class="cursor-pointer flex items-center text-gray-800 gap-1"
</NcSelect> </NcSelect>
<NcButton class="!rounded-l-none !border-l-0" size="small" type="secondary" @click="range.fk_to_column_id = null"> <NcButton class="!rounded-l-none !border-l-0" size="small" type="secondary" @click="range.fk_to_column_id = null">
<component :is="iconMap.delete" class="h-4 w-4" /> <component :is="iconMap.delete" class="h-4 w-4" />
</NcButton> --> </NcButton>
</div> </div>
</template> <NcButton
<!-- <NcButton v-if="index !== 0"
v-if="index !== 0" size="small"
size="small" type="secondary"
type="secondary" @click="
@click=" () => {
() => { form.calendar_range = form.calendar_range.filter((_, i) => i !== index)
form.calendar_range = form.calendar_range.filter((_, i) => i !== index) }
} "
" >
> <component :is="iconMap.close" />
<component :is="iconMap.close" /> </NcButton>
</NcButton> </template>
</div> </div> -->
<NcButton class="mt-2" size="small" type="secondary" @click="addCalendarRange">
<!-- <NcButton class="mt-2" size="small" type="secondary" @click="addCalendarRange">
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
Add another date field Add another date field
</NcButton> </NcButton> -->
--> </div>
</template>
</a-form> </a-form>
<div v-else-if="!isNecessaryColumnsPresent" class="flex flex-row p-4 border-gray-200 border-1 gap-x-4 rounded-lg w-full"> <div v-else-if="!isNecessaryColumnsPresent" class="flex flex-row p-4 border-gray-200 border-1 gap-x-4 rounded-lg w-full">
<GeneralIcon class="!text-5xl text-orange-500" icon="warning" /> <GeneralIcon class="!text-5xl text-orange-500" icon="warning" />

6
packages/nc-gui/components/nc/DateWeekSelector.vue

@ -152,7 +152,7 @@ const paginate = (action: 'next' | 'prev') => {
class="flex items-center" class="flex items-center"
> >
<NcTooltip> <NcTooltip>
<NcButton v-if="!disablePagination" size="medium" type="secondary" @click="paginate('prev')"> <NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.doubleLeftArrow" class="h-4 w-4" /> <component :is="iconMap.doubleLeftArrow" class="h-4 w-4" />
</NcButton> </NcButton>
<template #title> <template #title>
@ -165,11 +165,11 @@ const paginate = (action: 'next' | 'prev') => {
'text-xs': size === 'small', 'text-xs': size === 'small',
'text-sm': size === 'medium', 'text-sm': size === 'medium',
}" }"
class="font-bold text-gray-700" class="text-gray-700"
>{{ currentMonthYear }}</span >{{ currentMonthYear }}</span
> >
<NcTooltip> <NcTooltip>
<NcButton v-if="!disablePagination" size="medium" type="secondary" @click="paginate('next')"> <NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4" /> <component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
</NcButton> </NcButton>
<template #title> <template #title>

6
packages/nc-gui/components/nc/MonthYearSelector.vue

@ -95,7 +95,7 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
<span>{{ $t('labels.previous') }}</span> <span>{{ $t('labels.previous') }}</span>
</template> </template>
</NcTooltip> </NcTooltip>
<span class="font-bold text-gray-700">{{ isYearPicker ? $t('labels.selectYear') : pageDate.year() }}</span> <span class="text-gray-700">{{ isYearPicker ? $t('labels.selectYear') : pageDate.year() }}</span>
<NcTooltip> <NcTooltip>
<NcButton size="small" type="secondary" @click="paginate('next')"> <NcButton size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4" /> <component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
@ -114,7 +114,7 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
:class="{ :class="{
'!bg-brand-50 border-1 !border-brand-500': isMonthSelected(month), '!bg-brand-50 border-1 !border-brand-500': isMonthSelected(month),
}" }"
class="h-9 rounded-lg flex font-medium items-center justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer" class="h-9 rounded-lg flex items-center justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer"
@click="selectedDate = month" @click="selectedDate = month"
> >
{{ month.format('MMM') }} {{ month.format('MMM') }}
@ -127,7 +127,7 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
:class="{ :class="{
'!bg-brand-50 !border-1 !border-brand-500': compareYear(year, selectedDate), '!bg-brand-50 !border-1 !border-brand-500': compareYear(year, selectedDate),
}" }"
class="h-9 rounded-lg flex font-medium items-center justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer" class="h-9 rounded-lg flex items-center justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer"
@click="selectedDate = year" @click="selectedDate = year"
> >
{{ year.format('YYYY') }} {{ year.format('YYYY') }}

14
packages/nc-gui/components/smartsheet/calendar/Cell.vue

@ -356,6 +356,7 @@ const parseValue = (value: any, col: ColumnType): string => {
<template> <template>
<span <span
class="calendar-cell text-xs before:px-1"
:class="{ :class="{
'font-bold': bold, 'font-bold': bold,
'italic': italic, 'italic': italic,
@ -367,4 +368,15 @@ const parseValue = (value: any, col: ColumnType): string => {
</span> </span>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.calendar-cell {
&::before {
content: '•';
padding: 0 4px;
}
&:first-child::before {
content: '';
padding: 0;
}
}
</style>

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

@ -20,22 +20,22 @@ const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow() const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType) => { const getFieldStyle = (field: ColumnType) => {
const fi = _fields.value.find((f) => f.title === field.title) const fi = _fields.value?.find((f) => f.title === field.title)
return { return {
underline: fi.underline, underline: fi?.underline,
bold: fi.bold, bold: fi?.bold,
italic: fi.italic, italic: fi?.italic,
} }
} }
const fieldsWithoutDisplay = computed(() => fields.value.filter((f) => !isPrimary(f))) 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 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 // 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 = 28
if (!calendarRange.value) return [] if (!calendarRange.value) return []
@ -183,6 +183,17 @@ const dropEvent = (event: DragEvent) => {
updateRowProperty(newRow, updateProperty, false) updateRowProperty(newRow, updateProperty, false)
} }
} }
// TODO: Add Support for multiple ranges when multiple ranges are supported
const newRecord = () => {
if (!isUIAllowed('dataEdit') || !calendarRange.value?.length) return
const record = {
row: {
[calendarRange.value[0].fk_from_col!.title!]: selectedDate.value.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('newRecord', record)
}
</script> </script>
<template> <template>
@ -191,6 +202,7 @@ const dropEvent = (event: DragEvent) => {
ref="container" ref="container"
class="w-full relative h-[calc(100vh-10.8rem)] overflow-y-auto nc-scrollbar-md" class="w-full relative h-[calc(100vh-10.8rem)] overflow-y-auto nc-scrollbar-md"
data-testid="nc-calendar-day-view" data-testid="nc-calendar-day-view"
@dblclick="newRecord"
@drop="dropEvent" @drop="dropEvent"
> >
<div <div
@ -223,6 +235,7 @@ const dropEvent = (event: DragEvent) => {
</template> </template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id"> <template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell <LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]" v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold" :bold="getFieldStyle(field).bold"
:column="field" :column="field"
@ -240,6 +253,7 @@ const dropEvent = (event: DragEvent) => {
ref="container" ref="container"
class="w-full h-full flex text-md font-bold text-gray-500 items-center justify-center" class="w-full h-full flex text-md font-bold text-gray-500 items-center justify-center"
@drop="dropEvent" @drop="dropEvent"
@dblclick="newRecord"
> >
No records in this day No records in this day
</div> </div>

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

@ -52,6 +52,146 @@ const hours = computed(() => {
return hours return hours
}) })
const calculateNewDates = ({
endDate,
startDate,
scheduleStart,
scheduleEnd,
}: {
endDate: dayjs.Dayjs
startDate: dayjs.Dayjs
scheduleStart: dayjs.Dayjs
scheduleEnd: dayjs.Dayjs
}) => {
// If there is no end date, we add 15 minutes to the start date and use that as the end date
if (!endDate.isValid()) {
endDate = startDate.clone().add(15, '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.isSameOrBefore(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)) {
endDate = scheduleEnd
}
return { endDate, startDate }
}
/* const getGridTime = (date: dayjs.Dayjs, round = false) => {
const gridCalc = date.hour() * 60 + date.minute()
if (round) {
return Math.ceil(gridCalc)
} else {
return Math.floor(gridCalc)
}
}
const getGridTimeSlots = (from: dayjs.Dayjs, to: dayjs.Dayjs) => {
return {
from: getGridTime(from, false),
to: getGridTime(to, true) - 1,
}
} */
/* const hasSlotForRecord = (
record: Row,
columnArray: Row[],
dates: {
fromDate: dayjs.Dayjs
toDate: dayjs.Dayjs
},
) => {
const { fromDate, toDate } = dates
if (!fromDate || !toDate) return false
for (const column of columnArray) {
const columnFromCol = column.rowMeta.range?.fk_from_col
const columnToCol = column.rowMeta.range?.fk_to_col
if (!columnFromCol) return false
const { startDate: columnFromDate, endDate: columnToDate } = calculateNewDates({
startDate: dayjs(column.row[columnFromCol.title!]),
endDate: columnToCol ? dayjs(column.row[columnToCol.title!]) : dayjs(column.row[columnFromCol.title!]).add(1, 'hour'),
scheduleStart: dayjs(selectedDate.value).startOf('day'),
scheduleEnd: dayjs(selectedDate.value).endOf('day'),
})
if (
fromDate.isBetween(columnFromDate, columnToDate, null, '[]') ||
toDate.isBetween(columnFromDate, columnToDate, null, '[]')
) {
return false
}
}
return true
} */
/* const getMaxOfGrid = (
{
fromDate,
toDate,
}: {
fromDate: dayjs.Dayjs
toDate: dayjs.Dayjs
},
gridTimeMap: Map<number, number>,
) => {
let max = 0
const gridTimes = getGridTimeSlots(fromDate, toDate)
for (let gridCounter = gridTimes.from; gridCounter <= gridTimes.to; gridCounter++) {
if (gridTimeMap.has(gridCounter) && gridTimeMap.get(gridCounter) > max) {
max = gridTimeMap.get(gridCounter)
}
}
return max
} */
/* const isOverlaps = (row1: Row, row2: Row) => {
const fromCol1 = row1.rowMeta.range?.fk_from_col
const toCol1 = row1.rowMeta.range?.fk_to_col
const fromCol2 = row2.rowMeta.range?.fk_from_col
const toCol2 = row2.rowMeta.range?.fk_to_col
if (!fromCol1 || !fromCol2) return false
const { startDate: startDate1, endDate: endDate1 } = calculateNewDates({
endDate: toCol1 ? dayjs(row1.row[toCol1.title!]) : dayjs(row1.row[fromCol1.title!]).add(1, 'hour'),
startDate: dayjs(row1.row[fromCol1.title!]),
scheduleStart: dayjs(selectedDate.value).startOf('day'),
scheduleEnd: dayjs(selectedDate.value).endOf('day'),
})
const { startDate: startDate2, endDate: endDate2 } = calculateNewDates({
endDate: toCol2 ? dayjs(row2.row[toCol2.title!]) : dayjs(row2.row[fromCol2.title!]).add(1, 'hour'),
startDate: dayjs(row2.row[fromCol2.title!]),
scheduleStart: dayjs(selectedDate.value).startOf('day'),
scheduleEnd: dayjs(selectedDate.value).endOf('day'),
})
return startDate1.isBetween(startDate2, endDate2, null, '[]') || endDate1.isBetween(startDate2, endDate2, null, '[]')
} */
/* const getMaxOverlaps = ({ row, rowArray }: { row: Row; rowArray: Row[] }) => {
let maxOverlaps = row.rowMeta.numberOfOverlaps
for (const record of rowArray) {
if (isOverlaps(row, record)) {
if (!record.rowMeta.numberOfOverlaps || !row.rowMeta.numberOfOverlaps) continue
if (record.rowMeta.numberOfOverlaps > row.rowMeta.numberOfOverlaps) {
maxOverlaps = record.rowMeta.numberOfOverlaps
}
}
}
return maxOverlaps
} */
const recordsAcrossAllRange = computed<{ const recordsAcrossAllRange = computed<{
record: Row[] record: Row[]
count: { count: {
@ -82,6 +222,9 @@ const recordsAcrossAllRange = computed<{
const perRecordHeight = 80 const perRecordHeight = 80
/* const columnArray: Array<Array<Row>> = [[]]
const gridTimeMap = new Map() */
let recordsByRange: Array<Row> = [] let recordsByRange: Array<Row> = []
calendarRange.value.forEach((range) => { calendarRange.value.forEach((range) => {
@ -90,48 +233,36 @@ const recordsAcrossAllRange = computed<{
// We fetch all the records that match the calendar ranges in a single time. // 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 // 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]
const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null .filter((record) => {
const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null
if (fromCol && endCol) {
const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
const toDate = record.row[endCol.title!] ? dayjs(record.row[endCol.title!]) : null
return fromDate && toDate?.isValid() ? fromDate.isBefore(toDate) : true
} else if (fromCol && !endCol) {
return !!fromDate
}
return false
})
// If there is a start and end column, we calculate the top and height of the record based on the start and end date
if (fromCol && endCol) {
for (const record of sortedFormattedData) {
// We use this id during the drag and drop operation and to keep track of the number of records that overlap at a given time
const id = generateRandomNumber()
let startDate = dayjs(record.row[fromCol.title!])
let endDate = dayjs(record.row[endCol.title!])
// If no start date is provided or startDate is after the endDate, we skip the record
if (!startDate.isValid() || startDate.isAfter(endDate)) continue
// If there is no end date, we add 30 minutes to the start date and use that as the end date if (fromCol && endCol) {
if (!endDate.isValid()) { const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
endDate = startDate.clone().add(30, 'minutes') const toDate = record.row[endCol.title!] ? dayjs(record.row[endCol.title!]) : null
}
// If the start date is before the opened date, we use the schedule start as the start date return fromDate && toDate?.isValid() ? fromDate.isSameOrBefore(toDate) : true
// This is to ensure the generated style of the record is not outside the bounds of the calendar } else if (fromCol && !endCol) {
if (startDate.isBefore(scheduleStart, 'minutes')) { return !!fromDate
startDate = scheduleStart
} }
return false
})
.sort((a, b) => {
const aDate = dayjs(a.row[fromCol!.title!])
const bDate = dayjs(b.row[fromCol!.title!])
return aDate.isBefore(bDate) ? 1 : -1
})
// If the end date is after the schedule end, we use the schedule end as the end date for (const record of sortedFormattedData) {
// This is to ensure the generated style of the record is not outside the bounds of the calendar const id = record.rowMeta.id ?? generateRandomNumber()
if (endDate.isAfter(scheduleEnd, 'minutes')) {
endDate = scheduleEnd
}
if (fromCol && endCol) {
const { endDate, startDate } = calculateNewDates({
endDate: dayjs(record.row[endCol.title!]),
startDate: dayjs(record.row[fromCol.title!]),
scheduleStart,
scheduleEnd,
})
// The top of the record is calculated based on the start hour and minute // 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
@ -140,7 +271,6 @@ const recordsAcrossAllRange = computed<{
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()
let _startDate = startDate.clone() let _startDate = startDate.clone()
const style: Partial<CSSStyleDeclaration> = { const style: Partial<CSSStyleDeclaration> = {
@ -148,7 +278,7 @@ 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 // We loop through every 1 minutes between the start and end date and keep track of the number of records that overlap at a given time
// If the number of records exceeds 4, we hide the record and show a button to view more records // 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')
@ -199,18 +329,13 @@ const recordsAcrossAllRange = computed<{
range: range as any, range: range as any,
}, },
}) })
} } else if (fromCol) {
} else if (fromCol) { const { startDate, endDate } = calculateNewDates({
for (const record of sortedFormattedData) { startDate: dayjs(record.row[fromCol.title!]),
const id = generateRandomNumber() endDate: dayjs(record.row[fromCol.title!]).add(1, 'hour'),
scheduleStart,
const startDate = dayjs(record.row[fromCol.title!]) scheduleEnd,
})
let endDate = dayjs(record.row[fromCol.title!]).add(1, 'hour')
if (endDate.isAfter(scheduleEnd, 'minutes')) {
endDate = scheduleEnd
}
const startHour = startDate.hour() const startHour = startDate.hour()
@ -218,7 +343,7 @@ const recordsAcrossAllRange = computed<{
let _startDate = startDate.clone() let _startDate = startDate.clone()
// We loop through every minute between the start and end date and keep track of the number of records that overlap at a given time // We loop through every minute between the start and end date and keep track of the number of records that overlap at a given time
while (_startDate.isBefore(endDate)) { while (_startDate.isBefore(endDate, 'minute')) {
const timeKey = _startDate.format('HH:mm') const timeKey = _startDate.format('HH:mm')
if (!overlaps[timeKey]) { if (!overlaps[timeKey]) {
@ -239,6 +364,7 @@ const recordsAcrossAllRange = computed<{
display: 'none', display: 'none',
} }
} }
_startDate = _startDate.add(1, 'minute') _startDate = _startDate.add(1, 'minute')
} }
@ -273,6 +399,13 @@ const recordsAcrossAllRange = computed<{
} }
} }
}) })
/*
recordsByRange.sort((a, b) => {
const fromColA = a.rowMeta.range?.fk_from_col
const fromColB = b.rowMeta.range?.fk_from_col
if (!fromColA || !fromColB) return 0
return dayjs(a.row[fromColA.title!]).isBefore(dayjs(b.row[fromColB.title!])) ? -1 : 1
}) */
// We can't calculate the width & left of the records without knowing the number of records that overlap at a given time // 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 // 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
@ -287,12 +420,9 @@ const recordsAcrossAllRange = computed<{
overlapIndex = Math.max(overlaps[minutes].id.indexOf(record.rowMeta.id!), overlapIndex) overlapIndex = Math.max(overlaps[minutes].id.indexOf(record.rowMeta.id!), overlapIndex)
} }
} }
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 = {
...record.rowMeta.style, ...record.rowMeta.style,
left: `${leftPerRecord - 0.08}%`, left: `${leftPerRecord - 0.08}%`,
@ -301,6 +431,92 @@ const recordsAcrossAllRange = computed<{
return record return record
}) })
// TODO: Rewrite the calculations for the style of the records
/* for (const record of recordsByRange) {
const fromCol = record.rowMeta.range?.fk_from_col
const toCol = record.rowMeta.range?.fk_to_col
if (!fromCol) continue
const { startDate, endDate } = calculateNewDates({
startDate: dayjs(record.row[fromCol.title!]),
endDate: toCol ? dayjs(record.row[toCol.title!]) : dayjs(record.row[fromCol.title!]).add(1, 'hour'),
scheduleStart,
scheduleEnd,
})
const gridTimes = getGridTimeSlots(startDate, endDate)
for (let gridCounter = gridTimes.from; gridCounter <= gridTimes.to; gridCounter++) {
if (gridTimeMap.has(gridCounter)) {
gridTimeMap.set(gridCounter, gridTimeMap.get(gridCounter) + 1)
} else {
gridTimeMap.set(gridCounter, 1)
}
}
let foundAColumn = false
for (const column in columnArray) {
if (
hasSlotForRecord(record, columnArray[column], {
fromDate: startDate,
toDate: endDate,
})
) {
columnArray[column].push(record)
foundAColumn = true
break
}
}
if (!foundAColumn) {
columnArray.push([record])
}
}
for (const columnIndex in columnArray) {
for (const record of columnArray[columnIndex]) {
const recordRange = record.rowMeta.range
const fromCol = recordRange?.fk_from_col
const toCol = recordRange?.fk_to_col
if (!fromCol) continue
const { startDate, endDate } = calculateNewDates({
startDate: dayjs(record.row[fromCol.title!]),
endDate: toCol ? dayjs(record.row[toCol.title!]) : dayjs(record.row[fromCol.title!]).add(1, 'hour'),
scheduleStart: dayjs(selectedDate.value).startOf('day'),
scheduleEnd: dayjs(selectedDate.value).endOf('day'),
})
record.rowMeta.numberOfOverlaps =
getMaxOfGrid(
{
fromDate: startDate,
toDate: endDate,
},
gridTimeMap,
) - 1
record.rowMeta.overLapIteration = parseInt(columnIndex) + 1
}
}
for (const record of recordsByRange) {
record.rowMeta.numberOfOverlaps = getMaxOverlaps({
row: record,
rowArray: recordsByRange,
})
const width = 100 / columnArray.length
const left = width * (record.rowMeta.overLapIteration - 1)
record.rowMeta.style = {
...record.rowMeta.style,
width: `${width.toFixed(2)}%`,
left: `${left}%`,
}
} */
return { return {
count: overlaps, count: overlaps,
record: recordsByRange, record: recordsByRange,
@ -328,6 +544,7 @@ const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[],
// When the user is dragging a record, we calculate the new start and end date based on the mouse position // 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 calculateNewRow = (event: MouseEvent) => {
if (!container.value || !dragRecord.value) return { newRow: null, updateProperty: [] } if (!container.value || !dragRecord.value) return { newRow: null, updateProperty: [] }
const { top } = container.value.getBoundingClientRect() const { top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value const { scrollHeight } = container.value
@ -391,10 +608,7 @@ const calculateNewRow = (event: MouseEvent) => {
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!)
if (pk === newPk) { return pk === newPk ? newRow : r
return newRow
}
return r
}) })
} else { } 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 // 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
@ -408,10 +622,9 @@ const calculateNewRow = (event: MouseEvent) => {
} }
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 { top, bottom } = container.value.getBoundingClientRect()
const { top, bottom } = container.value.getBoundingClientRect()
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 the mouse position is near the top or bottom of the scroll container, we scroll the container
@ -425,26 +638,28 @@ const onResize = (event: MouseEvent) => {
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 hour = Math.max(Math.floor(percentY * 23), 0) const hour = Math.floor(percentY * 24) // Round down to the nearest hour
const minutes = Math.round((percentY * 24 * 60) % 60)
let newRow: Row | null = null let newRow: Row | null = null
let updateProperty: string[] = [] 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 // 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').add(minutes, 'minute')
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 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 // 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.add(1, 'hour'))) {
newEndDate = ogStartDate.clone() newEndDate = ogStartDate.clone().add(1, 'hour')
} }
if (!newEndDate.isValid()) return if (!newEndDate.isValid()) return
@ -457,14 +672,14 @@ const onResize = (event: MouseEvent) => {
}, },
} }
} 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').add(minutes, 'minute')
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 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 // 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.subtract(1, 'hour'))) {
newStartDate = dayjs(dayjs(ogEndDate)).clone() newStartDate = dayjs(dayjs(ogEndDate)).clone().add(-1, 'hour')
} }
if (!newStartDate) return if (!newStartDate) return
@ -532,6 +747,10 @@ const stopDrag = (event: MouseEvent) => {
dragElement.value = null dragElement.value = null
} }
if (dragRecord.value) {
dragRecord.value = undefined
}
if (!newRow) return if (!newRow) return
updateRowProperty(newRow, updateProperty, false) updateRowProperty(newRow, updateProperty, false)
@ -564,7 +783,6 @@ const dragStart = (event: MouseEvent, record: Row) => {
dragRecord.value = record dragRecord.value = record
dragElement.value = target dragElement.value = target
dragRecord.value = record
document.addEventListener('mousemove', onDrag) document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag) document.addEventListener('mouseup', stopDrag)
@ -615,11 +833,26 @@ const viewMore = (hour: dayjs.Dayjs) => {
selectedTime.value = hour selectedTime.value = hour
showSideMenu.value = true showSideMenu.value = true
} }
const selectHour = (hour: dayjs.Dayjs) => {
selectedTime.value = hour
dragRecord.value = null
}
// TODO: Add Support for multiple ranges when multiple ranges are supported
const newRecord = (hour: dayjs.Dayjs) => {
if (!isUIAllowed('dataEdit') || !calendarRange.value?.length) return
const record = {
row: {
[calendarRange.value[0].fk_from_col!.title!]: hour.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('newRecord', record)
}
</script> </script>
<template> <template>
<div <div
v-if="recordsAcrossAllRange.record.length"
ref="container" ref="container"
class="w-full relative no-selection h-[calc(100vh-10rem)] overflow-y-auto nc-scrollbar-md" class="w-full relative no-selection h-[calc(100vh-10rem)] overflow-y-auto nc-scrollbar-md"
data-testid="nc-calendar-day-view" data-testid="nc-calendar-day-view"
@ -632,7 +865,8 @@ const viewMore = (hour: dayjs.Dayjs) => {
}" }"
class="flex w-full min-h-20 relative border-1 group hover:bg-gray-50 border-white border-b-gray-100" class="flex w-full min-h-20 relative border-1 group hover:bg-gray-50 border-white border-b-gray-100"
data-testid="nc-calendar-day-hour" data-testid="nc-calendar-day-hour"
@click="selectedTime = hour" @click="selectHour(hour)"
@dblclick="newRecord(hour)"
> >
<div class="pt-2 px-4 text-xs text-gray-500 font-semibold h-20"> <div class="pt-2 px-4 text-xs text-gray-500 font-semibold h-20">
{{ dayjs(hour).format('H A') }} {{ dayjs(hour).format('H A') }}
@ -749,7 +983,8 @@ const viewMore = (hour: dayjs.Dayjs) => {
> >
<LazySmartsheetRow :row="record"> <LazySmartsheetRow :row="record">
<LazySmartsheetCalendarVRecordCard <LazySmartsheetCalendarVRecordCard
:hover="hoverRecord === record.rowMeta.id" :hover="hoverRecord === record.rowMeta.id || record.rowMeta.id === dragRecord?.rowMeta?.id"
:selected="record.rowMeta.id === dragRecord?.rowMeta?.id"
:position="record.rowMeta!.position" :position="record.rowMeta!.position"
:record="record" :record="record"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')" :resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
@ -761,13 +996,14 @@ const viewMore = (hour: dayjs.Dayjs) => {
v-if="!isRowEmpty(record, displayField!)" v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]" v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField!).bold" :bold="getFieldStyle(displayField!).bold"
:column="displayField" :column="displayField!"
:italic="getFieldStyle(displayField!).italic" :italic="getFieldStyle(displayField!).italic"
:underline="getFieldStyle(displayField!).underline" :underline="getFieldStyle(displayField!).underline"
/> />
</template> </template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id"> <template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell <LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]" v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold" :bold="getFieldStyle(field).bold"
:column="field" :column="field"
@ -781,8 +1017,6 @@ const viewMore = (hour: dayjs.Dayjs) => {
</div> </div>
</div> </div>
</div> </div>
<div v-else class="w-full h-full flex text-md font-bold text-gray-500 items-center justify-center">No records in this day</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

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

@ -110,9 +110,9 @@ const recordsToDisplay = computed<{
const perWidth = gridContainerWidth.value / 7 const perWidth = gridContainerWidth.value / 7
const perHeight = gridContainerHeight.value / dates.value.length const perHeight = gridContainerHeight.value / dates.value.length
const perRecordHeight = 40 const perRecordHeight = 24
const spaceBetweenRecords = 35 const spaceBetweenRecords = 26
// This object is used to keep track of the number of records in a day // 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 // The key is the date in the format YYYY-MM-DD
@ -133,13 +133,12 @@ const recordsToDisplay = computed<{
// Filter out records that don't satisfy the range and sort them by start date // 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
if (startCol && endCol) { if (startCol && endCol) {
const fromDate = record.row[startCol!.title!] ? dayjs(record.row[startCol!.title!]) : null const fromDate = record.row[startCol.title!] ? dayjs(record.row[startCol.title!]) : null
const toDate = record.row[endCol!.title!] ? dayjs(record.row[endCol!.title!]) : null const toDate = record.row[endCol.title!] ? dayjs(record.row[endCol.title!]) : null
return fromDate && toDate && !toDate.isBefore(fromDate) return fromDate && toDate && !toDate.isBefore(fromDate)
} else if (startCol && !endCol) { } else if (startCol && !endCol) {
const fromDate = record.row[startCol!.title!] ? dayjs(record.row[startCol!.title!]) : null
return !!fromDate return !!fromDate
} }
return false return false
@ -148,7 +147,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 // 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')
if (!recordsInDay[dateKey]) { if (!recordsInDay[dateKey]) {
@ -175,10 +174,10 @@ const recordsToDisplay = computed<{
// The top is calculated from the week index and the record index // 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 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 + 4)
// The 25 is obtained from the trial and error // The 25 is obtained from the trial and error
const heightRequired = perRecordHeight * recordIndex + spaceBetweenRecords + 25 const heightRequired = perRecordHeight * recordIndex + spaceBetweenRecords + 12
if (heightRequired > perHeight) { if (heightRequired > perHeight) {
style.display = 'none' style.display = 'none'
@ -200,12 +199,13 @@ const recordsToDisplay = computed<{
}) })
} else if (startCol && endCol) { } else if (startCol && endCol) {
// If the range specifies fromCol and 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 // Since the records can span multiple weeks, to display, we render multiple elements
// for each week the record spans. The id is used to identify the elements that belong to the same record // for each week the record spans. The id is used to identify the elements that belong to the same record
while ( while (
@ -219,6 +219,11 @@ const recordsToDisplay = computed<{
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
if (recordEnd.isBefore(dates.value[0][0])) {
currentWeekStart = currentWeekStart.add(1, 'week')
continue
}
// Update the recordsInDay object to keep track of the number of records in a day // 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)) {
@ -257,8 +262,7 @@ const recordsToDisplay = computed<{
overflowCount: 0, overflowCount: 0,
} }
} }
const recordIndex = recordsInDay[dateKey].count maxRecordCount = Math.max(maxRecordCount, recordsInDay[dateKey].count)
maxRecordCount = Math.max(maxRecordCount, recordIndex)
} }
const startDayIndex = Math.max( const startDayIndex = Math.max(
@ -279,8 +283,8 @@ const recordsToDisplay = computed<{
// The top is calculated from the week index and the record index // 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 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 // 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 + 4)
const heightRequired = perRecordHeight * maxRecordCount + spaceBetweenRecords const heightRequired = perRecordHeight * Math.max(maxRecordCount, 0) + spaceBetweenRecords + 12
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 // Here we are checking if the startDay is before all the dates shown in UI rather that the current month
@ -304,7 +308,7 @@ const recordsToDisplay = computed<{
// If the height required is more than the height of the week, we hide the record // 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 // and update the recordsInDay object for all the spanned days
if (heightRequired + 15 > perHeight) { if (heightRequired > perHeight) {
style.display = 'none' style.display = 'none'
for (let i = startDayIndex; i <= endDayIndex; i++) { for (let i = startDayIndex; i <= endDayIndex; i++) {
const week = dates.value[weekIndex] const week = dates.value[weekIndex]
@ -386,7 +390,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
updateProperty.push(toCol!.title!) updateProperty.push(toCol!.title!)
} }
if (!newRow) return if (!newRow) return { newRow: null, updateProperty: [] }
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!) const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
@ -528,6 +532,7 @@ const stopDrag = (event: MouseEvent) => {
dragElement.value = null dragElement.value = null
} }
dragRecord.value = undefined
updateRowProperty(newRow, updateProperty, false) updateRowProperty(newRow, updateProperty, false)
focusedDate.value = null focusedDate.value = null
@ -550,12 +555,10 @@ 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%'
} }
}) })
dragRecord.value = record
// selectedDate.value = null // selectedDate.value = null
isDragging.value = true isDragging.value = true
@ -621,6 +624,19 @@ const isDateSelected = (date: dayjs.Dayjs) => {
if (!selectedDate.value) return false if (!selectedDate.value) return false
return dayjs(date).isSame(selectedDate.value, 'day') return dayjs(date).isSame(selectedDate.value, 'day')
} }
// TODO: Add Support for multiple ranges when multiple ranges are supported
const addRecord = (date: dayjs.Dayjs) => {
if (!isUIAllowed('dataEdit') || !calendarRange.value) return
const fromCol = calendarRange.value[0].fk_from_col
if (!fromCol) return
const newRecord = {
row: {
[fromCol.title!]: date.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('newRecord', newRecord)
}
</script> </script>
<template> <template>
@ -652,10 +668,12 @@ const isDateSelected = (date: dayjs.Dayjs) => {
'border-brand-500 border-1 !border-r-1 border-b-1': 'border-brand-500 border-1 !border-r-1 border-b-1':
isDateSelected(day) || (focusedDate && dayjs(day).isSame(focusedDate, 'day')), isDateSelected(day) || (focusedDate && dayjs(day).isSame(focusedDate, 'day')),
'!text-gray-400': !isDayInPagedMonth(day), '!text-gray-400': !isDayInPagedMonth(day),
'!bg-gray-50': day.get('day') === 0 || day.get('day') === 6,
}" }"
class="text-right relative group last:border-r-0 text-sm h-full border-r-1 border-b-1 border-gray-100 font-medium hover:bg-gray-50 text-gray-800 bg-white" class="text-right relative group last:border-r-0 text-sm h-full border-r-1 border-b-1 border-gray-100 font-medium hover:bg-gray-50 text-gray-800 bg-white"
data-testid="nc-calendar-month-day" data-testid="nc-calendar-month-day"
@click="selectDate(day)" @click="selectDate(day)"
@dblclick="addRecord(day)"
> >
<div v-if="isUIAllowed('dataEdit')" class="flex justify-between p-1"> <div v-if="isUIAllowed('dataEdit')" class="flex justify-between p-1">
<span <span
@ -672,7 +690,7 @@ const isDateSelected = (date: dayjs.Dayjs) => {
'!block': isDateSelected(day), '!block': isDateSelected(day),
'!hidden': !isDateSelected(day), '!hidden': !isDateSelected(day),
}" }"
class="!group-hover:block" class="!group-hover:block rounded"
size="small" size="small"
type="secondary" type="secondary"
> >
@ -705,12 +723,13 @@ const isDateSelected = (date: dayjs.Dayjs) => {
</template> </template>
</NcDropdown> </NcDropdown>
<NcButton <NcButton
v-else
:class="{ :class="{
'!block': isDateSelected(day), '!block': isDateSelected(day),
'!hidden': !isDateSelected(day), '!hidden': !isDateSelected(day),
}" }"
class="!group-hover:block" class="!group-hover:block !w-6 !h-6 !rounded"
size="small" size="xsmall"
type="secondary" type="secondary"
@click=" @click="
() => { () => {
@ -723,13 +742,13 @@ const isDateSelected = (date: dayjs.Dayjs) => {
} }
" "
> >
<component :is="iconMap.plus" class="h-4 w-4" /> <component :is="iconMap.plus" />
</NcButton> </NcButton>
<span <span
:class="{ :class="{
'bg-brand-50 text-brand-500': day.isSame(dayjs(), 'date'), 'bg-brand-50 text-brand-500': day.isSame(dayjs(), 'date'),
}" }"
class="px-1.5 rounded-lg py-1 my-1" class="px-1.3 py-1 text-xs rounded-lg"
> >
{{ day.format('DD') }} {{ day.format('DD') }}
</span> </span>
@ -742,12 +761,12 @@ const isDateSelected = (date: dayjs.Dayjs) => {
recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflow && recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflow &&
!draggingId !draggingId
" "
class="!absolute bottom-1 text-center w-15 mx-auto inset-x-0 z-3 text-gray-500" class="!absolute bottom-1 right-1 text-center min-w-4.5 mx-auto z-3 text-gray-500"
size="xxsmall" size="xxsmall"
type="secondary" type="secondary"
@click="viewMore(day)" @click="viewMore(day)"
> >
<span class="text-xs"> + {{ recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflowCount }} more </span> <span class="text-xs px-1"> + {{ recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflowCount }} </span>
</NcButton> </NcButton>
</div> </div>
</div> </div>
@ -761,10 +780,6 @@ const isDateSelected = (date: dayjs.Dayjs) => {
:style="{ :style="{
...record.rowMeta.style, ...record.rowMeta.style,
zIndex: record.rowMeta.id === draggingId ? 100 : 0, zIndex: record.rowMeta.id === draggingId ? 100 : 0,
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 cursor-pointer pointer-events-auto" class="absolute group draggable-record cursor-pointer pointer-events-auto"
@mouseleave="hoverRecord = null" @mouseleave="hoverRecord = null"
@ -773,17 +788,11 @@ const isDateSelected = (date: dayjs.Dayjs) => {
> >
<LazySmartsheetRow :row="record"> <LazySmartsheetRow :row="record">
<LazySmartsheetCalendarRecordCard <LazySmartsheetCalendarRecordCard
:hover="hoverRecord === record.rowMeta.id" :hover="hoverRecord === record.rowMeta.id || record.rowMeta.id === draggingId"
:position="record.rowMeta.position" :position="record.rowMeta.position"
:record="record" :record="record"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')" :resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
:selected=" :selected="dragRecord?.rowMeta?.id === record.rowMeta.id || resizeRecord?.rowMeta?.id === record.rowMeta.id"
dragRecord
? dragRecord.rowMeta.id === record.rowMeta.id
: resizeRecord
? resizeRecord.rowMeta.id === record.rowMeta.id
: false
"
@resize-start="onResizeStart" @resize-start="onResizeStart"
@dblclick.stop="emit('expandRecord', record)" @dblclick.stop="emit('expandRecord', record)"
> >
@ -798,6 +807,7 @@ const isDateSelected = (date: dayjs.Dayjs) => {
</template> </template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id"> <template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell <LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]" v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold" :bold="getFieldStyle(field).bold"
:column="field" :column="field"

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

@ -26,11 +26,11 @@ const emit = defineEmits(['resize-start'])
<template> <template>
<div <div
:class="{ :class="{
'min-h-9': size === 'small', 'h-6': size === 'small',
'h-full': size === 'auto', 'h-full': size === 'auto',
'rounded-l-lg': position === 'leftRounded', 'rounded-l-md ml-1': position === 'leftRounded',
'rounded-r-lg': position === 'rightRounded', 'rounded-r-md mr-1': position === 'rightRounded',
'rounded-lg mx-1': position === 'rounded', 'rounded-md mx-1': position === 'rounded',
'rounded-none': position === 'none', 'rounded-none': position === 'none',
'bg-maroon-50': color === 'maroon', 'bg-maroon-50': color === 'maroon',
'bg-blue-50': color === 'blue', 'bg-blue-50': color === 'blue',
@ -39,7 +39,8 @@ const emit = defineEmits(['resize-start'])
'bg-pink-50': color === 'pink', 'bg-pink-50': color === 'pink',
'bg-purple-50': color === 'purple', 'bg-purple-50': color === 'purple',
'group-hover:(border-brand-500 border-1)': resize, 'group-hover:(border-brand-500 border-1)': resize,
'!border-brand-500 border-1': selected || hover, '!border-blue-200 border-1': selected,
'shadow-md': hover,
}" }"
class="relative transition-all flex items-center px-1 group border-1 border-transparent" class="relative transition-all flex items-center px-1 group border-1 border-transparent"
> >
@ -53,13 +54,13 @@ const emit = defineEmits(['resize-start'])
'bg-pink-500': color === 'pink', 'bg-pink-500': color === 'pink',
'bg-purple-500': color === 'purple', 'bg-purple-500': color === 'purple',
}" }"
class="w-1 min-h-5 bg-blue-500 rounded-x rounded-y-sm" class="w-1 min-h-4 bg-blue-500 rounded-x rounded-y-sm"
></div> ></div>
<div v-if="(position === 'leftRounded' || position === 'rounded') && resize" class="mt-0.7 h-7.1 absolute -left-4 resize"> <div v-if="(position === 'leftRounded' || position === 'rounded') && resize" class="mt-0.7 h-7.1 absolute -left-4 resize">
<NcButton <NcButton
:class="{ :class="{
'!block z-1 !border-brand-500': selected || hover, '!block z-2 !border-brand-500': selected || hover,
'!hidden': !selected && !hover, '!hidden': !selected && !hover,
}" }"
size="xsmall" size="xsmall"
@ -70,23 +71,23 @@ const emit = defineEmits(['resize-start'])
</NcButton> </NcButton>
</div> </div>
<div class="overflow-hidden items-center flex w-full ml-2 h-8"> <div class="overflow-hidden items-center justify-center flex w-full ml-2">
<span v-if="position === 'rightRounded' || position === 'none'" class="mr-1"> .... </span> <span v-if="position === 'rightRounded' || position === 'none'" class="mr-1"> .... </span>
<span <span
:class="{ :class="{
'pr-7': position === 'leftRounded', 'pr-7': position === 'leftRounded',
}" }"
class="text-sm pr-3 mr-3 break-word space-x-2 whitespace-nowrap gap-2 overflow-hidden text-ellipsis w-full truncate text-gray-800" class="text-sm pr-3 mb-0.5 mr-3 break-word whitespace-nowrap overflow-hidden text-ellipsis w-full truncate text-gray-800"
> >
<slot /> <slot />
</span> </span>
<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 v-if="(position === 'rightRounded' || position === 'rounded') && resize" class="absolute mt-0.3 z-1 -right-4 resize"> <div v-if="(position === 'rightRounded' || position === 'rounded') && resize" class="absolute mt-0.3 -right-4 resize">
<NcButton <NcButton
:class="{ :class="{
'!block !border-brand-500': selected || hover, '!block !border-brand-500 z-2': selected || hover,
'!hidden': !selected && !hover, '!hidden': !selected && !hover,
}" }"
size="xsmall" size="xsmall"

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

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { UITypes, isVirtualCol } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { type Row, computed, iconMap, isRowEmpty, ref } from '#imports' import { type Row, computed, iconMap, isRowEmpty, ref } from '#imports'
@ -348,8 +348,8 @@ onUnmounted(() => {
<component :is="iconMap.search" class="h-4 w-4 mr-1 text-gray-500" /> <component :is="iconMap.search" class="h-4 w-4 mr-1 text-gray-500" />
</template> </template>
</a-input> </a-input>
<NcSelect v-model:value="sideBarFilterOption" class="min-w-38 !text-gray-800" data-testid="nc-calendar-sidebar-filter"> <NcSelect v-model:value="sideBarFilterOption" class="min-w-38 !text-gray-600" data-testid="nc-calendar-sidebar-filter">
<a-select-option v-for="option in options" :key="option.value" :value="option.value" class="!text-gray-800"> <a-select-option v-for="option in options" :key="option.value" :value="option.value" class="!text-gray-600">
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
<div class="truncate flex-1"> <div class="truncate flex-1">
<NcTooltip :title="option.label" placement="top" show-on-truncate-only> <NcTooltip :title="option.label" placement="top" show-on-truncate-only>
@ -433,22 +433,7 @@ onUnmounted(() => {
@dragover.prevent @dragover.prevent
> >
<template v-if="!isRowEmpty(record, displayField)"> <template v-if="!isRowEmpty(record, displayField)">
<div :class="{}"> <LazySmartsheetCalendarCell v-model="record.row[displayField!.title!]" :column="displayField" />
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField)"
v-model="record.row[displayField.title]"
:column="displayField"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[displayField.title]"
:column="displayField"
:edit-enabled="false"
:read-only="true"
/>
</div>
</template> </template>
</LazySmartsheetCalendarSideRecordCard> </LazySmartsheetCalendarSideRecordCard>
</LazySmartsheetRow> </LazySmartsheetRow>

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

@ -29,8 +29,8 @@ const props = withDefaults(defineProps<Props>(), {
}" }"
class="block h-10 w-1 rounded" class="block h-10 w-1 rounded"
></span> ></span>
<div class="flex text-ellipsis gap-1 flex-col"> <div class="flex gap-1 flex-col">
<span class="text-sm max-w-40 truncate text-gray-800"> <span class="text-sm max-w-56 truncate text-gray-800">
<slot /> <slot />
</span> </span>
<span v-if="showDate" class="text-xs text-gray-500">{{ fromDate }} {{ toDate ? ` - ${toDate}` : '' }}</span> <span v-if="showDate" class="text-xs text-gray-500">{{ fromDate }} {{ toDate ? ` - ${toDate}` : '' }}</span>

15
packages/nc-gui/components/smartsheet/calendar/VRecordCard.vue

@ -26,7 +26,7 @@ const emit = defineEmits(['resize-start'])
> >
<NcButton <NcButton
:class="{ :class="{
'!flex rounded-lg border-brand-500': selected || hover, '!flex rounded-md border-brand-500': selected || hover,
}" }"
class="!group-hover:(border-brand-500) !border-1 text-gray-400 cursor-ns-resize" class="!group-hover:(border-brand-500) !border-1 text-gray-400 cursor-ns-resize"
size="xsmall" size="xsmall"
@ -38,9 +38,9 @@ const emit = defineEmits(['resize-start'])
</div> </div>
<div <div
:class="{ :class="{
'rounded-t-lg': position === 'topRounded', 'rounded-t-md': position === 'topRounded',
'rounded-b-lg': position === 'bottomRounded', 'rounded-b-md': position === 'bottomRounded',
'rounded-lg': position === 'rounded', 'rounded-md': position === 'rounded',
'rounded-none': position === 'none', 'rounded-none': position === 'none',
'bg-maroon-50': color === 'maroon', 'bg-maroon-50': color === 'maroon',
'bg-blue-50': color === 'blue', 'bg-blue-50': color === 'blue',
@ -49,7 +49,8 @@ const emit = defineEmits(['resize-start'])
'bg-pink-50': color === 'pink', 'bg-pink-50': color === 'pink',
'bg-purple-50': color === 'purple', 'bg-purple-50': color === 'purple',
'group-hover:(border-brand-500)': resize, 'group-hover:(border-brand-500)': resize,
'!border-brand-500 border-1': selected || hover, '!border-blue-200 border-1': selected,
'shadow-md': hover,
}" }"
class="relative flex items-center h-full ml-0.25 border-1 border-transparent" class="relative flex items-center h-full ml-0.25 border-1 border-transparent"
> >
@ -69,9 +70,7 @@ const emit = defineEmits(['resize-start'])
<div v-if="position === 'bottomRounded' || position === 'none'" class="ml-3">....</div> <div v-if="position === 'bottomRounded' || position === 'none'" class="ml-3">....</div>
<span <span class="pl-1 pr-1 text-sm h-[80%] text-gray-800 leading-7 break-all whitespace-normal truncate w-full overflow-y-hidden">
class="pl-1 pr-1 text-sm h-[80%] text-gray-800 leading-7 space-x-2 break-all whitespace-normal truncate w-full overflow-y-hidden"
>
<slot /> <slot />
</span> </span>

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

@ -23,7 +23,7 @@ const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow() const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType | undefined) => { const getFieldStyle = (field: ColumnType | undefined) => {
const fi = _fields.value?.find((f) => f.title === field.title) const fi = _fields.value?.find((f) => f.title === field?.title)
return { return {
underline: fi?.underline, underline: fi?.underline,
@ -111,7 +111,7 @@ const calendarData = computed(() => {
return !endDate.isBefore(startDate) return !endDate.isBefore(startDate)
})) { })) {
// Generate a unique id for the record if it doesn't have one // Generate a unique id for the record if it doesn't have one
const id = record.row.id ?? generateRandomNumber() const id = record.rowMeta.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!])
@ -196,14 +196,14 @@ const calendarData = computed(() => {
style: { style: {
width: widthStyle, width: widthStyle,
left: `${startDaysDiff * perDayWidth}px`, left: `${startDaysDiff * perDayWidth}px`,
top: `${suitableRow * 40}px`, top: `${suitableRow * 28}px`,
}, },
}, },
}) })
} }
} else if (fromCol) { } else if (fromCol) {
for (const record of formattedData.value) { for (const record of formattedData.value) {
const id = record.row.id ?? generateRandomNumber() const id = record.rowMeta.id ?? generateRandomNumber()
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)
@ -222,7 +222,7 @@ const calendarData = computed(() => {
style: { style: {
width: `calc(${perDayWidth}px)`, width: `calc(${perDayWidth}px)`,
left: `${startDaysDiff * perDayWidth}px`, left: `${startDaysDiff * perDayWidth}px`,
top: `${suitableRow * 40}px`, top: `${suitableRow * 28}px`,
}, },
}, },
}) })
@ -418,7 +418,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBarData?: boolean) => {
const onDrag = (event: MouseEvent) => { const onDrag = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return if (!isUIAllowed('dataEdit')) return
if (!container.value || !dragRecord.value) return if (!container.value || !dragRecord.value) return
calculateNewRow(event) calculateNewRow(event, false)
} }
const stopDrag = (event: MouseEvent) => { const stopDrag = (event: MouseEvent) => {
@ -443,6 +443,7 @@ const stopDrag = (event: MouseEvent) => {
dragElement.value.style.boxShadow = 'none' dragElement.value.style.boxShadow = 'none'
dragElement.value = null dragElement.value = null
} }
dragRecord.value = undefined
updateRowProperty(newRow, updateProperty, false) updateRowProperty(newRow, updateProperty, false)
@ -470,8 +471,6 @@ const dragStart = (event: MouseEvent, record: Row) => {
} }
}) })
dragRecord.value = record
isDragging.value = true isDragging.value = true
dragElement.value = target dragElement.value = target
dragRecord.value = record dragRecord.value = record
@ -512,9 +511,25 @@ const dropEvent = (event: DragEvent) => {
dragElement.value = null dragElement.value = null
} }
updateRowProperty(newRow, updateProperty, false) updateRowProperty(newRow, updateProperty, false)
}
}
dragRecord.value = null const selectDate = (day: dayjs.Dayjs) => {
selectedDate.value = day
dragRecord.value = undefined
}
// TODO: Add Support for multiple ranges when multiple ranges are supported
const addRecord = (date: dayjs.Dayjs) => {
if (!isUIAllowed('dataEdit') || !calendarRange.value) return
const fromCol = calendarRange.value[0].fk_from_col
if (!fromCol) return
const newRecord = {
row: {
[fromCol.title!]: date.format('YYYY-MM-DD HH:mm:ssZ'),
},
} }
emits('newRecord', newRecord)
} }
</script> </script>
@ -538,10 +553,12 @@ const dropEvent = (event: DragEvent) => {
:key="dateIndex" :key="dateIndex"
:class="{ :class="{
'!border-1 !border-t-0 border-brand-500': dayjs(date).isSame(selectedDate, 'day'), '!border-1 !border-t-0 border-brand-500': dayjs(date).isSame(selectedDate, 'day'),
'!bg-gray-50': date.get('day') === 0 || date.get('day') === 6,
}" }"
class="flex flex-col border-r-1 min-h-[100vh] last:border-r-0 items-center w-1/7" class="flex flex-col border-r-1 min-h-[100vh] last:border-r-0 items-center w-1/7"
data-testid="nc-calendar-week-day" data-testid="nc-calendar-week-day"
@click="selectedDate = dayjs(date)" @click="selectDate(date)"
@dblclick="addRecord(date)"
></div> ></div>
</div> </div>
<div <div
@ -555,31 +572,21 @@ const dropEvent = (event: DragEvent) => {
:data-unique-id="record.rowMeta.id" :data-unique-id="record.rowMeta.id"
:style="{ :style="{
...record.rowMeta.style, ...record.rowMeta.style,
boxShadow:
record.rowMeta.id === dragElement?.getAttribute('data-unique-id')
? '0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.10)'
: 'none',
}" }"
class="absolute group draggable-record pointer-events-auto nc-calendar-week-record-card" class="absolute group draggable-record pointer-events-auto nc-calendar-week-record-card"
@mousedown="dragStart($event, record)" @mousedown.stop="dragStart($event, record)"
@mouseleave="hoverRecord = null" @mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id" @mouseover="hoverRecord = record.rowMeta.id"
> >
<LazySmartsheetRow :row="record"> <LazySmartsheetRow :row="record">
<LazySmartsheetCalendarRecordCard <LazySmartsheetCalendarRecordCard
:hover="hoverRecord === record.rowMeta.id" :hover="hoverRecord === record.rowMeta.id || record.rowMeta.id === dragRecord?.rowMeta?.id"
:position="record.rowMeta.position" :position="record.rowMeta.position"
:record="record" :record="record"
:selected=" :selected="dragRecord?.rowMeta?.id === record.rowMeta.id"
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.stop="emits('expand-record', record)"
@resize-start="onResizeStart" @resize-start="onResizeStart"
> >
<template v-if="!isRowEmpty(record, displayField)"> <template v-if="!isRowEmpty(record, displayField)">
@ -593,6 +600,7 @@ const dropEvent = (event: DragEvent) => {
</template> </template>
<template v-for="(field, index) in fieldsWithoutDisplay" :key="index"> <template v-for="(field, index) in fieldsWithoutDisplay" :key="index">
<LazySmartsheetCalendarCell <LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]" v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold" :bold="getFieldStyle(field).bold"
:column="field" :column="field"

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

@ -394,8 +394,6 @@ const recordsAcrossAllRange = computed<{
} }
}) })
const dragElement = ref<HTMLElement | null>(null)
const resizeInProgress = ref(false) const resizeInProgress = ref(false)
const dragTimeout = ref<ReturnType<typeof setTimeout>>() const dragTimeout = ref<ReturnType<typeof setTimeout>>()
@ -444,12 +442,13 @@ const onResize = (event: MouseEvent) => {
const day = Math.floor(percentX * 7) const day = Math.floor(percentX * 7)
const hour = Math.floor(percentY * 23) const hour = Math.floor(percentY * 23)
const minutes = Math.round((percentY * 24 * 60) % 60)
let updateProperty: string[] = [] let updateProperty: string[] = []
let newRow: Row = resizeRecord.value let newRow: Row = resizeRecord.value
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').add(minutes, 'minute')
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 the new end date is before the start date, we set the new end date to the start date
@ -467,7 +466,7 @@ const onResize = (event: MouseEvent) => {
}, },
} }
} 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').add(minutes, 'minute')
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 the new start date is after the end date, we set the new start date to the end date
@ -520,7 +519,6 @@ const calculateNewRow = (
newRow: Row | null newRow: Row | null
updatedProperty: string[] updatedProperty: string[]
} => { } => {
if (!isUIAllowed('dataEdit') || !container.value || !dragRecord.value) return { newRow: null, updatedProperty: [] }
const { width, left, top } = container.value.getBoundingClientRect() const { width, left, top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value const { scrollHeight } = container.value
@ -620,11 +618,6 @@ const stopDrag = (event: MouseEvent) => {
el.style.opacity = '100%' el.style.opacity = '100%'
}) })
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value = null
}
if (newRow) { if (newRow) {
updateRowProperty(newRow, updatedProperty, false) updateRowProperty(newRow, updatedProperty, false)
} }
@ -654,10 +647,7 @@ const dragStart = (event: MouseEvent, record: Row) => {
} }
}) })
dragRecord.value = record
isDragging.value = true isDragging.value = true
dragElement.value = target
dragRecord.value = record dragRecord.value = record
document.addEventListener('mousemove', onDrag) document.addEventListener('mousemove', onDrag)
@ -732,6 +722,19 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
return { isOverflow, overflowCount } return { isOverflow, overflowCount }
} }
// TODO: Add Support for multiple ranges when multiple ranges are supported
const addRecord = (date: dayjs.Dayjs) => {
if (!isUIAllowed('dataEdit') || !calendarRange.value) return
const fromCol = calendarRange.value[0].fk_from_col
if (!fromCol) return
const newRecord = {
row: {
[fromCol.title!]: date.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emits('newRecord', newRecord)
}
</script> </script>
<template> <template>
@ -769,13 +772,16 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
:key="hourIndex" :key="hourIndex"
:class="{ :class="{
'border-1 !border-brand-500 bg-gray-50': hour.isSame(selectedTime, 'hour'), 'border-1 !border-brand-500 bg-gray-50': hour.isSame(selectedTime, 'hour'),
'!bg-gray-50': hour.get('day') === 0 || hour.get('day') === 6,
}" }"
class="text-center relative h-20 text-sm text-gray-500 w-full hover:bg-gray-50 py-1 border-transparent border-1 border-x-gray-100 border-t-gray-100" class="text-center relative h-20 text-sm text-gray-500 w-full hover:bg-gray-50 py-1 border-transparent border-1 border-x-gray-100 border-t-gray-100"
data-testid="nc-calendar-week-hour" data-testid="nc-calendar-week-hour"
@dblclick="addRecord(hour)"
@click=" @click="
() => { () => {
selectedTime = hour selectedTime = hour
selectedDate = hour selectedDate = hour
dragRecord = undefined
} }
" "
> >
@ -806,18 +812,19 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
:data-unique-id="record.rowMeta!.id" :data-unique-id="record.rowMeta!.id"
:style="record.rowMeta!.style " :style="record.rowMeta!.style "
class="absolute draggable-record w-1/7 group cursor-pointer pointer-events-auto" class="absolute draggable-record w-1/7 group cursor-pointer pointer-events-auto"
@mousedown="dragStart($event, record)" @mousedown.stop="dragStart($event, record)"
@mouseleave="hoverRecord = null" @mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id" @mouseover="hoverRecord = record.rowMeta.id"
@dragover.prevent @dragover.prevent
> >
<LazySmartsheetRow :row="record"> <LazySmartsheetRow :row="record">
<LazySmartsheetCalendarVRecordCard <LazySmartsheetCalendarVRecordCard
:hover="hoverRecord === record.rowMeta.id" :hover="hoverRecord === record.rowMeta.id || record.rowMeta.id === dragRecord?.rowMeta?.id"
:position="record.rowMeta!.position" :position="record.rowMeta!.position"
:record="record"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')" :resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
:record="record"
color="blue" color="blue"
:selected="record.rowMeta!.id === dragRecord?.rowMeta?.id"
@resize-start="onResizeStart" @resize-start="onResizeStart"
> >
<template v-if="!isRowEmpty(record, displayField)"> <template v-if="!isRowEmpty(record, displayField)">
@ -832,6 +839,7 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
</template> </template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id"> <template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell <LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]" v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold" :bold="getFieldStyle(field).bold"
:column="field" :column="field"

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

@ -169,7 +169,7 @@ const headerText = computed(() => {
<template> <template>
<div class="flex h-full flex-row" data-testid="nc-calendar-wrapper"> <div class="flex h-full flex-row" data-testid="nc-calendar-wrapper">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<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-2 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> {{ $t('labels.previous') }}</template> <template #title> {{ $t('labels.previous') }}</template>
@ -247,7 +247,7 @@ const headerText = computed(() => {
type="secondary" type="secondary"
@click="goToToday" @click="goToToday"
> >
<span class="text-gray-700"> <span class="text-gray-600 !text-sm">
{{ $t('activity.goToToday') }} {{ $t('activity.goToToday') }}
</span> </span>
</NcButton> </NcButton>
@ -265,7 +265,7 @@ const headerText = computed(() => {
type="secondary" type="secondary"
@click="showSideMenu = !showSideMenu" @click="showSideMenu = !showSideMenu"
> >
<component :is="iconMap.sidebar" class="h-4 w-4 text-gray-700 transition-all" /> <component :is="iconMap.sidebar" class="h-4 w-4 text-gray-600 transition-all" />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
</div> </div>
@ -321,6 +321,7 @@ const headerText = computed(() => {
<LazySmartsheetExpandedForm <LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg" v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg" v-model="expandedFormDlg"
close-after-save
:meta="meta" :meta="meta"
:row="expandedFormRow" :row="expandedFormRow"
:state="expandedFormRowState" :state="expandedFormRowState"
@ -332,6 +333,7 @@ const headerText = computed(() => {
<LazySmartsheetExpandedForm <LazySmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg" v-if="expandedFormOnRowIdDlg"
v-model="expandedFormOnRowIdDlg" v-model="expandedFormOnRowIdDlg"
close-after-save
:meta="meta" :meta="meta"
:row="{ row: {}, oldRow: {}, rowMeta: {} }" :row="{ row: {}, oldRow: {}, rowMeta: {} }"
:row-id="route.query.rowId" :row-id="route.query.rowId"

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

@ -28,7 +28,7 @@ const IsPublic = inject(IsPublicInj, ref(false))
const { loadViewColumns } = useViewColumnsOrThrow() const { loadViewColumns } = useViewColumnsOrThrow()
const { loadCalendarMeta, loadCalendarData, loadSidebarData } = useCalendarViewStoreOrThrow() const { loadCalendarMeta, loadCalendarData, loadSidebarData, fetchActiveDates } = useCalendarViewStoreOrThrow()
const calendarRangeDropdown = ref(false) const calendarRangeDropdown = ref(false)
@ -71,7 +71,7 @@ const saveCalendarRanges = async () => {
calendar_range: calRanges as CalendarRangeType[], calendar_range: calRanges as CalendarRangeType[],
}) })
await loadCalendarMeta() await loadCalendarMeta()
await Promise.all([loadCalendarData(), loadSidebarData()]) await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()])
} catch (e) { } catch (e) {
console.log(e) console.log(e)
message.error('There was an error while updating view!') message.error('There was an error while updating view!')
@ -93,8 +93,6 @@ const dateFieldOptions = computed<SelectProps['options']>(() => {
) )
}) })
// TODO: Add support for end date in future
// To add new calendar range
/* const addCalendarRange = async () => { /* const addCalendarRange = async () => {
_calendar_ranges.value.push({ _calendar_ranges.value.push({
fk_from_column_id: dateFieldOptions.value![0].value as string, fk_from_column_id: dateFieldOptions.value![0].value as string,
@ -132,7 +130,7 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</a-button> </a-button>
</div> </div>
<template #overlay> <template #overlay>
<div v-if="calendarRangeDropdown" class="w-full p-6 w-[22rem]" data-testid="nc-calendar-range-menu" @click.stop> <div v-if="calendarRangeDropdown" class="w-full p-6 w-[23rem]" data-testid="nc-calendar-range-menu" @click.stop>
<div <div
v-for="(range, id) in _calendar_ranges" v-for="(range, id) in _calendar_ranges"
:key="id" :key="id"
@ -167,69 +165,60 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</a-select-option> </a-select-option>
</NcSelect> </NcSelect>
<!-- <!-- <div
TODO: Add support for end date and multiple range in future v-if="range.fk_to_column_id === null && isEeUI"
<div class="flex cursor-pointer flex text-gray-800 items-center gap-1"
v-if="range.fk_to_column_id === null && isEeUI" data-testid="nc-calendar-range-add-end-date"
class="flex cursor-pointer flex text-gray-800 items-center gap-1" @click="saveCalendarRange(range, undefined)"
data-testid="nc-calendar-range-add-end-date" >
@click="saveCalendarRange(range, undefined)" <component :is="iconMap.plus" class="h-4 w-4" />
> {{ $t('activity.addEndDate') }}
<component :is="iconMap.plus" class="h-4 w-4" /> </div>
{{ $t('activity.addEndDate') }} <template v-else-if="isEeUI">
</div> <span>
<template v-else-if="isEeUI && false"> {{ $t('activity.withEndDate') }}
<span> </span>
{{ $t('activity.withEndDate') }} <div class="flex">
</span> <NcSelect
<div class="flex"> v-model:value="range.fk_to_column_id"
<NcSelect :disabled="!range.fk_from_column_id"
v-model:value="range.fk_to_column_id" :placeholder="$t('placeholder.notSelected')"
:disabled="!range.fk_from_column_id" class="!rounded-r-none nc-to-select"
:placeholder="$t('placeholder.notSelected')" data-testid="nc-calendar-range-to-field-select"
class="!rounded-r-none nc-to-select" @change="saveCalendarRanges"
data-testid="nc-calendar-range-to-field-select" >
@change="saveCalendarRanges" <a-select-option
> v-for="(option, opId) in [...dateFieldOptions].filter((f) => {
<a-select-option const firstRange = dateFieldOptions.find((f) => f.value === calendarRange[0].fk_from_column_id)
v-for="(option, opId) in [...dateFieldOptions].filter((f) => { return firstRange?.uidt === f.uidt
const firstRange = dateFieldOptions.find((f) => f.value === calendarRange[0].fk_from_column_id) })"
return firstRange?.uidt === f.uidt :key="opId"
})" :value="option.value"
:key="opId" >
:value="option.value" <div class="flex items-center">
> <SmartsheetHeaderIcon :column="option" />
<div class="flex items-center"> <NcTooltip class="truncate flex-1 max-w-18" placement="top" show-on-truncate-only>
<SmartsheetHeaderIcon :column="option" /> <template #title>{{ option.label }}</template>
<NcTooltip class="truncate flex-1 max-w-18" placement="top" show-on-truncate-only> {{ option.label }}
<template #title>{{ option.label }}</template> </NcTooltip>
{{ option.label }}
</NcTooltip>
</div>
</a-select-option>
</NcSelect>
<NcButton class="!rounded-l-none !border-l-0" size="small" type="secondary" @click="saveCalendarRange(range, null)">
<component :is="iconMap.delete" class="h-4 w-4" />
</NcButton>
</div>
</template>
<NcButton v-if="id !== 0" size="small" type="secondary" @click="removeRange(id)">
<component :is="iconMap.close" />
</NcButton>
</div> </div>
<NcButton </a-select-option>
v-if="false" </NcSelect>
class="mt-2" <NcButton class="!rounded-l-none !border-l-0" size="small" type="secondary" @click="saveCalendarRange(range, null)">
data-testid="nc-calendar-range-add-btn" <component :is="iconMap.delete" class="h-4 w-4" />
size="small" </NcButton>
type="secondary" </div>
@click="addCalendarRange" </template>
>
<component :is="iconMap.plus" /> <NcButton v-if="id !== 0" size="small" type="secondary" @click="removeRange(id)">
Add another date field <component :is="iconMap.close" />
</NcButton> </NcButton>
--> -->
</div> </div>
<!-- <NcButton class="mt-2" data-testid="nc-calendar-range-add-btn" size="small" type="secondary" @click="addCalendarRange">
<component :is="iconMap.plus" />
Add another date field
</NcButton> -->
</div> </div>
</template> </template>
</NcDropdown> </NcDropdown>

21
packages/nc-gui/composables/useCalendarViewStore.ts

@ -1,13 +1,14 @@
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { import type {
type Api, type Api,
CalendarRangeType,
type CalendarType, type CalendarType,
type ColumnType, type ColumnType,
type PaginatedType, type PaginatedType,
type TableType, type TableType,
UITypes,
type ViewType, type ViewType,
} from 'nocodb-sdk' } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { extractPkFromRow, extractSdkResponseErrorMsg, rowPkData } from '~/utils' import { extractPkFromRow, extractSdkResponseErrorMsg, rowPkData } from '~/utils'
import { IsPublicInj, type Row, ref, storeToRefs, useBase, useInjectionState, useUndoRedo } from '#imports' import { IsPublicInj, type Row, ref, storeToRefs, useBase, useInjectionState, useUndoRedo } from '#imports'
@ -110,14 +111,15 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const calendarRange = ref< const calendarRange = ref<
Array<{ Array<{
fk_from_col: ColumnType | null fk_from_col: ColumnType
fk_to_col: ColumnType | null fk_to_col?: ColumnType | null
id: string
}> }>
>([]) >([])
const calDataType = computed(() => { const calDataType = computed(() => {
if (!calendarRange.value || !calendarRange.value[0]) return null if (!calendarRange.value || !calendarRange.value[0]) return null
return calendarRange.value[0]!.fk_from_col!.uidt return calendarRange.value[0]?.fk_from_col?.uidt
}) })
const sideBarFilter = computed(() => { const sideBarFilter = computed(() => {
@ -418,8 +420,9 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const calMeta = typeof res.meta === 'string' ? JSON.parse(res.meta) : res.meta const calMeta = typeof res.meta === 'string' ? JSON.parse(res.meta) : res.meta
activeCalendarView.value = calMeta?.active_view activeCalendarView.value = calMeta?.active_view
if (!activeCalendarView.value) activeCalendarView.value = 'month' if (!activeCalendarView.value) activeCalendarView.value = 'month'
calendarRange.value = res?.calendar_range?.map((range: any) => { calendarRange.value = res?.calendar_range?.map((range: CalendarRangeType) => {
return { return {
id: range.id,
fk_from_col: meta.value?.columns?.find((col) => col.id === range.fk_from_column_id), fk_from_col: meta.value?.columns?.find((col) => col.id === range.fk_from_column_id),
fk_to_col: range.fk_to_column_id ? meta.value?.columns?.find((col) => col.id === range.fk_to_column_id) : null, fk_to_col: range.fk_to_column_id ? meta.value?.columns?.find((col) => col.id === range.fk_to_column_id) : null,
} }
@ -730,9 +733,8 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
watch(activeCalendarView, async (value, oldValue) => { watch(activeCalendarView, async (value, oldValue) => {
if (oldValue === 'week') { if (oldValue === 'week') {
pageDate.value = selectedDate.value pageDate.value = selectedDate.value
selectedDate.value = selectedDateRange.value.start selectedMonth.value = selectedDate.value ?? selectedDateRange.value.start
selectedMonth.value = selectedDateRange.value.start selectedTime.value = selectedDate.value ?? selectedDateRange.value.start
selectedTime.value = selectedDateRange.value.start
} else if (oldValue === 'month') { } else if (oldValue === 'month') {
selectedDate.value = selectedMonth.value selectedDate.value = selectedMonth.value
pageDate.value = selectedDate.value pageDate.value = selectedDate.value
@ -784,6 +786,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
}) })
return { return {
fetchActiveDates,
formattedSideBarData, formattedSideBarData,
loadMoreSidebarData, loadMoreSidebarData,
loadSidebarData, loadSidebarData,

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

@ -75,6 +75,10 @@ interface Row {
id?: string id?: string
position?: string position?: string
dayIndex?: number dayIndex?: number
overLapIteration?: number
numberOfOverlaps?: number
minutes?: number
} }
} }

12
packages/nocodb/src/db/conditionV2.ts

@ -902,7 +902,8 @@ const parseConditionV2 = async (
// If the column is a datetime and the client is pg and the value has a timezone offset at the end // If the column is a datetime and the client is pg and the value has a timezone offset at the end
// then we need to convert the value to timestamptz before comparing // then we need to convert the value to timestamptz before comparing
if ( if (
column.uidt === UITypes.DateTime && (column.uidt === UITypes.DateTime ||
column.uidt === UITypes.Date) &&
val.match(/[+-]\d{2}:\d{2}$/) val.match(/[+-]\d{2}:\d{2}$/)
) { ) {
if (qb.client.config.client === 'pg') { if (qb.client.config.client === 'pg') {
@ -944,7 +945,8 @@ const parseConditionV2 = async (
// If the column is a datetime and the client is pg and the value has a timezone offset at the end // If the column is a datetime and the client is pg and the value has a timezone offset at the end
// then we need to convert the value to timestamptz before comparing // then we need to convert the value to timestamptz before comparing
if ( if (
column.uidt === UITypes.DateTime && (column.uidt === UITypes.DateTime ||
column.uidt === UITypes.Date) &&
val.match(/[+-]\d{2}:\d{2}$/) val.match(/[+-]\d{2}:\d{2}$/)
) { ) {
if (qb.client.config.client === 'pg') { if (qb.client.config.client === 'pg') {
@ -985,7 +987,8 @@ const parseConditionV2 = async (
// If the column is a datetime and the client is pg and the value has a timezone offset at the end // If the column is a datetime and the client is pg and the value has a timezone offset at the end
// then we need to convert the value to timestamptz before comparing // then we need to convert the value to timestamptz before comparing
if ( if (
column.uidt === UITypes.DateTime && (column.uidt === UITypes.DateTime ||
column.uidt === UITypes.Date) &&
val.match(/[+-]\d{2}:\d{2}$/) val.match(/[+-]\d{2}:\d{2}$/)
) { ) {
if (qb.client.config.client === 'pg') { if (qb.client.config.client === 'pg') {
@ -1028,7 +1031,8 @@ const parseConditionV2 = async (
// If the column is a datetime and the client is pg and the value has a timezone offset at the end // If the column is a datetime and the client is pg and the value has a timezone offset at the end
// then we need to convert the value to timestamptz before comparing // then we need to convert the value to timestamptz before comparing
if ( if (
column.uidt === UITypes.DateTime && (column.uidt === UITypes.DateTime ||
column.uidt === UITypes.Date) &&
val.match(/[+-]\d{2}:\d{2}$/) val.match(/[+-]\d{2}:\d{2}$/)
) { ) {
if (qb.client.config.client === 'pg') { if (qb.client.config.client === 'pg') {

1
packages/nocodb/src/services/calendar-datas.service.ts

@ -53,6 +53,7 @@ export class CalendarDatasService {
return await this.datasService.dataList({ return await this.datasService.dataList({
...param, ...param,
...query, ...query,
viewName: view.id,
baseName: model.base_id, baseName: model.base_id,
tableName: model.id, tableName: model.id,
calendarLimitOverride: 3000, // TODO: make this configurable in env calendarLimitOverride: 3000, // TODO: make this configurable in env

Loading…
Cancel
Save