Browse Source

feat: calendar followup (#8643)

* fix: sidemenu update design

* fix: image roundness

* fix: image roundness

* feat: support current time indicator

* feat: support hide weekends feat: current time indicator

* fix: hiding on incorrect records

* fix: move to bottom

* fix: week view missing weekend hide

* fix:minor corrections

* fix: overlaps in weekview and day view

* fix: calendar minor fixes

* fix:styles

* fix minor changes

* fix: ui ux changes

* fix: limit max sidebar width to 20rem on lower viewport

* fix: update hover effects and width

* fix: add some hover effect
pull/8701/head
Anbarasu 5 months ago committed by GitHub
parent
commit
c1a537b3c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      packages/nc-gui/components/cell/attachment/Image.vue
  2. 10
      packages/nc-gui/components/dashboard/View.vue
  3. 2
      packages/nc-gui/components/nc/DateWeekSelector.vue
  4. 26
      packages/nc-gui/components/nc/MonthYearSelector.vue
  5. 74
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  6. 78
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  7. 104
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  8. 19
      packages/nc-gui/components/smartsheet/calendar/SideRecordCard.vue
  9. 37
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  10. 148
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  11. 2
      packages/nc-gui/components/smartsheet/calendar/YearView/index.vue
  12. 4
      packages/nc-gui/components/smartsheet/toolbar/Calendar/ActiveView.vue
  13. 10
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Header.vue
  14. 2
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Mode.vue
  15. 46
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Range.vue
  16. 8
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Today.vue
  17. 2
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  18. 36
      packages/nc-gui/composables/useCalendarViewStore.ts
  19. 1
      packages/nc-gui/lang/en.json

2
packages/nc-gui/components/cell/attachment/Image.vue

@ -14,7 +14,7 @@ const onError = () => index.value++
<template>
<LazyNuxtImg
v-if="index < props.srcs.length"
class="m-auto h-full max-h-full w-auto object-cover"
class="m-auto h-full max-h-full w-auto nc-attachment-image object-cover"
:src="props.srcs[index]"
:alt="props?.alt || ''"
placeholder

10
packages/nc-gui/components/dashboard/View.vue

@ -137,6 +137,16 @@ function onResize(widthPercent: any) {
const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
// If the viewport width is less than 1560px, the max sidebar width should be 20rem
if (viewportWidth.value <= 1560) {
if (width > remToPx(20)) {
sideBarSize.value.old = ((20 * fontSize) / viewportWidth.value) * 100
if (isLeftSidebarOpen.value) sideBarSize.value.current = sideBarSize.value.old
return
}
}
const widthRem = width / fontSize
if (widthRem < 16) {

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

@ -191,7 +191,7 @@ const paginate = (action: 'next' | 'prev') => {
<div v-if="!hideCalendar" class="max-w-[320px] rounded-y-xl">
<div class="py-1 px-2.5 h-10">
<div
class="flex gap-1"
class="flex justify-between gap-1"
:class="{
'border-b-1 border-gray-200 ': isCellInputField,
}"

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

@ -93,10 +93,10 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
<template>
<div class="flex flex-col">
<div
class="flex border-b-1 justify-between items-center"
class="flex border-b-1 nc-month-picker-pagination justify-between items-center"
:class="{
'px-2 py-1 h-10': isCellInputField,
'px-3 py-0.5': !isCellInputField,
'px-2 py-0.5': !isCellInputField,
}"
>
<div class="flex">
@ -137,10 +137,10 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
</div>
<div
v-if="!hideCalendar"
class="rounded-y-xl py-1 max-w-[350px]"
class="rounded-y-xl max-w-[350px]"
:class="{
'px-2': isCellInputField,
'px-2.5': !isCellInputField,
'px-2 py-1': isCellInputField,
'px-2.5 py-2': !isCellInputField,
}"
>
<div class="grid grid-cols-4 gap-2">
@ -153,10 +153,10 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
'bg-gray-300 !font-weight-600 ': isMonthSelected(month) && isCellInputField,
'hover:(border-1 border-gray-200 bg-gray-100)': !isMonthSelected(month),
'!text-brand-500': dayjs().isSame(month, 'month'),
'font-weight-400 rounded': isCellInputField,
'font-medium rounded-lg': !isCellInputField,
'font-weight-400': isCellInputField,
'font-medium': !isCellInputField,
}"
class="nc-month-item h-8 flex items-center transition-all justify-center text-gray-700 cursor-pointer"
class="nc-month-item h-8 flex items-center rounded transition-all justify-center text-gray-700 cursor-pointer"
:title="isCellInputField ? month.format('YYYY-MM') : undefined"
@click="selectedDate = month"
>
@ -168,14 +168,14 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
v-for="(year, id) in years"
:key="id"
:class="{
'bg-gray-200 !text-brand-500 !font-bold ': compareYear(year, selectedDate) && !isCellInputField,
'bg-gray-300 !font-weight-600 ': compareYear(year, selectedDate) && isCellInputField,
'bg-gray-200 !font-bold ': compareYear(year, selectedDate) && !isCellInputField,
'bg-gray-300 !text-brand-500 !font-weight-600 ': compareYear(year, selectedDate) && isCellInputField,
'hover:(border-1 border-gray-200 bg-gray-100)': !compareYear(year, selectedDate),
'!text-brand-500': dayjs().isSame(year, 'year'),
'font-weight-400 text-gray-700 rounded': isCellInputField,
'font-medium text-gray-900 rounded-lg': !isCellInputField,
'font-weight-400 text-gray-700': isCellInputField,
'font-medium text-gray-900': !isCellInputField,
}"
class="nc-year-item h-8 flex items-center transition-all justify-center cursor-pointer"
class="nc-year-item h-8 flex items-center rounded transition-all justify-center cursor-pointer"
:title="isCellInputField ? year.format('YYYY') : undefined"
@click="selectedDate = year"
>

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

