Browse Source

feat: remove calendar top bar (#8379)

* feat: remove calendar top bar

* fix: remove debug logs

* fix: update styles

* fix: update styles

* fix: posthog telementry

* fix: calendar tests

* fix: updates ui

* test: reorder options

* fix: month view - use local time with timezone

* fix: update styles and move components fix: tests

* fix: update styles and move components

* fix: update styles

* test: fix tests

* fix: update toolbar styles

* fix: failing tests

* fix: cmd f search shortcut

* fix: change side menu sizes

* fix: calendar test corrections

* fix(nc-gui): update size logic

* fix(nc-gui): update styles

* fix(nc-gui): update some more styles

* fix(nc-gui): update toolbar styles

* fix(nc-gui): update select component

* fix: update styles

* fix: calendar test

* fix: ux changes

* fix: final changes

* fix: calendar tests
pull/8475/head
Anbarasu 2 months ago committed by GitHub
parent
commit
fb67cafde7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      packages/nc-gui/assets/style.scss
  2. 4
      packages/nc-gui/components/cmd-k/index.vue
  3. 2
      packages/nc-gui/components/cmd-l/index.vue
  4. 2
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  5. 2
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  6. 64
      packages/nc-gui/components/dlg/ViewCreate.vue
  7. 2
      packages/nc-gui/components/dlg/ViewDelete.vue
  8. 67
      packages/nc-gui/components/nc/DateWeekSelector.vue
  9. 10
      packages/nc-gui/components/nc/Divider.vue
  10. 2
      packages/nc-gui/components/nc/Modal.vue
  11. 57
      packages/nc-gui/components/nc/MonthYearSelector.vue
  12. 2
      packages/nc-gui/components/nc/Select.vue
  13. 62
      packages/nc-gui/components/smartsheet/Toolbar.vue
  14. 7
      packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue
  15. 9
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  16. 34
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  17. 212
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  18. 7
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  19. 18
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  20. 28
      packages/nc-gui/components/smartsheet/calendar/YearView.vue
  21. 158
      packages/nc-gui/components/smartsheet/calendar/index.vue
  22. 11
      packages/nc-gui/components/smartsheet/toolbar/Calendar/ActiveView.vue
  23. 126
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Header.vue
  24. 69
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Mode.vue
  25. 64
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Range.vue
  26. 39
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Today.vue
  27. 13
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  28. 40
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  29. 21
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  30. 12
      packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue
  31. 4
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  32. 31
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  33. 10
      packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue
  34. 4
      packages/nc-gui/components/smartsheet/topbar/SelectMode.vue
  35. 2
      packages/nc-gui/components/tabs/Smartsheet.vue
  36. 75
      packages/nc-gui/composables/useCalendarViewStore.ts
  37. 1
      packages/nc-gui/composables/useViewColumns.ts
  38. 3
      packages/nc-gui/lang/en.json
  39. 3
      packages/nc-gui/utils/iconUtils.ts
  40. 1
      packages/nc-gui/windi.config.ts
  41. 30
      tests/playwright/pages/Dashboard/Calendar/CalendarSideMenu.ts
  42. 63
      tests/playwright/pages/Dashboard/Calendar/CalendarTopBar.ts
  43. 11
      tests/playwright/pages/Dashboard/Calendar/CalendarWeekDateTime.ts
  44. 1
      tests/playwright/pages/Dashboard/Calendar/CalendarYear.ts
  45. 8
      tests/playwright/pages/Dashboard/Calendar/index.ts
  46. 2
      tests/playwright/pages/Dashboard/ViewSidebar/index.ts
  47. 5
      tests/playwright/pages/Dashboard/common/Toolbar/CalendarViewMode.ts
  48. 17
      tests/playwright/pages/Dashboard/common/Toolbar/index.ts
  49. 2
      tests/playwright/tests/db/general/toolbarOperations.spec.ts
  50. 70
      tests/playwright/tests/db/views/viewCalendar.spec.ts

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

@ -11,6 +11,7 @@ body {
}
:root {
--toolbar-height: 2.25rem;
--topbar-height: 3.1rem;
--sidebar-bottom-height: 8.5rem;
--new-header-height: 3.5rem;
@ -515,7 +516,7 @@ a {
}
.nc-toolbar-btn {
@apply !shadow-none rounded hover:(ring-1 ring-gray-200 ring-opacity-100 bg-gray-100 !text-gray-800) focus:(ring-1 ring-gray-300 ring-opacity-100 !text-gray-800 bg-gray-100) text-gray-600 text-xs font-medium px-2 border-0;
@apply !shadow-none rounded hover:(bg-gray-50 !text-gray-800) focus:(!text-gray-800 bg-gray-50) text-gray-600 text-xs font-medium px-2 border-0;
}
.nc-toolbar-btn[disabled] {
@apply !text-gray-400 !cursor-not-allowed !hover:ring-0;

4
packages/nc-gui/components/cmd-k/index.vue

@ -427,14 +427,14 @@ defineExpose({
<component
:is="(iconMap as any)[act.icon]"
v-if="act.icon && typeof act.icon === 'string' && (iconMap as any)[act.icon]"
class="cmdk-action-icon"
:class="{
'!text-blue-500': act.icon === 'grid',
'!text-purple-500': act.icon === 'form',
'!text-[#FF9052]': act.icon === 'kanban',
'!text-pink-500': act.icon === 'gallery',
'!text-maroon-500': act.icon === 'calendar',
'!text-maroon-500 w-4 h-4': act.icon === 'calendar',
}"
class="cmdk-action-icon"
/>
<div v-else-if="act.icon" class="cmdk-action-icon max-w-4 flex items-center justify-center">
<LazyGeneralEmojiPicker class="!text-sm !h-4 !w-4" size="small" :emoji="act.icon" readonly />

2
packages/nc-gui/components/cmd-l/index.vue

@ -206,7 +206,7 @@ onMounted(() => {
<div class="cmdk-action-content">
<div class="flex w-1/2 items-center">
<div class="flex gap-2">
<GeneralViewIcon :meta="{ type: cmdOption.viewType }" class="mt-0.5" />
<GeneralViewIcon :meta="{ type: cmdOption.viewType }" class="mt-0.5 w-4 !min-h-4" />
<a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg">
<template #title>
{{ cmdOption.viewName }}

2
packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue

@ -158,7 +158,7 @@ async function onOpenModal({
<NcMenuItem data-testid="sidebar-view-create-calendar" @click="onOpenModal({ type: ViewTypes.CALENDAR })">
<div class="item">
<div class="item-inner">
<GeneralViewIcon :meta="{ type: ViewTypes.CALENDAR }" />
<GeneralViewIcon :meta="{ type: ViewTypes.CALENDAR }" class="!w-4 !h-4" />
<div>{{ $t('objects.viewType.calendar') }}</div>
</div>

2
packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue

@ -234,7 +234,7 @@ watch(isDropdownOpen, async () => {
@emoji-selected="emits('selectIcon', $event)"
>
<template #default>
<GeneralViewIcon :meta="props.view" class="nc-view-icon !text-[16px]"></GeneralViewIcon>
<GeneralViewIcon :meta="props.view" class="nc-view-icon w-4 !text-[16px]"></GeneralViewIcon>
</template>
</LazyGeneralEmojiPicker>
</div>

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

@ -137,7 +137,6 @@ function init() {
if (repeatCount) {
form.title = `${form.title}-${repeatCount}`
}
if (selectedViewId.value) {
form.copy_from_id = selectedViewId?.value
}
@ -320,11 +319,11 @@ onMounted(async () => {
<template>
<NcModal
v-model:visible="vModel"
:size="[ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(form.type) ? 'small' : 'small'"
:size="[ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(form.type) ? 'medium' : 'small'"
>
<template #header>
<div class="flex w-full flex-row justify-between items-center">
<div class="flex gap-x-1.5 items-center">
<div class="flex font-bold text-base gap-x-3 items-center">
<GeneralViewIcon :meta="{ type: form.type }" class="nc-view-icon !text-xl" />
<template v-if="form.type === ViewTypes.GRID">
<template v-if="form.copy_from_id">
@ -377,7 +376,7 @@ onMounted(async () => {
</div>
<a
v-if="!form.copy_from_id"
class="text-sm !text-gray-600 !hover:text-gray-600"
class="text-sm !text-gray-600 !font-default !hover:text-gray-600"
:href="`https://docs.nocodb.com/views/view-types/${typeAlias}`"
target="_blank"
>
@ -434,7 +433,12 @@ onMounted(async () => {
<span>
{{ $t('labels.organiseBy') }}
</span>
<NcSelect v-model:value="range.fk_from_column_id" :disabled="isMetaLoading" :loading="isMetaLoading">
<NcSelect
v-model:value="range.fk_from_column_id"
:disabled="isMetaLoading"
:loading="isMetaLoading"
class="nc-from-select"
>
<a-select-option
v-for="(option, id) in [...viewSelectFieldOptions!].filter((f) => {
// If the fk_from_column_id of first range is Date, then all the other ranges should be Date
@ -444,14 +448,24 @@ onMounted(async () => {
return firstRange?.uidt === f.uidt
})"
:key="id"
class="w-40"
:value="option.value"
>
<div class="flex items-center">
<SmartsheetHeaderIcon :column="option" />
<NcTooltip class="truncate flex-1 max-w-18" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
{{ option.label }}
</NcTooltip>
<div class="flex w-full gap-2 justify-between items-center">
<div class="flex gap-2 items-center">
<SmartsheetHeaderIcon :column="option" />
<NcTooltip class="truncate flex-1 max-w-18" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
{{ option.label }}
</NcTooltip>
</div>
<div class="flex-1" />
<component
:is="iconMap.check"
v-if="option.value === range.fk_from_column_id"
id="nc-selected-item-icon"
class="text-primary min-w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
@ -473,8 +487,8 @@ onMounted(async () => {
v-model:value="range.fk_to_column_id"
:disabled="isMetaLoading"
:loading="isMetaLoading"
:placeholder="$t('placeholder.notSelected')"
class="!rounded-r-none nc-to-select"
:placeholder="$t('placenc-to-seleholder.notSelected')"
class="!rounded-r-none ct"
>
<a-select-option
v-for="(option, id) in [...viewSelectFieldOptions].filter((f) => {
@ -523,10 +537,12 @@ onMounted(async () => {
</template>
</a-form>
<div v-else-if="!isNecessaryColumnsPresent" class="flex flex-row p-4 border-gray-200 border-1 gap-x-4 rounded-lg w-full">
<GeneralIcon class="!text-5xl text-orange-500" icon="warning" />
<div class="text-gray-500">
<h2 class="font-semibold text-sm text-gray-800">Suitable fields not present</h2>
{{ errorMessages[form.type] }}
<div class="text-gray-500 flex gap-4">
<GeneralIcon class="min-w-6 h-6 text-orange-500" icon="warning" />
<div class="flex flex-col gap-1">
<h2 class="font-semibold text-sm mb-0 text-gray-800">Suitable fields not present</h2>
<span class="text-gray-500 font-default"> {{ errorMessages[form.type] }}</span>
</div>
</div>
</div>
@ -550,7 +566,7 @@ onMounted(async () => {
</NcModal>
</template>
<style lang="scss">
<style lang="scss" scoped>
.ant-form-item-required {
@apply !text-gray-800 font-medium;
&:before {
@ -558,7 +574,19 @@ onMounted(async () => {
}
}
.nc-from-select .ant-select-selector {
@apply !mr-2;
}
.nc-to-select .ant-select-selector {
@apply !rounded-r-none;
}
.ant-input {
@apply border-gray-200;
}
.ant-form-item {
@apply !mb-6;
}
</style>

2
packages/nc-gui/components/dlg/ViewDelete.vue

@ -47,7 +47,7 @@ async function onDelete() {
<GeneralDeleteModal v-model:visible="vModel" :entity-name="$t('objects.view')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="view" class="flex flex-row items-center py-2 px-3 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralViewIcon :meta="props.view" class="nc-view-icon"></GeneralViewIcon>
<GeneralViewIcon :meta="props.view" class="nc-view-icon w-4 min-h-4"></GeneralViewIcon>
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-3"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"

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

@ -8,9 +8,10 @@ interface Props {
pageDate?: dayjs.Dayjs
activeDates?: Array<dayjs.Dayjs>
isMondayFirst?: boolean
disablePagination?: boolean
isWeekPicker?: boolean
disableHeader?: boolean
disablePagination?: boolean
hideCalendar?: boolean
selectedWeek?: {
start: dayjs.Dayjs
end: dayjs.Dayjs
@ -22,14 +23,15 @@ const props = withDefaults(defineProps<Props>(), {
selectedDate: null,
isDisabled: false,
isMondayFirst: true,
disablePagination: false,
pageDate: dayjs(),
isWeekPicker: false,
disableHeader: false,
disablePagination: false,
activeDates: [] as Array<dayjs.Dayjs>,
selectedWeek: null,
hideCalendar: false,
})
const emit = defineEmits(['change', 'update:selectedDate', 'update:pageDate', 'update:selectedWeek'])
const emit = defineEmits(['change', 'dblClick', 'update:selectedDate', 'update:pageDate', 'update:selectedWeek'])
// Page date is the date we use to manage which month/date that is currently being displayed
const pageDate = useVModel(props, 'pageDate', emit)
@ -47,7 +49,6 @@ const days = computed(() => {
}
})
// Used to display the current month and year
const currentMonthYear = computed(() => {
return dayjs(pageDate.value).format('MMMM YYYY')
})
@ -56,10 +57,12 @@ const selectWeek = (date: dayjs.Dayjs) => {
const dayOffset = +props.isMondayFirst
const dayOfWeek = (date.day() - dayOffset + 7) % 7
const startDate = date.subtract(dayOfWeek, 'day')
selectedWeek.value = {
const newWeek = {
start: startDate,
end: startDate.endOf('week'),
}
selectedWeek.value = newWeek
emit('update:selectedWeek', newWeek)
}
// Generates all dates should be displayed in the calendar
@ -136,30 +139,32 @@ const paginate = (action: 'next' | 'prev') => {
pageDate.value = newDate
emit('update:pageDate', newDate)
}
const emitDblClick = (date: dayjs.Dayjs) => {
emit('dblClick', date)
}
</script>
<template>
<div
:class="{
'gap-1': size === 'small',
'gap-4': size === 'medium' || size === 'large',
}"
class="flex flex-col"
>
<div
v-if="!disableHeader"
:class="{
' justify-between': !disablePagination,
' justify-center': disablePagination,
'!justify-center': disablePagination,
}"
class="flex items-center"
class="flex justify-between border-b-1 px-3 py-0.5 nc-date-week-header items-center"
>
<NcTooltip v-if="!disablePagination">
<NcButton size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.doubleLeftArrow" class="h-4 w-4" />
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.arrowLeft" class="h-4 w-4" />
</NcButton>
<template #title>
<span>{{ $t('labels.previousMonth') }}</span>
<span>{{ $t('labels.next') }}</span>
</template>
</NcTooltip>
@ -168,19 +173,21 @@ const paginate = (action: 'next' | 'prev') => {
'text-xs': size === 'small',
'text-sm': size === 'medium',
}"
class="text-gray-700"
class="text-gray-700 font-semibold"
>{{ currentMonthYear }}</span
>
<NcTooltip v-if="!disablePagination">
<NcButton size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
<NcButton class="!border-0" data-testid="nc-calendar-next-btn" size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.arrowRight" class="h-4 w-4" />
</NcButton>
<template #title>
<span>{{ $t('labels.nextMonth') }}</span>
<span>{{ $t('labels.next') }}</span>
</template>
</NcTooltip>
</div>
<div
v-if="!hideCalendar"
:class="{
'rounded-lg': size === 'small',
'rounded-y-xl': size !== 'small',
@ -189,12 +196,12 @@ const paginate = (action: 'next' | 'prev') => {
>
<div
:class="{
'gap-1 px-1': size === 'medium',
'gap-1 px-3.5': size === 'medium',
'gap-2': size === 'large',
'px-2 py-1 !rounded-t-lg': size === 'small',
'px-2 !rounded-t-lg': size === 'small',
'rounded-t-xl': size !== 'small',
}"
class="flex flex-row border-b-1 nc-date-week-header border-gray-200 justify-between"
class="flex py-1 flex-row nc-date-week-header border-gray-200 justify-between"
>
<span
v-for="(day, index) in days"
@ -211,7 +218,7 @@ const paginate = (action: 'next' | 'prev') => {
<div
:class="{
'gap-2 pt-2': size === 'large',
'gap-1 p-1': size === 'medium',
'gap-1 py-1 px-3.5': size === 'medium',
}"
class="grid nc-date-week-grid-wrapper grid-cols-7"
>
@ -220,35 +227,37 @@ const paginate = (action: 'next' | 'prev') => {
:key="index"
:class="{
'rounded-lg': !isWeekPicker,
'bg-gray-200 border-1 font-bold text-brand-500': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date),
'bg-gray-200 border-1 font-bold ': 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,
'nc-selected-week !font-semibold z-1': isDateInSelectedWeek(date) && isWeekPicker,
'border-none': isWeekPicker,
'border-transparent': !isWeekPicker,
'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 text-brand-500 nc-calendar-today ': isSameDate(date, dayjs()) && isDateInCurrentMonth(date),
'rounded-md text-brand-500 !font-semibold nc-calendar-today ':
isSameDate(date, dayjs()) && isDateInCurrentMonth(date),
'h-9 w-9': size === 'large',
'text-gray-500': date.get('day') === 0 || date.get('day') === 6,
'h-8 w-8': size === 'medium',
'h-6 w-6 text-[10px]': size === 'small',
}"
class="px-1 py-1 relative border-1 font-medium flex items-center cursor-pointer justify-center"
data-testid="nc-calendar-date"
@dblclick="emitDblClick(date)"
@click="handleSelectDate(date)"
>
<span
v-if="isActiveDate(date)"
:class="{
'h-2 w-2': size === 'large',
'h-1 w-1': size === 'medium',
'h-0.75 w-0.75': size === 'small',
'h-1.5 w-1.5': size === 'medium',
'h-1.25 w-1.25 top-0.5 right-0.5': 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()),
'!border-brand-50': isSameDate(date, dayjs()),
}"
class="absolute z-2 rounded-full border-2 border-white bg-brand-500"
class="absolute z-2 border-1 rounded-full border-white bg-brand-500"
></span>
<span class="z-2">
{{ date.get('date') }}
@ -267,7 +276,7 @@ const paginate = (action: 'next' | 'prev') => {
.nc-selected-week:before {
@apply absolute top-0 left-0 w-full h-full bg-gray-200;
content: '';
width: 124%;
width: 134%;
height: 100%;
}

10
packages/nc-gui/components/nc/Divider.vue

@ -1,5 +1,13 @@
<script lang="ts" setup>
const props = defineProps<{
dividerClass?: string
}>()
const dividerClass = toRef(props, 'dividerClass')
</script>
<template>
<a-divider class="nc-divider" />
<a-divider :class="dividerClass" class="nc-divider" />
</template>
<style lang="scss">

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

@ -103,7 +103,7 @@ const slots = useSlots()
<div
v-if="slots.header"
:class="{
'border-b-1 border-gray-100': showSeparator,
'border-b-1 border-gray-200': showSeparator,
}"
class="flex pb-2 mb-2 text-lg font-medium"
>

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

@ -7,6 +7,7 @@ interface Props {
pageDate?: dayjs.Dayjs
isYearPicker?: boolean
hideHeader?: boolean
hideCalendar?: boolean
}
const props = withDefaults(defineProps<Props>(), {
@ -15,6 +16,7 @@ const props = withDefaults(defineProps<Props>(), {
pageDate: dayjs(),
hideHeader: false,
isYearPicker: false,
hideCalendar: false,
})
const emit = defineEmits(['update:selectedDate', 'update:pageDate'])
@ -87,34 +89,42 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
</script>
<template>
<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" />
</NcButton>
<template #title>
<span>{{ $t('labels.previous') }}</span>
</template>
</NcTooltip>
<span class="text-gray-700">{{ isYearPicker ? $t('labels.selectYear') : pageDate.year() }}</span>
<NcTooltip>
<NcButton size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
</NcButton>
<template #title>
<span>{{ $t('labels.next') }}</span>
</template>
</NcTooltip>
<div class="flex flex-col">
<div v-if="!hideHeader" class="flex px-2 border-b-1 py-0.5 justify-between items-center">
<div class="flex">
<NcTooltip>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.arrowLeft" class="h-4 w-4" />
</NcButton>
<template #title>
<span>{{ $t('labels.next') }}</span>
</template>
</NcTooltip>
</div>
<span class="text-gray-700 font-semibold">{{
isYearPicker ? dayjs(selectedDate).year() : dayjs(pageDate).format('YYYY')
}}</span>
<div class="flex">
<NcTooltip>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.arrowRight" class="h-4 w-4" />
</NcButton>
<template #title>
<span>{{ $t('labels.next') }}</span>
</template>
</NcTooltip>
</div>
</div>
<div class="rounded-y-xl max-w-[350px]">
<div class="grid grid-cols-4 gap-2 py-3">
<div v-if="!hideCalendar" class="rounded-y-xl px-2.5 py-1 max-w-[350px]">
<div class="grid grid-cols-4 gap-2">
<template v-if="!isYearPicker">
<span
v-for="(month, id) in months"
:key="id"
:class="{
'!bg-gray-200 !font-bold !text-brand-500': isMonthSelected(month),
'!bg-gray-200 !text-brand-500 !font-bold ': isMonthSelected(month),
'!text-brand-500': dayjs().isSame(month, 'month'),
}"
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"
@ -127,7 +137,8 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
v-for="(year, id) in years"
:key="id"
:class="{
'!bg-gray-200 !font-bold !text-brand-500': compareYear(year, selectedDate),
'!bg-gray-200 !text-brand-500 !font-bold ': compareYear(year, selectedDate),
'!text-brand-500': dayjs().isSame(year, 'year'),
}"
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"

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

@ -84,7 +84,7 @@ const onChange = (value: string) => {
height: fit-content;
.ant-select-selector {
box-shadow: 0px 5px 3px -2px rgba(0, 0, 0, 0.02), 0px 3px 1px -2px rgba(0, 0, 0, 0.06);
@apply border-1 border-gray-200 rounded-lg;
@apply border-1 border-gray-200 rounded-lg !px-3;
}
.ant-select-selection-item {

62
packages/nc-gui/components/smartsheet/Toolbar.vue

@ -3,24 +3,18 @@ const { isGrid, isGallery, isKanban, isMap, isCalendar } = useSmartsheetStoreOrT
const isPublic = inject(IsPublicInj, ref(false))
const { isViewsLoading } = storeToRefs(useViewsStore())
const { isMobileMode } = useGlobal()
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const containerRef = ref<HTMLElement>()
const isTab = ref(true)
const { isViewsLoading } = storeToRefs(useViewsStore())
const handleResize = () => {
isTab.value = containerRef.value.offsetWidth > 810
}
const containerRef = ref<HTMLElement>()
onMounted(() => {
window.addEventListener('resize', handleResize)
})
const { width } = useElementSize(containerRef)
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
const isTab = computed(() => {
if (!isCalendar.value) return false
return width.value > 1200
})
const { allowCSVDownload } = useSharedView()
@ -28,32 +22,40 @@ const { allowCSVDownload } = useSharedView()
<template>
<div
v-if="!isMobileMode || !isCalendar"
v-if="!isMobileMode"
ref="containerRef"
class="nc-table-toolbar relative py-1 px-2.25 xs:(px-1) flex gap-2 items-center border-b border-gray-200 overflow-hidden xs:(min-h-14) max-h-[var(--topbar-height)] min-h-[var(--topbar-height)] z-7"
class="nc-table-toolbar relative px-3 xs:(px-1) flex gap-2 items-center border-b border-gray-200 overflow-hidden xs:(min-h-14) min-h-9 max-h-9 z-7"
>
<template v-if="isViewsLoading">
<a-skeleton-input :active="true" class="!w-44 !h-4 ml-2 !rounded overflow-hidden" />
</template>
<template v-else>
<LazySmartsheetToolbarMappedBy v-if="isMap" />
<LazySmartsheetToolbarCalendarRange v-if="isCalendar" />
<div
:class="{
'min-w-34/100': !isMobileMode && isLeftSidebarOpen && isCalendar,
'min-w-39/100': !isMobileMode && !isLeftSidebarOpen && isCalendar,
'gap-1': isCalendar,
}"
class="flex items-center gap-3"
>
<LazySmartsheetToolbarMappedBy v-if="isMap" />
<LazySmartsheetToolbarCalendarHeader v-if="isCalendar" />
<LazySmartsheetToolbarCalendarToday v-if="isCalendar" />
<LazySmartsheetToolbarFieldsMenu
v-if="isGrid || isGallery || isKanban || isMap || isCalendar"
:show-system-fields="false"
/>
<LazySmartsheetToolbarCalendarRange v-if="isCalendar" />
<LazySmartsheetToolbarStackedBy v-if="isKanban" />
<LazySmartsheetToolbarFieldsMenu v-if="isGrid || isGallery || isKanban || isMap" :show-system-fields="false" />
<LazySmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery || isKanban || isMap || isCalendar" />
<LazySmartsheetToolbarStackedBy v-if="isKanban" />
<LazySmartsheetToolbarGroupByMenu v-if="isGrid" />
<LazySmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery || isKanban || isMap" />
<LazySmartsheetToolbarSortListMenu v-if="isGrid || isGallery || isKanban || isCalendar" />
<LazySmartsheetToolbarGroupByMenu v-if="isGrid" />
<div v-if="isCalendar && isTab" class="flex-1" />
<LazySmartsheetToolbarCalendarMode v-if="isCalendar" v-model:tab="isTab" />
<LazySmartsheetToolbarSortListMenu v-if="isGrid || isGallery || isKanban" />
</div>
<LazySmartsheetToolbarCalendarMode v-if="isCalendar && isTab" :tab="isTab" />
<template v-if="!isMobileMode">
<LazySmartsheetToolbarRowHeight v-if="isGrid" />
@ -65,6 +67,8 @@ const { allowCSVDownload } = useSharedView()
<div class="flex-1" />
</template>
<LazySmartsheetToolbarCalendarActiveView v-if="isCalendar" />
<LazySmartsheetToolbarSearchData
v-if="isGrid || isGallery || isKanban"
:class="{
@ -72,6 +76,10 @@ const { allowCSVDownload } = useSharedView()
'w-full': isMobileMode,
}"
/>
<LazySmartsheetToolbarCalendarMode v-if="isCalendar && !isTab" :tab="isTab" />
<LazySmartsheetToolbarFieldsMenu v-if="isCalendar" :show-system-fields="false" />
<LazySmartsheetToolbarColumnFilterMenu v-if="isCalendar" />
</template>
</div>
</template>

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

@ -183,6 +183,7 @@ const dropEvent = (event: DragEvent) => {
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)
$e('c:calendar:day:drag-record')
}
}
@ -202,7 +203,7 @@ const newRecord = () => {
<div
v-if="recordsAcrossAllRange.length"
ref="container"
class="w-full relative h-[calc(100vh-10.8rem)] overflow-y-auto nc-scrollbar-md"
class="w-full cursor-pointer relative h-[calc(100vh-10.8rem)] overflow-y-auto nc-scrollbar-md"
data-testid="nc-calendar-day-view"
@dblclick="newRecord"
@drop="dropEvent"
@ -223,7 +224,7 @@ const newRecord = () => {
:resize="false"
color="blue"
size="small"
@click="emit('expandRecord', record)"
@click.prevent="emit('expandRecord', record)"
>
<template v-for="(field, id) in fields" :key="id">
<LazySmartsheetPlainCell
@ -244,7 +245,7 @@ const newRecord = () => {
<div
v-else
ref="container"
class="w-full h-full flex text-md font-bold text-gray-500 items-center justify-center"
class="w-full h-full cursor-pointer flex text-md font-bold text-gray-500 items-center justify-center"
@drop="dropEvent"
@dblclick="newRecord"
>

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

@ -16,6 +16,8 @@ const {
showSideMenu,
} = useCalendarViewStoreOrThrow()
const { $e } = useNuxtApp()
const container = ref<null | HTMLElement>(null)
const { isUIAllowed } = useRoles()
@ -672,6 +674,8 @@ const stopDrag = (event: MouseEvent) => {
if (!newRow) return
updateRowProperty(newRow, updateProperty, false)
$e('c:calendar:day:drag-record')
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
}
@ -802,6 +806,7 @@ const dropEvent = (event: DragEvent) => {
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)
$e('c:calendar:day:drag-record')
}
}
@ -859,7 +864,7 @@ watch(
<template>
<div
ref="container"
class="w-full flex relative no-selection h-[calc(100vh-10rem)] overflow-y-auto nc-scrollbar-md"
class="w-full flex relative no-selection h-[calc(100vh-5.3rem)] overflow-y-auto nc-scrollbar-md"
data-testid="nc-calendar-day-view"
@drop="dropEvent"
>
@ -872,7 +877,7 @@ watch(
@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">
<div class="w-16 border-b-0 pr-2 pl-2 text-right text-xs text-gray-400 font-semibold h-13">
{{ dayjs(hour).format('hh a') }}
</div>
</div>

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

@ -1,6 +1,7 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import type { ColumnType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
const emit = defineEmits(['newRecord', 'expandRecord'])
@ -9,6 +10,7 @@ const {
selectedMonth,
formattedData,
formattedSideBarData,
calDataType,
sideBarFilterOption,
displayField,
calendarRange,
@ -16,6 +18,8 @@ const {
updateRowProperty,
} = useCalendarViewStoreOrThrow()
const { $e } = useNuxtApp()
const isMondayFirst = ref(true)
const { isUIAllowed } = useRoles()
@ -115,7 +119,7 @@ const recordsToDisplay = computed<{
const perHeight = gridContainerHeight.value / dates.value.length
const perRecordHeight = 24
const spaceBetweenRecords = 26
const spaceBetweenRecords = 27
// This object is used to keep track of the number of records in a day
// The key is the date in the format YYYY-MM-DD
@ -357,19 +361,28 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean, skipChangeC
const fromCol = dragRecord.value?.rowMeta.range?.fk_from_col
const toCol = dragRecord.value?.rowMeta.range?.fk_to_col
if (!fromCol) return { newRow: null, updateProperty: [] }
const week = Math.floor(percentY * dates.value.length)
const day = Math.floor(percentX * 7)
const newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
let newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
if (!newStartDate) return { newRow: null, updateProperty: [] }
const fromDate = dayjs(dragRecord.value.row[fromCol.title!])
newStartDate = newStartDate.add(fromDate.hour(), 'hour').add(fromDate.minute(), 'minute').add(fromDate.second(), 'second')
let endDate
const newRow = {
...dragRecord.value,
row: {
...dragRecord.value?.row,
[fromCol!.title!]: dayjs(newStartDate).utc().format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol!.title!]:
calDataType.value === UITypes.Date
? dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ')
: dayjs(newStartDate).utc().format('YYYY-MM-DD HH:mm:ssZ'),
},
}
@ -389,7 +402,10 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean, skipChangeC
endDate = newStartDate.clone()
}
newRow.row[toCol!.title!] = dayjs(endDate).utc().format('YYYY-MM-DD HH:mm:ssZ')
newRow.row[toCol!.title!] =
calDataType.value === UITypes.Date
? dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
: dayjs(endDate).utc().format('YYYY-MM-DD HH:mm:ssZ')
updateProperty.push(toCol!.title!)
}
@ -544,6 +560,8 @@ const stopDrag = (event: MouseEvent) => {
updateRowProperty(newRow, updateProperty, false)
focusedDate.value = null
$e('c:calendar:month:drag-record')
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
}
@ -611,6 +629,7 @@ const dropEvent = (event: DragEvent) => {
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)
$e('c:calendar:day:drag-record')
}
}
@ -655,7 +674,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-200 font-semibold text-gray-500"
class="text-center bg-gray-50 py-1 border-b-1 border-r-1 last:border-r-0 border-gray-200 font-regular uppercase text-xs text-gray-500"
>
{{ day }}
</div>
@ -667,7 +686,8 @@ const addRecord = (date: dayjs.Dayjs) => {
'grid-rows-6': dates.length === 6,
'grid-rows-7': dates.length === 7,
}"
class="grid h-full pb-7.5"
class="grid"
style="height: calc(100% - 1.59rem)"
@drop="dropEvent"
>
<div v-for="(week, weekIndex) in dates" :key="weekIndex" class="grid grid-cols-7 grow" data-testid="nc-calendar-month-week">
@ -807,7 +827,7 @@ const addRecord = (date: dayjs.Dayjs) => {
@resize-start="onResizeStart"
@dblclick.stop="emit('expandRecord', record)"
>
<template #time>
<template v-if="calDataType === UITypes.DateTime" #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>

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

@ -13,7 +13,11 @@ const INFINITY_SCROLL_THRESHOLD = 100
const { isUIAllowed } = useRoles()
const { appInfo } = useGlobal()
const { $e } = useNuxtApp()
const { appInfo, isMobileMode } = useGlobal()
const { height } = useWindowSize()
const meta = inject(MetaInj, ref())
@ -36,6 +40,7 @@ const {
loadMoreSidebarData,
searchQuery,
sideBarFilterOption,
showSideMenu,
} = useCalendarViewStoreOrThrow()
const sideBarListRef = ref<VNodeRef | null>(null)
@ -269,82 +274,119 @@ const newRecord = () => {
emit('newRecord', { row, oldRow: {}, rowMeta: { new: true } })
}
const width = ref(0)
const toggleSideMenu = () => {
$e('c:calendar:toggle-sidebar', showSideMenu.value)
showSideMenu.value = !showSideMenu.value
}
const showSearch = ref(false)
const searchRef = ref()
const widthListener = () => {
width.value = window.innerWidth
const clickSearch = () => {
showSearch.value = true
nextTick(() => {
searchRef.value?.focus()
})
}
onMounted(() => {
window.addEventListener('resize', widthListener)
})
const toggleSearch = () => {
if (!searchQuery.value.length) {
showSearch.value = false
} else {
searchRef.value?.blur()
}
}
onUnmounted(() => {
window.removeEventListener('resize', widthListener)
useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
if (cmdOrCtrl) {
switch (e.key.toLowerCase()) {
case 'f':
e.preventDefault()
clickSearch()
break
}
}
})
onClickOutside(searchRef, toggleSearch)
</script>
<template>
<NcTooltip
:class="{
'right-2': !showSideMenu,
'right-74': showSideMenu,
}"
class="absolute transition-all ease-in-out top-2 z-30"
>
<template #title> {{ $t('activity.toggleSidebar') }}</template>
<NcButton v-if="!isMobileMode" data-testid="nc-calendar-side-bar-btn" size="small" type="secondary" @click="toggleSideMenu">
<component :is="iconMap.sidebar" class="h-4 w-4 text-gray-600 transition-all" />
</NcButton>
</NcTooltip>
<div
:class="{
'!w-0': !props.visible,
'min-w-[324px]': width > 1440 && props.visible,
'min-w-[288px]': width <= 1440 && props.visible,
'nc-calendar-side-menu-open': props.visible,
'!w-0 hidden': !props.visible,
'nc-calendar-side-menu-open block !min-w-[288px]': props.visible,
}"
class="h-full border-l-1 border-gray-200 transition-all"
class="h-full relative border-l-1 border-gray-200 transition-all"
data-testid="nc-calendar-side-menu"
>
<div
:class="{
'!hidden': width <= 1440,
'px-3 py-3 ': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const),
}"
class="flex flex-col"
>
<div class="flex flex-col">
<NcDateWeekSelector
v-if="activeCalendarView === ('day' as const)"
v-model:active-dates="activeDates"
v-model:page-date="pageDate"
v-model:selected-date="selectedDate"
size="medium"
:hide-calendar="height < 700"
/>
<NcDateWeekSelector
v-else-if="activeCalendarView === ('week' as const)"
v-model:active-dates="activeDates"
v-model:page-date="pageDate"
v-model:selected-week="selectedDateRange"
:hide-calendar="height < 700"
is-week-picker
size="medium"
/>
<NcMonthYearSelector
v-else-if="activeCalendarView === ('month' as const)"
v-model:page-date="pageDate"
v-model:selected-date="selectedMonth"
:hide-calendar="height < 700"
size="medium"
/>
<NcMonthYearSelector
v-else-if="activeCalendarView === ('year' as const)"
v-model:page-date="pageDate"
v-model:selected-date="selectedDate"
:hide-calendar="height < 700"
is-year-picker
size="medium"
/>
</div>
<div
:class="{
'!border-t-0': width <= 1440,
'!border-t-0 ': height < 700,
'pt-6': height >= 700,
}"
class="border-t-1 border-gray-200 relative flex flex-col gap-y-4 pt-3"
class="border-t-1 !pt-3 border-gray-200 relative flex flex-col gap-y-3"
>
<div class="flex px-4 items-center gap-3">
<span class="capitalize text-base font-bold">{{ $t('objects.records') }}</span>
<span class="capitalize font-medium text-gray-700">{{ $t('objects.records') }}</span>
<NcSelect v-model:value="sideBarFilterOption" class="w-full !text-gray-600" data-testid="nc-calendar-sidebar-filter">
<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">
<div class="flex items-center w-full justify-between gap-2">
<div class="truncate">
<NcTooltip :title="option.label" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
{{ option.label }}
</NcTooltip>
</div>
<component
:is="iconMap.check"
v-if="sideBarFilterOption === option.value"
@ -355,47 +397,87 @@ onUnmounted(() => {
</a-select-option>
</NcSelect>
</div>
<div class="flex px-4 items-center gap-2">
<div
:class="{
hidden: !showSearch,
}"
class="mx-4"
>
<a-input
ref="searchRef"
v-model:value="searchQuery.value"
:class="{
'!border-brand-500': searchQuery.value.length > 0,
'!hidden': !showSearch,
}"
class="!rounded-lg !h-8 !placeholder:text-gray-500 !border-gray-200 !px-4"
data-testid="nc-calendar-sidebar-search"
placeholder="Search records"
@keydown.esc="toggleSearch"
>
<template #prefix>
<component :is="iconMap.search" class="h-4 w-4 mr-1 text-gray-500" />
</template>
</a-input>
</div>
<div class="mx-4 gap-2 flex items-center">
<NcButton
v-if="!showSearch"
data-testid="nc-calendar-sidebar-search-btn"
size="small"
class="!h-7"
type="secondary"
@click="clickSearch"
>
<component :is="iconMap.search" />
</NcButton>
<LazySmartsheetToolbarSortListMenu />
<div class="flex-1" />
<div
v-if="calendarRange?.length && !isCalendarMetaLoading"
:ref="sideBarListRef"
: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="nc-scrollbar-md pl-4 pr-4 overflow-y-auto"
data-testid="nc-calendar-side-menu-list"
@scroll="sideBarListScrollHandle"
>
<NcButton
v-if="isUIAllowed('dataEdit') && props.visible"
v-e="['c:calendar:calendar-sidemenu-new-record-btn']"
class="!absolute right-5 !border-brand-500 bottom-5 !h-12 !w-12"
data-testid="nc-calendar-side-menu-new-btn"
class="!h-7"
size="small"
type="secondary"
@click="newRecord"
>
<div class="px-4 flex items-center gap-2 justify-center">
<component :is="iconMap.plus" class="h-6 w-6 text-lg text-brand-500" />
<div class="flex items-center gap-2">
<component :is="iconMap.plus" />
Record
</div>
</NcButton>
</div>
<div
v-if="calendarRange?.length && !isCalendarMetaLoading"
:ref="sideBarListRef"
:class="{
'!h-[calc(100svh-22.15rem)]':
height > 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && !showSearch,
'!h-[calc(100svh-24.9rem)]':
height > 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && showSearch,
'!h-[calc(100svh-13.85rem)]':
height <= 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && !showSearch,
'!h-[calc(100svh-16.61rem)]':
height <= 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && showSearch,
'!h-[calc(100svh-30.15rem)]':
height > 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && !showSearch,
' !h-[calc(100svh-32.9rem)]':
height > 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && showSearch,
'!h-[calc(100svh-13.8rem)]':
height <= 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && !showSearch,
'!h-[calc(100svh-16.6rem)]':
height <= 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && showSearch,
}"
class="nc-scrollbar-md pl-4 pr-4 overflow-y-auto"
data-testid="nc-calendar-side-menu-list"
@scroll="sideBarListScrollHandle"
>
<div v-if="renderData.length === 0 || isSidebarLoading" class="flex h-full items-center justify-center">
<GeneralLoader v-if="isSidebarLoading" size="large" />
@ -431,8 +513,8 @@ onUnmounted(() => {
"
color="blue"
data-testid="nc-sidebar-record-card"
@dragstart="dragStart($event, record)"
@click="emit('expandRecord', record)"
@dragstart="dragStart($event, record)"
@dragover.prevent
>
<template v-if="!isRowEmpty(record, displayField)">
@ -446,10 +528,23 @@ onUnmounted(() => {
<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,
}"
'!h-[calc(100svh-22.15rem)]':
height > 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && !showSearch,
'!h-[calc(100svh-24.9rem)]':
height > 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && showSearch,
'!h-[calc(100svh-13.85rem)]':
height <= 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && !showSearch,
'!h-[calc(100svh-16.61rem)]':
height <= 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && showSearch,
'!h-[calc(100svh-30.15rem)]':
height > 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && !showSearch,
' !h-[calc(100svh-32.9rem)]':
height > 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && showSearch,
'!h-[calc(100svh-13.8rem)]':
height <= 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && !showSearch,
'!h-[calc(100svh-16.6rem)]':
height <= 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && showSearch,
}"
class="flex items-center justify-center h-full"
>
<GeneralLoader size="xlarge" />
@ -458,10 +553,23 @@ onUnmounted(() => {
<div
v-else
: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,
}"
'!h-[calc(100svh-22.15rem)]':
height > 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && !showSearch,
'!h-[calc(100svh-24.9rem)]':
height > 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && showSearch,
'!h-[calc(100svh-13.85rem)]':
height <= 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && !showSearch,
'!h-[calc(100svh-16.61rem)]':
height <= 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && showSearch,
'!h-[calc(100svh-30.15rem)]':
height > 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && !showSearch,
' !h-[calc(100svh-32.9rem)]':
height > 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && showSearch,
'!h-[calc(100svh-13.8rem)]':
height <= 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && !showSearch,
'!h-[calc(100svh-16.6rem)]':
height <= 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && showSearch,
}"
class="flex items-center justify-center h-full"
>
{{ $t('activity.noRange') }}

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

@ -526,6 +526,7 @@ const dropEvent = (event: DragEvent) => {
dragElement.value = null
}
updateRowProperty(newRow, updateProperty, false)
$e('c:calendar:day:drag-record')
}
}
@ -557,7 +558,9 @@ const addRecord = (date: dayjs.Dayjs) => {
:class="{
'!border-brand-500 !border-b-gray-200': dayjs(date).isSame(selectedDate, 'day'),
}"
class="w-1/7 text-center text-sm text-gray-500 w-full py-1 border-gray-200 border-l-gray-50 border-t-gray-50 last:border-r-0 border-1 bg-gray-50"
class="w-1/7 cursor-pointer text-center font-regular uppercase text-xs text-gray-500 w-full py-1 border-gray-200 border-l-gray-50 border-t-gray-50 last:border-r-0 border-1 bg-gray-50"
@click="selectDate(date)"
@dblclick="addRecord(date)"
>
{{ dayjs(date).format('DD ddd') }}
</div>
@ -570,7 +573,7 @@ const addRecord = (date: dayjs.Dayjs) => {
'!border-1 !border-t-0 border-brand-500': dayjs(date).isSame(selectedDate, 'day'),
'!bg-gray-50': date.get('day') === 0 || date.get('day') === 6,
}"
class="flex flex-col border-r-1 min-h-[100vh] last:border-r-0 items-center w-1/7"
class="flex cursor-pointer flex-col border-r-1 min-h-[100vh] last:border-r-0 items-center w-1/7"
data-testid="nc-calendar-week-day"
@click="selectDate(date)"
@dblclick="addRecord(date)"

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

@ -17,6 +17,8 @@ const {
showSideMenu,
} = useCalendarViewStoreOrThrow()
const { $e } = useNuxtApp()
const container = ref<null | HTMLElement>(null)
const scrollContainer = ref<null | HTMLElement>(null)
@ -181,7 +183,6 @@ const getMaxOverlaps = ({
const dayIndex = row.rowMeta.dayIndex
const overlapIndex = columnArray[dayIndex].findIndex((column) => column.findIndex((r) => r.rowMeta.id === id) !== -1) + 1
const dfs = (id: string): number => {
visited.add(id)
let maxOverlaps = 1
@ -194,6 +195,7 @@ const getMaxOverlaps = ({
}
}
}
return maxOverlaps
}
@ -476,7 +478,11 @@ const recordsAcrossAllRange = computed<{
}
}
for (const record of recordsToDisplay) {
const { maxOverlaps, overlapIndex } = getMaxOverlaps({
const {
maxOverlaps,
overlapIndex,
dayIndex: tDayIndex,
} = getMaxOverlaps({
row: record,
columnArray,
graph: graph.get(record.rowMeta.dayIndex!) ?? new Map(),
@ -749,6 +755,7 @@ const stopDrag = (event: MouseEvent) => {
if (newRow) {
updateRowProperty(newRow, updatedProperty, false)
}
$e('c:calendar:week:drag-record')
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
@ -811,6 +818,7 @@ const dropEvent = (event: DragEvent) => {
if (newRow) {
updateRowProperty(newRow, updatedProperty, false)
$e('c:calendar:day:drag-record')
}
}
}
@ -865,7 +873,7 @@ watch(
<template>
<div
ref="scrollContainer"
class="h-[calc(100vh-9.9rem)] prevent-select relative flex w-full overflow-y-auto nc-scrollbar-md"
class="h-[calc(100vh-5.4rem)] prevent-select relative flex w-full overflow-y-auto nc-scrollbar-md"
data-testid="nc-calendar-week-view"
@drop="dropEvent"
>
@ -876,7 +884,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-200 last:border-r-0 border-b-1 border-l-1 border-r-0 bg-gray-50"
class="w-1/7 text-center font-regular uppercase text-xs 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>
@ -885,7 +893,7 @@ watch(
<div
v-for="(hour, index) in datesHours[0]"
:key="index"
class="h-13 first:mt-0 pt-7.1 nc-calendar-day-hour text-center font-semibold text-xs text-gray-400 py-1"
class="h-13 first:mt-0 pt-7.1 nc-calendar-day-hour text-right pr-2 font-semibold text-xs text-gray-400 py-1"
>
{{ hour.format('hh a') }}
</div>

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

@ -1,5 +1,7 @@
<script lang="ts" setup>
const { selectedDate, activeDates } = useCalendarViewStoreOrThrow()
import type dayjs from 'dayjs'
const { selectedDate, activeDates, activeCalendarView } = useCalendarViewStoreOrThrow()
const months = computed(() => {
const months = []
@ -25,6 +27,11 @@ const handleResize = () => {
}
}
const changeView = (date: dayjs.Dayjs) => {
selectedDate.value = date
activeCalendarView.value = 'day'
}
onMounted(() => {
handleResize()
})
@ -33,18 +40,25 @@ watch(width, handleResize)
</script>
<template>
<div ref="calendarContainer" class="overflow-auto flex my-3 justify-center nc-scrollbar-md">
<div class="grid grid-cols-4 justify-items-center gap-3" data-testid="nc-calendar-year-view">
<div ref="calendarContainer" class="overflow-auto flex my-2 justify-center nc-scrollbar-md">
<div
:class="{
'!gap-12': size === 'large',
}"
class="grid grid-cols-4 justify-items-center gap-6 scale-1"
data-testid="nc-calendar-year-view"
>
<NcDateWeekSelector
v-for="(_, index) in months"
:key="index"
v-model:active-dates="activeDates"
v-model:page-date="months[index]"
v-model:selected-date="selectedDate"
class="nc-year-view-calendar"
:size="size"
class="nc-year-view-calendar"
data-testid="nc-calendar-year-view-month-selector"
disable-pagination
@dbl-click="changeView"
/>
</div>
</div>
@ -53,11 +67,7 @@ watch(width, handleResize)
<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;
@apply border-gray-200;
}
}
</style>

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

@ -1,8 +1,9 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { UITypes } from 'nocodb-sdk'
import type { Row as RowType } from '#imports'
const { $e } = useNuxtApp()
const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
@ -32,18 +33,10 @@ const {
loadSidebarData,
isCalendarDataLoading,
isCalendarMetaLoading,
selectedDate,
selectedMonth,
activeDates,
pageDate,
fetchActiveDates,
showSideMenu,
selectedDateRange,
paginateCalendarView,
} = useCalendarViewStoreOrThrow()
const calendarRangeDropdown = ref(false)
const router = useRouter()
const route = useRoute()
@ -86,8 +79,7 @@ const expandRecord = (row: RowType, state?: Record<string, any>) => {
}
const newRecord = (row: RowType) => {
// TODO: The default values has to be filled based on the active calendar view
// and selected sidebar filter option
$e('c:calendar:new-record', activeCalendarView.value)
expandRecord({
row: {
...rowDefaultData(meta.value?.columns),
@ -119,153 +111,11 @@ reloadViewDataHook?.on(async (params: void | { shouldShowLoading?: boolean }) =>
fetchActiveDates(),
])
})
const goToToday = () => {
selectedDate.value = dayjs()
pageDate.value = dayjs()
selectedMonth.value = dayjs()
selectedDateRange.value = {
start: dayjs().startOf('week'),
end: dayjs().endOf('week'),
}
document?.querySelector('.nc-calendar-today')?.scrollIntoView({
behavior: 'smooth',
block: 'center',
})
}
const headerText = computed(() => {
switch (activeCalendarView.value) {
case 'day':
return dayjs(selectedDate.value).format('D MMMM YYYY')
case 'week':
if (selectedDateRange.value.start.isSame(selectedDateRange.value.end, 'month')) {
return `${selectedDateRange.value.start.format('D')} - ${selectedDateRange.value.end.format('D MMM YY')}`
} else if (selectedDateRange.value.start.isSame(selectedDateRange.value.end, 'year')) {
return `${selectedDateRange.value.start.format('D MMM')} - ${selectedDateRange.value.end.format('D MMM YY')}`
} else {
return `${selectedDateRange.value.start.format('D MMM YY')} - ${selectedDateRange.value.end.format('D MMM YY')}`
}
case 'month':
return dayjs(selectedMonth.value).format('MMMM YYYY')
case 'year':
return dayjs(selectedDate.value).format('YYYY')
}
})
</script>
<template>
<div class="flex h-full flex-row" data-testid="nc-calendar-wrapper">
<div class="flex h-full relative flex-row" data-testid="nc-calendar-wrapper">
<div class="flex flex-col w-full">
<div class="flex justify-between p-2 items-center border-b-1 border-gray-200" data-testid="nc-calendar-topbar">
<div class="flex justify-start gap-3 items-center">
<NcTooltip>
<template #title> {{ $t('labels.previous') }}</template>
<NcButton
v-e="`['c:calendar:calendar-${activeCalendarView}-prev-btn']`"
data-testid="nc-calendar-prev-btn"
size="small"
type="secondary"
@click="paginateCalendarView('prev')"
>
<component :is="iconMap.doubleLeftArrow" class="h-4 w-4" />
</NcButton>
</NcTooltip>
<NcDropdown v-model:visible="calendarRangeDropdown" :auto-close="false" :trigger="['click']">
<NcButton :class="{ '!w-22': activeCalendarView === 'year' }" class="w-45" full-width size="small" type="secondary">
<div class="flex px-2 w-full items-center justify-between">
<div class="flex gap-1 text-brand-500" data-testid="nc-calendar-active-date">
<span class="font-bold text-center">{{
activeCalendarView === 'month' ? headerText.split(' ')[0] : headerText
}}</span>
<span v-if="activeCalendarView === 'month'">
{{ ` ${headerText.split(' ')[1]}` }}
</span>
</div>
<component :is="iconMap.arrowDown" class="h-4 w-4 text-gray-700" />
</div>
</NcButton>
<template #overlay>
<div
v-if="calendarRangeDropdown"
:class="{
'px-4 pt-3 pb-4 ': activeCalendarView === 'week' || activeCalendarView === 'day',
}"
class="min-w-[22.1rem]"
@click.stop
>
<NcDateWeekSelector
v-if="activeCalendarView === ('day' as const)"
v-model:active-dates="activeDates"
v-model:page-date="pageDate"
v-model:selected-date="selectedDate"
/>
<NcDateWeekSelector
v-else-if="activeCalendarView === ('week' as const)"
v-model:active-dates="activeDates"
v-model:page-date="pageDate"
v-model:selected-week="selectedDateRange"
is-week-picker
/>
<NcMonthYearSelector
v-else-if="activeCalendarView === ('month' as const)"
v-model:page-date="pageDate"
v-model:selected-date="selectedMonth"
/>
<NcMonthYearSelector
v-else-if="activeCalendarView === ('year' as const)"
v-model:page-date="pageDate"
v-model:selected-date="selectedDate"
is-year-picker
/>
</div>
</template>
</NcDropdown>
<NcTooltip>
<template #title> {{ $t('labels.next') }}</template>
<NcButton
v-e="`['c:calendar:calendar-${activeCalendarView}-next-btn']`"
data-testid="nc-calendar-next-btn"
size="small"
type="secondary"
@click="paginateCalendarView('next')"
>
<component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
</NcButton>
</NcTooltip>
<NcButton
v-e="`['c:calendar:calendar-${activeCalendarView}-today-btn']`"
data-testid="nc-calendar-today-btn"
size="small"
type="secondary"
@click="goToToday"
>
<span class="text-gray-600 !text-sm">
{{ $t('activity.goToToday') }}
</span>
</NcButton>
<span class="opacity-0" data-testid="nc-active-calendar-view">
{{ activeCalendarView }}
</span>
</div>
<NcTooltip>
<template #title> {{ $t('activity.toggleSidebar') }}</template>
<NcButton
v-if="!isMobileMode"
v-e="`['c:calendar:calendar-${activeCalendarView}-toggle-sidebar']`"
data-testid="nc-calendar-side-bar-btn"
size="small"
type="secondary"
@click="showSideMenu = !showSideMenu"
>
<component :is="iconMap.sidebar" class="h-4 w-4 text-gray-600 transition-all" />
</NcButton>
</NcTooltip>
</div>
<template v-if="calendarRange?.length && !isCalendarMetaLoading">
<LazySmartsheetCalendarYearView v-if="activeCalendarView === 'year'" />
<template v-if="!isCalendarDataLoading">

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

@ -0,0 +1,11 @@
<script lang="ts" setup>
const { activeCalendarView } = useCalendarViewStoreOrThrow()
</script>
<template>
<span class="opacity-0" data-testid="nc-active-calendar-view">
{{ activeCalendarView }}
</span>
</template>
<style lang="scss" scoped></style>

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

@ -0,0 +1,126 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { computed } from '#imports'
const { selectedDate, selectedMonth, selectedDateRange, activeCalendarView, paginateCalendarView, activeDates, pageDate } =
useCalendarViewStoreOrThrow()
const calendarRangeDropdown = ref(false)
const headerText = computed(() => {
switch (activeCalendarView.value) {
case 'day':
return dayjs(selectedDate.value).format('D MMM YYYY')
case 'week':
if (selectedDateRange.value.start.isSame(selectedDateRange.value.end, 'month')) {
return `${selectedDateRange.value.start.format('D')} - ${selectedDateRange.value.end.format('D MMM YY')}`
} else if (selectedDateRange.value.start.isSame(selectedDateRange.value.end, 'year')) {
return `${selectedDateRange.value.start.format('D MMM')} - ${selectedDateRange.value.end.format('D MMM YY')}`
} else {
return `${selectedDateRange.value.start.format('D MMM YY')} - ${selectedDateRange.value.end.format('D MMM YY')}`
}
case 'month':
return dayjs(selectedMonth.value).format('MMM YYYY')
case 'year':
return dayjs(selectedDate.value).format('YYYY')
default:
return ''
}
})
</script>
<template>
<div class="flex gap-1">
<NcTooltip>
<template #title> {{ $t('labels.previous') }}</template>
<a-button
v-e="`['c:calendar:calendar-${activeCalendarView}-prev-btn']`"
class="w-6 h-6 !rounded-lg flex items-center justify-center !bg-gray-100 !border-0"
data-testid="nc-calendar-prev-btn"
size="small"
@click="paginateCalendarView('prev')"
>
<component :is="iconMap.arrowLeft" class="h-4 !mb-0.9 !-ml-0.8 w-4" />
</a-button>
</NcTooltip>
<NcDropdown v-model:visible="calendarRangeDropdown" :auto-close="false" :trigger="['click']">
<NcButton
:class="{
'w-20': activeCalendarView === 'year',
'w-26.5': activeCalendarView === 'month',
'w-29': activeCalendarView === 'day',
'w-38': activeCalendarView === 'week',
}"
class="!h-6 !bg-gray-100 !border-0"
full-width
size="small"
type="secondary"
>
<div class="flex w-full px-1 items-center justify-between">
<span
:class="{
'max-w-38 truncate': activeCalendarView === 'week',
}"
class="font-bold text-[13px] text-center text-gray-800"
data-testid="nc-calendar-active-date"
>{{ headerText }}</span
>
<div class="flex-1" />
<component :is="iconMap.arrowDown" class="h-4 min-w-4 text-gray-700" />
</div>
</NcButton>
<template #overlay>
<div v-if="calendarRangeDropdown" class="w-[287px]" @click.stop>
<NcDateWeekSelector
v-if="activeCalendarView === ('day' as const)"
v-model:active-dates="activeDates"
v-model:page-date="pageDate"
v-model:selected-date="selectedDate"
size="medium"
/>
<NcDateWeekSelector
v-else-if="activeCalendarView === ('week' as const)"
v-model:active-dates="activeDates"
v-model:page-date="pageDate"
v-model:selected-week="selectedDateRange"
is-week-picker
size="medium"
/>
<NcMonthYearSelector
v-else-if="activeCalendarView === ('month' as const)"
v-model:page-date="pageDate"
v-model:selected-date="selectedMonth"
size="medium"
/>
<NcMonthYearSelector
v-else-if="activeCalendarView === ('year' as const)"
v-model:page-date="pageDate"
v-model:selected-date="selectedDate"
is-year-picker
size="medium"
/>
</div>
</template>
</NcDropdown>
<NcTooltip>
<template #title> {{ $t('labels.next') }}</template>
<a-button
v-e="`['c:calendar:calendar-${activeCalendarView}-next-btn']`"
class="w-6 h-6 !rounded-lg flex items-center !bg-gray-100 !border-0 justify-center"
data-testid="nc-calendar-next-btn"
size="small"
@click="paginateCalendarView('next')"
>
<component :is="iconMap.arrowRight" class="h-4 !mb-0.8 !-ml-0.5 w-4" />
</a-button>
</NcTooltip>
</div>
</template>
<style lang="scss" scoped>
.nc-cal-toolbar-header {
@apply !h-6 !w-6;
}
</style>

69
packages/nc-gui/components/smartsheet/toolbar/CalendarMode.vue → packages/nc-gui/components/smartsheet/toolbar/Calendar/Mode.vue

@ -31,67 +31,78 @@ watch(activeCalendarView, () => {
<template>
<div
v-if="props.tab"
class="flex flex-row relative px-1 mx-3 mt-3 rounded-lg gap-x-0.5 nc-calendar-mode-tab"
class="flex flex-row px-1 pointer-events-auto 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>
<div
v-for="mode in ['day', 'week', 'month', 'year']"
:key="mode"
v-e="`['c:calendar:change-calendar-view-${mode}']`"
:class="{ active: activeCalendarView === mode }"
:data-testid="`nc-calendar-view-mode-${mode}`"
class="tab"
@click="setActiveCalendarMode(mode, $event)"
>
<div class="tab-title nc-tab">{{ $t(`objects.${mode}`) }}</div>
<div class="tab-title !text-xs nc-tab">{{ $t(`objects.${mode}`) }}</div>
</div>
</div>
<div v-else>
<NcDropdown :trigger="['click']">
<NcButton size="small" type="secondary">
{{ $t(`objects.${activeCalendarView}`) }}
<component :is="iconMap.arrowDown" />
</NcButton>
<template #overlay>
<NcMenu>
<NcMenuItem
v-for="mode in ['day', 'week', 'month', 'year']"
:key="mode"
v-e="`['c:calendar:change-calendar-view-${mode}']`"
@click="changeCalendarView(mode)"
>
{{ $t(`objects.${mode}`) }}
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</div>
<NcSelect v-else v-model:value="activeCalendarView" class="!w-22" data-testid="nc-calendar-view-mode" size="small">
<a-select-option v-for="option in ['day', 'week', 'month', 'year']" :key="option" :value="option" class="!h-7 !w-20">
<div class="flex gap-2 mt-0.5 items-center">
<NcTooltip class="truncate !capitalize flex-1 max-w-18" placement="top" show-on-truncate-only>
<template #title>
<span class="capitalize">
{{ option }}
</span>
</template>
{{ option }}
</NcTooltip>
<component
:is="iconMap.check"
v-if="option === activeCalendarView"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
</template>
<style lang="scss" scoped>
.highlight {
@apply absolute h-9 w-20 transition-all border-b-2 border-brand-500 duration-200;
@apply absolute h-6.5 w-14 transition-all border-b-2 border-brand-500 duration-200;
z-index: 0;
}
.nc-calendar-mode-menu {
:deep(.nc-menu-item-inner) {
@apply !text-[13px];
}
}
.tab {
@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;
@apply flex items-center h-7 w-14 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 mb-3 pointer-events-none;
word-break: 'keep-all';
@apply min-w-0 mb-3 pointer-events-none;
word-break: keep-all;
white-space: 'nowrap';
display: 'inline';
line-height: 0.95;
}
.active {
@apply !text-brand-500 font-medium bg-transparent;
@apply !text-brand-500 !font-bold bg-transparent;
}
.nc-calendar-mode-tab {
@apply mr-120 relative;
@apply relative;
}
:deep(.ant-select-selector) {
@apply !h-7;
}
</style>

64
packages/nc-gui/components/smartsheet/toolbar/CalendarRange.vue → packages/nc-gui/components/smartsheet/toolbar/Calendar/Range.vue

@ -1,7 +1,6 @@
<script lang="ts" setup>
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { type CalendarRangeType, UITypes, isSystemColumn } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
import { type CalendarRangeType } from '~/lib/types'
const meta = inject(MetaInj, ref())
@ -101,22 +100,42 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
<template>
<NcDropdown v-if="!IsPublic" v-model:visible="calendarRangeDropdown" :trigger="['click']" class="!xs:hidden">
<div class="nc-calendar-btn">
<a-button
<NcButton
v-e="['c:calendar:change-calendar-range']"
:disabled="isLocked"
class="nc-toolbar-btn"
class="nc-toolbar-btn !border-0 group !h-6"
size="small"
type="secondary"
data-testid="nc-calendar-range-btn"
>
<div class="flex items-center gap-2">
<component :is="iconMap.calendar" class="h-4 w-4" />
<span class="text-capitalize !text-sm font-medium">
{{ $t('activity.viewSettings') }}
<component :is="iconMap.calendar" class="h-4 w-4 transition-all group-hover:text-brand-500" />
<span class="text-capitalize !group-hover:text-brand-500 !text-[13px] font-medium">
{{ $t('activity.settings') }}
</span>
</div>
</a-button>
</NcButton>
</div>
<template #overlay>
<div v-if="calendarRangeDropdown" class="w-full p-6" data-testid="nc-calendar-range-menu" @click.stop>
<div v-if="calendarRangeDropdown" class="w-98 space-y-6 rounded-2xl p-6" data-testid="nc-calendar-range-menu" @click.stop>
<div>
<div class="flex justify-between">
<div class="flex items-center gap-3">
<component :is="iconMap.calendar" class="text-maroon-500 w-5 h-5" />
<span class="font-bold"> {{ `${$t('activity.calendar')} ${$t('activity.viewSettings')}` }}</span>
</div>
<a
class="text-sm !text-gray-600 !font-default !hover:text-gray-600"
href="`https://docs.nocodb.com/views/view-types/calendar`"
target="_blank"
>
Go to Docs
</a>
</div>
<NcDivider divider-class="!border-gray-200" />
</div>
<div
v-for="(range, id) in _calendar_ranges"
:key="id"
@ -139,14 +158,24 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
return firstRange?.uidt === r.uidt
})"
:key="opId"
class="w-40"
:value="option.value"
>
<div class="flex items-center">
<SmartsheetHeaderIcon :column="option" />
<NcTooltip class="truncate flex-1 max-w-18" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
{{ option.label }}
</NcTooltip>
<div class="flex w-full gap-2 justify-between items-center">
<div class="flex items-center">
<SmartsheetHeaderIcon :column="option" />
<NcTooltip class="truncate flex-1 max-w-18" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
{{ option.label }}
</NcTooltip>
</div>
<component
:is="iconMap.check"
v-if="option.value === range.fk_from_column_id"
id="nc-selected-item-icon"
class="text-primary min-w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
@ -201,6 +230,11 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</NcButton>
-->
</div>
<!--
<div class="text-[13px] text-gray-500 py-2">Records in this view will be based on the specified date field.</div>
-->
<NcButton
v-if="_calendar_ranges.length === 0"
class="mt-2"

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

@ -0,0 +1,39 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
const { selectedDate, selectedMonth, selectedDateRange, pageDate, activeCalendarView } = useCalendarViewStoreOrThrow()
const { $e } = useNuxtApp()
const goToToday = () => {
$e('c:calendar:calendar-today-btn', activeCalendarView.value)
selectedDate.value = dayjs()
pageDate.value = dayjs()
selectedMonth.value = dayjs()
selectedDateRange.value = {
start: dayjs().startOf('week'),
end: dayjs().endOf('week'),
}
document?.querySelector('.nc-calendar-today')?.scrollIntoView({
behavior: 'smooth',
block: 'center',
})
}
</script>
<template>
<NcButton
class="!border-0 !h-6 !bg-gray-100"
data-testid="nc-calendar-today-btn"
size="small"
type="secondary"
@click="goToToday"
>
<span class="text-gray-700 !text-[13px]">
{{ $t('labels.today') }}
</span>
</NcButton>
</template>
<style lang="scss" scoped></style>

13
packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue

@ -61,17 +61,16 @@ eventBus.on(async (event, column: ColumnType) => {
overlay-class-name="nc-dropdown-filter-menu nc-toolbar-dropdown"
class="!xs:hidden"
>
<div :class="{ 'nc-active-btn': filtersLength }">
<a-button v-e="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn txt-sm" :disabled="isLocked">
<NcButton v-e="['c:filter']" :disabled="isLocked" class="nc-filter-menu-btn nc-toolbar-btn !border-0 !h-7" size="small" type="secondary">
<div class="flex items-center gap-1">
<div class="flex items-center gap-2">
<component :is="iconMap.filter" class="h-4 w-4" />
<!-- Filter -->
<span v-if="!isMobileMode" class="text-capitalize !text-sm font-medium">{{ $t('activity.filter') }}</span>
<span v-if="filtersLength" class="bg-brand-50 text-brand-500 py-1 px-2 text-md rounded-md">{{ filtersLength }}</span>
<span v-if="!isMobileMode" class="text-capitalize !text-[13px] font-medium">{{ $t('activity.filter') }}</span>
</div>
</a-button>
</div>
<span v-if="filtersLength" class="bg-brand-50 text-brand-500 py-1 px-2 text-md rounded-md">{{ filtersLength }}</span>
</div>
</NcButton>
<template #overlay>
<SmartsheetToolbarColumnFilter

40
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -311,29 +311,31 @@ useMenuCloseOnEsc(open)
overlay-class-name="nc-dropdown-fields-menu nc-toolbar-dropdown"
>
<div :class="{ 'nc-active-btn': numberOfHiddenFields }">
<a-button v-e="['c:fields']" :disabled="isLocked" class="nc-fields-menu-btn nc-toolbar-btn">
<div class="flex items-center gap-2">
<GeneralIcon
v-if="activeView?.type === ViewTypes.KANBAN || activeView?.type === ViewTypes.GALLERY"
class="h-4 w-4"
icon="creditCard"
/>
<component :is="iconMap.fields" v-else class="h-4 w-4" />
<!-- Fields -->
<span v-if="!isMobileMode" class="text-capitalize text-sm font-medium">
<template v-if="activeView?.type === ViewTypes.KANBAN || activeView?.type === ViewTypes.GALLERY">
{{ $t('title.editCards') }}
</template>
<template v-else>
{{ $t('objects.fields') }}
</template>
</span>
<NcButton v-e="['c:fields']" :disabled="isLocked" class="nc-fields-menu-btn nc-toolbar-btn !h-7 !border-0" size="small" type="secondary">
<div class="flex items-center gap-1">
<div class="flex items-center gap-2">
<GeneralIcon
v-if="activeView?.type === ViewTypes.KANBAN || activeView?.type === ViewTypes.GALLERY"
class="h-4 w-4"
icon="creditCard"
/>
<component :is="iconMap.fields" v-else class="h-4 w-4" />
<!-- Fields -->
<span v-if="!isMobileMode" class="text-capitalize !text-[13px] font-medium">
<template v-if="activeView?.type === ViewTypes.KANBAN || activeView?.type === ViewTypes.GALLERY">
{{ $t('title.editCards') }}
</template>
<template v-else>
{{ $t('objects.fields') }}
</template>
</span>
</div>
<span v-if="numberOfHiddenFields" class="bg-brand-50 text-brand-500 py-1 px-2 text-md rounded-md">
{{ numberOfHiddenFields }}
</span>
</div>
</a-button>
</NcButton>
</div>
<template #overlay>

21
packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue

@ -164,18 +164,25 @@ eventBus.on(async (event, column) => {
overlay-class-name="nc-dropdown-group-by-menu nc-toolbar-dropdown overflow-hidden"
>
<div :class="{ 'nc-active-btn': groupedByColumnIds?.length }">
<a-button v-e="['c:group-by']" class="nc-group-by-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-2">
<component :is="iconMap.group" class="h-4 w-4" />
<!-- Group By -->
<span v-if="!isMobileMode" class="text-capitalize !text-sm font-medium">{{ $t('activity.group') }}</span>
<NcButton
v-e="['c:group-by']"
:disabled="isLocked"
class="nc-group-by-menu-btn nc-toolbar-btn !border-0 !h-7"
size="small"
type="secondary"
>
<div class="flex items-center gap-1">
<div class="flex items-center gap-2">
<component :is="iconMap.group" class="h-4 w-4" />
<!-- Group By -->
<span v-if="!isMobileMode" class="text-capitalize !text-[13px] font-medium">{{ $t('activity.group') }}</span>
</div>
<span v-if="groupedByColumnIds?.length" class="bg-brand-50 text-brand-500 py-1 px-2 text-md rounded-md">{{
groupedByColumnIds.length
}}</span>
</div>
</a-button>
</NcButton>
</div>
<template #overlay>
<SmartsheetToolbarCreateGroupBy

12
packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import { OrgUserRoles, ProjectRoles, extractRolesObj } from 'nocodb-sdk'
import type { GridType } from 'nocodb-sdk'
import { OrgUserRoles, ProjectRoles, extractRolesObj } from 'nocodb-sdk'
const rowHeightOptions: { icon: keyof typeof iconMap; heightClass: string }[] = [
{
@ -84,12 +84,18 @@ useMenuCloseOnEsc(open)
<template>
<a-dropdown v-model:visible="open" offset-y class="" :trigger="['click']" overlay-class-name="nc-dropdown-height-menu">
<div>
<a-button v-e="['c:row-height']" class="nc-height-menu-btn nc-toolbar-btn" :disabled="isLocked">
<NcButton
v-e="['c:row-height']"
:disabled="isLocked"
class="nc-height-menu-btn nc-toolbar-btn !border-0 !h-7"
size="small"
type="secondary"
>
<div class="flex items-center gap-0.5">
<component :is="iconMap.rowHeight" class="!h-3.75 !w-3.75" />
<!-- <span v-if="!isMobileMode" class="!text-sm !font-medium">{{ $t('objects.rowHeight') }}</span> -->
</div>
</a-button>
</NcButton>
</div>
<template #overlay>
<div

4
packages/nc-gui/components/smartsheet/toolbar/SearchData.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
const reloadData = inject(ReloadViewDataHookInj)!
@ -95,7 +95,7 @@ onClickOutside(globalSearchWrapperRef, (e) => {
</a-button>
<div
v-else
class="flex flex-row border-1 rounded-lg h-8 xs:(h-10 ml-0) ml-1 border-gray-200 overflow-hidden focus-within:border-primary"
class="flex flex-row border-1 rounded-lg h-7 xs:(h-10 ml-0) ml-1 border-gray-200 overflow-hidden focus-within:border-primary"
:class="{ 'border-primary': search.query.length !== 0 }"
>
<NcDropdown

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import { PlanLimitTypes, RelationTypes, UITypes, getEquivalentUIType, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { PlanLimitTypes, RelationTypes, UITypes, getEquivalentUIType, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
@ -20,6 +20,8 @@ const { isMobileMode } = useGlobal()
const { getPlanLimit } = useWorkspace()
const isCalendar = inject(IsCalendarInj, ref(false))
eventBus.on((event) => {
if (event === SmartsheetStoreEvents.SORT_RELOAD) {
loadSorts()
@ -110,16 +112,27 @@ onMounted(() => {
overlay-class-name="nc-dropdown-sort-menu nc-toolbar-dropdown"
>
<div :class="{ 'nc-active-btn': sorts?.length }">
<a-button v-e="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-2">
<component :is="iconMap.sort" class="h-4 w-4 text-inherit" />
<!-- Sort -->
<span v-if="!isMobileMode" class="text-capitalize !text-sm font-medium">{{ $t('activity.sort') }}</span>
<NcButton
v-e="['c:sort']"
:class="{
'!border-1 !rounded-lg !h-7': isCalendar,
'!border-0 ': !isCalendar,
}"
:disabled="isLocked"
class="nc-sort-menu-btn nc-toolbar-btn"
size="small"
type="secondary"
>
<div class="flex items-center gap-1">
<div class="flex items-center gap-2">
<component :is="iconMap.sort" class="h-4 w-4 text-inherit" />
<!-- Sort -->
<span v-if="!isMobileMode" class="text-capitalize !text-[13px] font-medium">{{ $t('activity.sort') }}</span>
</div>
<span v-if="sorts?.length" class="bg-brand-50 text-brand-500 py-1 px-2 text-md rounded-md">{{ sorts.length }}</span>
</div>
</a-button>
</NcButton>
</div>
<template #overlay>
<SmartsheetToolbarCreateSort v-if="!sorts.length" :is-parent-open="open" @created="addSort" />

10
packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import { UITypes } from 'nocodb-sdk'
import type { KanbanType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
provide(IsKanbanInj, ref(true))
@ -96,9 +96,11 @@ const handleChange = () => {
class="!xs:hidden"
>
<div class="nc-kanban-btn">
<a-button
<NcButton
v-e="['c:kanban:change-grouping-field']"
class="nc-kanban-stacked-by-menu-btn nc-toolbar-btn"
class="nc-kanban-stacked-by-menu-btn nc-toolbar-btn !border-0 !h-7"
size="small"
type="secondary"
:disabled="isLocked"
>
<div class="flex items-center gap-1">
@ -108,7 +110,7 @@ const handleChange = () => {
<span class="font-bold ml-0.25">{{ groupingField }}</span>
</span>
</div>
</a-button>
</NcButton>
</div>
<template #overlay>
<div v-if="open" class="p-6 w-90 bg-white shadow-lg nc-table-toolbar-menu !border-1 border-gray-50 rounded-2xl" @click.stop>

4
packages/nc-gui/components/smartsheet/topbar/SelectMode.vue

@ -24,7 +24,7 @@ const onClickDetails = () => {
}"
@click="onViewsTabChange('view')"
>
<GeneralViewIcon v-if="activeView?.type" class="tab-icon" :meta="{ type: activeView?.type }" ignore-color />
<GeneralViewIcon v-if="activeView?.type" :meta="{ type: activeView?.type }" class="tab-icon" ignore-color />
<GeneralLoader v-else class="tab-icon" />
<div class="tab-title nc-tab">{{ $t('general.data') }}</div>
</div>
@ -56,7 +56,7 @@ const onClickDetails = () => {
.tab-icon {
font-size: 1.1rem !important;
@apply min-w-4.5;
@apply w-4.5;
}
.tab .tab-title {
@apply min-w-0;

2
packages/nc-gui/components/tabs/Smartsheet.vue

@ -162,7 +162,7 @@ const onResize = (sizes: { min: number; max: number; size: number }[]) => {
<Splitpanes v-if="openedViewsTab === 'view'" class="nc-extensions-content-resizable-wrapper" @resized="onResize">
<Pane class="flex flex-col h-full flex-1 min-w-0" size="60">
<LazySmartsheetToolbar v-if="!isForm" />
<div class="flex flex-row w-full" :style="{ height: isForm ? '100%' : 'calc(100% - var(--topbar-height))' }">
<div :style="{ height: isForm ? '100%' : 'calc(100% - var(--toolbar-height))' }" class="flex flex-row w-full">
<Transition name="layout" mode="out-in">
<div v-if="openedViewsTab === 'view'" class="flex flex-1 min-h-0 w-3/4">
<div class="h-full flex-1 min-w-0 min-h-0 bg-white">

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

@ -15,7 +15,7 @@ const formatData = (list: Record<string, any>[]) =>
const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
(
meta: Ref<((CalendarType & { id: string }) | TableType) | undefined>,
meta: Ref<TableType | undefined>,
viewMeta:
| Ref<(ViewType | CalendarType | undefined) & { id: string }>
| ComputedRef<
@ -54,7 +54,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const isCalendarMetaLoading = ref<boolean>(false)
const showSideMenu = ref(false)
const showSideMenu = ref(true)
const selectedDateRange = ref<{
start: dayjs.Dayjs
@ -80,7 +80,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const { base } = storeToRefs(useBase())
const { $api } = useNuxtApp()
const { $api, $e } = useNuxtApp()
const { t } = useI18n()
@ -371,7 +371,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
sortsArr: sorts.value,
filtersArr: activeDateFilter,
})
activeDates.value = res.dates.map((dateObj: unknown) => dayjs(dateObj))
activeDates.value = res.dates.map((dateObj: unknown) => dayjs(dateObj as string))
if (res.count > 3000 && activeCalendarView.value !== 'year') {
message.warning(
@ -380,12 +380,20 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
}
} catch (e) {
activeDates.value = []
message.error(`${t('msg.error.fetchingActiveDates')} ${await extractSdkResponseErrorMsg(e)}`)
message.error(
`${t('msg.error.fetchingActiveDates')} ${await extractSdkResponseErrorMsg(
e as Error & {
response: { data: { message: string } }
},
)}`,
)
console.log(e)
}
}
const changeCalendarView = async (view: 'month' | 'year' | 'day' | 'week') => {
$e('c:calendar:change-calendar-view', view)
try {
activeCalendarView.value = view
await updateCalendarMeta({
@ -414,15 +422,27 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const calMeta = typeof res.meta === 'string' ? JSON.parse(res.meta) : res.meta
activeCalendarView.value = calMeta?.active_view
if (!activeCalendarView.value) activeCalendarView.value = 'month'
calendarRange.value = res?.calendar_range?.map((range: CalendarRangeType) => {
return {
id: range.id,
fk_from_col: meta.value?.columns?.find((col) => col.id === range.fk_from_column_id),
fk_to_col: range.fk_to_column_id ? meta.value?.columns?.find((col) => col.id === range.fk_to_column_id) : null,
}
}) as any
} catch (e) {
message.error(`Error loading calendar meta ${await extractSdkResponseErrorMsg(e)}`)
calendarRange.value = res?.calendar_range?.map(
(
range: CalendarRangeType & {
id?: string
},
) => {
return {
id: range?.id,
fk_from_col: meta.value?.columns?.find((col) => col.id === range.fk_from_column_id),
fk_to_col: range.fk_to_column_id ? meta.value?.columns?.find((col) => col.id === range.fk_to_column_id) : null,
}
},
) as any
} catch (e: unknown) {
message.error(
`Error loading calendar meta ${await extractSdkResponseErrorMsg(
e as Error & {
response: { data: { message: string } }
},
)}`,
)
} finally {
isCalendarMetaLoading.value = false
}
@ -459,12 +479,6 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
nextDate = toDate.add(1, 'day').startOf('day')
break
}
case 'year':
fromDate = selectedDate.value.startOf('year')
toDate = selectedDate.value.endOf('year')
prevDate = fromDate.subtract(1, 'day').endOf('day')
nextDate = toDate.add(1, 'day').startOf('day')
break
case 'day':
fromDate = selectedDate.value.startOf('day')
toDate = selectedDate.value.endOf('day')
@ -504,7 +518,13 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
})
formattedData.value = formatData(res!.list)
} catch (e) {
message.error(`${t('msg.error.fetchingCalendarData')} ${await extractSdkResponseErrorMsg(e)}`)
message.error(
`${t('msg.error.fetchingCalendarData')} ${await extractSdkResponseErrorMsg(
e as Error & {
response: { data: { message: string } }
},
)}`,
)
console.log(e)
} finally {
isCalendarDataLoading.value = false
@ -604,7 +624,13 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
formattedSideBarData.value = formatData(res!.list)
} catch (e) {
message.error(`${t('msg.error.fetchingCalendarData')} ${await extractSdkResponseErrorMsg(e)}`)
message.error(
`${t('msg.error.fetchingCalendarData')} ${await extractSdkResponseErrorMsg(
e as Error & {
response: { data: { message: string } }
},
)}`,
)
console.log(e)
} finally {
isSidebarLoading.value = false
@ -635,9 +661,6 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
viewMeta?.value?.id as string,
id,
updateObj,
{
query: { ignoreWebhook: !undo },
},
// todo:
// {
// query: { ignoreWebhook: !saved }
@ -769,6 +792,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
})
watch(sideBarFilterOption, async () => {
$e('a:calendar:sidebar-filter', sideBarFilterOption.value)
await loadSidebarData()
})
@ -801,7 +825,6 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
isSidebarLoading,
showSideMenu,
selectedTime,
updateCalendarMeta,
calendarMetaData,
updateRowProperty,
activeCalendarView,

1
packages/nc-gui/composables/useViewColumns.ts

@ -291,6 +291,7 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
const fieldIndex = fields.value?.findIndex((f) => f.fk_column_id === field.fk_column_id)
if (!fieldIndex && fieldIndex !== 0) return
field[style] = status
$e('a:fields:style', { style, status })
saveOrUpdate(field, fieldIndex, true)
}

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

@ -448,6 +448,7 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results"
},
"labels": {
"today": "Today",
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
@ -585,7 +586,7 @@
"untitledToken": "Untitled token",
"tableName": "Table name",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create view",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",

3
packages/nc-gui/utils/iconUtils.ts

@ -74,6 +74,7 @@ import Record from '~icons/nc-icons/record'
import Project from '~icons/nc-icons/project'
import LookupIcon from '~icons/nc-icons/lookup'
import FileImageIcon from '~icons/nc-icons/file-image'
import Calendar from '~icons/lucide/calendar'
import PhUsers from '~icons/ph/users'
import PhUser from '~icons/ph/user'
@ -389,6 +390,7 @@ export const iconMap = {
workspaceDefault: MsGroup,
project: Project,
search: NcSearch,
calendar: Calendar,
error: h('span', { class: 'material-symbols' }, 'error'),
info: h(MsInfo, {}, () => 'info'),
inbox: h('span', { class: 'material-symbols' }, 'inbox'),
@ -493,7 +495,6 @@ export const iconMap = {
web: h('span', { class: 'material-symbols' }, 'web'),
webhook: h('span', { class: 'material-symbols' }, 'webhook'),
boolean: h('span', { class: 'material-symbols' }, 'check_box'),
calendar: h('span', { class: 'material-symbols' }, 'event_note'),
singleSelect: h('span', { class: 'material-symbols' }, 'radio_button_checked'),
multiSelect: h('span', { class: 'material-symbols' }, 'check_box_outline_blank'),
datetime: h('span', { class: 'material-symbols' }, 'date_range'),

1
packages/nc-gui/windi.config.ts

@ -80,6 +80,7 @@ export default defineConfig({
extraLight: 250,
light: 350,
normal: 450,
default: 500,
medium: 550,
bold: 650,
black: 750,

30
tests/playwright/pages/Dashboard/Calendar/CalendarSideMenu.ts

@ -7,11 +7,19 @@ export class CalendarSideMenuPage extends BasePage {
readonly new_record_btn: Locator;
readonly prev_btn: Locator;
readonly next_btn: Locator;
readonly searchToggleBtn: Locator;
constructor(parent: CalendarPage) {
super(parent.rootPage);
this.parent = parent;
this.new_record_btn = this.get().getByTestId('nc-calendar-side-menu-new-btn');
this.next_btn = this.parent.toolbar.get().getByTestId('nc-calendar-next-btn');
this.prev_btn = this.parent.toolbar.get().getByTestId('nc-calendar-prev-btn');
this.searchToggleBtn = this.get().getByTestId('nc-calendar-sidebar-search-btn');
}
get() {
@ -25,10 +33,32 @@ export class CalendarSideMenuPage extends BasePage {
}
async searchRecord({ query }: { query: string }) {
if (await this.searchToggleBtn.isVisible()) {
await this.searchToggleBtn.click();
}
const searchInput = this.get().getByTestId('nc-calendar-sidebar-search');
await searchInput.fill(query);
}
async clickPrev() {
await this.prev_btn.click();
}
async clickNext() {
await this.next_btn.click();
}
async moveToDate({ date, action }: { date: string; action: 'prev' | 'next' }) {
console.log(await this.parent.toolbar.getActiveDate());
while ((await this.parent.toolbar.getActiveDate()) !== date) {
if (action === 'prev') {
await this.clickPrev();
} else {
await this.clickNext();
}
}
}
async verifySideBarRecords({ records }: { records: string[] }) {
let attempts = 0;
let sideBarRecords: Locator;

63
tests/playwright/pages/Dashboard/Calendar/CalendarTopBar.ts

@ -1,63 +0,0 @@
import { expect, Locator } from '@playwright/test';
import BasePage from '../../Base';
import { CalendarPage } from './index';
export class CalendarTopbarPage extends BasePage {
readonly parent: CalendarPage;
readonly today_btn: Locator;
readonly prev_btn: Locator;
readonly next_btn: Locator;
readonly side_bar_btn: Locator;
constructor(parent: CalendarPage) {
super(parent.rootPage);
this.parent = parent;
this.next_btn = this.get().getByTestId('nc-calendar-next-btn');
this.prev_btn = this.get().getByTestId('nc-calendar-prev-btn');
this.today_btn = this.get().getByTestId('nc-calendar-today-btn');
this.side_bar_btn = this.get().getByTestId('nc-calendar-side-bar-btn');
}
get() {
return this.rootPage.getByTestId('nc-calendar-topbar');
}
async getActiveDate() {
return this.get().getByTestId('nc-calendar-active-date').textContent();
}
async verifyActiveCalendarView({ view }: { view: string }) {
const activeView = this.get().getByTestId('nc-active-calendar-view');
await expect(activeView).toContainText(view);
}
async clickPrev() {
await this.prev_btn.click();
}
async clickNext() {
await this.next_btn.click();
}
async clickToday() {
await this.today_btn.click();
}
async moveToDate({ date, action }: { date: string; action: 'prev' | 'next' }) {
while ((await this.getActiveDate()) !== date) {
if (action === 'prev') {
await this.clickPrev();
} else {
await this.clickNext();
}
}
}
async toggleSideBar() {
await this.side_bar_btn.click();
await this.rootPage.waitForTimeout(500);
}
}

11
tests/playwright/pages/Dashboard/Calendar/CalendarWeekDateTime.ts

@ -29,6 +29,8 @@ export class CalendarWeekDateTimePage extends BasePage {
}) {
const recordContainer = this.getRecordContainer();
const recordCard = recordContainer.getByTestId(`nc-calendar-week-record-${record}`);
await recordCard.scrollIntoViewIfNeeded();
const toDay = this.get()
.getByTestId('nc-calendar-week-day')
.nth(to.dayIndex)
@ -40,7 +42,10 @@ export class CalendarWeekDateTimePage extends BasePage {
await this.rootPage.mouse.down();
await this.rootPage.waitForTimeout(500);
await this.rootPage.mouse.move(cord.x + cord.width / 2, cord.y + cord.height / 2);
await this.rootPage.mouse.move(cord.x + Math.ceil(cord.width / 2), cord.y + Math.ceil(cord.height / 2));
// await toDay.scrollIntoViewIfNeeded();
await this.rootPage.waitForTimeout(500);
await this.rootPage.mouse.up();
}
@ -54,8 +59,8 @@ export class CalendarWeekDateTimePage extends BasePage {
hour.click({
force: true,
position: {
x: -1,
y: -1,
x: 1,
y: 1,
},
}),
requestUrlPathToMatch: '/api/v1/db/data/noco',

1
tests/playwright/pages/Dashboard/Calendar/CalendarYear.ts

@ -1,4 +1,3 @@
import { expect, Locator } from '@playwright/test';
import BasePage from '../../Base';
import { CalendarPage } from './index';

8
tests/playwright/pages/Dashboard/Calendar/index.ts

@ -3,7 +3,6 @@ import BasePage from '../../Base';
import { ToolbarPage } from '../common/Toolbar';
import { expect } from '@playwright/test';
import { TopbarPage } from '../common/Topbar';
import { CalendarTopbarPage } from './CalendarTopBar';
import { CalendarSideMenuPage } from './CalendarSideMenu';
import { CalendarMonthPage } from './CalendarMonth';
import { CalendarYearPage } from './CalendarYear';
@ -16,7 +15,6 @@ export class CalendarPage extends BasePage {
readonly dashboard: DashboardPage;
readonly toolbar: ToolbarPage;
readonly topbar: TopbarPage;
readonly calendarTopbar: CalendarTopbarPage;
readonly sideMenu: CalendarSideMenuPage;
readonly calendarMonth: CalendarMonthPage;
readonly calendarYear: CalendarYearPage;
@ -30,7 +28,6 @@ export class CalendarPage extends BasePage {
this.dashboard = dashboard;
this.toolbar = new ToolbarPage(this);
this.topbar = new TopbarPage(this);
this.calendarTopbar = new CalendarTopbarPage(this);
this.sideMenu = new CalendarSideMenuPage(this);
this.calendarMonth = new CalendarMonthPage(this);
this.calendarYear = new CalendarYearPage(this);
@ -60,4 +57,9 @@ export class CalendarPage extends BasePage {
async waitLoading() {
await this.rootPage.waitForTimeout(2000);
}
async toggleSideBar() {
await this.rootPage.getByTestId('nc-calendar-side-bar-btn').click();
await this.rootPage.waitForTimeout(500);
}
}

2
tests/playwright/pages/Dashboard/ViewSidebar/index.ts

@ -161,7 +161,7 @@ export class ViewSidebarPage extends BasePage {
force: true,
});
const submitAction = () =>
this.rootPage.locator('.ant-modal-content').locator('button:has-text("Create a View"):visible').click();
this.rootPage.locator('.ant-modal-content').locator('button:has-text("Create view"):visible').click();
await this.waitForResponse({
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/api/v1/db/meta/tables/',

5
tests/playwright/pages/Dashboard/common/Toolbar/CalendarViewMode.ts

@ -13,6 +13,9 @@ export class ToolbarCalendarViewModePage extends BasePage {
}
async changeCalendarView({ title }: { title: string }) {
await this.get().getByTestId(`nc-calendar-view-mode-${title}`).click();
await this.get().click({ force: true });
await this.rootPage.waitForTimeout(500);
await this.rootPage.locator('.rc-virtual-list-holder-inner > div').locator(`text="${title}"`).click();
}
}

17
tests/playwright/pages/Dashboard/common/Toolbar/index.ts

@ -42,6 +42,7 @@ export class ToolbarPage extends BasePage {
readonly btn_rowHeight: Locator;
readonly btn_groupBy: Locator;
readonly btn_calendarSettings: Locator;
readonly today_btn: Locator;
constructor(parent: GridPage | GalleryPage | FormPage | KanbanPage | MapPage | CalendarPage) {
super(parent.rootPage);
@ -65,6 +66,8 @@ export class ToolbarPage extends BasePage {
this.btn_rowHeight = this.get().locator(`button.nc-height-menu-btn`);
this.btn_groupBy = this.get().locator(`button.nc-group-by-menu-btn`);
this.btn_calendarSettings = this.get().getByTestId('nc-calendar-range-btn');
this.today_btn = this.get().getByTestId('nc-calendar-today-btn');
}
get() {
@ -88,6 +91,10 @@ export class ToolbarPage extends BasePage {
if (menuOpen) await this.calendarRange.get().waitFor({ state: 'hidden' });
}
async getActiveDate() {
return this.get().getByTestId('nc-calendar-active-date').textContent();
}
async clickFields() {
const menuOpen = await this.fields.get().isVisible();
@ -145,6 +152,12 @@ export class ToolbarPage extends BasePage {
}
}
async verifyActiveCalendarView({ view }: { view: string }) {
const activeView = this.get().getByTestId('nc-active-calendar-view');
await expect(activeView).toContainText(view);
}
async clickFilter({
// `networkValidation` is used to verify that api calls are made when the button is clicked
// which happens when the filter is opened for the first time
@ -206,6 +219,10 @@ export class ToolbarPage extends BasePage {
await this.get().locator(`.nc-toolbar-btn.nc-height-menu-btn`).click();
}
async clickToday() {
await this.today_btn.click();
}
async verifyStackByButton({ title }: { title: string }) {
await this.get().locator(`.nc-toolbar-btn.nc-kanban-stacked-by-menu-btn`).waitFor({ state: 'visible' });
await expect(

2
tests/playwright/tests/db/general/toolbarOperations.spec.ts

@ -506,7 +506,7 @@ test.describe('Toolbar operations (GRID)', () => {
});
test('Duplicate View and Verify GroupBy', async () => {
if (enableQuickRun()) test.skip();
// if (enableQuickRun()) test.skip();
await dashboard.treeView.openTable({ title: 'Film' });
await dashboard.viewSidebar.createGridView({ title: 'Film Grid' });

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

@ -36,7 +36,7 @@ const dateRecords = [
{
Id: 1,
Title: 'Team Catchup',
StartDate: '2024-01-01 09:00',
StartDate: '2024-01-01 08:00',
EndDate: '2024-01-01 10:00',
},
{
@ -161,40 +161,38 @@ test.describe('Calendar View', () => {
// Verify Sidebar
const calendar = dashboard.calendar;
await calendar.calendarTopbar.toggleSideBar();
await calendar.verifySideBarOpen();
await calendar.calendarTopbar.toggleSideBar();
await calendar.toggleSideBar();
await calendar.verifySideBarClosed();
await calendar.calendarTopbar.toggleSideBar();
await calendar.toggleSideBar();
await calendar.verifySideBarOpen();
// Verify Calendar View Modes
await calendar.calendarTopbar.verifyActiveCalendarView({ view: 'month' });
await calendar.toolbar.verifyActiveCalendarView({ view: 'month' });
await toolbar.calendarViewMode.changeCalendarView({ title: 'week' });
await calendar.calendarTopbar.verifyActiveCalendarView({ view: 'week' });
await calendar.toolbar.verifyActiveCalendarView({ view: 'week' });
await toolbar.calendarViewMode.changeCalendarView({ title: 'day' });
await calendar.calendarTopbar.verifyActiveCalendarView({ view: 'day' });
await calendar.toolbar.verifyActiveCalendarView({ view: 'day' });
await toolbar.calendarViewMode.changeCalendarView({ title: 'month' });
await calendar.calendarTopbar.verifyActiveCalendarView({ view: 'month' });
await calendar.toolbar.verifyActiveCalendarView({ view: 'month' });
await toolbar.calendarViewMode.changeCalendarView({ title: 'year' });
await calendar.calendarTopbar.verifyActiveCalendarView({ view: 'year' });
await calendar.toolbar.verifyActiveCalendarView({ view: 'year' });
await toolbar.calendarViewMode.changeCalendarView({ title: 'month' });
await calendar.calendarTopbar.moveToDate({ date: 'January 2024', action: 'prev' });
await calendar.sideMenu.moveToDate({ date: 'Jan 2024', action: 'prev' });
// Verify Sidebar Records & Filters
@ -250,25 +248,25 @@ test.describe('Calendar View', () => {
await calendar.toolbar.calendarViewMode.changeCalendarView({ title: 'day' });
await calendar.calendarTopbar.moveToDate({
date: '1 January 2024',
await calendar.sideMenu.moveToDate({
date: '1 Jan 2024',
action: 'prev',
});
await calendar.sideMenu.verifySideBarRecords({ records: dateRecords.filter(f => f.Title).map(f => f.Title) });
await calendar.calendarDayDateTime.selectHour({ hourIndex: 10 });
await calendar.sideMenu.updateFilter({ filter: 'In selected hours' });
await calendar.calendarDayDateTime.selectHour({ hourIndex: 10 });
await calendar.sideMenu.verifySideBarRecords({ records: ['Team Catchup'] });
await calendar.calendarDayDateTime.selectHour({ hourIndex: 1 });
await calendar.sideMenu.verifySideBarRecords({ records: [] });
await calendar.calendarTopbar.moveToDate({
date: '3 January 2024',
await calendar.sideMenu.moveToDate({
date: '3 Jan 2024',
action: 'next',
});
@ -276,10 +274,10 @@ test.describe('Calendar View', () => {
await toolbar.calendarViewMode.changeCalendarView({ title: 'week' });
await calendar.calendarWeekDateTime.selectHour({ dayIndex: 0, hourIndex: 10 });
await calendar.sideMenu.updateFilter({ filter: 'In selected hours' });
await calendar.calendarWeekDateTime.selectHour({ dayIndex: 0, hourIndex: 10 });
await calendar.sideMenu.verifySideBarRecords({ records: ['Team Catchup'] });
await calendar.calendarWeekDateTime.selectHour({ dayIndex: 0, hourIndex: 1 });
@ -307,9 +305,9 @@ test.describe('Calendar View', () => {
const calendar = dashboard.calendar;
await calendar.calendarTopbar.toggleSideBar();
// await calendar.toggleSideBar();
await calendar.calendarTopbar.moveToDate({ date: 'January 2024', action: 'prev' });
await calendar.sideMenu.moveToDate({ date: 'Jan 2024', action: 'prev' });
await calendar.calendarMonth.dragAndDrop({
record: 'Team Catchup',
@ -330,7 +328,7 @@ test.describe('Calendar View', () => {
await calendar.toolbar.calendarViewMode.changeCalendarView({ title: 'week' });
await calendar.calendarTopbar.moveToDate({
await calendar.sideMenu.moveToDate({
date: '1 - 7 Jan 24',
action: 'prev',
});
@ -339,13 +337,13 @@ test.describe('Calendar View', () => {
record: 'Team Catchup',
to: {
dayIndex: 0,
hourIndex: 5,
hourIndex: 7,
},
});
await calendar.sideMenu.updateFilter({ filter: 'In selected hours' });
await calendar.calendarWeekDateTime.selectHour({ dayIndex: 0, hourIndex: 5 });
await calendar.calendarWeekDateTime.selectHour({ dayIndex: 0, hourIndex: 7 });
await calendar.sideMenu.verifySideBarRecords({ records: ['Team Catchup'] });
@ -392,29 +390,29 @@ test.describe('Calendar View', () => {
const calendar = dashboard.calendar;
await calendar.calendarTopbar.verifyActiveCalendarView({ view: 'month' });
await calendar.toolbar.verifyActiveCalendarView({ view: 'month' });
await calendar.toolbar.calendarViewMode.changeCalendarView({ title: 'week' });
await calendar.calendarTopbar.verifyActiveCalendarView({ view: 'week' });
await calendar.toolbar.verifyActiveCalendarView({ view: 'week' });
await calendar.toolbar.calendarViewMode.changeCalendarView({ title: 'day' });
await calendar.calendarTopbar.verifyActiveCalendarView({ view: 'day' });
await calendar.toolbar.verifyActiveCalendarView({ view: 'day' });
await calendar.toolbar.calendarViewMode.changeCalendarView({ title: 'month' });
await calendar.calendarTopbar.verifyActiveCalendarView({ view: 'month' });
await calendar.toolbar.verifyActiveCalendarView({ view: 'month' });
await toolbar.calendarViewMode.changeCalendarView({ title: 'year' });
await calendar.calendarTopbar.verifyActiveCalendarView({ view: 'year' });
await calendar.toolbar.verifyActiveCalendarView({ view: 'year' });
await toolbar.calendarViewMode.changeCalendarView({ title: 'month' });
await calendar.calendarTopbar.moveToDate({ date: 'January 2024', action: 'prev' });
// await calendar.toggleSideBar();
await calendar.calendarTopbar.toggleSideBar();
await calendar.sideMenu.moveToDate({ date: 'Jan 2024', action: 'prev' });
await calendar.sideMenu.verifySideBarRecords({ records: dateRecords.filter(f => f.Title).map(f => f.Title) });
@ -452,21 +450,21 @@ test.describe('Calendar View', () => {
await toolbar.calendarViewMode.changeCalendarView({ title: 'week' });
await dashboard.calendar.calendarTopbar.verifyActiveCalendarView({ view: 'week' });
await dashboard.calendar.toolbar.verifyActiveCalendarView({ view: 'week' });
await toolbar.calendarViewMode.changeCalendarView({ title: 'day' });
await dashboard.calendar.calendarTopbar.verifyActiveCalendarView({ view: 'day' });
await dashboard.calendar.toolbar.verifyActiveCalendarView({ view: 'day' });
const calendar = dashboard.calendar;
await calendar.calendarTopbar.moveToDate({ date: '1 January 2024', action: 'prev' });
// await calendar.toggleSideBar();
await calendar.calendarTopbar.toggleSideBar();
await calendar.sideMenu.moveToDate({ date: '1 Jan 2024', action: 'prev' });
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.sideMenu.moveToDate({ date: '2 Jan 2024', action: 'next' });
await calendar.calendarDayDate.verifyRecord({ records: [] });

Loading…
Cancel
Save