Browse Source

fix: update calendar grid styles (#8262)

* fix: update calendar grid styles

* fix: delete record should be removed from the calendar grid

* fix: drag drop from side menu

* fix: optimize data fetching

* fix: hide attachment links

* fix: prepare for drag drop from all sidebar filters

* fix: prepare for drag drop from all sidebar filters

* fix: show tooltips

* fix: hide sidemenu by default

* fix: update calendar tab styles

* fix: update year & date selectors

* fix: update year & date selectors

* fix: showing calendar range is null

* fix: update playwright tests

* fix: update side bar styles

* fix: minor font corrections

* fix: time alignment, weekend shade

* fix: space between date & time display in record sidebar

* fix: view settings modal width

---------

Co-authored-by: Raju Udava <86527202+dstala@users.noreply.github.com>
pull/8297/head
Anbarasu 7 months ago committed by GitHub
parent
commit
7d1cdad90f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      packages/nc-gui/assets/style.scss
  2. 41
      packages/nc-gui/components/nc/DateWeekSelector.vue
  3. 16
      packages/nc-gui/components/nc/MonthYearSelector.vue
  4. 6
      packages/nc-gui/components/nc/Tooltip.vue
  5. 7
      packages/nc-gui/components/smartsheet/PlainCell.vue
  6. 270
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  7. 19
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  8. 11
      packages/nc-gui/components/smartsheet/calendar/RecordCard.vue
  9. 66
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  10. 18
      packages/nc-gui/components/smartsheet/calendar/SideRecordCard.vue
  11. 15
      packages/nc-gui/components/smartsheet/calendar/VRecordCard.vue
  12. 17
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  13. 13
      packages/nc-gui/components/smartsheet/calendar/YearView.vue
  14. 8
      packages/nc-gui/components/smartsheet/calendar/index.vue
  15. 4
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  16. 14
      packages/nc-gui/components/smartsheet/toolbar/CalendarMode.vue
  17. 2
      packages/nc-gui/components/smartsheet/toolbar/CalendarRange.vue
  18. 16
      packages/nc-gui/composables/useCalendarViewStore.ts
  19. 17
      tests/playwright/tests/db/views/viewCalendar.spec.ts

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

@ -808,3 +808,7 @@ svg.nc-virtual-cell-icon {
.ant-switch-checked:focus-visible { .ant-switch-checked:focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff; box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
} }
.text-nowrap{
text-wrap: nowrap;
}

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

@ -9,6 +9,7 @@ interface Props {
activeDates?: Array<dayjs.Dayjs> activeDates?: Array<dayjs.Dayjs>
isMondayFirst?: boolean isMondayFirst?: boolean
isWeekPicker?: boolean isWeekPicker?: boolean
disableHeader?: boolean
disablePagination?: boolean disablePagination?: boolean
selectedWeek?: { selectedWeek?: {
start: dayjs.Dayjs start: dayjs.Dayjs
@ -23,6 +24,7 @@ const props = withDefaults(defineProps<Props>(), {
isMondayFirst: true, isMondayFirst: true,
pageDate: dayjs(), pageDate: dayjs(),
isWeekPicker: false, isWeekPicker: false,
disableHeader: false,
disablePagination: false, disablePagination: false,
activeDates: [] as Array<dayjs.Dayjs>, activeDates: [] as Array<dayjs.Dayjs>,
selectedWeek: null, selectedWeek: null,
@ -145,14 +147,15 @@ const paginate = (action: 'next' | 'prev') => {
class="flex flex-col" class="flex flex-col"
> >
<div <div
v-if="!disableHeader"
:class="{ :class="{
'justify-center': disablePagination, ' justify-between': !disablePagination,
'justify-between': !disablePagination, ' justify-center': disablePagination,
}" }"
class="flex items-center" class="flex items-center"
> >
<NcTooltip> <NcTooltip v-if="!disablePagination">
<NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('prev')"> <NcButton size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.doubleLeftArrow" class="h-4 w-4" /> <component :is="iconMap.doubleLeftArrow" class="h-4 w-4" />
</NcButton> </NcButton>
<template #title> <template #title>
@ -168,8 +171,8 @@ const paginate = (action: 'next' | 'prev') => {
class="text-gray-700" class="text-gray-700"
>{{ currentMonthYear }}</span >{{ currentMonthYear }}</span
> >
<NcTooltip> <NcTooltip v-if="!disablePagination">
<NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('next')"> <NcButton size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4" /> <component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
</NcButton> </NcButton>
<template #title> <template #title>
@ -182,16 +185,16 @@ const paginate = (action: 'next' | 'prev') => {
'rounded-lg': size === 'small', 'rounded-lg': size === 'small',
'rounded-y-xl': size !== 'small', 'rounded-y-xl': size !== 'small',
}" }"
class="border-1 border-gray-200 max-w-[320px]" class="max-w-[320px]"
> >
<div <div
:class="{ :class="{
'gap-1 px-1': size === 'medium', 'gap-1 px-1': size === 'medium',
'gap-2 px-2': size === 'large', 'gap-2': size === 'large',
'px-2 py-1 !rounded-t-lg': size === 'small', 'px-2 py-1 !rounded-t-lg': size === 'small',
'rounded-t-xl': size !== 'small', 'rounded-t-xl': size !== 'small',
}" }"
class="flex flex-row bg-gray-100 justify-between" class="flex flex-row border-b-1 nc-date-week-header border-gray-200 justify-between"
> >
<span <span
v-for="(day, index) in days" v-for="(day, index) in days"
@ -201,23 +204,23 @@ const paginate = (action: 'next' | 'prev') => {
'w-8 h-8': size === 'medium', 'w-8 h-8': size === 'medium',
'text-[10px]': size === 'small', 'text-[10px]': size === 'small',
}" }"
class="flex items-center uppercase justify-center text-gray-500" class="flex items-center uppercase font-medium justify-center text-gray-500"
>{{ day[0] }}</span >{{ day[0] }}</span
> >
</div> </div>
<div <div
:class="{ :class="{
'gap-2 p-2': size === 'large', 'gap-2 pt-2': size === 'large',
'gap-1 p-1': size === 'medium', 'gap-1 p-1': size === 'medium',
}" }"
class="grid grid-cols-7" class="grid nc-date-week-grid-wrapper grid-cols-7"
> >
<span <span
v-for="(date, index) in dates" v-for="(date, index) in dates"
:key="index" :key="index"
:class="{ :class="{
'rounded-lg': !isWeekPicker, 'rounded-lg': !isWeekPicker,
'bg-brand-50 border-1 !border-brand-500': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date), 'bg-gray-200 border-1 font-bold text-brand-500': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date),
'hover:(border-1 border-gray-200 bg-gray-100)': !isSelectedDate(date) && !isWeekPicker, 'hover:(border-1 border-gray-200 bg-gray-100)': !isSelectedDate(date) && !isWeekPicker,
'nc-selected-week z-1': isDateInSelectedWeek(date) && isWeekPicker, 'nc-selected-week z-1': isDateInSelectedWeek(date) && isWeekPicker,
'border-none': isWeekPicker, 'border-none': isWeekPicker,
@ -225,25 +228,27 @@ const paginate = (action: 'next' | 'prev') => {
'text-gray-400': !isDateInCurrentMonth(date), 'text-gray-400': !isDateInCurrentMonth(date),
'nc-selected-week-start': isSameDate(date, selectedWeek?.start), 'nc-selected-week-start': isSameDate(date, selectedWeek?.start),
'nc-selected-week-end': isSameDate(date, selectedWeek?.end), 'nc-selected-week-end': isSameDate(date, selectedWeek?.end),
'rounded-md bg-brand-50 nc-calendar-today text-brand-500': isSameDate(date, dayjs()) && isDateInCurrentMonth(date), 'rounded-md bg-brand-50 text-brand-500 nc-calendar-today ': isSameDate(date, dayjs()) && isDateInCurrentMonth(date),
'h-9 w-9': size === 'large', 'h-9 w-9': size === 'large',
'h-8 w-8': size === 'medium', 'h-8 w-8': size === 'medium',
'h-6 w-6 text-[10px]': size === 'small', 'h-6 w-6 text-[10px]': size === 'small',
}" }"
class="px-1 py-1 relative border-1 font-large flex items-center cursor-pointer justify-center" class="px-1 py-1 relative border-1 font-medium flex items-center cursor-pointer justify-center"
data-testid="nc-calendar-date" data-testid="nc-calendar-date"
@click="handleSelectDate(date)" @click="handleSelectDate(date)"
> >
<span <span
v-if="isActiveDate(date)" v-if="isActiveDate(date)"
:class="{ :class="{
'h-1.5 w-1.5': size === 'large', 'h-2 w-2': size === 'large',
'h-1 w-1': size === 'medium', 'h-1 w-1': size === 'medium',
'h-0.75 w-0.75': size === 'small', 'h-0.75 w-0.75': size === 'small',
'top-1 right-1': size !== 'small', 'top-1 right-1': size !== 'small',
'top-0.5 right-0.5': size === 'small', 'top-0.5 right-0.5': size === 'small',
'!border-white': isSelectedDate(date),
'border-brand-50': isSameDate(date, dayjs()),
}" }"
class="absolute z-2 rounded-full bg-brand-500" class="absolute z-2 rounded-full border-2 border-white bg-brand-500"
></span> ></span>
<span class="z-2"> <span class="z-2">
{{ date.get('date') }} {{ date.get('date') }}
@ -260,7 +265,7 @@ const paginate = (action: 'next' | 'prev') => {
} }
.nc-selected-week:before { .nc-selected-week:before {
@apply absolute top-0 left-0 w-full h-full border-y-1 bg-brand-50 border-brand-500; @apply absolute top-0 left-0 w-full h-full bg-gray-200;
content: ''; content: '';
width: 124%; width: 124%;
height: 100%; height: 100%;

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

@ -6,12 +6,14 @@ interface Props {
isDisabled?: boolean isDisabled?: boolean
pageDate?: dayjs.Dayjs pageDate?: dayjs.Dayjs
isYearPicker?: boolean isYearPicker?: boolean
hideHeader?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
selectedDate: null, selectedDate: null,
isDisabled: false, isDisabled: false,
pageDate: dayjs(), pageDate: dayjs(),
hideHeader: false,
isYearPicker: false, isYearPicker: false,
}) })
const emit = defineEmits(['update:selectedDate', 'update:pageDate']) const emit = defineEmits(['update:selectedDate', 'update:pageDate'])
@ -85,8 +87,8 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
</script> </script>
<template> <template>
<div class="px-4 pt-3 pb-4 flex flex-col gap-4"> <div class="px-3 pb-3 pt-2 flex flex-col">
<div class="flex justify-between items-center"> <div v-if="!hideHeader" class="flex justify-between items-center">
<NcTooltip> <NcTooltip>
<NcButton size="small" type="secondary" @click="paginate('prev')"> <NcButton size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.doubleLeftArrow" class="h-4 w-4" /> <component :is="iconMap.doubleLeftArrow" class="h-4 w-4" />
@ -106,15 +108,15 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
</NcTooltip> </NcTooltip>
</div> </div>
<div class="rounded-y-xl max-w-[350px]"> <div class="rounded-y-xl max-w-[350px]">
<div class="grid grid-cols-4 gap-2 p-2"> <div class="grid grid-cols-4 gap-2 py-3">
<template v-if="!isYearPicker"> <template v-if="!isYearPicker">
<span <span
v-for="(month, id) in months" v-for="(month, id) in months"
:key="id" :key="id"
:class="{ :class="{
'!bg-brand-50 border-1 !border-brand-500': isMonthSelected(month), '!bg-gray-200 !font-bold !text-brand-500': isMonthSelected(month),
}" }"
class="h-9 rounded-lg flex 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 font-medium justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer"
@click="selectedDate = month" @click="selectedDate = month"
> >
{{ month.format('MMM') }} {{ month.format('MMM') }}
@ -125,9 +127,9 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
v-for="(year, id) in years" v-for="(year, id) in years"
:key="id" :key="id"
:class="{ :class="{
'!bg-brand-50 !border-1 !border-brand-500': compareYear(year, selectedDate), '!bg-gray-200 !font-bold !text-brand-500': compareYear(year, selectedDate),
}" }"
class="h-9 rounded-lg flex 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 font-medium justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer"
@click="selectedDate = year" @click="selectedDate = year"
> >
{{ year.format('YYYY') }} {{ year.format('YYYY') }}

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

@ -14,6 +14,7 @@ interface Props {
showOnTruncateOnly?: boolean showOnTruncateOnly?: boolean
hideOnClick?: boolean hideOnClick?: boolean
overlayClassName?: string overlayClassName?: string
wrapChild?: keyof HTMLElementTagNameMap
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -24,6 +25,7 @@ const disabled = computed(() => props.disabled)
const showOnTruncateOnly = computed(() => props.showOnTruncateOnly) const showOnTruncateOnly = computed(() => props.showOnTruncateOnly)
const hideOnClick = computed(() => props.hideOnClick) const hideOnClick = computed(() => props.hideOnClick)
const placement = computed(() => props.placement ?? 'top') const placement = computed(() => props.placement ?? 'top')
const wrapChild = computed(() => props.wrapChild ?? 'div')
const el = ref() const el = ref()
@ -124,9 +126,9 @@ const onClick = () => {
<slot name="title" /> <slot name="title" />
</template> </template>
<div ref="el" v-bind="divStyles" @mousedown="onClick"> <component :is="wrapChild" ref="el" v-bind="divStyles" @mousedown="onClick">
<slot /> <slot />
</div> </component>
</a-tooltip> </a-tooltip>
</template> </template>

7
packages/nc-gui/components/smartsheet/PlainCell.vue

@ -261,7 +261,7 @@ const getLookupValue = (modelValue: string | null | number | Array<any>, col: Co
const getAttachmentValue = (modelValue: string | null | number | Array<any>) => { const getAttachmentValue = (modelValue: string | null | number | Array<any>) => {
if (Array.isArray(modelValue)) { if (Array.isArray(modelValue)) {
return modelValue.map((v) => `${v.title} (${getPossibleAttachmentSrc(v).join(', ')})`).join(', ') return modelValue.map((v) => `${v.title}`).join(', ')
} }
return modelValue as string return modelValue as string
} }
@ -358,7 +358,7 @@ const parseValue = (value: any, col: ColumnType): string => {
<span <span
class="plain-cell before:px-1" class="plain-cell before:px-1"
:class="{ :class="{
'font-bold': bold, '!font-bold': bold,
'italic': italic, 'italic': italic,
'underline': underline, 'underline': underline,
}" }"
@ -378,5 +378,8 @@ const parseValue = (value: any, col: ColumnType): string => {
content: ''; content: '';
padding: 0; padding: 0;
} }
&:first-child {
padding-left: 0;
}
} }
</style> </style>

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

@ -213,7 +213,7 @@ const recordsAcrossAllRange = computed<{
} }
} = {} } = {}
const perRecordHeight = 60 const perRecordHeight = 52
const columnArray: Array<Array<Row>> = [[]] const columnArray: Array<Array<Row>> = [[]]
const gridTimeMap = new Map< const gridTimeMap = new Map<
@ -263,15 +263,15 @@ const recordsAcrossAllRange = computed<{
scheduleEnd, scheduleEnd,
}) })
// The top of the record is calculated based on the start hour and minute // The top of the record is calculated based on the start hour and minute
const topInPixels = startDate.hour() + startDate.minute() const topInPixels = (startDate.minute() / 60 + startDate.hour()) * perRecordHeight
// A minimum height of 80px is set for each record // A minimum height of 52px is set for each record
// The height of the record is calculated based on the difference between the start and end date // The height of the record is calculated based on the difference between the start and end date
const heightInPixels = Math.max(endDate.diff(startDate, 'minute'), perRecordHeight) const heightInPixels = Math.max(endDate.diff(startDate, 'minute'), perRecordHeight)
const style: Partial<CSSStyleDeclaration> = { const style: Partial<CSSStyleDeclaration> = {
height: `${heightInPixels}px`, height: `${heightInPixels - 8}px`,
top: `${topInPixels}px`, top: `${topInPixels + 4}px`,
} }
// This property is used to determine which side the record should be rounded. It can be top, bottom, both or none // This property is used to determine which side the record should be rounded. It can be top, bottom, both or none
@ -316,14 +316,14 @@ const recordsAcrossAllRange = computed<{
// The top of the record is calculated based on the start hour // The top of the record is calculated based on the start hour
// Update such that it is also based on Minutes // Update such that it is also based on Minutes
const topInPixels = startDate.minute() + startDate.hour() * 60 const topInPixels = (startDate.minute() / 60 + startDate.hour()) * perRecordHeight
// A minimum height of 80px is set for each record // A minimum height of 80px is set for each record
const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 60, perRecordHeight) const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 52, perRecordHeight)
style = { style = {
...style, ...style,
top: `${topInPixels + 1}px`, top: `${topInPixels + 4}px`,
height: `${heightInPixels - 2}px`, height: `${heightInPixels - 8}px`,
} }
recordsByRange.push({ recordsByRange.push({
@ -734,6 +734,94 @@ const dragStart = (event: MouseEvent, record: Row) => {
document.addEventListener('mouseup', onMouseUp) document.addEventListener('mouseup', onMouseUp)
} }
// We support drag and drop from the sidebar to the day view of the date field
const dropEvent = (event: DragEvent) => {
if (!isUIAllowed('dataEdit') || !container.value) return
event.preventDefault()
const data = event.dataTransfer?.getData('text/plain')
if (data) {
const {
record,
isWithoutDates,
}: {
record: Row
initialClickOffsetY: number
initialClickOffsetX: number
isWithoutDates: boolean
} = JSON.parse(data)
const fromCol = record.rowMeta.range?.fk_from_col
const toCol = record.rowMeta.range?.fk_to_col
if (!fromCol) return
const { top } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
// We calculate the percentage of the mouse position in the scroll container
const percentY = (event.clientY - top + container.value.scrollTop) / scrollHeight
const hour = Math.max(Math.floor(percentY * 23), 0)
const minutes = Math.min(Math.max(Math.round(Math.floor((percentY * 23 - hour) * 60) / 15) * 15, 0), 60)
const newStartDate = dayjs(selectedDate.value).startOf('day').add(hour, 'hour').add(minutes, 'minute')
let endDate
const newRow = {
...record,
row: {
...record.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
const updateProperty = [fromCol.title!]
if (toCol) {
const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
const toDate = record.row[toCol.title!] ? dayjs(record.row[toCol.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
} else if (fromDate && !toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else if (!fromDate && toDate) {
endDate = dayjs(newStartDate).endOf('day')
} else {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
updateProperty.push(toCol.title!)
}
if (!newRow) return
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
if (dragElement.value && !isWithoutDates) {
formattedData.value = formattedData.value.map((r) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk === newPk ? newRow : r
})
} else {
formattedData.value = [...formattedData.value, newRow]
if (sideBarFilterOption.value !== 'allRecords') {
formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
return extractPkFromRow(r.row, meta.value!.columns!) !== newPk
})
}
}
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)
}
}
const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => { const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
let startOfHour = hour.startOf('hour') let startOfHour = hour.startOf('hour')
const endOfHour = hour.endOf('hour') const endOfHour = hour.endOf('hour')
@ -802,47 +890,59 @@ watch(
<template> <template>
<div <div
ref="container" ref="container"
class="w-full relative no-selection h-[calc(100vh-10rem)] overflow-y-auto nc-scrollbar-md" class="w-full flex relative no-selection h-[calc(100vh-10rem)] overflow-y-auto nc-scrollbar-md"
data-testid="nc-calendar-day-view" data-testid="nc-calendar-day-view"
@drop="dropEvent"
> >
<div <div>
v-for="(hour, index) in hours" <div
:key="index" v-for="(hour, index) in hours"
:class="{ :key="index"
'!border-brand-500': hour.isSame(selectedTime), class="flex h-13 relative border-1 group hover:bg-gray-50 border-white"
}" data-testid="nc-calendar-day-hour"
class="flex w-full h-15 nc-calendar-day-hour relative border-1 group hover:bg-gray-50 border-white border-b-gray-100" @click="selectHour(hour)"
data-testid="nc-calendar-day-hour" @dblclick="newRecord(hour)"
@click="selectHour(hour)" >
@dblclick="newRecord(hour)" <div class="w-16 border-b-0 pr-3 pl-2 text-right text-xs text-gray-400 font-semibold h-13">
> {{ dayjs(hour).format('hh a') }}
<div class="pt-2 px-4 text-xs text-gray-500 font-semibold h-15"> </div>
{{ dayjs(hour).format('H A') }}
</div> </div>
<div></div> </div>
<NcDropdown <div class="w-full">
v-if="calendarRange.length > 1" <div
v-for="(hour, index) in hours"
:key="index"
:class="{ :class="{
'!block': hour.isSame(selectedTime), '!border-brand-500': hour.isSame(selectedTime),
'!hidden': !hour.isSame(selectedTime),
}" }"
auto-close class="flex w-full border-l-gray-100 h-13 nc-calendar-day-hour relative border-1 group hover:bg-gray-50 border-white border-b-gray-100"
data-testid="nc-calendar-day-hour"
@click="selectHour(hour)"
@dblclick="newRecord(hour)"
> >
<NcButton <NcDropdown
class="!group-hover:block mr-4 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block absolute" v-if="calendarRange.length > 1"
size="xsmall" :class="{
type="secondary" '!block': hour.isSame(selectedTime),
'!hidden': !hour.isSame(selectedTime),
}"
auto-close
> >
<component :is="iconMap.plus" class="h-4 w-4" /> <NcButton
</NcButton> class="!group-hover:block mr-4 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block absolute"
<template #overlay> size="xsmall"
<NcMenu class="w-64"> type="secondary"
<NcMenuItem> Select date field to add </NcMenuItem> >
<NcMenuItem <component :is="iconMap.plus" class="h-4 w-4" />
v-for="(range, calIndex) in calendarRange" </NcButton>
:key="calIndex" <template #overlay>
class="text-gray-800 font-semibold text-sm" <NcMenu class="w-64">
@click=" <NcMenuItem> Select date field to add </NcMenuItem>
<NcMenuItem
v-for="(range, calIndex) in calendarRange"
:key="calIndex"
class="text-gray-800 font-semibold text-sm"
@click="
() => { () => {
let record = { let record = {
row: { row: {
@ -860,25 +960,25 @@ watch(
emit('newRecord', record) emit('newRecord', record)
} }
" "
> >
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<LazySmartsheetHeaderCellIcon :column-meta="range.fk_from_col" /> <LazySmartsheetHeaderCellIcon :column-meta="range.fk_from_col" />
<span class="ml-1">{{ range.fk_from_col!.title! }}</span> <span class="ml-1">{{ range.fk_from_col!.title! }}</span>
</div> </div>
</NcMenuItem> </NcMenuItem>
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>
<NcButton <NcButton
v-else v-else
:class="{ :class="{
'!block': hour.isSame(selectedTime), '!block': hour.isSame(selectedTime),
'!hidden': !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-4 my-auto ml-auto z-10 top-0 bottom-0 !group-hover:block absolute"
size="xsmall" size="xsmall"
type="secondary" type="secondary"
@click=" @click="
() => { () => {
let record = { let record = {
row: { row: {
@ -897,27 +997,28 @@ watch(
emit('newRecord', record) emit('newRecord', record)
} }
" "
> >
<component :is="iconMap.plus" class="h-4 w-4" /> <component :is="iconMap.plus" class="h-4 w-4" />
</NcButton> </NcButton>
<NcButton <NcButton
v-if="isOverflowAcrossHourRange(hour).isOverflow" v-if="isOverflowAcrossHourRange(hour).isOverflow"
v-e="`['c:calendar:week-view-more']`" v-e="`['c:calendar:week-view-more']`"
class="!absolute bottom-2 text-center w-15 mx-auto inset-x-0 z-3 text-gray-500" class="!absolute bottom-2 text-center w-15 mx-auto inset-x-0 z-3 text-gray-500"
size="xxsmall" size="xxsmall"
type="secondary" type="secondary"
@click="viewMore(hour)" @click="viewMore(hour)"
> >
<span class="text-xs"> <span class="text-xs">
+ +
{{ isOverflowAcrossHourRange(hour).overflowCount }} {{ isOverflowAcrossHourRange(hour).overflowCount }}
more more
</span> </span>
</NcButton> </NcButton>
</div>
</div> </div>
<div class="absolute inset-0 pointer-events-none"> <div class="absolute inset-0 pointer-events-none">
<div class="relative !ml-[60px]" data-testid="nc-calendar-day-record-container"> <div class="relative !ml-[68px] !mr-1 nc-calendar-day-record-container" data-testid="nc-calendar-day-record-container">
<template v-for="(record, rowIndex) in recordsAcrossAllRange.record" :key="rowIndex"> <template v-for="(record, rowIndex) in recordsAcrossAllRange.record" :key="rowIndex">
<div <div
v-if="record.rowMeta.style?.display !== 'none'" v-if="record.rowMeta.style?.display !== 'none'"
@ -944,13 +1045,18 @@ watch(
<LazySmartsheetPlainCell <LazySmartsheetPlainCell
v-if="!isRowEmpty(record, field!)" v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]" v-model="record.row[field!.title!]"
class="text-xs" class="text-xs font-medium"
:bold="getFieldStyle(field).bold" :bold="getFieldStyle(field).bold"
:column="field" :column="field"
:italic="getFieldStyle(field).italic" :italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline" :underline="getFieldStyle(field).underline"
/> />
</template> </template>
<template #time>
<div class="text-xs font-medium text-gray-400">
{{ dayjs(record.row[record.rowMeta.range?.fk_from_col!.title!]).format('h:mm a') }}
</div>
</template>
</LazySmartsheetCalendarVRecordCard> </LazySmartsheetCalendarVRecordCard>
</LazySmartsheetRow> </LazySmartsheetRow>
</div> </div>

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

@ -356,7 +356,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
const day = Math.floor(percentX * 7) const day = Math.floor(percentX * 7)
const newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null const newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
if (!newStartDate) return if (!newStartDate) return { newRow: null, updateProperty: [] }
let endDate let endDate
@ -586,13 +586,15 @@ const dropEvent = (event: DragEvent) => {
if (data) { if (data) {
const { const {
record, record,
isWithoutDates,
}: { }: {
record: Row record: Row
isWithoutDates: boolean
} = JSON.parse(data) } = JSON.parse(data)
dragRecord.value = record dragRecord.value = record
const { newRow, updateProperty } = calculateNewRow(event, true) const { newRow, updateProperty } = calculateNewRow(event, isWithoutDates)
if (dragElement.value) { if (dragElement.value) {
dragElement.value.style.boxShadow = 'none' dragElement.value.style.boxShadow = 'none'
@ -643,7 +645,7 @@ const addRecord = (date: dayjs.Dayjs) => {
<div <div
v-for="(day, index) in days" v-for="(day, index) in days"
:key="index" :key="index"
class="text-center bg-gray-50 py-1 text-sm border-b-1 border-r-1 last:border-r-0 border-gray-100 font-semibold text-gray-500" class="text-center bg-gray-50 py-1 text-sm border-b-1 border-r-1 last:border-r-0 border-gray-200 font-semibold text-gray-500"
> >
{{ day }} {{ day }}
</div> </div>
@ -668,7 +670,7 @@ const addRecord = (date: dayjs.Dayjs) => {
'!text-gray-400': !isDayInPagedMonth(day), '!text-gray-400': !isDayInPagedMonth(day),
'!bg-gray-50': day.get('day') === 0 || day.get('day') === 6, '!bg-gray-50': day.get('day') === 0 || day.get('day') === 6,
}" }"
class="text-right relative group last:border-r-0 text-sm h-full border-r-1 border-b-1 border-gray-100 font-medium hover:bg-gray-50 text-gray-800 bg-white" class="text-right relative group last:border-r-0 text-sm h-full border-r-1 border-b-1 border-gray-200 font-medium hover:bg-gray-50 text-gray-800 bg-white"
data-testid="nc-calendar-month-day" data-testid="nc-calendar-month-day"
@click="selectDate(day)" @click="selectDate(day)"
@dblclick="addRecord(day)" @dblclick="addRecord(day)"
@ -744,9 +746,9 @@ const addRecord = (date: dayjs.Dayjs) => {
</NcButton> </NcButton>
<span <span
:class="{ :class="{
'bg-brand-50 text-brand-500': day.isSame(dayjs(), 'date'), 'bg-brand-50 text-brand-500 !font-bold': day.isSame(dayjs(), 'date'),
}" }"
class="px-1.3 py-1 text-xs rounded-lg" class="px-1.3 py-1 text-sm font-medium rounded-lg"
> >
{{ day.format('DD') }} {{ day.format('DD') }}
</span> </span>
@ -795,6 +797,11 @@ const addRecord = (date: dayjs.Dayjs) => {
@resize-start="onResizeStart" @resize-start="onResizeStart"
@dblclick.stop="emit('expandRecord', record)" @dblclick.stop="emit('expandRecord', record)"
> >
<template #time>
<span class="text-xs font-medium text-gray-400">
{{ dayjs(record.row[record.rowMeta.range?.fk_from_col!.title!]).format('h:mma').slice(0, -1) }}
</span>
</template>
<template v-for="(field, id) in fields" :key="id"> <template v-for="(field, id) in fields" :key="id">
<LazySmartsheetPlainCell <LazySmartsheetPlainCell
v-if="!isRowEmpty(record, field!)" v-if="!isRowEmpty(record, field!)"

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

@ -42,7 +42,7 @@ const emit = defineEmits(['resize-start'])
'!border-blue-200 border-1': selected, '!border-blue-200 border-1': selected,
'shadow-md': hover, 'shadow-md': hover,
}" }"
class="relative transition-all flex items-center px-1 group border-1 border-transparent" class="relative transition-all flex items-center px-1 gap-2 group border-1 border-transparent"
> >
<div <div
v-if="position === 'leftRounded' || position === 'rounded'" v-if="position === 'leftRounded' || position === 'rounded'"
@ -71,15 +71,16 @@ const emit = defineEmits(['resize-start'])
</NcButton> </NcButton>
</div> </div>
<div class="overflow-hidden items-center justify-center flex w-full ml-2"> <div class="overflow-hidden items-center justify-center gap-2 flex w-full">
<span v-if="position === 'rightRounded' || position === 'none'" class="mr-1"> .... </span> <span v-if="position === 'rightRounded' || position === 'none'" class="ml-2"> .... </span>
<slot name="time" />
<span <span
:class="{ :class="{
'pr-7': position === 'leftRounded', 'pr-7': position === 'leftRounded',
}" }"
class="text-sm pr-3 mb-0.5 mr-3 break-word whitespace-nowrap overflow-hidden text-ellipsis w-full truncate text-gray-800" class="text-sm mb-0.5 break-word whitespace-nowrap overflow-hidden text-ellipsis w-full truncate text-gray-800"
> >
<NcTooltip :disabled="selected" show-on-truncate-only> <NcTooltip :disabled="selected" class="inline-block" show-on-truncate-only wrap-child="span">
<slot /> <slot />
<template #title> <template #title>
<slot /> <slot />

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

@ -31,6 +31,7 @@ const {
activeDates, activeDates,
activeCalendarView, activeCalendarView,
isSidebarLoading, isSidebarLoading,
isCalendarMetaLoading,
formattedSideBarData, formattedSideBarData,
calDataType, calDataType,
loadMoreSidebarData, loadMoreSidebarData,
@ -55,7 +56,6 @@ const dragElement = ref<HTMLElement | null>(null)
const dragStart = (event: DragEvent, record: Row) => { const dragStart = (event: DragEvent, record: Row) => {
dragElement.value = event.target as HTMLElement dragElement.value = event.target as HTMLElement
dragElement.value.style.boxShadow = '0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.10)'
const eventRect = dragElement.value.getBoundingClientRect() const eventRect = dragElement.value.getBoundingClientRect()
const initialClickOffsetX = event.clientX - eventRect.left const initialClickOffsetX = event.clientX - eventRect.left
@ -65,6 +65,7 @@ const dragStart = (event: DragEvent, record: Row) => {
'text/plain', 'text/plain',
JSON.stringify({ JSON.stringify({
record, record,
isWithoutDates: sideBarFilterOption.value === 'withoutDates',
initialClickOffsetY, initialClickOffsetY,
initialClickOffsetX, initialClickOffsetX,
}), }),
@ -288,8 +289,8 @@ onUnmounted(() => {
<div <div
:class="{ :class="{
'!w-0': !props.visible, '!w-0': !props.visible,
'min-w-[356px]': width > 1440 && props.visible, 'min-w-[324px]': width > 1440 && props.visible,
'min-w-[264px]': width <= 1440 && props.visible, 'min-w-[288px]': width <= 1440 && props.visible,
'nc-calendar-side-menu-open': props.visible, 'nc-calendar-side-menu-open': props.visible,
}" }"
class="h-full border-l-1 border-gray-200 transition-all" class="h-full border-l-1 border-gray-200 transition-all"
@ -298,7 +299,7 @@ onUnmounted(() => {
<div <div
:class="{ :class="{
'!hidden': width <= 1440, '!hidden': width <= 1440,
'px-4 pt-3 pb-4 ': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const), 'px-3 py-3 ': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const),
}" }"
class="flex flex-col" class="flex flex-col"
> >
@ -334,21 +335,9 @@ onUnmounted(() => {
}" }"
class="border-t-1 border-gray-200 relative flex flex-col gap-y-4 pt-3" class="border-t-1 border-gray-200 relative flex flex-col gap-y-4 pt-3"
> >
<div class="flex px-4 items-center gap-2"> <div class="flex px-4 items-center gap-3">
<a-input <span class="capitalize text-base font-bold">{{ $t('objects.records') }}</span>
v-model:value="searchQuery.value" <NcSelect v-model:value="sideBarFilterOption" class="w-full !text-gray-600" data-testid="nc-calendar-sidebar-filter">
:class="{
'!border-brand-500': searchQuery.value.length > 0,
}"
class="!rounded-lg !h-8 !border-gray-200 !px-4"
data-testid="nc-calendar-sidebar-search"
placeholder="Search records"
>
<template #prefix>
<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-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"> <a-select-option v-for="option in options" :key="option.value" :value="option.value" class="!text-gray-600">
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
<div class="truncate flex-1"> <div class="truncate flex-1">
@ -367,12 +356,27 @@ onUnmounted(() => {
</a-select-option> </a-select-option>
</NcSelect> </NcSelect>
</div> </div>
<div class="flex px-4 items-center gap-2">
<a-input
v-model:value="searchQuery.value"
:class="{
'!border-brand-500': searchQuery.value.length > 0,
}"
class="!rounded-lg !h-8 !placeholder:text-gray-500 !border-gray-200 !px-4"
data-testid="nc-calendar-sidebar-search"
placeholder="Search records"
>
<template #prefix>
<component :is="iconMap.search" class="h-4 w-4 mr-1 text-gray-500" />
</template>
</a-input>
</div>
<div <div
v-if="calendarRange?.length" v-if="calendarRange?.length && !isCalendarMetaLoading"
:ref="sideBarListRef" :ref="sideBarListRef"
:class="{ :class="{
'!h-[calc(100vh-10.5rem)]': width <= 1440, '!h-[calc(100vh-13.5rem)]': width <= 1440,
'h-[calc(100vh-36.2rem)]': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const) && width >= 1440, 'h-[calc(100vh-36.2rem)]': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const) && width >= 1440,
'h-[calc(100vh-25.1rem)]': activeCalendarView === ('month' as const) || activeCalendarView === ('year' as const) && width >= 1440, 'h-[calc(100vh-25.1rem)]': activeCalendarView === ('month' as const) || activeCalendarView === ('year' as const) && width >= 1440,
@ -409,7 +413,7 @@ onUnmounted(() => {
record.rowMeta.range?.fk_from_col record.rowMeta.range?.fk_from_col
? calDataType === UITypes.Date ? 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')
: 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('DD MMM HH:mm A')
: null : null
" "
:invalid=" :invalid="
@ -423,13 +427,13 @@ onUnmounted(() => {
record.rowMeta.range!.fk_to_col record.rowMeta.range!.fk_to_col
? calDataType === UITypes.Date ? calDataType === UITypes.Date
? dayjs(record.row[record.rowMeta.range!.fk_to_col.title!]).format('DD MMM') ? dayjs(record.row[record.rowMeta.range!.fk_to_col.title!]).format('DD MMM')
: dayjs(record.row[record.rowMeta.range!.fk_to_col.title!]).format('DD MMM•HH:mm A') : dayjs(record.row[record.rowMeta.range!.fk_to_col.title!]).format('DD MMM HH:mm A')
: null : null
" "
color="blue" color="blue"
data-testid="nc-sidebar-record-card" data-testid="nc-sidebar-record-card"
@click="emit('expand-record', record)"
@dragstart="dragStart($event, record)" @dragstart="dragStart($event, record)"
@click="emit('expandRecord', record)"
@dragover.prevent @dragover.prevent
> >
<template v-if="!isRowEmpty(record, displayField)"> <template v-if="!isRowEmpty(record, displayField)">
@ -440,10 +444,22 @@ onUnmounted(() => {
</div> </div>
</template> </template>
</div> </div>
<template v-else-if="isCalendarMetaLoading">
<div
:class="{
'!h-[calc(100vh-13.5rem)]': width <= 1440,
'h-[calc(100vh-36.2rem)]': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const) && width >= 1440,
'h-[calc(100vh-25.1rem)]': activeCalendarView === ('month' as const) || activeCalendarView === ('year' as const) && width >= 1440,
}"
class="flex items-center justify-center h-full"
>
<GeneralLoader size="xlarge" />
</div>
</template>
<div <div
v-else v-else
:class="{ :class="{
'!h-[calc(100vh-10.5rem)]': width <= 1440, '!h-[calc(100vh-13.5rem)]': width <= 1440,
'h-[calc(100vh-36.2rem)]': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const) && width >= 1440, 'h-[calc(100vh-36.2rem)]': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const) && width >= 1440,
'h-[calc(100vh-25.1rem)]': activeCalendarView === ('month' as const) || activeCalendarView === ('year' as const) && width >= 1440, 'h-[calc(100vh-25.1rem)]': activeCalendarView === ('month' as const) || activeCalendarView === ('year' as const) && width >= 1440,
}" }"

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

@ -16,8 +16,18 @@ const props = withDefaults(defineProps<Props>(), {
</script> </script>
<template> <template>
<div class="border-1 cursor-pointer border-gray-200 items-center px-2 py-3 rounded-lg"> <div
<div class="flex items-center pl-1 gap-2"> :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="flex items-center pl-2 gap-2">
<span <span
:class="{ :class="{
'bg-maroon-500': props.color === 'maroon', 'bg-maroon-500': props.color === 'maroon',
@ -30,10 +40,10 @@ const props = withDefaults(defineProps<Props>(), {
class="block h-10 w-1 rounded" class="block h-10 w-1 rounded"
></span> ></span>
<div class="flex gap-1 flex-col"> <div class="flex gap-1 flex-col">
<span class="text-sm max-w-56 truncate text-gray-800"> <span class="text-sm max-w-56 font-medium truncate text-gray-800">
<slot /> <slot />
</span> </span>
<span v-if="showDate" class="text-xs text-gray-500">{{ fromDate }} {{ toDate ? ` - ${toDate}` : '' }}</span> <span v-if="showDate" class="text-xs font-medium text-gray-500">{{ fromDate }} {{ toDate ? ` - ${toDate}` : '' }}</span>
</div> </div>
</div> </div>

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

@ -52,7 +52,7 @@ const emit = defineEmits(['resize-start'])
'!border-blue-200 border-1': selected, '!border-blue-200 border-1': selected,
'shadow-md': hover, 'shadow-md': hover,
}" }"
class="relative flex items-center h-full ml-0.25 border-1 border-transparent" class="relative flex gap-2 items-center !pr-1 h-full ml-0.25 border-1 border-transparent"
> >
<div class="h-full py-1"> <div class="h-full py-1">
<div <div
@ -64,20 +64,23 @@ const emit = defineEmits(['resize-start'])
'bg-pink-500': color === 'pink', 'bg-pink-500': color === 'pink',
'bg-purple-500': color === 'purple', 'bg-purple-500': color === 'purple',
}" }"
class="block h-full min-h-5 ml-1 w-1 rounded" class="block h-full min-h-9 ml-1 w-1 rounded"
></div> ></div>
</div> </div>
<div v-if="position === 'bottomRounded' || position === 'none'" class="ml-3">....</div> <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 break-all whitespace-normal truncate w-full overflow-y-hidden"> <div
<NcTooltip :disabled="selected" show-on-truncate-only> class="flex overflow-x-hidden break-word whitespace-nowrap overflow-hidden text-ellipsis w-full truncate text-ellipsis flex-col gap-1"
<slot /> >
<NcTooltip :disabled="selected" class="inline-block" show-on-truncate-only wrap-child="span">
<slot class="pl-1 pr-2 text-sm text-nowrap text-gray-800 leading-7" />
<template #title> <template #title>
<slot /> <slot />
</template> </template>
</NcTooltip> </NcTooltip>
</span> <slot name="time" />
</div>
<div v-if="position === 'topRounded' || position === 'none'" class="h-full pb-7 flex items-end ml-3">....</div> <div v-if="position === 'topRounded' || position === 'none'" class="h-full pb-7 flex items-end ml-3">....</div>
</div> </div>

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

@ -89,7 +89,7 @@ const recordsAcrossAllRange = computed<{
count: {}, count: {},
} }
const perWidth = containerWidth.value / 7 const perWidth = containerWidth.value / 7
const perHeight = 60 const perHeight = 52
const scheduleStart = dayjs(selectedDateRange.value.start).startOf('day') const scheduleStart = dayjs(selectedDateRange.value.start).startOf('day')
const scheduleEnd = dayjs(selectedDateRange.value.end).endOf('day') const scheduleEnd = dayjs(selectedDateRange.value.end).endOf('day')
@ -190,7 +190,7 @@ const recordsAcrossAllRange = computed<{
dayIndex = 6 dayIndex = 6
} }
const minutes = ogStartDate.minute() + ogStartDate.hour() * 60 const minutes = (ogStartDate.minute() / 60 + ogStartDate.hour()) * 52
style = { style = {
...style, ...style,
@ -744,7 +744,7 @@ watch(
:class="{ :class="{
'text-brand-500': date[0].isSame(dayjs(), 'date'), 'text-brand-500': date[0].isSame(dayjs(), 'date'),
}" }"
class="w-1/7 text-center text-sm text-gray-500 w-full py-1 border-gray-100 last:border-r-0 border-b-0 border-l-1 border-r-0 bg-gray-50" class="w-1/7 text-center text-sm 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') }} {{ dayjs(date[0]).format('DD ddd') }}
</div> </div>
@ -753,9 +753,9 @@ watch(
<div <div
v-for="(hour, index) in datesHours[0]" v-for="(hour, index) in datesHours[0]"
:key="index" :key="index"
class="h-15 first:mt-0 pt-7.1 nc-calendar-day-hour text-center text-xs text-gray-500 py-1" class="h-13 first:mt-0 pt-7.1 nc-calendar-day-hour text-center font-semibold text-xs text-gray-400 py-1"
> >
{{ hour.format('h A') }} {{ hour.format('hh a') }}
</div> </div>
</div> </div>
<div ref="container" class="absolute ml-16 flex w-[calc(100%-64px)]"> <div ref="container" class="absolute ml-16 flex w-[calc(100%-64px)]">
@ -767,7 +767,7 @@ watch(
'border-1 !border-brand-500 bg-gray-50': hour.isSame(selectedTime, 'hour'), 'border-1 !border-brand-500 bg-gray-50': hour.isSame(selectedTime, 'hour'),
'!bg-gray-50': hour.get('day') === 0 || hour.get('day') === 6, '!bg-gray-50': hour.get('day') === 0 || hour.get('day') === 6,
}" }"
class="text-center relative h-15 text-sm text-gray-500 w-full hover:bg-gray-50 py-1 border-transparent border-1 border-x-gray-100 border-t-gray-100" class="text-center relative h-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"
data-testid="nc-calendar-week-hour" data-testid="nc-calendar-week-hour"
@dblclick="addRecord(hour)" @dblclick="addRecord(hour)"
@click=" @click="
@ -832,6 +832,11 @@ watch(
:underline="getFieldStyle(field).underline" :underline="getFieldStyle(field).underline"
/> />
</template> </template>
<template #time>
<div class="text-xs font-medium text-gray-400">
{{ dayjs(record.row[record.rowMeta.range?.fk_from_col!.title!]).format('h:mm a') }}
</div>
</template>
</LazySmartsheetCalendarVRecordCard> </LazySmartsheetCalendarVRecordCard>
</LazySmartsheetRow> </LazySmartsheetRow>
</div> </div>

13
packages/nc-gui/components/smartsheet/calendar/YearView.vue

@ -41,6 +41,7 @@ watch(width, handleResize)
v-model:active-dates="activeDates" v-model:active-dates="activeDates"
v-model:page-date="months[index]" v-model:page-date="months[index]"
v-model:selected-date="selectedDate" v-model:selected-date="selectedDate"
class="nc-year-view-calendar"
:size="size" :size="size"
data-testid="nc-calendar-year-view-month-selector" data-testid="nc-calendar-year-view-month-selector"
disable-pagination disable-pagination
@ -49,4 +50,14 @@ watch(width, handleResize)
</div> </div>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.nc-year-view-calendar {
:deep(.nc-date-week-header) {
@apply !bg-gray-100 border-x-1 border-t-1;
}
:deep(.nc-date-week-grid-wrapper) {
@apply !border-x-1 border-b-1 rounded-b-lg;
}
}
</style>

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

@ -48,6 +48,7 @@ const {
loadCalendarData, loadCalendarData,
loadSidebarData, loadSidebarData,
isCalendarDataLoading, isCalendarDataLoading,
isCalendarMetaLoading,
selectedDate, selectedDate,
selectedMonth, selectedMonth,
activeDates, activeDates,
@ -282,7 +283,7 @@ const headerText = computed(() => {
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
</div> </div>
<template v-if="calendarRange?.length"> <template v-if="calendarRange?.length && !isCalendarMetaLoading">
<LazySmartsheetCalendarYearView v-if="activeCalendarView === 'year'" /> <LazySmartsheetCalendarYearView v-if="activeCalendarView === 'year'" />
<template v-if="!isCalendarDataLoading"> <template v-if="!isCalendarDataLoading">
<LazySmartsheetCalendarMonthView <LazySmartsheetCalendarMonthView
@ -316,6 +317,11 @@ const headerText = computed(() => {
<GeneralLoader size="xlarge" /> <GeneralLoader size="xlarge" />
</div> </div>
</template> </template>
<template v-else-if="isCalendarMetaLoading">
<div class="flex w-full items-center h-full justify-center">
<GeneralLoader size="xlarge" />
</div>
</template>
<template v-else> <template v-else>
<div class="flex w-full items-center h-full justify-center"> <div class="flex w-full items-center h-full justify-center">
{{ $t('activity.noRange') }} {{ $t('activity.noRange') }}

4
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -436,7 +436,9 @@ const onConfirmDeleteRowClick = async () => {
showDeleteRowModal.value = false showDeleteRowModal.value = false
await deleteRowById(primaryKey.value) await deleteRowById(primaryKey.value)
message.success(t('msg.rowDeleted')) message.success(t('msg.rowDeleted'))
reloadTrigger.trigger() await reloadViewDataTrigger.trigger({
shouldShowLoading: false,
})
onClose() onClose()
showDeleteRowModal.value = false showDeleteRowModal.value = false
} }

14
packages/nc-gui/components/smartsheet/toolbar/CalendarMode.vue

@ -24,10 +24,6 @@ const updateHighlightPosition = () => {
}) })
} }
onMounted(() => {
updateHighlightPosition()
})
watch(activeCalendarView, () => { watch(activeCalendarView, () => {
if (!props.tab) return if (!props.tab) return
updateHighlightPosition() updateHighlightPosition()
@ -37,7 +33,7 @@ watch(activeCalendarView, () => {
<template> <template>
<div <div
v-if="props.tab" v-if="props.tab"
class="flex flex-row relative p-1 mx-3 mt-3 mb-3 bg-gray-100 rounded-lg gap-x-0.5 nc-calendar-mode-tab" class="flex flex-row relative px-1 mx-3 mt-3 rounded-lg gap-x-0.5 nc-calendar-mode-tab"
data-testid="nc-calendar-view-mode" data-testid="nc-calendar-view-mode"
> >
<div :style="highlightStyle" class="highlight"></div> <div :style="highlightStyle" class="highlight"></div>
@ -77,16 +73,16 @@ watch(activeCalendarView, () => {
<style lang="scss" scoped> <style lang="scss" scoped>
.highlight { .highlight {
@apply absolute h-7.5 w-20 shadow bg-white rounded-lg transition-all duration-300; @apply absolute h-9 w-20 transition-all border-b-2 border-brand-500 duration-200;
z-index: 0; z-index: 0;
} }
.tab { .tab {
@apply flex items-center h-7.5 w-20 z-10 justify-center px-2 py-1 rounded-lg gap-x-1.5 text-gray-500 hover:text-black cursor-pointer transition-all duration-300 select-none; @apply flex items-center h-9 w-20 z-10 justify-center px-2 py-1 rounded-lg gap-x-1.5 text-gray-500 hover:text-black cursor-pointer transition-all duration-300 select-none;
} }
.tab .tab-title { .tab .tab-title {
@apply min-w-0 pointer-events-none; @apply min-w-0 mb-3 pointer-events-none;
word-break: 'keep-all'; word-break: 'keep-all';
white-space: 'nowrap'; white-space: 'nowrap';
display: 'inline'; display: 'inline';
@ -94,7 +90,7 @@ watch(activeCalendarView, () => {
} }
.active { .active {
@apply text-black bg-transparent; @apply !text-brand-500 font-medium bg-transparent;
} }
.nc-calendar-mode-tab { .nc-calendar-mode-tab {

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

@ -129,7 +129,7 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</a-button> </a-button>
</div> </div>
<template #overlay> <template #overlay>
<div v-if="calendarRangeDropdown" class="w-full p-6 w-[23rem]" data-testid="nc-calendar-range-menu" @click.stop> <div v-if="calendarRangeDropdown" class="w-full p-6" data-testid="nc-calendar-range-menu" @click.stop>
<div <div
v-for="(range, id) in _calendar_ranges" v-for="(range, id) in _calendar_ranges"
:key="id" :key="id"

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

@ -62,7 +62,9 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const isCalendarDataLoading = ref<boolean>(false) const isCalendarDataLoading = ref<boolean>(false)
const showSideMenu = ref(true) const isCalendarMetaLoading = ref<boolean>(false)
const showSideMenu = ref(false)
const selectedDateRange = ref<{ const selectedDateRange = ref<{
start: dayjs.Dayjs start: dayjs.Dayjs
@ -415,6 +417,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
async function loadCalendarMeta() { async function loadCalendarMeta() {
if (!viewMeta?.value?.id || !meta?.value?.columns) return if (!viewMeta?.value?.id || !meta?.value?.columns) return
isCalendarMetaLoading.value = true
try { try {
const res = isPublic.value ? (sharedView.value?.view as CalendarType) : await $api.dbView.calendarRead(viewMeta.value.id) const res = isPublic.value ? (sharedView.value?.view as CalendarType) : await $api.dbView.calendarRead(viewMeta.value.id)
calendarMetaData.value = res calendarMetaData.value = res
@ -430,6 +433,8 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
}) as any }) as any
} catch (e) { } catch (e) {
message.error(`Error loading calendar meta ${await extractSdkResponseErrorMsg(e)}`) message.error(`Error loading calendar meta ${await extractSdkResponseErrorMsg(e)}`)
} finally {
isCalendarMetaLoading.value = false
} }
} }
@ -715,16 +720,14 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
}) })
watch(selectedTime, async () => { watch(selectedTime, async () => {
if (calDataType.value !== UITypes.Date && showSideMenu.value) { if (calDataType.value !== UITypes.Date && showSideMenu.value && sideBarFilterOption.value === 'selectedHours') {
await loadSidebarData() await loadSidebarData()
} }
}) })
watch(selectedMonth, async (value, oldValue) => { watch(selectedMonth, async () => {
if (activeCalendarView.value !== 'month') return if (activeCalendarView.value !== 'month') return
if (value.month() !== oldValue.month()) { await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()])
await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()])
}
}) })
watch(selectedDateRange, async () => { watch(selectedDateRange, async () => {
@ -797,6 +800,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
searchQuery, searchQuery,
activeDates, activeDates,
isCalendarDataLoading, isCalendarDataLoading,
isCalendarMetaLoading,
changeCalendarView, changeCalendarView,
calDataType, calDataType,
loadCalendarMeta, loadCalendarMeta,

17
tests/playwright/tests/db/views/viewCalendar.spec.ts

@ -135,7 +135,6 @@ test.describe('Calendar View', () => {
test('Calendar Sidebar Verify Sidebar Filter, Calendar View Mode', async () => { test('Calendar Sidebar Verify Sidebar Filter, Calendar View Mode', async () => {
// Create & Verify Calendar View // Create & Verify Calendar View
test.slow();
await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` }); await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` });
await dashboard.treeView.openTable({ title: 'Social Media Calendar' }); await dashboard.treeView.openTable({ title: 'Social Media Calendar' });
@ -162,6 +161,8 @@ test.describe('Calendar View', () => {
// Verify Sidebar // Verify Sidebar
const calendar = dashboard.calendar; const calendar = dashboard.calendar;
await calendar.calendarTopbar.toggleSideBar();
await calendar.verifySideBarOpen(); await calendar.verifySideBarOpen();
await calendar.calendarTopbar.toggleSideBar(); await calendar.calendarTopbar.toggleSideBar();
@ -289,8 +290,6 @@ test.describe('Calendar View', () => {
}); });
test('Calendar Drag and Drop & Undo Redo Operations', async () => { test('Calendar Drag and Drop & Undo Redo Operations', async () => {
test.slow();
await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` }); await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` });
await dashboard.treeView.openTable({ title: 'Social Media Calendar' }); await dashboard.treeView.openTable({ title: 'Social Media Calendar' });
@ -308,6 +307,8 @@ test.describe('Calendar View', () => {
const calendar = dashboard.calendar; const calendar = dashboard.calendar;
await calendar.calendarTopbar.toggleSideBar();
await calendar.calendarTopbar.moveToDate({ date: 'January 2024', action: 'prev' }); await calendar.calendarTopbar.moveToDate({ date: 'January 2024', action: 'prev' });
await calendar.calendarMonth.dragAndDrop({ await calendar.calendarMonth.dragAndDrop({
@ -367,8 +368,6 @@ test.describe('Calendar View', () => {
}); });
test('Calendar shared view operations', async ({ page }) => { test('Calendar shared view operations', async ({ page }) => {
test.slow();
await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` }); await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` });
await dashboard.treeView.openTable({ title: 'Social Media Calendar' }); await dashboard.treeView.openTable({ title: 'Social Media Calendar' });
@ -381,8 +380,6 @@ test.describe('Calendar View', () => {
index: 0, index: 0,
}); });
await dashboard.rootPage.waitForTimeout(12000);
// Share view // Share view
const sharedLink = await topbar.getSharedViewUrl(); const sharedLink = await topbar.getSharedViewUrl();
@ -417,6 +414,8 @@ test.describe('Calendar View', () => {
await calendar.calendarTopbar.moveToDate({ date: 'January 2024', action: 'prev' }); await calendar.calendarTopbar.moveToDate({ date: 'January 2024', action: 'prev' });
await calendar.calendarTopbar.toggleSideBar();
await calendar.sideMenu.verifySideBarRecords({ records: dateRecords.filter(f => f.Title).map(f => f.Title) }); await calendar.sideMenu.verifySideBarRecords({ records: dateRecords.filter(f => f.Title).map(f => f.Title) });
await calendar.sideMenu.updateFilter({ await calendar.sideMenu.updateFilter({
@ -427,8 +426,6 @@ test.describe('Calendar View', () => {
}); });
test('Calendar Operations Date Fields', async () => { test('Calendar Operations Date Fields', async () => {
test.slow();
await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` }); await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` });
await dashboard.treeView.openTable({ title: 'Social Media Calendar' }); await dashboard.treeView.openTable({ title: 'Social Media Calendar' });
@ -465,6 +462,8 @@ test.describe('Calendar View', () => {
await calendar.calendarTopbar.moveToDate({ date: '1 January 2024', action: 'prev' }); await calendar.calendarTopbar.moveToDate({ date: '1 January 2024', action: 'prev' });
await calendar.calendarTopbar.toggleSideBar();
await calendar.sideMenu.verifySideBarRecords({ records: dateRecords.filter(f => f.Title).map(f => f.Title) }); await calendar.sideMenu.verifySideBarRecords({ records: dateRecords.filter(f => f.Title).map(f => f.Title) });
await calendar.calendarTopbar.moveToDate({ date: '2 January 2024', action: 'next' }); await calendar.calendarTopbar.moveToDate({ date: '2 January 2024', action: 'next' });

Loading…
Cancel
Save