@ -26,6 +26,8 @@ const meta = inject(MetaInj, ref())
const fields = inject(FieldsInj, ref())
const isPublic = inject(IsPublicInj, ref(false))
const { fields: _fields } = useViewColumnsOrThrow()
const fieldStyles = computed(() => {
@ -56,6 +58,29 @@ const hours = computed(() => {
return hours
})
const currTime = ref(dayjs())
const overlayTop = computed(() => {
const perRecordHeight = 52
const minutes = currTime.value.minute() + currTime.value.hour() * 60
const top = (perRecordHeight / 60) * minutes
return top
})
onMounted(() => {
const intervalId = setInterval(() => {
currTime.value = dayjs()
}, 10000) // 10000 ms = 10 seconds
// Clean up the interval when the component is unmounted
onUnmounted(() => {
clearInterval(intervalId)
})
})
const calculateNewDates = useMemoize(
({
endDate,
@ -166,12 +191,21 @@ const getMaxOverlaps = ({
return maxOverlaps
}
let maxOverlaps = 1
const id = row.rowMeta.id as string
if (graph.has(id)) {
maxOverlaps = dfs(id)
dfs(id)
}
return maxOverlaps
const overlapIterations: Array<number> = []
columnArray
.flat()
.filter((record) => visited.has(record.rowMeta.id!))
.forEach((record) => {
overlapIterations.push(record.rowMeta.overLapIteration!)
})
return Math.max(...overlapIterations)
}
const recordsAcrossAllRange = computed<{
@ -246,8 +280,8 @@ const recordsAcrossAllRange = computed<{
const heightInPixels = Math.max(endDate.diff(startDate, 'minute'), perRecordHeight)
const style: Partial<CSSStyleDeclaration> = {
height: `${heightInPixels - 8}px`,
top: `${topInPixels + 4}px`,
height: `${heightInPixels - 2}px`,
top: `${topInPixels + 1}px`,
}
// This property is used to determine which side the record should be rounded. It can be top, bottom, both or none
@ -298,8 +332,8 @@ const recordsAcrossAllRange = computed<{
const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 52, perRecordHeight)
style = {
...style,
top: `${topInPixels + 4}px`,
height: `${heightInPixels - 8}px`,
top: `${topInPixels + 1}px`,
height: `${heightInPixels - 2}px`,
}
recordsByRange.push({
@ -868,6 +902,24 @@ watch(
data-testid="nc-calendar-day-view"
@drop="dropEvent"
>
<div
v-if="!isPublic && dayjs().isSame(selectedDate, 'day')"
class="absolute ml-2 pointer-events-none w-full z-4"
:style="{
top: `${overlayTop}px`,
}"
>
<div class="flex w-full items-center">
<span
class="text-brand-500 text-xs rounded-md border-1 pointer-events-auto px-0.5 border-brand-200 cursor-pointer bg-brand-50"
@click="newRecord(dayjs())"
>
{{ dayjs().format('hh:mm A') }}
</span>
<div class="flex-1 border-b-1 border-brand-500"></div>
</div>
</div>
<div>
<div
v-for="(hour, index) in hours"
@ -895,7 +947,7 @@ watch(
@dblclick="newRecord(hour)"
>
<NcDropdown
v-if="calendarRange.length > 1"
v-if="calendarRange.length > 1 && !isPublic"
:class="{
'!block': hour.isSame(selectedTime),
'!hidden': !hour.isSame(selectedTime),
@ -903,7 +955,7 @@ watch(
auto-close
>
<NcButton
class="!group-hover:block mr-4 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block absolute"
class="!group-hover:block mr-12 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block absolute"
size="xsmall"
type="secondary"
>
@ -944,12 +996,12 @@ watch(
</template>
</NcDropdown>
<NcButton
v-else
v-else-if="!isPublic"
:class="{
'!block': hour.isSame(selectedTime),
'!hidden': !hour.isSame(selectedTime),
}"
class="!group-hover:block mr-4 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block absolute"
class="!group-hover:block mr-12 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block absolute"
size="xsmall"
type="secondary"
@click="

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

@ -14,6 +14,7 @@ const {
sideBarFilterOption,
displayField,
calendarRange,
viewMetaProperties,
showSideMenu,
updateRowProperty,
} = useCalendarViewStoreOrThrow()
@ -26,12 +27,24 @@ const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref())
const maxVisibleDays = computed(() => {
return viewMetaProperties.value?.hide_weekend ? 5 : 7
})
const days = computed(() => {
let days = []
if (isMondayFirst.value) {
return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
} else {
return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
}
if (maxVisibleDays.value === 5) {
days = days.filter((day) => day !== 'Sat' && day !== 'Sun')
}
return days
})
const calendarGridContainer = ref()
@ -42,6 +55,14 @@ const isDayInPagedMonth = (date: dayjs.Dayjs) => {
return date.month() === selectedMonth.value.month()
}
const getDayIndex = (date: dayjs.Dayjs) => {
let dayIndex = date.day() - 1
if (dayIndex === -1) {
dayIndex = 6
}
return dayIndex
}
const dragElement = ref<HTMLElement | null>(null)
const draggingId = ref<string | null>(null)
@ -115,7 +136,7 @@ const recordsToDisplay = computed<{
}>(() => {
if (!dates.value || !calendarRange.value) return []
const perWidth = gridContainerWidth.value / 7
const perWidth = gridContainerWidth.value / maxVisibleDays.value
const perHeight = gridContainerHeight.value / dates.value.length
const perRecordHeight = 24
@ -176,6 +197,12 @@ const recordsToDisplay = computed<{
width: `${perWidth}px`,
}
if (maxVisibleDays.value === 5) {
if (dayIndex === 5 || dayIndex === 6) {
style.display = 'none'
}
}
// Number of records in that day
const recordIndex = recordsInDay[dateKey].count
@ -364,12 +391,15 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean, skipChangeC
if (!fromCol) return { newRow: null, updateProperty: [] }
const week = Math.floor(percentY * dates.value.length)
const day = Math.floor(percentX * 7)
const day = Math.floor(percentX * maxVisibleDays.value)
let newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
if (!newStartDate) return { newRow: null, updateProperty: [] }
const fromDate = dayjs(dragRecord.value.row[fromCol.title!])
let fromDate = dayjs(dragRecord.value.row[fromCol.title!])
if (!fromDate.isValid()) {
fromDate = dayjs()
}
newStartDate = newStartDate.add(fromDate.hour(), 'hour').add(fromDate.minute(), 'minute').add(fromDate.second(), 'second')
@ -461,7 +491,7 @@ const onResize = (event: MouseEvent) => {
const toCol = resizeRecord.value.rowMeta.range?.fk_to_col
const week = Math.floor(percentY * dates.value.length)
const day = Math.floor(percentX * 7)
const day = Math.floor(percentX * maxVisibleDays.value)
let updateProperty: string[] = []
let newRow: Row
@ -671,11 +701,17 @@ const addRecord = (date: dayjs.Dayjs) => {
<template>
<div v-if="calendarRange" class="h-full prevent-select relative" data-testid="nc-calendar-month-view">
<div class="grid grid-cols-7">
<div
class="grid"
:class="{
'grid-cols-7': maxVisibleDays === 7,
'grid-cols-5': maxVisibleDays === 5,
}"
>
<div
v-for="(day, index) in days"
:key="index"
class="text-center bg-gray-50 py-1 border-b-1 border-r-1 last:border-r-0 border-gray-200 font-regular uppercase text-xs text-gray-500"
class="text-center bg-gray-50 py-1 border-r-1 last:border-r-0 border-gray-200 font-semibold leading-4 uppercase text-[10px] text-gray-500"
>
{{ day }}
</div>
@ -691,15 +727,26 @@ const addRecord = (date: dayjs.Dayjs) => {
style="height: calc(100% - 1.59rem)"
@drop="dropEvent"
>
<div v-for="(week, weekIndex) in dates" :key="weekIndex" class="grid grid-cols-7 grow" data-testid="nc-calendar-month-week">
<div
v-for="(day, dateIndex) in week"
v-for="(week, weekIndex) in dates"
:key="weekIndex"
:class="{
'grid-cols-7': maxVisibleDays === 7,
'grid-cols-5': maxVisibleDays === 5,
}"
class="grid grow"
data-testid="nc-calendar-month-week"
>
<template v-for="(day, dateIndex) in week">
<div
v-if="maxVisibleDays === 5 ? day.get('day') !== 0 && day.get('day') !== 6 : true"
:key="`${weekIndex}-${dateIndex}`"
:class="{
'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,
'!bg-gray-50 !hover:bg-gray-100': day.get('day') === 0 || day.get('day') === 6,
'border-t-1': weekIndex === 0,
}"
class="text-right relative group last:border-r-0 transition text-sm h-full border-r-1 border-b-1 border-gray-200 font-medium hover:bg-gray-50 text-gray-800 bg-white"
data-testid="nc-calendar-month-day"
@ -779,12 +826,12 @@ const addRecord = (date: dayjs.Dayjs) => {
:class="{
'bg-brand-50 text-brand-500 !font-bold': day.isSame(dayjs(), 'date'),
}"
class="px-1.3 py-1 text-sm font-medium rounded-lg"
class="px-1.3 py-1 text-sm leading-3 font-medium rounded-lg"
>
{{ day.format('DD') }}
</span>
</div>
<div v-if="!isUIAllowed('dataEdit')" class="p-3">{{ dayjs(day).format('DD') }}</div>
<div v-if="!isUIAllowed('dataEdit')" class="leading-3 p-3">{{ dayjs(day).format('DD') }}</div>
<NcButton
v-if="
@ -801,6 +848,7 @@ const addRecord = (date: dayjs.Dayjs) => {
<span class="text-xs px-1"> + {{ recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflowCount }} </span>
</NcButton>
</div>
</template>
</div>
</div>
<div class="absolute inset-0 pointer-events-none mt-8 pb-7.5" data-testid="nc-calendar-month-record-container">
@ -857,4 +905,8 @@ const addRecord = (date: dayjs.Dayjs) => {
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
}
.grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
</style>

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

@ -9,6 +9,10 @@ const props = defineProps<{
const emit = defineEmits(['expandRecord', 'newRecord'])
interface Attachment {
url: string
}
const INFINITY_SCROLL_THRESHOLD = 100
const { isUIAllowed } = useRoles()
@ -21,6 +25,10 @@ const { height } = useWindowSize()
const meta = inject(MetaInj, ref())
const { fields } = useViewColumnsOrThrow()
const { getPossibleAttachmentSrc } = useAttachment()
const { t } = useI18n()
const {
@ -45,6 +53,23 @@ const {
const sideBarListRef = ref<VNodeRef | null>(null)
const coverImageColumns: any = computed(() => {
if (!fields.value || !meta.value?.columns) return
return meta.value.columns.find((c) => c.uidt === UITypes.Attachment && fields.value?.find((f) => f.fk_column_id === c.id).show)
})
const attachments = (record: any): Attachment[] => {
const col = coverImageColumns.value
try {
if (col?.title && record.row[col.title]) {
return typeof record.row[col.title] === 'string' ? JSON.parse(record.row[col.title]) : record.row[col.title]
}
return []
} catch (e) {
return []
}
}
const pushToArray = (arr: Array<Row>, record: Row, range) => {
arr.push({
...record,
@ -381,9 +406,14 @@ onClickOutside(searchRef, toggleSearch)
>
<div class="flex px-4 items-center gap-3">
<span class="capitalize font-medium text-gray-700">{{ $t('objects.records') }}</span>
<NcSelect v-model:value="sideBarFilterOption" class="w-full !text-gray-600" data-testid="nc-calendar-sidebar-filter">
<NcSelect
v-model:value="sideBarFilterOption"
size="small"
class="w-full !h-7 !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 w-full justify-between gap-2">
<div class="flex items-center h-7 w-full justify-between gap-2">
<div class="truncate">
<NcTooltip :title="option.label" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
@ -429,7 +459,7 @@ onClickOutside(searchRef, toggleSearch)
v-if="!showSearch"
data-testid="nc-calendar-sidebar-search-btn"
size="small"
class="!h-7"
class="!h-7 !rounded-md"
type="secondary"
@click="clickSearch"
>
@ -444,7 +474,7 @@ onClickOutside(searchRef, toggleSearch)
v-if="isUIAllowed('dataEdit') && props.visible"
v-e="['c:calendar:calendar-sidemenu-new-record-btn']"
data-testid="nc-calendar-side-menu-new-btn"
class="!h-7"
class="!h-7 !rounded-md"
size="small"
type="secondary"
@click="newRecord"
@ -497,8 +527,8 @@ onClickOutside(searchRef, toggleSearch)
:from-date="
record.rowMeta.range?.fk_from_col
? calDataType === UITypes.Date
? dayjs(record.row[record.rowMeta.range.fk_from_col.title!]).format('DD MMM')
: dayjs(record.row[record.rowMeta.range.fk_from_col.title!]).format('DD MMM • HH:mm A')
? dayjs(record.row[record.rowMeta.range.fk_from_col.title!]).format('D MMM')
: dayjs(record.row[record.rowMeta.range.fk_from_col.title!]).format('D MMM • h:mm a')
: null
"
:invalid="
@ -521,6 +551,50 @@ onClickOutside(searchRef, toggleSearch)
@dragstart="dragStart($event, record)"
@dragover.prevent
>
<template v-if="coverImageColumns" #image>
<a-carousel
v-if="attachments(record).length"
class="gallery-carousel rounded-md !border-1 !border-gray-200"
arrows
>
<template #customPaging>
<a>
<div class="pt-[12px]">
<div></div>
</div>
</a>
</template>
<template #prevArrow>
<div class="z-10 arrow">
<MdiChevronLeft
class="text-gray-700 w-6 h-6 absolute left-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
</div>
</template>
<template #nextArrow>
<div class="z-10 arrow">
<MdiChevronRight
class="text-gray-700 w-6 h-6 absolute right-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
</div>
</template>
<template v-for="(attachment, index) in attachments(record)">
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-10 !w-10 !object-contain"
:srcs="getPossibleAttachmentSrc(attachment)"
/>
</template>
</a-carousel>
<div v-else class="h-10 w-10 !flex flex-row !border-1 rounded-md !border-gray-200 items-center justify-center">
<img class="object-contain w-[40px] h-[40px]" src="~assets/icons/FileIconImageBox.png" />
</div>
</template>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetPlainCell v-model="record.row[displayField!.title!]" :column="displayField" />
</template>
@ -582,4 +656,20 @@ onClickOutside(searchRef, toggleSearch)
</div>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
:deep(.nc-attachment-image) {
@apply rounded-md;
}
:deep(.ant-select-selector) {
@apply !h-7;
}
:deep(.nc-month-picker-pagination) {
@apply !border-b-0;
}
:deep(.nc-date-week-header) {
@apply !border-b-0;
}
</style>

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

@ -16,17 +16,7 @@ const props = withDefaults(defineProps<Props>(), {
</script>
<template>
<div
:class="{
'bg-maroon-50': props.color === 'maroon',
'bg-blue-50': props.color === 'blue',
'bg-green-50': props.color === 'green',
'bg-yellow-50': props.color === 'yellow',
'bg-pink-50': props.color === 'pink',
'bg-purple-50': props.color === 'purple',
}"
class="border-1 cursor-pointer h-14 border-gray-200 flex gap-2 items-center rounded-lg"
>
<div class="border-1 cursor-pointer h-14 border-gray-200 flex gap-2 items-center rounded-lg">
<div class="flex items-center pl-2 gap-2">
<span
:class="{
@ -39,11 +29,14 @@ const props = withDefaults(defineProps<Props>(), {
}"
class="block h-10 w-1 rounded"
></span>
<slot name="image" />
<div class="flex gap-1 flex-col">
<span class="text-sm max-w-56 font-medium truncate text-gray-800">
<span class="text-[13px] leading-4 max-w-56 font-medium truncate text-gray-800">
<slot />
</span>
<span v-if="showDate" class="text-xs font-medium text-gray-500">{{ fromDate }} {{ toDate ? ` - ${toDate}` : '' }}</span>
<span v-if="showDate" class="text-xs font-medium leading-4 text-gray-600"
>{{ fromDate }} {{ toDate ? ` - ${toDate}` : '' }}</span
>
</div>
</div>

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

@ -5,8 +5,20 @@ import type { Row } from '~/lib/types'
const emits = defineEmits(['expandRecord', 'newRecord'])
const { selectedDateRange, formattedData, formattedSideBarData, calendarRange, selectedDate, displayField, updateRowProperty } =
useCalendarViewStoreOrThrow()
const {
selectedDateRange,
formattedData,
formattedSideBarData,
calendarRange,
selectedDate,
displayField,
updateRowProperty,
viewMetaProperties,
} = useCalendarViewStoreOrThrow()
const maxVisibleDays = computed(() => {
return viewMetaProperties.value?.hide_weekend ? 5 : 7
})
const container = ref<null | HTMLElement>(null)
@ -41,12 +53,17 @@ const getFieldStyle = (field: ColumnType) => {
// Calculate the dates of the week
const weekDates = computed(() => {
let startOfWeek = dayjs(selectedDateRange.value.start)
const endOfWeek = dayjs(selectedDateRange.value.end)
let endOfWeek = dayjs(selectedDateRange.value.end)
if (maxVisibleDays.value === 5) {
endOfWeek = endOfWeek.subtract(2, 'day')
}
const datesArray = []
while (startOfWeek.isBefore(endOfWeek) || startOfWeek.isSame(endOfWeek, 'day')) {
datesArray.push(dayjs(startOfWeek))
startOfWeek = startOfWeek.add(1, 'day')
}
return datesArray
})
@ -111,7 +128,7 @@ const calendarData = computed(() => {
}
const recordsInRange: Array<Row> = []
const perDayWidth = containerWidth.value / 7
const perDayWidth = containerWidth.value / maxVisibleDays.value
calendarRange.value.forEach((range) => {
const fromCol = range.fk_from_col
@ -284,7 +301,7 @@ const onResize = (event: MouseEvent) => {
const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
const ogStartDate = dayjs(resizeRecord.value.row[fromCol.title!])
const day = Math.floor(percentX * 7)
const day = Math.floor(percentX * maxVisibleDays.value)
let updateProperty: string[] = []
let updateRecord: Row
@ -370,7 +387,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBarData?: boolean) => {
// Calculate the day index based on the percentage of the width
// The day index is a number between 0 and 6
const day = Math.floor(percentX * 7)
const day = Math.floor(percentX * maxVisibleDays.value)
// Calculate the new start date based on the day index by adding the day index to the start date of the selected date range
const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
@ -557,8 +574,10 @@ const addRecord = (date: dayjs.Dayjs) => {
:key="weekIndex"
:class="{
'!border-brand-500 !border-b-gray-200': dayjs(date).isSame(selectedDate, 'day'),
'w-1/5': maxVisibleDays === 5,
'w-1/7': maxVisibleDays === 7,
}"
class="w-1/7 cursor-pointer text-center text-[10px] font-semibold leading-4 flex items-center justify-center uppercase text-gray-500 w-full py-1 border-gray-200 border-l-gray-50 border-t-gray-50 last:border-r-0 border-1 bg-gray-50"
class="cursor-pointer text-center text-[10px] font-semibold leading-4 flex items-center justify-center uppercase text-gray-500 w-full py-1 border-gray-200 border-l-gray-50 border-t-gray-50 last:border-r-0 border-1 bg-gray-50"
@click="selectDate(date)"
@dblclick="addRecord(date)"
>
@ -572,8 +591,10 @@ const addRecord = (date: dayjs.Dayjs) => {
: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,
'w-1/5': maxVisibleDays === 5,
'w-1/7': maxVisibleDays === 7,
}"
class="flex cursor-pointer flex-col border-r-1 min-h-[100vh] last:border-r-0 items-center w-1/7"
class="flex cursor-pointer flex-col border-r-1 min-h-[100vh] last:border-r-0 items-center"
data-testid="nc-calendar-week-day"
@click="selectDate(date)"
@dblclick="addRecord(date)"

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

@ -11,6 +11,7 @@ const {
formattedSideBarData,
calendarRange,
displayField,
viewMetaProperties,
selectedTime,
updateRowProperty,
sideBarFilterOption,
@ -25,6 +26,8 @@ const scrollContainer = ref<null | HTMLElement>(null)
const { width: containerWidth } = useElementSize(container)
const isPublic = inject(IsPublicInj, ref(false))
const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref())
@ -47,6 +50,50 @@ const fieldStyles = computed(() => {
)
})
const getDayIndex = (date: dayjs.Dayjs) => {
let dayIndex = date.day() - 1
if (dayIndex === -1) {
dayIndex = 6
}
return dayIndex
}
const maxVisibleDays = computed(() => {
return viewMetaProperties.value?.hide_weekend ? 5 : 7
})
const currTime = ref(dayjs())
const overlayStyle = computed(() => {
if (!containerWidth.value)
return {
top: 0,
left: 0,
}
const left = (containerWidth.value / maxVisibleDays.value) * getDayIndex(currTime.value)
const minutes = currTime.value.hour() * 60 + currTime.value.minute()
const top = (52 / 60) * minutes
return {
width: `${containerWidth.value / maxVisibleDays.value}px`,
top: `${top}px`,
left: `${left}px`,
}
})
onMounted(() => {
const intervalId = setInterval(() => {
currTime.value = dayjs()
}, 10000) // 10000 ms = 10 seconds
// Clean up the interval when the component is unmounted
onUnmounted(() => {
clearInterval(intervalId)
})
})
const getFieldStyle = (field: ColumnType) => {
return fieldStyles.value.get(field.id)
}
@ -85,7 +132,11 @@ const calculateNewDates = useMemoize(
const datesHours = computed(() => {
const datesHours: Array<Array<dayjs.Dayjs>> = []
let startOfWeek = dayjs(selectedDateRange.value.start) ?? dayjs().startOf('week')
const endOfWeek = dayjs(selectedDateRange.value.end) ?? dayjs().endOf('week')
let endOfWeek = dayjs(selectedDateRange.value.end) ?? dayjs().endOf('week')
if (maxVisibleDays.value === 5) {
endOfWeek = endOfWeek.subtract(2, 'day')
}
while (startOfWeek.isSameOrBefore(endOfWeek)) {
const hours: Array<dayjs.Dayjs> = []
@ -107,14 +158,6 @@ const datesHours = computed(() => {
return datesHours
})
const getDayIndex = (date: dayjs.Dayjs) => {
let dayIndex = date.day() - 1
if (dayIndex === -1) {
dayIndex = 6
}
return dayIndex
}
const getGridTime = (date: dayjs.Dayjs, round = false) => {
const gridCalc = date.hour() * 60 + date.minute()
if (round) {
@ -201,8 +244,20 @@ const getMaxOverlaps = ({
let maxOverlaps = 1
if (graph.has(id)) {
maxOverlaps = dfs(id)
dfs(id)
}
const overlapIterations: Array<number> = []
columnArray[dayIndex]
.flat()
.filter((record) => visited.has(record.rowMeta.id!))
.forEach((record) => {
overlapIterations.push(record.rowMeta.overLapIteration!)
})
maxOverlaps = Math.max(...overlapIterations)
return { maxOverlaps, dayIndex, overlapIndex }
}
@ -224,11 +279,15 @@ const recordsAcrossAllRange = computed<{
records: [],
gridTimeMap: new Map(),
}
const perWidth = containerWidth.value / 7
const perWidth = containerWidth.value / maxVisibleDays.value
const perHeight = 52
const scheduleStart = dayjs(selectedDateRange.value.start).startOf('day')
const scheduleEnd = dayjs(selectedDateRange.value.end).endOf('day')
let scheduleEnd = dayjs(selectedDateRange.value.end).endOf('day')
if (maxVisibleDays.value === 5) {
scheduleEnd = scheduleEnd.subtract(2, 'day')
}
const columnArray: Array<Array<Array<Row>>> = [[[]]]
const gridTimeMap = new Map<
@ -489,17 +548,25 @@ const recordsAcrossAllRange = computed<{
})
const dayIndex = record.rowMeta.dayIndex ?? tDayIndex
let display = 'block'
if (maxVisibleDays.value === 5) {
if (dayIndex === 5 || dayIndex === 6) {
display = 'none'
}
}
record.rowMeta.numberOfOverlaps = maxOverlaps
let width = 0
let left = 100
const majorLeft = dayIndex * perWidth
let display = 'block'
if (record.rowMeta.overLapIteration! - 1 > 2) {
display = 'none'
} else {
width = 100 / Math.min(maxOverlaps, 3) / 7
width = 100 / Math.min(maxOverlaps, 3) / maxVisibleDays.value
left = width * (overlapIndex - 1)
}
record.rowMeta.style = {
@ -563,7 +630,7 @@ const onResize = (event: MouseEvent) => {
const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
const ogStartDate = dayjs(resizeRecord.value.row[fromCol.title!])
const day = Math.floor(percentX * 7)
const day = Math.floor(percentX * maxVisibleDays.value)
const hour = Math.floor(percentY * 23)
const minutes = Math.round((percentY * 24 * 60) % 60)
@ -656,7 +723,7 @@ const calculateNewRow = (
if (!fromCol) return { newRow: null, updatedProperty: [] }
const day = Math.max(0, Math.min(6, Math.floor(percentX * 7)))
const day = Math.max(0, Math.min(6, Math.floor(percentX * maxVisibleDays.value)))
const hour = Math.max(0, Math.min(23, Math.floor(percentY * 24)))
const minutes = Math.round(((percentY * 24 * 60) % 60) / 15) * 15
@ -877,14 +944,32 @@ watch(
data-testid="nc-calendar-week-view"
@drop="dropEvent"
>
<div
v-if="!isPublic && dayjs().isBetween(selectedDateRange.start, selectedDateRange.end)"
class="absolute ml-16 mt-7 pointer-events-none z-4"
:style="overlayStyle"
>
<div class="flex w-full items-center">
<span
class="text-brand-500 rounded-md text-xs border-1 pointer-events-auto px-0.5 border-brand-200 cursor-pointer bg-brand-50"
@click="addRecord(dayjs())"
>
{{ dayjs().format('hh:mm A') }}
</span>
<div class="flex-1 border-b-1 border-brand-500"></div>
</div>
</div>
<div class="flex sticky h-6 z-1 top-0 pl-16 bg-gray-50 w-full">
<div
v-for="date in datesHours"
:key="date[0].toISOString()"
:class="{
'text-brand-500': date[0].isSame(dayjs(), 'date'),
'w-1/5': maxVisibleDays === 5,
'w-1/7': maxVisibleDays === 7,
}"
class="w-1/7 text-center text-[10px] font-semibold leading-4 flex items-center justify-center uppercase text-gray-500 w-full py-1 border-gray-200 last:border-r-0 border-b-1 border-l-1 border-r-0 bg-gray-50"
class="text-center text-[10px] font-semibold leading-4 flex items-center justify-center uppercase text-gray-500 w-full py-1 border-gray-200 last:border-r-0 border-b-1 border-l-1 border-r-0 bg-gray-50"
>
{{ dayjs(date[0]).format('DD ddd') }}
</div>
@ -899,15 +984,27 @@ watch(
</div>
</div>
<div ref="container" class="absolute ml-16 flex w-[calc(100%-64px)]">
<div v-for="(date, index) in datesHours" :key="index" class="h-full w-1/7 mt-7.1" data-testid="nc-calendar-week-day">
<div
v-for="(date, index) in datesHours"
:key="index"
:class="{
'w-1/5': maxVisibleDays === 5,
'w-1/7': maxVisibleDays === 7,
}"
class="h-full mt-5.95"
data-testid="nc-calendar-week-day"
>
<div
v-for="(hour, hourIndex) in date"
:key="hourIndex"
:class="{
'border-1 !border-brand-500 !bg-gray-100':
hour.isSame(selectedTime, 'hour') && (hour.get('day') === 6 || hour.get('day') === 0),
'border-1 !border-brand-500 bg-gray-50': hour.isSame(selectedTime, 'hour'),
'!bg-gray-50': hour.get('day') === 0 || hour.get('day') === 6,
'bg-gray-50 hover:bg-gray-100': hour.get('day') === 0 || hour.get('day') === 6,
'hover:bg-gray-50': hour.get('day') !== 0 && hour.get('day') !== 6,
}"
class="text-center relative transition h-13 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 border-l-gray-200"
class="text-center relative transition h-13 text-sm text-gray-500 w-full py-1 border-transparent border-1 border-x-gray-100 border-t-gray-100 border-l-gray-200"
data-testid="nc-calendar-week-hour"
@dblclick="addRecord(hour)"
@click="
@ -934,17 +1031,18 @@ watch(
</div>
</div>
<div
class="absolute pointer-events-none inset-0 overflow-hidden !mt-[29px]"
data-testid="nc-calendar-week-record-container"
>
<div class="absolute pointer-events-none inset-0 overflow-hidden !mt-5.95" data-testid="nc-calendar-week-record-container">
<template v-for="(record, rowIndex) in recordsAcrossAllRange.records" :key="rowIndex">
<div
v-if="record.rowMeta.style?.display !== 'none'"
:data-testid="`nc-calendar-week-record-${record.row[displayField!.title!]}`"
:data-unique-id="record.rowMeta!.id"
:style="record.rowMeta!.style "
class="absolute transition draggable-record w-1/7 group cursor-pointer pointer-events-auto"
:class="{
'w-1/5': maxVisibleDays === 5,
'w-1/7': maxVisibleDays === 7,
}"
class="absolute transition draggable-record group cursor-pointer pointer-events-auto"
@mousedown.stop="dragStart($event, record)"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id"

2
packages/nc-gui/components/smartsheet/calendar/YearView/index.vue

@ -22,7 +22,7 @@ const handleResize = () => {
if (width.value > 1250) {
size.value = 'medium'
cols.value = 4
} else if (width.value > 850) {
} else if (width.value > 950) {
size.value = 'medium'
cols.value = 3
} else if (width.value > 680) {

4
packages/nc-gui/components/smartsheet/toolbar/Calendar/ActiveView.vue

@ -1,9 +1,11 @@
<script lang="ts" setup>
const { activeCalendarView } = useCalendarViewStoreOrThrow()
const { isMobileMode } = useGlobal()
</script>
<template>
<span class="opacity-0" data-testid="nc-active-calendar-view">
<span v-if="!isMobileMode" class="opacity-0" data-testid="nc-active-calendar-view">
{{ activeCalendarView }}
</span>
</template>

10
packages/nc-gui/components/smartsheet/toolbar/Calendar/Header.vue

@ -35,7 +35,7 @@ const headerText = computed(() => {
<template #title> {{ $t('labels.previous') }}</template>
<a-button
v-e="`['c:calendar:calendar-${activeCalendarView}-prev-btn']`"
class="w-6 h-6 !rounded-lg flex items-center justify-center !bg-gray-100 !border-0"
class="w-6 h-6 prev-next-btn !hover:text-gray-700 transition-all !rounded-lg flex items-center justify-center !bg-gray-100 !border-0"
data-testid="nc-calendar-prev-btn"
size="small"
@click="paginateCalendarView('prev')"
@ -52,7 +52,7 @@ const headerText = computed(() => {
'w-29': activeCalendarView === 'day',
'w-38': activeCalendarView === 'week',
}"
class="!h-6 !bg-gray-100 !border-0"
class="!h-6 prev-next-btn !bg-gray-100 !border-0"
full-width
size="small"
type="secondary"
@ -108,7 +108,7 @@ const headerText = computed(() => {
<template #title> {{ $t('labels.next') }}</template>
<a-button
v-e="`['c:calendar:calendar-${activeCalendarView}-next-btn']`"
class="w-6 h-6 !rounded-lg flex items-center !bg-gray-100 !border-0 justify-center"
class="w-6 h-6 !rounded-lg flex items-center !hover:text-gray-700 prev-next-btn !bg-gray-100 !border-0 justify-center"
data-testid="nc-calendar-next-btn"
size="small"
@click="paginateCalendarView('next')"
@ -123,4 +123,8 @@ const headerText = computed(() => {
.nc-cal-toolbar-header {
@apply !h-6 !w-6;
}
.prev-next-btn {
@apply !hover:bg-gray-200;
}
</style>

2
packages/nc-gui/components/smartsheet/toolbar/Calendar/Mode.vue

@ -62,7 +62,9 @@ watch(activeCalendarView, () => {
{{ option }}
</span>
</template>
<span class="text-[13px]">
{{ option }}
</span>
</NcTooltip>
<component

46
packages/nc-gui/components/smartsheet/toolbar/Calendar/Range.vue

@ -14,10 +14,22 @@ const IsPublic = inject(IsPublicInj, ref(false))
const { loadViewColumns } = useViewColumnsOrThrow()
const { loadCalendarMeta, loadCalendarData, loadSidebarData, fetchActiveDates } = useCalendarViewStoreOrThrow()
const { loadCalendarMeta, loadCalendarData, loadSidebarData, fetchActiveDates, updateCalendarMeta, viewMetaProperties } =
useCalendarViewStoreOrThrow()
const calendarRangeDropdown = ref(false)
const hideWeekends = computed({
get: () => viewMetaProperties.value?.hide_weekend ?? false,
set: (newValue) => {
updateCalendarMeta({
meta: {
hide_weekend: newValue,
},
})
},
})
watch(
() => activeView.value?.id,
async (newVal, oldVal) => {
@ -135,29 +147,6 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</div>
<template #overlay>
<div v-if="calendarRangeDropdown" class="w-98 space-y-6 rounded-2xl p-6" data-testid="nc-calendar-range-menu" @click.stop>
<div>
<div class="flex mb-3 justify-between">
<div class="flex items-center gap-3">
<GeneralViewIcon
:meta="{
type: ViewTypes.CALENDAR,
}"
class="w-6 h-6"
/>
<span class="font-bold text-base"> {{ `${$t('activity.calendar')} ${$t('activity.viewSettings')}` }}</span>
</div>
<a
class="text-sm !text-gray-600 !font-default !hover:text-gray-600"
href="`https://docs.nocodb.com/views/view-types/calendar`"
target="_blank"
>
Go to Docs
</a>
</div>
<NcDivider divider-class="!border-gray-200" />
</div>
<div
v-for="(range, id) in _calendar_ranges"
:key="id"
@ -252,11 +241,20 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</NcButton>
-->
</div>
<div v-if="!isSetup" class="flex items-center gap-2 !mt-2">
<GeneralIcon icon="warning" class="text-sm mt-0.5 text-orange-500" />
<span class="text-sm text-gray-500"> Date field is required! </span>
</div>
<div>
<NcSwitch v-model:checked="hideWeekends">
<span class="text-gray-800">
{{ $t('activity.hideWeekends') }}
</span>
</NcSwitch>
</div>
<!--
<div class="text-[13px] text-gray-500 py-2">Records in this view will be based on the specified date field.</div>
-->

8
packages/nc-gui/components/smartsheet/toolbar/Calendar/Today.vue

@ -24,7 +24,7 @@ const goToToday = () => {
<template>
<NcButton
class="!border-0 !h-6 !bg-gray-100"
class="!border-0 !h-6 today-btn !bg-gray-100"
data-testid="nc-calendar-today-btn"
size="small"
type="secondary"
@ -36,4 +36,8 @@ const goToToday = () => {
</NcButton>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.today-btn {
@apply !hover:bg-gray-200;
}
</style>

2
packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue

@ -115,7 +115,7 @@ onMounted(() => {
<NcButton
v-e="['c:sort']"
:class="{
'!border-1 !rounded-lg': isCalendar,
'!border-1 !rounded-md': isCalendar,
'!border-0': !isCalendar,
}"
:disabled="isLocked"

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

@ -116,6 +116,24 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
return calendarRange.value[0]?.fk_from_col?.uidt
})
const viewMetaProperties = computed<{
active_view: string
hide_weekend: boolean
}>(() => {
let meta = calendarMetaData.value?.meta ?? {}
if (typeof meta === 'string') {
try {
meta = JSON.parse(meta)
} catch (e) {}
}
return meta as {
active_view: string
hide_weekend: boolean
}
})
const sideBarFilter = computed(() => {
let combinedFilters: any = []
@ -467,8 +485,15 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
case 'week':
fromDate = selectedDateRange.value.start.startOf('day')
toDate = selectedDateRange.value.end.endOf('day')
prevDate = selectedDateRange.value.start.subtract(1, 'day').endOf('day')
nextDate = selectedDateRange.value.end.add(1, 'day').startOf('day')
// Hide weekends
if (viewMetaProperties.value?.hide_weekend) {
toDate = toDate.subtract(2, 'day')
nextDate = nextDate.subtract(2, 'day')
}
break
case 'month': {
const startOfMonth = selectedMonth.value.startOf('month')
@ -809,10 +834,20 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
await fetchActiveDates()
})
watch(
() => viewMetaProperties.value.hide_weekend,
async () => {
if (activeCalendarView.value === 'week') {
await loadCalendarData()
}
},
)
return {
fetchActiveDates,
formattedSideBarData,
loadMoreSidebarData,
updateCalendarMeta,
loadSidebarData,
displayField,
sideBarFilterOption,
@ -838,6 +873,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
selectedMonth,
selectedDateRange,
paginateCalendarView,
viewMetaProperties,
}
},
)

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

@ -783,6 +783,7 @@
"coverImageArea": "Cover image"
},
"activity": {
"hideWeekends": "Hide weekends",
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",

Loading…
Cancel
Save