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 7 months ago committed by GitHub
parent
commit
15fb48d0e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      packages/nc-gui/assets/style.scss
  2. 24
      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. 366
      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. 29
      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

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

@ -785,3 +785,4 @@ svg.nc-cell-icon, svg.nc-virtual-cell-icon {
}
}
}

24
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 () => {
form.calendar_range.push({
fk_from_column_id: viewSelectFieldOptions.value[0].value as string,
@ -318,7 +317,7 @@ onMounted(async () => {
<template>
<NcModal
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>
<div class="flex w-full flex-row justify-between items-center">
@ -453,8 +452,7 @@ onMounted(async () => {
</div>
</a-select-option>
</NcSelect>
<!--
<div
<!-- <div
v-if="range.fk_to_column_id === null && isEeUI"
class="cursor-pointer flex items-center text-gray-800 gap-1"
@click="range.fk_to_column_id = undefined"
@ -467,8 +465,6 @@ class="cursor-pointer flex items-center text-gray-800 gap-1"
{{ $t('activity.withEndDate') }}
</span>
TODO: Add support for end date and multiple range in future
<div class="flex">
<NcSelect
v-model:value="range.fk_to_column_id"
@ -499,10 +495,9 @@ class="cursor-pointer flex items-center text-gray-800 gap-1"
</NcSelect>
<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" />
</NcButton> -->
</NcButton>
</div>
</template>
<!-- <NcButton
<NcButton
v-if="index !== 0"
size="small"
type="secondary"
@ -514,12 +509,15 @@ class="cursor-pointer flex items-center text-gray-800 gap-1"
>
<component :is="iconMap.close" />
</NcButton>
</div>
<NcButton class="mt-2" size="small" type="secondary" @click="addCalendarRange">
</template>
</div> -->
<!-- <NcButton class="mt-2" size="small" type="secondary" @click="addCalendarRange">
<component :is="iconMap.plus" />
Add another date field
</NcButton>
-->
</NcButton> -->
</div>
</template>
</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">
<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"
>
<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" />
</NcButton>
<template #title>
@ -165,11 +165,11 @@ const paginate = (action: 'next' | 'prev') => {
'text-xs': size === 'small',
'text-sm': size === 'medium',
}"
class="font-bold text-gray-700"
class="text-gray-700"
>{{ currentMonthYear }}</span
>
<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" />
</NcButton>
<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>
</template>
</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>
<NcButton size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
@ -114,7 +114,7 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
:class="{
'!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"
>
{{ month.format('MMM') }}
@ -127,7 +127,7 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
:class="{
'!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"
>
{{ year.format('YYYY') }}

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

@ -356,6 +356,7 @@ const parseValue = (value: any, col: ColumnType): string => {
<template>
<span
class="calendar-cell text-xs before:px-1"
:class="{
'font-bold': bold,
'italic': italic,
@ -367,4 +368,15 @@ const parseValue = (value: any, col: ColumnType): string => {
</span>
</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 getFieldStyle = (field: ColumnType) => {
const fi = _fields.value.find((f) => f.title === field.title)
const fi = _fields.value?.find((f) => f.title === field.title)
return {
underline: fi.underline,
bold: fi.bold,
italic: fi.italic,
underline: fi?.underline,
bold: fi?.bold,
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 only need to calculate the top, of the record since there is no overlap in the day view of date Field
const recordsAcrossAllRange = computed<Row[]>(() => {
let dayRecordCount = 0
const perRecordHeight = 40
const perRecordHeight = 28
if (!calendarRange.value) return []
@ -183,6 +183,17 @@ const dropEvent = (event: DragEvent) => {
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>
<template>
@ -191,6 +202,7 @@ const dropEvent = (event: DragEvent) => {
ref="container"
class="w-full relative h-[calc(100vh-10.8rem)] overflow-y-auto nc-scrollbar-md"
data-testid="nc-calendar-day-view"
@dblclick="newRecord"
@drop="dropEvent"
>
<div
@ -223,6 +235,7 @@ const dropEvent = (event: DragEvent) => {
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
@ -240,6 +253,7 @@ const dropEvent = (event: DragEvent) => {
ref="container"
class="w-full h-full flex text-md font-bold text-gray-500 items-center justify-center"
@drop="dropEvent"
@dblclick="newRecord"
>
No records in this day
</div>

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

@ -52,6 +52,146 @@ const hours = computed(() => {
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<{
record: Row[]
count: {
@ -82,6 +222,9 @@ const recordsAcrossAllRange = computed<{
const perRecordHeight = 80
/* const columnArray: Array<Array<Row>> = [[]]
const gridTimeMap = new Map() */
let recordsByRange: Array<Row> = []
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.
// But not all fetched records are valid for the certain range, so we filter them out & sort them
const sortedFormattedData = [...formattedData.value].filter((record) => {
const sortedFormattedData = [...formattedData.value]
.filter((record) => {
const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null
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
return fromDate && toDate?.isValid() ? fromDate.isSameOrBefore(toDate) : true
} else if (fromCol && !endCol) {
return !!fromDate
}
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 there is a start and end column, we calculate the top and height of the record based on the start and end date
if (fromCol && endCol) {
for (const record of sortedFormattedData) {
// We use this id during the drag and drop operation and to keep track of the number of records that overlap at a given time
const id = generateRandomNumber()
let startDate = dayjs(record.row[fromCol.title!])
let endDate = dayjs(record.row[endCol.title!])
// If no start date is provided or startDate is after the endDate, we skip the record
if (!startDate.isValid() || startDate.isAfter(endDate)) continue
// If there is no end date, we add 30 minutes to the start date and use that as the end date
if (!endDate.isValid()) {
endDate = startDate.clone().add(30, 'minutes')
}
// If the start date is before the opened date, we use the schedule start as the start date
// This is to ensure the generated style of the record is not outside the bounds of the calendar
if (startDate.isBefore(scheduleStart, 'minutes')) {
startDate = scheduleStart
}
// If the end date is after the schedule end, we use the schedule end as the end date
// This is to ensure the generated style of the record is not outside the bounds of the calendar
if (endDate.isAfter(scheduleEnd, 'minutes')) {
endDate = scheduleEnd
}
const id = record.rowMeta.id ?? generateRandomNumber()
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
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 startHour = startDate.hour()
let _startDate = startDate.clone()
const style: Partial<CSSStyleDeclaration> = {
@ -148,7 +278,7 @@ const recordsAcrossAllRange = computed<{
top: `${topInPixels + 5 + startHour * 2}px`,
}
// We loop through every 15 minutes between the start and end date and keep track of the number of records that overlap at a given time
// 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
while (_startDate.isBefore(endDate)) {
const timeKey = _startDate.format('HH:mm')
@ -199,18 +329,13 @@ const recordsAcrossAllRange = computed<{
range: range as any,
},
})
}
} else if (fromCol) {
for (const record of sortedFormattedData) {
const id = generateRandomNumber()
const startDate = dayjs(record.row[fromCol.title!])
let endDate = dayjs(record.row[fromCol.title!]).add(1, 'hour')
if (endDate.isAfter(scheduleEnd, 'minutes')) {
endDate = scheduleEnd
}
const { startDate, endDate } = calculateNewDates({
startDate: dayjs(record.row[fromCol.title!]),
endDate: dayjs(record.row[fromCol.title!]).add(1, 'hour'),
scheduleStart,
scheduleEnd,
})
const startHour = startDate.hour()
@ -218,7 +343,7 @@ const recordsAcrossAllRange = computed<{
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
while (_startDate.isBefore(endDate)) {
while (_startDate.isBefore(endDate, 'minute')) {
const timeKey = _startDate.format('HH:mm')
if (!overlaps[timeKey]) {
@ -239,6 +364,7 @@ const recordsAcrossAllRange = computed<{
display: 'none',
}
}
_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
// 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)
}
}
const spacing = 0.25
const widthPerRecord = (100 - spacing * (maxOverlaps - 1)) / maxOverlaps
const leftPerRecord = (widthPerRecord + spacing) * overlapIndex
record.rowMeta.style = {
...record.rowMeta.style,
left: `${leftPerRecord - 0.08}%`,
@ -301,6 +431,92 @@ const recordsAcrossAllRange = computed<{
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 {
count: overlaps,
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
const calculateNewRow = (event: MouseEvent) => {
if (!container.value || !dragRecord.value) return { newRow: null, updateProperty: [] }
const { top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
@ -391,10 +608,7 @@ const calculateNewRow = (event: MouseEvent) => {
if (dragElement.value) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
if (pk === newPk) {
return newRow
}
return r
return pk === newPk ? newRow : r
})
} else {
// If the old row is not found, we add the new row to the formattedData array and remove the old row from the formattedSideBarData array
@ -408,10 +622,9 @@ const calculateNewRow = (event: MouseEvent) => {
}
const onResize = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit')) return
if (!container.value || !resizeRecord.value) return
const { top, bottom } = container.value.getBoundingClientRect()
if (!isUIAllowed('dataEdit') || !container.value || !resizeRecord.value) return
const { top, bottom } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
// 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 toCol = resizeRecord.value.rowMeta.range?.fk_to_col
if (!fromCol || !toCol) return
const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
const ogStartDate = dayjs(resizeRecord.value.row[fromCol.title!])
const 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 updateProperty: string[] = []
if (resizeDirection.value === 'right') {
// If the user is resizing the record to the right, we calculate the new end date based on the mouse position
let newEndDate = dayjs(selectedDate.value).add(hour, 'hour')
let newEndDate = dayjs(selectedDate.value).add(hour, 'hour').add(minutes, 'minute')
updateProperty = [toCol.title!]
// If the new end date is before the start date, we set the new end date to the start date
// This is to ensure the end date is always same or after the start date
if (dayjs(newEndDate).isBefore(ogStartDate, 'day')) {
newEndDate = ogStartDate.clone()
if (dayjs(newEndDate).isBefore(ogStartDate.add(1, 'hour'))) {
newEndDate = ogStartDate.clone().add(1, 'hour')
}
if (!newEndDate.isValid()) return
@ -457,14 +672,14 @@ const onResize = (event: MouseEvent) => {
},
}
} 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!]
// If the new start date is after the end date, we set the new start date to the end date
// This is to ensure the start date is always before or same the end date
if (dayjs(newStartDate).isAfter(ogEndDate)) {
newStartDate = dayjs(dayjs(ogEndDate)).clone()
if (dayjs(newStartDate).isAfter(ogEndDate.subtract(1, 'hour'))) {
newStartDate = dayjs(dayjs(ogEndDate)).clone().add(-1, 'hour')
}
if (!newStartDate) return
@ -532,6 +747,10 @@ const stopDrag = (event: MouseEvent) => {
dragElement.value = null
}
if (dragRecord.value) {
dragRecord.value = undefined
}
if (!newRow) return
updateRowProperty(newRow, updateProperty, false)
@ -564,7 +783,6 @@ const dragStart = (event: MouseEvent, record: Row) => {
dragRecord.value = record
dragElement.value = target
dragRecord.value = record
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
@ -615,11 +833,26 @@ const viewMore = (hour: dayjs.Dayjs) => {
selectedTime.value = hour
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>
<template>
<div
v-if="recordsAcrossAllRange.record.length"
ref="container"
class="w-full relative no-selection h-[calc(100vh-10rem)] overflow-y-auto nc-scrollbar-md"
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"
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">
{{ dayjs(hour).format('H A') }}
@ -749,7 +983,8 @@ const viewMore = (hour: dayjs.Dayjs) => {
>
<LazySmartsheetRow :row="record">
<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"
:record="record"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
@ -761,13 +996,14 @@ const viewMore = (hour: dayjs.Dayjs) => {
v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField!).bold"
:column="displayField"
:column="displayField!"
:italic="getFieldStyle(displayField!).italic"
:underline="getFieldStyle(displayField!).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
@ -781,8 +1017,6 @@ const viewMore = (hour: dayjs.Dayjs) => {
</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>
<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 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
// 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
const sortedFormattedData = [...formattedData.value].filter((record) => {
const fromDate = record.row[startCol!.title!] ? dayjs(record.row[startCol!.title!]) : null
if (startCol && endCol) {
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 fromDate = record.row[startCol.title!] ? dayjs(record.row[startCol.title!]) : null
const toDate = record.row[endCol.title!] ? dayjs(record.row[endCol.title!]) : null
return fromDate && toDate && !toDate.isBefore(fromDate)
} else if (startCol && !endCol) {
const fromDate = record.row[startCol!.title!] ? dayjs(record.row[startCol!.title!]) : null
return !!fromDate
}
return false
@ -148,7 +147,7 @@ const recordsToDisplay = computed<{
sortedFormattedData.forEach((record: Row) => {
if (!endCol && startCol) {
// If there is no end date, we just display the record on the start date
const startDate = dayjs(record.row[startCol!.title!])
const startDate = dayjs(record.row[startCol.title!])
const dateKey = startDate.format('YYYY-MM-DD')
if (!recordsInDay[dateKey]) {
@ -175,10 +174,10 @@ const recordsToDisplay = computed<{
// The top is calculated from the week index and the record index
// If the record in 1st week and no record in that date them the top will be 0
const top = weekIndex * perHeight + spaceBetweenRecords + (recordIndex - 1) * perRecordHeight
const top = weekIndex * perHeight + spaceBetweenRecords + (recordIndex - 1) * (perRecordHeight + 4)
// The 25 is obtained from the trial and error
const heightRequired = perRecordHeight * recordIndex + spaceBetweenRecords + 25
const heightRequired = perRecordHeight * recordIndex + spaceBetweenRecords + 12
if (heightRequired > perHeight) {
style.display = 'none'
@ -200,12 +199,13 @@ const recordsToDisplay = computed<{
})
} else if (startCol && endCol) {
// If the range specifies fromCol and endCol
const startDate = dayjs(record.row[startCol!.title!])
const endDate = dayjs(record.row[endCol!.title!])
const startDate = dayjs(record.row[startCol.title!])
const endDate = dayjs(record.row[endCol.title!])
let currentWeekStart = startDate.startOf('week')
const id = record.rowMeta.id ?? generateRandomNumber()
// Since the records can span multiple weeks, to display, we render multiple records
// 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
while (
@ -219,6 +219,11 @@ const recordsToDisplay = computed<{
const recordStart = currentWeekStart.isBefore(startDate) ? startDate : currentWeekStart
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
let day = recordStart.clone()
while (day.isSameOrBefore(recordEnd)) {
@ -257,8 +262,7 @@ const recordsToDisplay = computed<{
overflowCount: 0,
}
}
const recordIndex = recordsInDay[dateKey].count
maxRecordCount = Math.max(maxRecordCount, recordIndex)
maxRecordCount = Math.max(maxRecordCount, recordsInDay[dateKey].count)
}
const startDayIndex = Math.max(
@ -279,8 +283,8 @@ const recordsToDisplay = computed<{
// The top is calculated from the week index and the record index
// If the record in 1st week and no record in that date them the top will be 0
// If the record in 1st week and 1 record in that date then the top will be perRecordHeight + spaceBetweenRecords
const top = weekIndex * perHeight + spaceBetweenRecords + Math.max(maxRecordCount - 1, 0) * perRecordHeight
const heightRequired = perRecordHeight * maxRecordCount + spaceBetweenRecords
const top = weekIndex * perHeight + spaceBetweenRecords + Math.max(maxRecordCount - 1, 0) * (perRecordHeight + 4)
const heightRequired = perRecordHeight * Math.max(maxRecordCount, 0) + spaceBetweenRecords + 12
let position = 'rounded'
// 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
// and update the recordsInDay object for all the spanned days
if (heightRequired + 15 > perHeight) {
if (heightRequired > perHeight) {
style.display = 'none'
for (let i = startDayIndex; i <= endDayIndex; i++) {
const week = dates.value[weekIndex]
@ -386,7 +390,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
updateProperty.push(toCol!.title!)
}
if (!newRow) return
if (!newRow) return { newRow: null, updateProperty: [] }
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
@ -528,6 +532,7 @@ const stopDrag = (event: MouseEvent) => {
dragElement.value = null
}
dragRecord.value = undefined
updateRowProperty(newRow, updateProperty, false)
focusedDate.value = null
@ -550,12 +555,10 @@ const dragStart = (event: MouseEvent, record: Row) => {
const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => {
if (!el.getAttribute('data-unique-id').includes(record.rowMeta.id!)) {
// el.style.visibility = 'hidden'
el.style.opacity = '30%'
}
})
dragRecord.value = record
// selectedDate.value = null
isDragging.value = true
@ -621,6 +624,19 @@ const isDateSelected = (date: dayjs.Dayjs) => {
if (!selectedDate.value) return false
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>
<template>
@ -652,10 +668,12 @@ const isDateSelected = (date: dayjs.Dayjs) => {
'border-brand-500 border-1 !border-r-1 border-b-1':
isDateSelected(day) || (focusedDate && dayjs(day).isSame(focusedDate, '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"
data-testid="nc-calendar-month-day"
@click="selectDate(day)"
@dblclick="addRecord(day)"
>
<div v-if="isUIAllowed('dataEdit')" class="flex justify-between p-1">
<span
@ -672,7 +690,7 @@ const isDateSelected = (date: dayjs.Dayjs) => {
'!block': isDateSelected(day),
'!hidden': !isDateSelected(day),
}"
class="!group-hover:block"
class="!group-hover:block rounded"
size="small"
type="secondary"
>
@ -705,12 +723,13 @@ const isDateSelected = (date: dayjs.Dayjs) => {
</template>
</NcDropdown>
<NcButton
v-else
:class="{
'!block': isDateSelected(day),
'!hidden': !isDateSelected(day),
}"
class="!group-hover:block"
size="small"
class="!group-hover:block !w-6 !h-6 !rounded"
size="xsmall"
type="secondary"
@click="
() => {
@ -723,13 +742,13 @@ const isDateSelected = (date: dayjs.Dayjs) => {
}
"
>
<component :is="iconMap.plus" class="h-4 w-4" />
<component :is="iconMap.plus" />
</NcButton>
<span
:class="{
'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') }}
</span>
@ -742,12 +761,12 @@ const isDateSelected = (date: dayjs.Dayjs) => {
recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflow &&
!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"
type="secondary"
@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>
</div>
</div>
@ -761,10 +780,6 @@ const isDateSelected = (date: dayjs.Dayjs) => {
:style="{
...record.rowMeta.style,
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"
@mouseleave="hoverRecord = null"
@ -773,17 +788,11 @@ const isDateSelected = (date: dayjs.Dayjs) => {
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarRecordCard
:hover="hoverRecord === record.rowMeta.id"
:hover="hoverRecord === record.rowMeta.id || record.rowMeta.id === draggingId"
:position="record.rowMeta.position"
:record="record"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
:selected="
dragRecord
? dragRecord.rowMeta.id === record.rowMeta.id
: resizeRecord
? resizeRecord.rowMeta.id === record.rowMeta.id
: false
"
:selected="dragRecord?.rowMeta?.id === record.rowMeta.id || resizeRecord?.rowMeta?.id === record.rowMeta.id"
@resize-start="onResizeStart"
@dblclick.stop="emit('expandRecord', record)"
>
@ -798,6 +807,7 @@ const isDateSelected = (date: dayjs.Dayjs) => {
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"

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

@ -26,11 +26,11 @@ const emit = defineEmits(['resize-start'])
<template>
<div
:class="{
'min-h-9': size === 'small',
'h-6': size === 'small',
'h-full': size === 'auto',
'rounded-l-lg': position === 'leftRounded',
'rounded-r-lg': position === 'rightRounded',
'rounded-lg mx-1': position === 'rounded',
'rounded-l-md ml-1': position === 'leftRounded',
'rounded-r-md mr-1': position === 'rightRounded',
'rounded-md mx-1': position === 'rounded',
'rounded-none': position === 'none',
'bg-maroon-50': color === 'maroon',
'bg-blue-50': color === 'blue',
@ -39,7 +39,8 @@ const emit = defineEmits(['resize-start'])
'bg-pink-50': color === 'pink',
'bg-purple-50': color === 'purple',
'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"
>
@ -53,13 +54,13 @@ const emit = defineEmits(['resize-start'])
'bg-pink-500': color === 'pink',
'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 v-if="(position === 'leftRounded' || position === 'rounded') && resize" class="mt-0.7 h-7.1 absolute -left-4 resize">
<NcButton
:class="{
'!block z-1 !border-brand-500': selected || hover,
'!block z-2 !border-brand-500': selected || hover,
'!hidden': !selected && !hover,
}"
size="xsmall"
@ -70,23 +71,23 @@ const emit = defineEmits(['resize-start'])
</NcButton>
</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
:class="{
'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 />
</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="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
:class="{
'!block !border-brand-500': selected || hover,
'!block !border-brand-500 z-2': selected || hover,
'!hidden': !selected && !hover,
}"
size="xsmall"

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

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import dayjs from 'dayjs'
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" />
</template>
</a-input>
<NcSelect v-model:value="sideBarFilterOption" class="min-w-38 !text-gray-800" data-testid="nc-calendar-sidebar-filter">
<a-select-option v-for="option in options" :key="option.value" :value="option.value" class="!text-gray-800">
<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-600">
<div class="flex items-center justify-between gap-2">
<div class="truncate flex-1">
<NcTooltip :title="option.label" placement="top" show-on-truncate-only>
@ -433,22 +433,7 @@ onUnmounted(() => {
@dragover.prevent
>
<template v-if="!isRowEmpty(record, displayField)">
<div :class="{}">
<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>
<LazySmartsheetCalendarCell v-model="record.row[displayField!.title!]" :column="displayField" />
</template>
</LazySmartsheetCalendarSideRecordCard>
</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"
></span>
<div class="flex text-ellipsis gap-1 flex-col">
<span class="text-sm max-w-40 truncate text-gray-800">
<div class="flex gap-1 flex-col">
<span class="text-sm max-w-56 truncate text-gray-800">
<slot />
</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
: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"
size="xsmall"
@ -38,9 +38,9 @@ const emit = defineEmits(['resize-start'])
</div>
<div
:class="{
'rounded-t-lg': position === 'topRounded',
'rounded-b-lg': position === 'bottomRounded',
'rounded-lg': position === 'rounded',
'rounded-t-md': position === 'topRounded',
'rounded-b-md': position === 'bottomRounded',
'rounded-md': position === 'rounded',
'rounded-none': position === 'none',
'bg-maroon-50': color === 'maroon',
'bg-blue-50': color === 'blue',
@ -49,7 +49,8 @@ const emit = defineEmits(['resize-start'])
'bg-pink-50': color === 'pink',
'bg-purple-50': color === 'purple',
'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"
>
@ -69,9 +70,7 @@ const emit = defineEmits(['resize-start'])
<div v-if="position === 'bottomRounded' || position === 'none'" class="ml-3">....</div>
<span
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"
>
<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">
<slot />
</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 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 {
underline: fi?.underline,
@ -111,7 +111,7 @@ const calendarData = computed(() => {
return !endDate.isBefore(startDate)
})) {
// Generate a unique id for the record if it doesn't have one
const id = record.row.id ?? generateRandomNumber()
const id = record.rowMeta.id ?? generateRandomNumber()
let startDate = dayjs(record.row[fromCol.title!])
const ogStartDate = startDate.clone()
const endDate = dayjs(record.row[toCol.title!])
@ -196,14 +196,14 @@ const calendarData = computed(() => {
style: {
width: widthStyle,
left: `${startDaysDiff * perDayWidth}px`,
top: `${suitableRow * 40}px`,
top: `${suitableRow * 28}px`,
},
},
})
}
} else if (fromCol) {
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 startDaysDiff = Math.max(startDate.diff(selectedDateRange.value.start, 'day'), 0)
@ -222,7 +222,7 @@ const calendarData = computed(() => {
style: {
width: `calc(${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) => {
if (!isUIAllowed('dataEdit')) return
if (!container.value || !dragRecord.value) return
calculateNewRow(event)
calculateNewRow(event, false)
}
const stopDrag = (event: MouseEvent) => {
@ -443,6 +443,7 @@ const stopDrag = (event: MouseEvent) => {
dragElement.value.style.boxShadow = 'none'
dragElement.value = null
}
dragRecord.value = undefined
updateRowProperty(newRow, updateProperty, false)
@ -470,8 +471,6 @@ const dragStart = (event: MouseEvent, record: Row) => {
}
})
dragRecord.value = record
isDragging.value = true
dragElement.value = target
dragRecord.value = record
@ -512,9 +511,25 @@ const dropEvent = (event: DragEvent) => {
dragElement.value = null
}
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>
@ -538,10 +553,12 @@ const dropEvent = (event: DragEvent) => {
:key="dateIndex"
:class="{
'!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"
data-testid="nc-calendar-week-day"
@click="selectedDate = dayjs(date)"
@click="selectDate(date)"
@dblclick="addRecord(date)"
></div>
</div>
<div
@ -555,31 +572,21 @@ const dropEvent = (event: DragEvent) => {
:data-unique-id="record.rowMeta.id"
: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"
@mousedown="dragStart($event, record)"
@mousedown.stop="dragStart($event, record)"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id"
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarRecordCard
:hover="hoverRecord === record.rowMeta.id"
:hover="hoverRecord === record.rowMeta.id || record.rowMeta.id === dragRecord?.rowMeta?.id"
:position="record.rowMeta.position"
:record="record"
:selected="
dragRecord
? dragRecord.rowMeta.id === record.rowMeta.id
: resizeRecord
? resizeRecord.rowMeta.id === record.rowMeta.id
: false
"
:selected="dragRecord?.rowMeta?.id === record.rowMeta.id"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
color="blue"
@dblclick="emits('expand-record', record)"
@dblclick.stop="emits('expand-record', record)"
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
@ -593,6 +600,7 @@ const dropEvent = (event: DragEvent) => {
</template>
<template v-for="(field, index) in fieldsWithoutDisplay" :key="index">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
: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 dragTimeout = ref<ReturnType<typeof setTimeout>>()
@ -444,12 +442,13 @@ const onResize = (event: MouseEvent) => {
const day = Math.floor(percentX * 7)
const hour = Math.floor(percentY * 23)
const minutes = Math.round((percentY * 24 * 60) % 60)
let updateProperty: string[] = []
let newRow: Row = resizeRecord.value
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!]
// 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') {
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!]
// 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
updatedProperty: string[]
} => {
if (!isUIAllowed('dataEdit') || !container.value || !dragRecord.value) return { newRow: null, updatedProperty: [] }
const { width, left, top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
@ -620,11 +618,6 @@ const stopDrag = (event: MouseEvent) => {
el.style.opacity = '100%'
})
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value = null
}
if (newRow) {
updateRowProperty(newRow, updatedProperty, false)
}
@ -654,10 +647,7 @@ const dragStart = (event: MouseEvent, record: Row) => {
}
})
dragRecord.value = record
isDragging.value = true
dragElement.value = target
dragRecord.value = record
document.addEventListener('mousemove', onDrag)
@ -732,6 +722,19 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
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>
<template>
@ -769,13 +772,16 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
:key="hourIndex"
:class="{
'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"
data-testid="nc-calendar-week-hour"
@dblclick="addRecord(hour)"
@click="
() => {
selectedTime = hour
selectedDate = hour
dragRecord = undefined
}
"
>
@ -806,18 +812,19 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
:data-unique-id="record.rowMeta!.id"
:style="record.rowMeta!.style "
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"
@mouseover="hoverRecord = record.rowMeta.id"
@dragover.prevent
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarVRecordCard
:hover="hoverRecord === record.rowMeta.id"
:hover="hoverRecord === record.rowMeta.id || record.rowMeta.id === dragRecord?.rowMeta?.id"
:position="record.rowMeta!.position"
:record="record"
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
:record="record"
color="blue"
:selected="record.rowMeta!.id === dragRecord?.rowMeta?.id"
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
@ -832,6 +839,7 @@ const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"

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

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

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

@ -28,7 +28,7 @@ const IsPublic = inject(IsPublicInj, ref(false))
const { loadViewColumns } = useViewColumnsOrThrow()
const { loadCalendarMeta, loadCalendarData, loadSidebarData } = useCalendarViewStoreOrThrow()
const { loadCalendarMeta, loadCalendarData, loadSidebarData, fetchActiveDates } = useCalendarViewStoreOrThrow()
const calendarRangeDropdown = ref(false)
@ -71,7 +71,7 @@ const saveCalendarRanges = async () => {
calendar_range: calRanges as CalendarRangeType[],
})
await loadCalendarMeta()
await Promise.all([loadCalendarData(), loadSidebarData()])
await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()])
} catch (e) {
console.log(e)
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 () => {
_calendar_ranges.value.push({
fk_from_column_id: dateFieldOptions.value![0].value as string,
@ -132,7 +130,7 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</a-button>
</div>
<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
v-for="(range, id) in _calendar_ranges"
:key="id"
@ -167,9 +165,7 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</a-select-option>
</NcSelect>
<!--
TODO: Add support for end date and multiple range in future
<div
<!-- <div
v-if="range.fk_to_column_id === null && isEeUI"
class="flex cursor-pointer flex text-gray-800 items-center gap-1"
data-testid="nc-calendar-range-add-end-date"
@ -178,7 +174,7 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
<component :is="iconMap.plus" class="h-4 w-4" />
{{ $t('activity.addEndDate') }}
</div>
<template v-else-if="isEeUI && false">
<template v-else-if="isEeUI">
<span>
{{ $t('activity.withEndDate') }}
</span>
@ -213,23 +209,16 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</NcButton>
</div>
</template>
<NcButton v-if="id !== 0" size="small" type="secondary" @click="removeRange(id)">
<component :is="iconMap.close" />
</NcButton>
-->
</div>
<NcButton
v-if="false"
class="mt-2"
data-testid="nc-calendar-range-add-btn"
size="small"
type="secondary"
@click="addCalendarRange"
>
<!-- <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>
</NcButton> -->
</div>
</template>
</NcDropdown>

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

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

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

@ -75,6 +75,10 @@ interface Row {
id?: string
position?: string
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
// then we need to convert the value to timestamptz before comparing
if (
column.uidt === UITypes.DateTime &&
(column.uidt === UITypes.DateTime ||
column.uidt === UITypes.Date) &&
val.match(/[+-]\d{2}:\d{2}$/)
) {
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
// then we need to convert the value to timestamptz before comparing
if (
column.uidt === UITypes.DateTime &&
(column.uidt === UITypes.DateTime ||
column.uidt === UITypes.Date) &&
val.match(/[+-]\d{2}:\d{2}$/)
) {
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
// then we need to convert the value to timestamptz before comparing
if (
column.uidt === UITypes.DateTime &&
(column.uidt === UITypes.DateTime ||
column.uidt === UITypes.Date) &&
val.match(/[+-]\d{2}:\d{2}$/)
) {
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
// then we need to convert the value to timestamptz before comparing
if (
column.uidt === UITypes.DateTime &&
(column.uidt === UITypes.DateTime ||
column.uidt === UITypes.Date) &&
val.match(/[+-]\d{2}:\d{2}$/)
) {
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({
...param,
...query,
viewName: view.id,
baseName: model.base_id,
tableName: model.id,
calendarLimitOverride: 3000, // TODO: make this configurable in env

Loading…
Cancel
Save