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. 140
      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. 14
      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 {
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>
isMondayFirst?: boolean
isWeekPicker?: boolean
disableHeader?: boolean
disablePagination?: boolean
selectedWeek?: {
start: dayjs.Dayjs
@ -23,6 +24,7 @@ const props = withDefaults(defineProps<Props>(), {
isMondayFirst: true,
pageDate: dayjs(),
isWeekPicker: false,
disableHeader: false,
disablePagination: false,
activeDates: [] as Array<dayjs.Dayjs>,
selectedWeek: null,
@ -145,14 +147,15 @@ const paginate = (action: 'next' | 'prev') => {
class="flex flex-col"
>
<div
v-if="!disableHeader"
:class="{
'justify-center': disablePagination,
'justify-between': !disablePagination,
' justify-between': !disablePagination,
' justify-center': disablePagination,
}"
class="flex items-center"
>
<NcTooltip>
<NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('prev')">
<NcTooltip v-if="!disablePagination">
<NcButton size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.doubleLeftArrow" class="h-4 w-4" />
</NcButton>
<template #title>
@ -168,8 +171,8 @@ const paginate = (action: 'next' | 'prev') => {
class="text-gray-700"
>{{ currentMonthYear }}</span
>
<NcTooltip>
<NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('next')">
<NcTooltip v-if="!disablePagination">
<NcButton size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
</NcButton>
<template #title>
@ -182,16 +185,16 @@ const paginate = (action: 'next' | 'prev') => {
'rounded-lg': size === 'small',
'rounded-y-xl': size !== 'small',
}"
class="border-1 border-gray-200 max-w-[320px]"
class="max-w-[320px]"
>
<div
:class="{
'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',
'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
v-for="(day, index) in days"
@ -201,23 +204,23 @@ const paginate = (action: 'next' | 'prev') => {
'w-8 h-8': size === 'medium',
'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
>
</div>
<div
:class="{
'gap-2 p-2': size === 'large',
'gap-2 pt-2': size === 'large',
'gap-1 p-1': size === 'medium',
}"
class="grid grid-cols-7"
class="grid nc-date-week-grid-wrapper grid-cols-7"
>
<span
v-for="(date, index) in dates"
:key="index"
:class="{
'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,
'nc-selected-week z-1': isDateInSelectedWeek(date) && isWeekPicker,
'border-none': isWeekPicker,
@ -225,25 +228,27 @@ const paginate = (action: 'next' | 'prev') => {
'text-gray-400': !isDateInCurrentMonth(date),
'nc-selected-week-start': isSameDate(date, selectedWeek?.start),
'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-8 w-8': size === 'medium',
'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"
@click="handleSelectDate(date)"
>
<span
v-if="isActiveDate(date)"
:class="{
'h-1.5 w-1.5': size === 'large',
'h-2 w-2': size === 'large',
'h-1 w-1': size === 'medium',
'h-0.75 w-0.75': size === 'small',
'top-1 right-1': 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 class="z-2">
{{ date.get('date') }}
@ -260,7 +265,7 @@ const paginate = (action: 'next' | 'prev') => {
}
.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: '';
width: 124%;
height: 100%;

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

@ -6,12 +6,14 @@ interface Props {
isDisabled?: boolean
pageDate?: dayjs.Dayjs
isYearPicker?: boolean
hideHeader?: boolean
}
const props = withDefaults(defineProps<Props>(), {
selectedDate: null,
isDisabled: false,
pageDate: dayjs(),
hideHeader: false,
isYearPicker: false,
})
const emit = defineEmits(['update:selectedDate', 'update:pageDate'])
@ -85,8 +87,8 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
</script>
<template>
<div class="px-4 pt-3 pb-4 flex flex-col gap-4">
<div class="flex justify-between items-center">
<div class="px-3 pb-3 pt-2 flex flex-col">
<div v-if="!hideHeader" class="flex justify-between items-center">
<NcTooltip>
<NcButton size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.doubleLeftArrow" class="h-4 w-4" />
@ -106,15 +108,15 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
</NcTooltip>
</div>
<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">
<span
v-for="(month, id) in months"
:key="id"
: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"
>
{{ month.format('MMM') }}
@ -125,9 +127,9 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
v-for="(year, id) in years"
:key="id"
: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"
>
{{ year.format('YYYY') }}

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

@ -14,6 +14,7 @@ interface Props {
showOnTruncateOnly?: boolean
hideOnClick?: boolean
overlayClassName?: string
wrapChild?: keyof HTMLElementTagNameMap
}
const props = defineProps<Props>()
@ -24,6 +25,7 @@ const disabled = computed(() => props.disabled)
const showOnTruncateOnly = computed(() => props.showOnTruncateOnly)
const hideOnClick = computed(() => props.hideOnClick)
const placement = computed(() => props.placement ?? 'top')
const wrapChild = computed(() => props.wrapChild ?? 'div')
const el = ref()
@ -124,9 +126,9 @@ const onClick = () => {
<slot name="title" />
</template>
<div ref="el" v-bind="divStyles" @mousedown="onClick">
<component :is="wrapChild" ref="el" v-bind="divStyles" @mousedown="onClick">
<slot />
</div>
</component>
</a-tooltip>
</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>) => {
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
}
@ -358,7 +358,7 @@ const parseValue = (value: any, col: ColumnType): string => {
<span
class="plain-cell before:px-1"
:class="{
'font-bold': bold,
'!font-bold': bold,
'italic': italic,
'underline': underline,
}"
@ -378,5 +378,8 @@ const parseValue = (value: any, col: ColumnType): string => {
content: '';
padding: 0;
}
&:first-child {
padding-left: 0;
}
}
</style>

140
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 gridTimeMap = new Map<
@ -263,15 +263,15 @@ const recordsAcrossAllRange = computed<{
scheduleEnd,
})
// 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
const heightInPixels = Math.max(endDate.diff(startDate, 'minute'), perRecordHeight)
const style: Partial<CSSStyleDeclaration> = {
height: `${heightInPixels}px`,
top: `${topInPixels}px`,
height: `${heightInPixels - 8}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
@ -316,14 +316,14 @@ const recordsAcrossAllRange = computed<{
// The top of the record is calculated based on the start hour
// 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
const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 60, perRecordHeight)
const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 52, perRecordHeight)
style = {
...style,
top: `${topInPixels + 1}px`,
height: `${heightInPixels - 2}px`,
top: `${topInPixels + 4}px`,
height: `${heightInPixels - 8}px`,
}
recordsByRange.push({
@ -734,6 +734,94 @@ const dragStart = (event: MouseEvent, record: Row) => {
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) => {
let startOfHour = hour.startOf('hour')
const endOfHour = hour.endOf('hour')
@ -802,24 +890,36 @@ watch(
<template>
<div
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"
@drop="dropEvent"
>
<div>
<div
v-for="(hour, index) in hours"
:key="index"
class="flex h-13 relative border-1 group hover:bg-gray-50 border-white"
data-testid="nc-calendar-day-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>
</div>
</div>
<div class="w-full">
<div
v-for="(hour, index) in hours"
:key="index"
:class="{
'!border-brand-500': hour.isSame(selectedTime),
}"
class="flex w-full h-15 nc-calendar-day-hour relative border-1 group hover:bg-gray-50 border-white border-b-gray-100"
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)"
>
<div class="pt-2 px-4 text-xs text-gray-500 font-semibold h-15">
{{ dayjs(hour).format('H A') }}
</div>
<div></div>
<NcDropdown
v-if="calendarRange.length > 1"
:class="{
@ -916,8 +1016,9 @@ watch(
</span>
</NcButton>
</div>
</div>
<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">
<div
v-if="record.rowMeta.style?.display !== 'none'"
@ -944,13 +1045,18 @@ watch(
<LazySmartsheetPlainCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"
class="text-xs"
class="text-xs font-medium"
:bold="getFieldStyle(field).bold"
:column="field"
:italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline"
/>
</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>
</LazySmartsheetRow>
</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 newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
if (!newStartDate) return
if (!newStartDate) return { newRow: null, updateProperty: [] }
let endDate
@ -586,13 +586,15 @@ const dropEvent = (event: DragEvent) => {
if (data) {
const {
record,
isWithoutDates,
}: {
record: Row
isWithoutDates: boolean
} = JSON.parse(data)
dragRecord.value = record
const { newRow, updateProperty } = calculateNewRow(event, true)
const { newRow, updateProperty } = calculateNewRow(event, isWithoutDates)
if (dragElement.value) {
dragElement.value.style.boxShadow = 'none'
@ -643,7 +645,7 @@ const addRecord = (date: dayjs.Dayjs) => {
<div
v-for="(day, index) in days"
: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 }}
</div>
@ -668,7 +670,7 @@ const addRecord = (date: dayjs.Dayjs) => {
'!text-gray-400': !isDayInPagedMonth(day),
'!bg-gray-50': day.get('day') === 0 || day.get('day') === 6,
}"
class="text-right relative group last:border-r-0 text-sm h-full border-r-1 border-b-1 border-gray-100 font-medium hover:bg-gray-50 text-gray-800 bg-white"
class="text-right relative group last:border-r-0 text-sm h-full border-r-1 border-b-1 border-gray-200 font-medium hover:bg-gray-50 text-gray-800 bg-white"
data-testid="nc-calendar-month-day"
@click="selectDate(day)"
@dblclick="addRecord(day)"
@ -744,9 +746,9 @@ const addRecord = (date: dayjs.Dayjs) => {
</NcButton>
<span
: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') }}
</span>
@ -795,6 +797,11 @@ const addRecord = (date: dayjs.Dayjs) => {
@resize-start="onResizeStart"
@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">
<LazySmartsheetPlainCell
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,
'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
v-if="position === 'leftRounded' || position === 'rounded'"
@ -71,15 +71,16 @@ const emit = defineEmits(['resize-start'])
</NcButton>
</div>
<div class="overflow-hidden items-center justify-center flex w-full ml-2">
<span v-if="position === 'rightRounded' || position === 'none'" class="mr-1"> .... </span>
<div class="overflow-hidden items-center justify-center gap-2 flex w-full">
<span v-if="position === 'rightRounded' || position === 'none'" class="ml-2"> .... </span>
<slot name="time" />
<span
:class="{
'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 />
<template #title>
<slot />

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

@ -31,6 +31,7 @@ const {
activeDates,
activeCalendarView,
isSidebarLoading,
isCalendarMetaLoading,
formattedSideBarData,
calDataType,
loadMoreSidebarData,
@ -55,7 +56,6 @@ const dragElement = ref<HTMLElement | null>(null)
const dragStart = (event: DragEvent, record: Row) => {
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 initialClickOffsetX = event.clientX - eventRect.left
@ -65,6 +65,7 @@ const dragStart = (event: DragEvent, record: Row) => {
'text/plain',
JSON.stringify({
record,
isWithoutDates: sideBarFilterOption.value === 'withoutDates',
initialClickOffsetY,
initialClickOffsetX,
}),
@ -288,8 +289,8 @@ onUnmounted(() => {
<div
:class="{
'!w-0': !props.visible,
'min-w-[356px]': width > 1440 && props.visible,
'min-w-[264px]': width <= 1440 && props.visible,
'min-w-[324px]': width > 1440 && props.visible,
'min-w-[288px]': width <= 1440 && props.visible,
'nc-calendar-side-menu-open': props.visible,
}"
class="h-full border-l-1 border-gray-200 transition-all"
@ -298,7 +299,7 @@ onUnmounted(() => {
<div
:class="{
'!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"
>
@ -334,21 +335,9 @@ onUnmounted(() => {
}"
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">
<a-input
v-model:value="searchQuery.value"
: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">
<div class="flex px-4 items-center gap-3">
<span class="capitalize text-base font-bold">{{ $t('objects.records') }}</span>
<NcSelect v-model:value="sideBarFilterOption" class="w-full !text-gray-600" data-testid="nc-calendar-sidebar-filter">
<a-select-option v-for="option in options" :key="option.value" :value="option.value" class="!text-gray-600">
<div class="flex items-center justify-between gap-2">
<div class="truncate flex-1">
@ -367,12 +356,27 @@ onUnmounted(() => {
</a-select-option>
</NcSelect>
</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
v-if="calendarRange?.length"
v-if="calendarRange?.length && !isCalendarMetaLoading"
:ref="sideBarListRef"
: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-25.1rem)]': activeCalendarView === ('month' as const) || activeCalendarView === ('year' as const) && width >= 1440,
@ -409,7 +413,7 @@ onUnmounted(() => {
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('DD MMM HH:mm A')
: null
"
:invalid="
@ -423,13 +427,13 @@ onUnmounted(() => {
record.rowMeta.range!.fk_to_col
? 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•HH:mm A')
: dayjs(record.row[record.rowMeta.range!.fk_to_col.title!]).format('DD MMM HH:mm A')
: null
"
color="blue"
data-testid="nc-sidebar-record-card"
@click="emit('expand-record', record)"
@dragstart="dragStart($event, record)"
@click="emit('expandRecord', record)"
@dragover.prevent
>
<template v-if="!isRowEmpty(record, displayField)">
@ -440,10 +444,22 @@ onUnmounted(() => {
</div>
</template>
</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
v-else
: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-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>
<template>
<div class="border-1 cursor-pointer border-gray-200 items-center px-2 py-3 rounded-lg">
<div class="flex items-center pl-1 gap-2">
<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="flex items-center pl-2 gap-2">
<span
:class="{
'bg-maroon-500': props.color === 'maroon',
@ -30,10 +40,10 @@ const props = withDefaults(defineProps<Props>(), {
class="block h-10 w-1 rounded"
></span>
<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 />
</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>

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

@ -52,7 +52,7 @@ const emit = defineEmits(['resize-start'])
'!border-blue-200 border-1': selected,
'shadow-md': hover,
}"
class="relative flex items-center h-full ml-0.25 border-1 border-transparent"
class="relative flex gap-2 items-center !pr-1 h-full ml-0.25 border-1 border-transparent"
>
<div class="h-full py-1">
<div
@ -64,20 +64,23 @@ const emit = defineEmits(['resize-start'])
'bg-pink-500': color === 'pink',
'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 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">
<NcTooltip :disabled="selected" show-on-truncate-only>
<slot />
<div
class="flex overflow-x-hidden break-word whitespace-nowrap overflow-hidden text-ellipsis w-full truncate text-ellipsis flex-col gap-1"
>
<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>
<slot />
</template>
</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>

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

@ -89,7 +89,7 @@ const recordsAcrossAllRange = computed<{
count: {},
}
const perWidth = containerWidth.value / 7
const perHeight = 60
const perHeight = 52
const scheduleStart = dayjs(selectedDateRange.value.start).startOf('day')
const scheduleEnd = dayjs(selectedDateRange.value.end).endOf('day')
@ -190,7 +190,7 @@ const recordsAcrossAllRange = computed<{
dayIndex = 6
}
const minutes = ogStartDate.minute() + ogStartDate.hour() * 60
const minutes = (ogStartDate.minute() / 60 + ogStartDate.hour()) * 52
style = {
...style,
@ -744,7 +744,7 @@ watch(
:class="{
'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') }}
</div>
@ -753,9 +753,9 @@ watch(
<div
v-for="(hour, index) in datesHours[0]"
: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 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'),
'!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"
@dblclick="addRecord(hour)"
@click="
@ -832,6 +832,11 @@ watch(
:underline="getFieldStyle(field).underline"
/>
</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>
</LazySmartsheetRow>
</div>

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

@ -41,6 +41,7 @@ watch(width, handleResize)
v-model:active-dates="activeDates"
v-model:page-date="months[index]"
v-model:selected-date="selectedDate"
class="nc-year-view-calendar"
:size="size"
data-testid="nc-calendar-year-view-month-selector"
disable-pagination
@ -49,4 +50,14 @@ watch(width, handleResize)
</div>
</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,
loadSidebarData,
isCalendarDataLoading,
isCalendarMetaLoading,
selectedDate,
selectedMonth,
activeDates,
@ -282,7 +283,7 @@ const headerText = computed(() => {
</NcButton>
</NcTooltip>
</div>
<template v-if="calendarRange?.length">
<template v-if="calendarRange?.length && !isCalendarMetaLoading">
<LazySmartsheetCalendarYearView v-if="activeCalendarView === 'year'" />
<template v-if="!isCalendarDataLoading">
<LazySmartsheetCalendarMonthView
@ -316,6 +317,11 @@ const headerText = computed(() => {
<GeneralLoader size="xlarge" />
</div>
</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>
<div class="flex w-full items-center h-full justify-center">
{{ $t('activity.noRange') }}

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

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

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

@ -24,10 +24,6 @@ const updateHighlightPosition = () => {
})
}
onMounted(() => {
updateHighlightPosition()
})
watch(activeCalendarView, () => {
if (!props.tab) return
updateHighlightPosition()
@ -37,7 +33,7 @@ watch(activeCalendarView, () => {
<template>
<div
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"
>
<div :style="highlightStyle" class="highlight"></div>
@ -77,16 +73,16 @@ watch(activeCalendarView, () => {
<style lang="scss" scoped>
.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;
}
.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 {
@apply min-w-0 pointer-events-none;
@apply min-w-0 mb-3 pointer-events-none;
word-break: 'keep-all';
white-space: 'nowrap';
display: 'inline';
@ -94,7 +90,7 @@ watch(activeCalendarView, () => {
}
.active {
@apply text-black bg-transparent;
@apply !text-brand-500 font-medium bg-transparent;
}
.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>
</div>
<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
v-for="(range, id) in _calendar_ranges"
:key="id"

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

@ -62,7 +62,9 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const isCalendarDataLoading = ref<boolean>(false)
const showSideMenu = ref(true)
const isCalendarMetaLoading = ref<boolean>(false)
const showSideMenu = ref(false)
const selectedDateRange = ref<{
start: dayjs.Dayjs
@ -415,6 +417,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
async function loadCalendarMeta() {
if (!viewMeta?.value?.id || !meta?.value?.columns) return
isCalendarMetaLoading.value = true
try {
const res = isPublic.value ? (sharedView.value?.view as CalendarType) : await $api.dbView.calendarRead(viewMeta.value.id)
calendarMetaData.value = res
@ -430,6 +433,8 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
}) as any
} catch (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 () => {
if (calDataType.value !== UITypes.Date && showSideMenu.value) {
if (calDataType.value !== UITypes.Date && showSideMenu.value && sideBarFilterOption.value === 'selectedHours') {
await loadSidebarData()
}
})
watch(selectedMonth, async (value, oldValue) => {
watch(selectedMonth, async () => {
if (activeCalendarView.value !== 'month') return
if (value.month() !== oldValue.month()) {
await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()])
}
})
watch(selectedDateRange, async () => {
@ -797,6 +800,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
searchQuery,
activeDates,
isCalendarDataLoading,
isCalendarMetaLoading,
changeCalendarView,
calDataType,
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 () => {
// Create & Verify Calendar View
test.slow();
await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` });
await dashboard.treeView.openTable({ title: 'Social Media Calendar' });
@ -162,6 +161,8 @@ test.describe('Calendar View', () => {
// Verify Sidebar
const calendar = dashboard.calendar;
await calendar.calendarTopbar.toggleSideBar();
await calendar.verifySideBarOpen();
await calendar.calendarTopbar.toggleSideBar();
@ -289,8 +290,6 @@ test.describe('Calendar View', () => {
});
test('Calendar Drag and Drop & Undo Redo Operations', async () => {
test.slow();
await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` });
await dashboard.treeView.openTable({ title: 'Social Media Calendar' });
@ -308,6 +307,8 @@ test.describe('Calendar View', () => {
const calendar = dashboard.calendar;
await calendar.calendarTopbar.toggleSideBar();
await calendar.calendarTopbar.moveToDate({ date: 'January 2024', action: 'prev' });
await calendar.calendarMonth.dragAndDrop({
@ -367,8 +368,6 @@ test.describe('Calendar View', () => {
});
test('Calendar shared view operations', async ({ page }) => {
test.slow();
await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` });
await dashboard.treeView.openTable({ title: 'Social Media Calendar' });
@ -381,8 +380,6 @@ test.describe('Calendar View', () => {
index: 0,
});
await dashboard.rootPage.waitForTimeout(12000);
// Share view
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.toggleSideBar();
await calendar.sideMenu.verifySideBarRecords({ records: dateRecords.filter(f => f.Title).map(f => f.Title) });
await calendar.sideMenu.updateFilter({
@ -427,8 +426,6 @@ test.describe('Calendar View', () => {
});
test('Calendar Operations Date Fields', async () => {
test.slow();
await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` });
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.toggleSideBar();
await calendar.sideMenu.verifySideBarRecords({ records: dateRecords.filter(f => f.Title).map(f => f.Title) });
await calendar.calendarTopbar.moveToDate({ date: '2 January 2024', action: 'next' });

Loading…
Cancel
Save