Browse Source

chore: run lint

pull/7611/head
DarkPhoenix2704 10 months ago
parent
commit
1ca038ff64
  1. 224
      packages/nc-gui/components/dlg/ViewCreate.vue
  2. 188
      packages/nc-gui/components/nc/DateWeekSelector.vue
  3. 159
      packages/nc-gui/components/nc/MonthYearSelector.vue
  4. 2
      packages/nc-gui/components/smartsheet/Gallery.vue
  5. 2
      packages/nc-gui/components/smartsheet/Kanban.vue
  6. 5
      packages/nc-gui/components/smartsheet/Toolbar.vue
  7. 43
      packages/nc-gui/components/smartsheet/calendar/DayView.vue
  8. 189
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  9. 50
      packages/nc-gui/components/smartsheet/calendar/RecordCard.vue
  10. 112
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  11. 39
      packages/nc-gui/components/smartsheet/calendar/SideRecordCard.vue
  12. 20
      packages/nc-gui/components/smartsheet/calendar/YearView.vue
  13. 62
      packages/nc-gui/components/smartsheet/calendar/index.vue
  14. 2
      packages/nc-gui/components/smartsheet/grid/index.vue
  15. 37
      packages/nc-gui/components/smartsheet/toolbar/CalendarMode.vue
  16. 144
      packages/nc-gui/components/smartsheet/toolbar/CalendarRange.vue
  17. 41
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  18. 14
      packages/nc-gui/components/tabs/Smartsheet.vue
  19. 259
      packages/nc-gui/composables/useCalendarViewStore.ts
  20. 2
      packages/nc-gui/utils/dateTimeUtils.ts
  21. 22
      packages/nocodb/src/controllers/calendars.controller.spec.ts
  22. 110
      packages/nocodb/src/controllers/calendars.controller.ts
  23. 12
      packages/nocodb/src/meta/meta.service.ts
  24. 4
      packages/nocodb/src/meta/migrations/v2/nc_041_calander_view.ts
  25. 265
      packages/nocodb/src/models/CalendarRange.ts
  26. 208
      packages/nocodb/src/models/CalendarView.ts
  27. 331
      packages/nocodb/src/models/CalendarViewColumn.ts
  28. 80
      packages/nocodb/src/models/View.ts
  29. 137
      packages/nocodb/src/services/calendars.service.ts

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

@ -1,20 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type {ComponentPublicInstance} from '@vue/runtime-core' import type { ComponentPublicInstance } from '@vue/runtime-core'
import {capitalize} from '@vue/runtime-core' import { capitalize } from '@vue/runtime-core'
import type {Form as AntForm, SelectProps} from 'ant-design-vue' import type { Form as AntForm, SelectProps } from 'ant-design-vue'
import { import type { CalendarType, FormType, GalleryType, GridType, KanbanType, MapType, TableType } from 'nocodb-sdk'
CalendarType, import { UITypes, ViewTypes, isSystemColumn } from 'nocodb-sdk'
FormType, import { computed, message, nextTick, onBeforeMount, reactive, ref, useApi, useI18n, useVModel, watch } from '#imports'
GalleryType,
GridType,
isSystemColumn,
KanbanType,
MapType,
TableType,
UITypes,
ViewTypes
} from 'nocodb-sdk'
import {computed, message, nextTick, onBeforeMount, reactive, ref, useApi, useI18n, useVModel, watch} from '#imports'
interface Props { interface Props {
modelValue: boolean modelValue: boolean
@ -24,8 +14,8 @@ interface Props {
groupingFieldColumnId?: string groupingFieldColumnId?: string
geoDataFieldColumnId?: string geoDataFieldColumnId?: string
tableId: string tableId: string
calendar_range: Array<{ calendarRange: Array<{
fk_from_column_id: string, fk_from_column_id: string
fk_to_column_id: string | null // for ee only fk_to_column_id: string | null // for ee only
}> }>
} }
@ -45,8 +35,8 @@ interface Form {
fk_geo_data_col_id: string | null fk_geo_data_col_id: string | null
// for calendar view only // for calendar view only
calendar_range: Array<{ calendarRange: Array<{
fk_from_column_id: string, fk_from_column_id: string
fk_to_column_id: string | null // for ee only fk_to_column_id: string | null // for ee only
}> }>
} }
@ -55,7 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
selectedViewId: undefined, selectedViewId: undefined,
groupingFieldColumnId: undefined, groupingFieldColumnId: undefined,
geoDataFieldColumnId: undefined, geoDataFieldColumnId: undefined,
calendar_range: undefined, calendarRange: undefined,
}) })
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
@ -98,10 +88,15 @@ const form = reactive<Form>({
copy_from_id: null, copy_from_id: null,
fk_grp_col_id: null, fk_grp_col_id: null,
fk_geo_data_col_id: null, fk_geo_data_col_id: null,
calendar_range: ViewTypes.CALENDAR === props.type ? props.calendar_range : [{ calendarRange:
fk_from_column_id: '', ViewTypes.CALENDAR === props.type
fk_to_column_id: null ? props.calendarRange
}], : [
{
fk_from_column_id: '',
fk_to_column_id: null,
},
],
}) })
const viewSelectFieldOptions = ref<SelectProps['options']>([]) const viewSelectFieldOptions = ref<SelectProps['options']>([])
@ -289,10 +284,12 @@ onMounted(async () => {
if (viewSelectFieldOptions.value?.length) { if (viewSelectFieldOptions.value?.length) {
// take the first option // take the first option
form.calendar_range = [{ form.calendarRange = [
fk_from_column_id: viewSelectFieldOptions.value[0].value as string, {
fk_to_column_id: null // for ee only fk_from_column_id: viewSelectFieldOptions.value[0].value as string,
}] fk_to_column_id: null, // for ee only
},
]
} else { } else {
// if there is no grouping field column, disable the create button // if there is no grouping field column, disable the create button
isNecessaryColumnsPresent.value = false isNecessaryColumnsPresent.value = false
@ -315,58 +312,62 @@ onMounted(async () => {
<template #header> <template #header>
<div class="flex w-full flex-row justify-between items-center"> <div class="flex w-full flex-row justify-between items-center">
<div class="flex gap-x-1.5 items-center"> <div class="flex gap-x-1.5 items-center">
<GeneralViewIcon :meta="{ type: form.type }" class="nc-view-icon !text-xl" /> <GeneralViewIcon :meta="{ type: form.type }" class="nc-view-icon !text-xl" />
<template v-if="form.type === ViewTypes.GRID"> <template v-if="form.type === ViewTypes.GRID">
<template v-if="form.copy_from_id"> <template v-if="form.copy_from_id">
{{ $t('labels.duplicateGridView') }} {{ $t('labels.duplicateGridView') }}
</template>
<template v-else>
{{ $t('labels.createGridView') }}
</template>
</template> </template>
<template v-else> <template v-else-if="form.type === ViewTypes.GALLERY">
{{ $t('labels.createGridView') }} <template v-if="form.copy_from_id">
</template> {{ $t('labels.duplicateGalleryView') }}
</template> </template>
<template v-else-if="form.type === ViewTypes.GALLERY"> <template v-else>
<template v-if="form.copy_from_id"> {{ $t('labels.createGalleryView') }}
{{ $t('labels.duplicateGalleryView') }} </template>
</template>
<template v-else>
{{ $t('labels.createGalleryView') }}
</template>
</template>
<template v-else-if="form.type === ViewTypes.FORM">
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateFormView') }}
</template>
<template v-else>
{{ $t('labels.createFormView') }}
</template>
</template>
<template v-else-if="form.type === ViewTypes.KANBAN">
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateKanbanView') }}
</template>
<template v-else>
{{ $t('labels.createKanbanView') }}
</template> </template>
</template> <template v-else-if="form.type === ViewTypes.FORM">
<template v-else-if="form.type === ViewTypes.CALENDAR"> <template v-if="form.copy_from_id">
<template v-if="form.copy_from_id"> {{ $t('labels.duplicateFormView') }}
{{ $t('labels.duplicateCalendarView') }} </template>
<template v-else>
{{ $t('labels.createFormView') }}
</template>
</template> </template>
<template v-else> <template v-else-if="form.type === ViewTypes.KANBAN">
{{ $t('labels.createCalendarView') }} <template v-if="form.copy_from_id">
{{ $t('labels.duplicateKanbanView') }}
</template>
<template v-else>
{{ $t('labels.createKanbanView') }}
</template>
</template> </template>
</template> <template v-else-if="form.type === ViewTypes.CALENDAR">
<template v-else> <template v-if="form.copy_from_id">
<template v-if="form.copy_from_id"> {{ $t('labels.duplicateCalendarView') }}
{{ $t('labels.duplicateMapView') }} </template>
<template v-else>
{{ $t('labels.createCalendarView') }}
</template>
</template> </template>
<template v-else> <template v-else>
{{ $t('labels.duplicateView') }} <template v-if="form.copy_from_id">
{{ $t('labels.duplicateMapView') }}
</template>
<template v-else>
{{ $t('labels.duplicateView') }}
</template>
</template> </template>
</template>
</div> </div>
<a v-if="!form.copy_from_id" href="https://docs.nocodb.com/views/view-types/calendar/" target="_blank" <a
class="text-sm !no-underline !hover:text-brand-500 text-brand-500 "> v-if="!form.copy_from_id"
href="https://docs.nocodb.com/views/view-types/calendar/"
target="_blank"
class="text-sm !no-underline !hover:text-brand-500 text-brand-500"
>
Go to Docs Go to Docs
</a> </a>
</div> </div>
@ -415,47 +416,52 @@ onMounted(async () => {
:not-found-content="$t('placeholder.selectGeoFieldNotFound')" :not-found-content="$t('placeholder.selectGeoFieldNotFound')"
/> />
</a-form-item> </a-form-item>
<div v-if="form.type === ViewTypes.CALENDAR" v-for="range in form.calendar_range" class="flex w-full gap-3"> <template v-if="form.type === ViewTypes.CALENDAR">
<div class="flex flex-col gap-2 w-1/2"> <div v-for="(range, index) in form.calendarRange" :key="`range-${index}`" class="flex w-full gap-3">
<span> <div class="flex flex-col gap-2 w-1/2">
{{ $t('labels.organizeRecordsBy') }} <span>
</span> {{ $t('labels.organizeRecordsBy') }}
<NcSelect </span>
<NcSelect
v-model:value="range.fk_from_column_id" v-model:value="range.fk_from_column_id"
class="w-full" class="w-full"
:disabled="isMetaLoading" :disabled="isMetaLoading"
:loading="isMetaLoading" :loading="isMetaLoading"
:options="viewSelectFieldOptions" :options="viewSelectFieldOptions"
/> />
</div>
<div v-if="range.fk_to_column_id === null && isEeUI" class="flex flex-col justify-end w-1/2">
<div class="cursor-pointer flex items-center font-medium gap-1 mb-1"
@click="range.fk_to_column_id = ''">
<component :is="iconMap.plus" class="h-4 w-4"/>
{{ $t('activity.setEndDate') }}
</div> </div>
<div v-if="range.fk_to_column_id === null && isEeUI" class="flex flex-col justify-end w-1/2">
</div> <div class="cursor-pointer flex items-center font-medium gap-1 mb-1" @click="range.fk_to_column_id = ''">
<div v-else-if="isEeUI" class="flex gap-2 flex-col w-1/2"> <component :is="iconMap.plus" class="h-4 w-4" />
<div class="flex flex-row justify-between"> {{ $t('activity.setEndDate') }}
<span> </div>
{{ $t('labels.endDateField') }}
</span>
<component :is="iconMap.delete" class="h-4 w-4 cursor-pointer text-red-500"
@click="() => {
range.fk_to_column_id = null
}"/>
</div> </div>
<NcSelect <div v-else-if="isEeUI" class="flex gap-2 flex-col w-1/2">
<div class="flex flex-row justify-between">
<span>
{{ $t('labels.endDateField') }}
</span>
<component
:is="iconMap.delete"
class="h-4 w-4 cursor-pointer text-red-500"
@click="
() => {
range.fk_to_column_id = null
}
"
/>
</div>
<NcSelect
v-model:value="range.fk_to_column_id" v-model:value="range.fk_to_column_id"
class="w-full" class="w-full"
:disabled="isMetaLoading" :disabled="isMetaLoading"
:loading="isMetaLoading" :loading="isMetaLoading"
:options="viewSelectFieldOptions" :options="viewSelectFieldOptions"
:placeholder="$t('placeholder.notSelected')" :placeholder="$t('placeholder.notSelected')"
/> />
</div>
</div> </div>
</div> </template>
</a-form> </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"> <div v-else-if="!isNecessaryColumnsPresent" class="flex flex-row p-4 border-gray-200 border-1 gap-x-4 rounded-lg w-full">
<GeneralIcon icon="warning" class="!text-5xl text-orange-500" /> <GeneralIcon icon="warning" class="!text-5xl text-orange-500" />

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

@ -1,20 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup>
interface Props { interface Props {
selectedDate?: Date | null; selectedDate?: Date | null
isDisabled?: boolean; isDisabled?: boolean
pageDate?: Date; pageDate?: Date
activeDates?: Date[]; activeDates?: Array<Date>
isMondayFirst?: boolean; isMondayFirst?: boolean
weekPicker?: boolean; weekPicker?: boolean
disablePagination?: boolean; disablePagination?: boolean
selectedWeek?: { selectedWeek?: {
start: Date; start: Date
end: Date; end: Date
} | null } | null
} }
const emit = defineEmits(['change', 'update:selected-date', 'update:page-date', 'update:selected-week']);
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
selectedDate: null, selectedDate: null,
isDisabled: false, isDisabled: false,
@ -24,165 +22,164 @@ const props = withDefaults(defineProps<Props>(), {
disablePagination: false, disablePagination: false,
activeDates: [], activeDates: [],
selectedWeek: null, selectedWeek: null,
}); })
const emit = defineEmits(['change', 'update:selected-date', 'update:page-date', 'update:selected-week'])
// Page date is the date we use to manage which month/date that is currently being displayed // Page date is the date we use to manage which month/date that is currently being displayed
const pageDate = useVModel(props, 'pageDate', emit); const pageDate = useVModel(props, 'pageDate', emit)
const selectedDate = useVModel(props, 'selectedDate', emit); const selectedDate = useVModel(props, 'selectedDate', emit)
const activeDates = useVModel(props, 'activeDates', emit); const activeDates = useVModel(props, 'activeDates', emit)
const selectedWeek = useVModel(props, 'selectedWeek', emit); const selectedWeek = useVModel(props, 'selectedWeek', emit)
const days = computed(() => { const days = computed(() => {
if (props.isMondayFirst) { if (props.isMondayFirst) {
return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
} else { } else {
return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
} }
}) })
// Used to display the current month and year // Used to display the current month and year
const currentMonth = computed(() => { const currentMonth = computed(() => {
return pageDate.value.toLocaleString('default', {month: 'long'}) + ' ' + pageDate.value.getFullYear(); return `${pageDate.value.toLocaleString('default', { month: 'long' })} ${pageDate.value.getFullYear()}`
}); })
const selectWeek = (date: Date) => { const selectWeek = (date: Date) => {
if (!date) return; if (!date) return
const dayOffset = props.isMondayFirst ? 1 : 0; const dayOffset = props.isMondayFirst ? 1 : 0
const dayOfWeek = (date.getDay() - dayOffset + 7) % 7; const dayOfWeek = (date.getDay() - dayOffset + 7) % 7
const startDate = new Date(date); const startDate = new Date(date)
startDate.setDate(date.getDate() - dayOfWeek); startDate.setDate(date.getDate() - dayOfWeek)
selectedWeek.value = { selectedWeek.value = {
start: startDate, start: startDate,
end: new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() + 6) end: new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() + 6),
}; }
} }
// Generates all dates should be displayed in the calendar // Generates all dates should be displayed in the calendar
// Includes all blank days at the start and end of the month // Includes all blank days at the start and end of the month
const dates = computed(() => { const dates = computed(() => {
const startOfMonth = new Date(pageDate.value.getFullYear(), pageDate.value.getMonth(), 1); const startOfMonth = new Date(pageDate.value.getFullYear(), pageDate.value.getMonth(), 1)
const dayOffset = props.isMondayFirst ? 1 : 0; const dayOffset = props.isMondayFirst ? 1 : 0
const dayOfWeek = (startOfMonth.getDay() - dayOffset + 7) % 7; const dayOfWeek = (startOfMonth.getDay() - dayOffset + 7) % 7
startOfMonth.setDate(startOfMonth.getDate() - dayOfWeek); startOfMonth.setDate(startOfMonth.getDate() - dayOfWeek)
const datesArray = []; const datesArray = []
while (datesArray.length < 42) { while (datesArray.length < 42) {
datesArray.push(new Date(startOfMonth)); datesArray.push(new Date(startOfMonth))
startOfMonth.setDate(startOfMonth.getDate() + 1); startOfMonth.setDate(startOfMonth.getDate() + 1)
} }
return datesArray; return datesArray
}); })
// Check if the date is in the selected week // Check if the date is in the selected week
const isDateInSelectedWeek = (date: Date) => { const isDateInSelectedWeek = (date: Date) => {
if (!selectedWeek.value) return false; if (!selectedWeek.value) return false
return date >= selectedWeek.value.start && date <= selectedWeek.value.end; return date >= selectedWeek.value.start && date <= selectedWeek.value.end
}
// Used to check if two dates are the same
const isSameDate = (date1: Date, date2: Date) => {
if (!date1 || !date2) return false
return (
date1.getDate() === date2.getDate() && date1.getMonth() === date2.getMonth() && date1.getFullYear() === date2.getFullYear()
)
} }
// Used in DatePicker for checking if the date is currently selected // Used in DatePicker for checking if the date is currently selected
const isSelectedDate = (dObj: Date) => { const isSelectedDate = (dObj: Date) => {
if (!selectedDate.value) return false; if (!selectedDate.value) return false
const propDate = new Date(selectedDate.value); const propDate = new Date(selectedDate.value)
return props.selectedDate ? isSameDate(propDate, dObj) : false; return props.selectedDate ? isSameDate(propDate, dObj) : false
} }
const isDayInPagedMonth = (date: Date) => { const isDayInPagedMonth = (date: Date) => {
return date.getMonth() === pageDate.value.getMonth(); return date.getMonth() === pageDate.value.getMonth()
} }
// Since we are using the same component for week picker and date picker we need to handle the date selection differently // Since we are using the same component for week picker and date picker we need to handle the date selection differently
const handleSelectDate = (date: Date) => { const handleSelectDate = (date: Date) => {
if (props.weekPicker) { if (props.weekPicker) {
selectWeek(date); selectWeek(date)
} else { } else {
if (!isDayInPagedMonth(date)) { if (!isDayInPagedMonth(date)) {
pageDate.value = new Date(date); pageDate.value = new Date(date)
emit('update:page-date', date); emit('update:page-date', date)
} }
selectedDate.value = date; selectedDate.value = date
emit('update:selected-date', date); emit('update:selected-date', date)
} }
}; }
// Used to check if a date is in the current month // Used to check if a date is in the current month
const isDateInCurrentMonth = (date: Date) => { const isDateInCurrentMonth = (date: Date) => {
return date.getMonth() === pageDate.value.getMonth(); return date.getMonth() === pageDate.value.getMonth()
}; }
// Used to check if two dates are the same
const isSameDate = (date1: Date, date2: Date) => {
if (!date1 || !date2) return false;
return date1.getDate() === date2.getDate() &&
date1.getMonth() === date2.getMonth() &&
date1.getFullYear() === date2.getFullYear();
};
// Used to Check if an event is in the date // Used to Check if an event is in the date
const isActiveDate = (date: Date) => { const isActiveDate = (date: Date) => {
return activeDates.value.some((d) => isSameDate(d, date)); return activeDates.value.some((d) => isSameDate(d, date))
}; }
// Paginate the calendar // Paginate the calendar
const paginate = (action: 'next' | 'prev') => { const paginate = (action: 'next' | 'prev') => {
const newDate = new Date(pageDate.value); const newDate = new Date(pageDate.value)
if (action === 'next') { if (action === 'next') {
newDate.setMonth(newDate.getMonth() + 1); newDate.setMonth(newDate.getMonth() + 1)
} else { } else {
newDate.setMonth(newDate.getMonth() - 1); newDate.setMonth(newDate.getMonth() - 1)
} }
pageDate.value = newDate; pageDate.value = newDate
emit('update:page-date', newDate); emit('update:page-date', newDate)
}; }
</script> </script>
<template> <template>
<div class="p-4 flex flex-col gap-4 "> <div class="p-4 flex flex-col gap-4">
<div :class="{ <div
'justify-center': disablePagination, :class="{
'justify-between': !disablePagination, 'justify-center': disablePagination,
}" class="flex items-center"> 'justify-between': !disablePagination,
}"
class="flex items-center"
>
<NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('prev')"> <NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.doubleLeftArrow" class="h-4 w-4"/> <component :is="iconMap.doubleLeftArrow" class="h-4 w-4" />
</NcButton> </NcButton>
<span class="font-bold text-gray-700">{{ currentMonth }}</span> <span class="font-bold text-gray-700">{{ currentMonth }}</span>
<NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('next')"> <NcButton v-if="!disablePagination" size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4"/> <component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
</NcButton> </NcButton>
</div> </div>
<div class="border-1 border-gray-200 rounded-y-xl max-w-[320px]"> <div class="border-1 border-gray-200 rounded-y-xl max-w-[320px]">
<div class="flex flex-row bg-gray-100 gap-2 rounded-t-xl justify-between p-2"> <div class="flex flex-row bg-gray-100 gap-2 rounded-t-xl justify-between p-2">
<span v-for="day in days" class="h-9 flex items-center justify-center w-9 text-gray-500">{{ day }}</span> <span v-for="day in days" :key="day" class="h-9 flex items-center justify-center w-9 text-gray-500">{{ day }}</span>
</div> </div>
<div class="grid grid-cols-7 gap-2 p-2"> <div class="grid grid-cols-7 gap-2 p-2">
<span v-for="(date) in dates" :key="date" :class="{ <span
'rounded-lg' : !weekPicker, v-for="date in dates"
'bg-brand-50 border-2 !border-brand-500' : isSelectedDate(date) && !weekPicker && isDayInPagedMonth(date), :key="date"
'hover:(border-1 border-gray-200 bg-gray-100)' : !isSelectedDate(date) && !weekPicker, :class="{
'nc-selected-week z-1': isDateInSelectedWeek(date) && weekPicker, 'rounded-lg': !weekPicker,
'text-gray-400': !isDateInCurrentMonth(date), 'bg-brand-50 border-2 !border-brand-500': isSelectedDate(date) && !weekPicker && isDayInPagedMonth(date),
'nc-selected-week-start': isSameDate(date, selectedWeek?.start), 'hover:(border-1 border-gray-200 bg-gray-100)': !isSelectedDate(date) && !weekPicker,
'nc-selected-week-end': isSameDate(date, selectedWeek?.end), 'nc-selected-week z-1': isDateInSelectedWeek(date) && weekPicker,
'text-gray-400': !isDateInCurrentMonth(date),
}" class="h-9 w-9 px-1 py-2 relative font-medium flex items-center cursor-pointer justify-center" 'nc-selected-week-start': isSameDate(date, selectedWeek?.start),
@click="handleSelectDate(date)" 'nc-selected-week-end': isSameDate(date, selectedWeek?.end),
}"
class="h-9 w-9 px-1 py-2 relative font-medium flex items-center cursor-pointer justify-center"
@click="handleSelectDate(date)"
> >
<span v-if="isActiveDate(date)" <span v-if="isActiveDate(date)" class="absolute z-2 h-1.5 w-1.5 rounded-full bg-brand-500 top-1 right-1"></span>
class="absolute z-2 h-1.5 w-1.5 rounded-full bg-brand-500 top-1 right-1"></span>
<span class="z-2"> <span class="z-2">
{{ date.getDate() }} {{ date.getDate() }}
</span> </span>
</span> </span>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.nc-selected-week { .nc-selected-week {
@apply relative; @apply relative;
} }
@ -202,5 +199,4 @@ const paginate = (action: 'next' | 'prev') => {
width: 100%; width: 100%;
@apply !border-r-2 !rounded-r-lg; @apply !border-r-2 !rounded-r-lg;
} }
</style> </style>

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

@ -1,176 +1,179 @@
<script lang="ts" setup> <script lang="ts" setup>
interface Props { interface Props {
selectedDate?: Date | null; selectedDate?: Date | null
isDisabled?: boolean; isDisabled?: boolean
pageDate?: Date; pageDate?: Date
yearPicker?: boolean; yearPicker?: boolean
} }
const emit = defineEmits(['update:selected-date', 'update:page-date']);
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
selectedDate: null, selectedDate: null,
isDisabled: false, isDisabled: false,
pageDate: new Date(), pageDate: new Date(),
yearPicker: false yearPicker: false,
}); })
const emit = defineEmits(['update:selected-date', 'update:page-date'])
const pageDate = useVModel(props, 'pageDate', emit); const pageDate = useVModel(props, 'pageDate', emit)
const selectedDate = useVModel(props, 'selectedDate', emit); const selectedDate = useVModel(props, 'selectedDate', emit)
const years = computed(() => { const years = computed(() => {
const date = new Date(pageDate.value); const date = new Date(pageDate.value)
const startYear = date.getFullYear(); const startYear = date.getFullYear()
const endYear = date.getFullYear() + 11; const endYear = date.getFullYear() + 11
const years = []; const years = []
for (let i = startYear; i <= endYear; i++) { for (let i = startYear; i <= endYear; i++) {
years.push({ years.push({
label: i, label: i,
value: new Date(i, 0, 1) value: new Date(i, 0, 1),
}); })
} }
return years; return years
}); })
const currentYear = computed(() => { const currentYear = computed(() => {
return pageDate.value.getFullYear(); return pageDate.value.getFullYear()
}); })
const months = computed(() => { const months = computed(() => {
const date = new Date(pageDate.value); const date = new Date(pageDate.value)
return [ return [
{ {
label: 'January', label: 'January',
value: new Date(date.getFullYear(), 0, 1) value: new Date(date.getFullYear(), 0, 1),
}, },
{ {
label: 'February', label: 'February',
value: new Date(date.getFullYear(), 1, 1) value: new Date(date.getFullYear(), 1, 1),
}, },
{ {
label: 'March', label: 'March',
value: new Date(date.getFullYear(), 2, 1) value: new Date(date.getFullYear(), 2, 1),
}, },
{ {
label: 'April', label: 'April',
value: new Date(date.getFullYear(), 3, 1) value: new Date(date.getFullYear(), 3, 1),
}, },
{ {
label: 'May', label: 'May',
value: new Date(date.getFullYear(), 4, 1) value: new Date(date.getFullYear(), 4, 1),
}, },
{ {
label: 'June', label: 'June',
value: new Date(date.getFullYear(), 5, 1) value: new Date(date.getFullYear(), 5, 1),
}, },
{ {
label: 'July', label: 'July',
value: new Date(date.getFullYear(), 6, 1) value: new Date(date.getFullYear(), 6, 1),
}, },
{ {
label: 'August', label: 'August',
value: new Date(date.getFullYear(), 7, 1) value: new Date(date.getFullYear(), 7, 1),
}, },
{ {
label: 'September', label: 'September',
value: new Date(date.getFullYear(), 8, 1) value: new Date(date.getFullYear(), 8, 1),
}, },
{ {
label: 'October', label: 'October',
value: new Date(date.getFullYear(), 9, 1) value: new Date(date.getFullYear(), 9, 1),
}, },
{ {
label: 'November', label: 'November',
value: new Date(date.getFullYear(), 10, 1) value: new Date(date.getFullYear(), 10, 1),
}, },
{ {
label: 'December', label: 'December',
value: new Date(date.getFullYear(), 11, 1) value: new Date(date.getFullYear(), 11, 1),
} },
] ]
}) })
const compareDates = (date1: Date, date2: Date) => { const compareDates = (date1: Date, date2: Date) => {
if (!date1 || !date2) return false; if (!date1 || !date2) return false
return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth(); return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth()
} }
const isMonthSelected = (date: Date) => { const isMonthSelected = (date: Date) => {
if (!selectedDate.value) return false; if (!selectedDate.value) return false
return compareDates(date, selectedDate.value); return compareDates(date, selectedDate.value)
} }
const paginateMonth = (action: 'next' | 'prev') => { const paginateMonth = (action: 'next' | 'prev') => {
const date = new Date(pageDate.value); const date = new Date(pageDate.value)
if (action === 'next') { if (action === 'next') {
date.setFullYear(date.getFullYear() + 1); date.setFullYear(date.getFullYear() + 1)
} else { } else {
date.setFullYear(date.getFullYear() - 1); date.setFullYear(date.getFullYear() - 1)
} }
pageDate.value = date; pageDate.value = date
}; }
const paginateYear = (action: 'next' | 'prev') => { const paginateYear = (action: 'next' | 'prev') => {
const date = new Date(pageDate.value); const date = new Date(pageDate.value)
if (action === 'next') { if (action === 'next') {
date.setFullYear(date.getFullYear() + 12); date.setFullYear(date.getFullYear() + 12)
} else { } else {
date.setFullYear(date.getFullYear() - 12); date.setFullYear(date.getFullYear() - 12)
} }
pageDate.value = date; pageDate.value = date
}; }
const paginate = (action: 'next' | 'prev') => { const paginate = (action: 'next' | 'prev') => {
if (props.yearPicker) { if (props.yearPicker) {
paginateYear(action); paginateYear(action)
} else { } else {
paginateMonth(action); paginateMonth(action)
} }
}; }
const compareYear = (date1: Date, date2: Date) => { const compareYear = (date1: Date, date2: Date) => {
if (!date1 || !date2) return false; if (!date1 || !date2) return false
return date1.getFullYear() === date2.getFullYear(); return date1.getFullYear() === date2.getFullYear()
} }
</script> </script>
<template> <template>
<div class="p-4 flex flex-col gap-4"> <div class="p-4 flex flex-col gap-4">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<NcButton size="small" type="secondary" @click="paginate('prev')"> <NcButton size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.doubleLeftArrow" class="h-4 w-4"/> <component :is="iconMap.doubleLeftArrow" class="h-4 w-4" />
</NcButton> </NcButton>
<span class="font-bold text-gray-700">{{ yearPicker ? 'Select Year' : currentYear }}</span> <span class="font-bold text-gray-700">{{ yearPicker ? 'Select Year' : currentYear }}</span>
<NcButton size="small" type="secondary" @click="paginate('next')"> <NcButton size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4"/> <component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
</NcButton> </NcButton>
</div> </div>
<div class="border-1 border-gray-200 rounded-y-xl max-w-[350px]"> <div class="border-1 border-gray-200 rounded-y-xl max-w-[350px]">
<div class="grid grid-cols-4 gap-2 p-2"> <div class="grid grid-cols-4 gap-2 p-2">
<span v-for="(month) in months" v-if="!yearPicker" :key="month.value" :class="{ <template v-if="!yearPicker">
'!bg-brand-50 !border-2 !border-brand-500' : isMonthSelected(month.value), <span
}" v-for="month in months"
class="h-9 rounded-lg flex font-medium items-center justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-500 cursor-pointer" :key="month.value"
@click="selectedDate = month.value" :class="{
> '!bg-brand-50 !border-2 !border-brand-500': isMonthSelected(month.value),
{{ month.label.slice(0, 3) }} }"
</span> class="h-9 rounded-lg flex font-medium items-center justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-500 cursor-pointer"
<span v-for="(year) in years" v-if="yearPicker" :key="year" :class="{ @click="selectedDate = month.value"
'!bg-brand-50 !border-2 !border-brand-500' : compareYear(year.value, selectedDate) >
}" {{ month.label.slice(0, 3) }}
class="h-9 rounded-lg flex font-medium items-center justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-500 cursor-pointer" </span>
@click="selectedDate = year.value" </template>
> <template v-else>
{{ year.label }} <span
</span> v-for="year in years"
:key="year"
:class="{
'!bg-brand-50 !border-2 !border-brand-500': compareYear(year.value, selectedDate),
}"
class="h-9 rounded-lg flex font-medium items-center justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-500 cursor-pointer"
@click="selectedDate = year.value"
>
{{ year.label }}
</span>
</template>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped></style>
</style>

2
packages/nc-gui/components/smartsheet/Gallery.vue

@ -4,9 +4,9 @@ import type { Row as RowType } from '#imports'
import { import {
ActiveViewInj, ActiveViewInj,
FieldsInj, FieldsInj,
IsCalendarInj,
IsFormInj, IsFormInj,
IsGalleryInj, IsGalleryInj,
IsCalendarInj,
IsGridInj, IsGridInj,
MetaInj, MetaInj,
NavigateDir, NavigateDir,

2
packages/nc-gui/components/smartsheet/Kanban.vue

@ -7,7 +7,6 @@ import {
IsFormInj, IsFormInj,
IsGalleryInj, IsGalleryInj,
IsGridInj, IsGridInj,
IsCalendarInj,
IsKanbanInj, IsKanbanInj,
IsLockedInj, IsLockedInj,
IsPublicInj, IsPublicInj,
@ -105,7 +104,6 @@ provide(IsGridInj, ref(false))
provide(IsKanbanInj, ref(true)) provide(IsKanbanInj, ref(true))
const hasEditPermission = computed(() => isUIAllowed('dataEdit')) const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
const fields = inject(FieldsInj, ref([])) const fields = inject(FieldsInj, ref([]))

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

@ -32,7 +32,10 @@ const { allowCSVDownload } = useSharedView()
<LazySmartsheetToolbarMappedBy v-if="isMap" /> <LazySmartsheetToolbarMappedBy v-if="isMap" />
<LazySmartsheetToolbarCalendarRange v-if="isCalendar" /> <LazySmartsheetToolbarCalendarRange v-if="isCalendar" />
<LazySmartsheetToolbarFieldsMenu v-if="isGrid || isGallery || isKanban || isMap || isCalendar" :show-system-fields="false" /> <LazySmartsheetToolbarFieldsMenu
v-if="isGrid || isGallery || isKanban || isMap || isCalendar"
:show-system-fields="false"
/>
<LazySmartsheetToolbarStackedBy v-if="isKanban" /> <LazySmartsheetToolbarStackedBy v-if="isKanban" />

43
packages/nc-gui/components/smartsheet/calendar/DayView.vue

@ -1,33 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import { UITypes } from 'nocodb-sdk'
import {UITypes} from "nocodb-sdk"; const emit = defineEmits(['expand-record'])
const { pageDate, selectedDate, calDataType } = useCalendarViewStoreOrThrow(); const { pageDate, selectedDate, calDataType } = useCalendarViewStoreOrThrow()
const emit = defineEmits(['expand-record']);
const events = ref([ const events = ref([
{ {
"Id": 1, Id: 1,
"Title": "Event 01", Title: 'Event 01',
"from_date_time": "2023-12-15", from_date_time: '2023-12-15',
"to_date_time": "2023-12-20" to_date_time: '2023-12-20',
}, },
{ {
"Id": 2, Id: 2,
"Title": "Event 02", Title: 'Event 02',
"from_date_time": "2023-12-20", from_date_time: '2023-12-20',
"to_date_time": "2023-12-25" to_date_time: '2023-12-25',
} },
]) ])
</script> </script>
<template> <template>
<div v-if="calDataType === UITypes.Date" class="flex flex-col px-2 gap-4 pt-4"> <div v-if="calDataType === UITypes.Date" class="flex flex-col px-2 gap-4 pt-4">
<LazySmartsheetCalendarRecordCard v-for="event in events" :name="event.Title" :date="event.from_date_time" color="blue" size="medium" @click="emit('expand-record', 'xxx')" /> <LazySmartsheetCalendarRecordCard
</div> v-for="event in events"
:key="event.Id"
:name="event.Title"
:date="event.from_date_time"
color="blue"
size="medium"
@click="emit('expand-record', 'xxx')"
/>
</div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss"></style>
</style>

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

@ -1,162 +1,175 @@
<script setup lang="ts"> <script setup lang="ts">
const { pageDate, selectedDate } = useCalendarViewStoreOrThrow()
const { pageDate, selectedDate } = useCalendarViewStoreOrThrow();
const events = ref([ const events = ref([
{ {
"Id": 1, Id: 1,
"Title": "Event 01", Title: 'Event 01',
"from_date_time": "2023-12-15", from_date_time: '2023-12-15',
"to_date_time": "2023-12-20" to_date_time: '2023-12-20',
}, },
{ {
"Id": 2, Id: 2,
"Title": "Event 02", Title: 'Event 02',
"from_date_time": "2023-12-20", from_date_time: '2023-12-20',
"to_date_time": "2023-12-25" to_date_time: '2023-12-25',
} },
]) ])
interface EventData { interface EventData {
id: string; id: string
from_col_id: string; from_col_id: string
to_col_id: string | null; to_col_id: string | null
} }
const isMondayFirst = ref(false); const isMondayFirst = ref(false)
const fields = inject(FieldsInj, ref([])) const fields = inject(FieldsInj, ref([]))
const days = computed(() => { const days = computed(() => {
if (isMondayFirst.value) { if (isMondayFirst.value) {
return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
} else { } else {
return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
} }
}) })
const isDayInPagedMonth = (date: Date) => { const isDayInPagedMonth = (date: Date) => {
return date.getMonth() === pageDate.value.getMonth(); return date.getMonth() === pageDate.value.getMonth()
} }
const dates = computed(() => { const dates = computed(() => {
const startOfMonth = new Date(pageDate.value.getFullYear(), pageDate.value.getMonth(), 1); const startOfMonth = new Date(pageDate.value.getFullYear(), pageDate.value.getMonth(), 1)
const dayOffset = isMondayFirst.value ? 1 : 0; const dayOffset = isMondayFirst.value ? 1 : 0
const dayOfWeek = (startOfMonth.getDay() - dayOffset + 7) % 7; const dayOfWeek = (startOfMonth.getDay() - dayOffset + 7) % 7
startOfMonth.setDate(startOfMonth.getDate() - dayOfWeek); startOfMonth.setDate(startOfMonth.getDate() - dayOfWeek)
const datesArray = []; const datesArray = []
while (datesArray.length < 42) { while (datesArray.length < 42) {
datesArray.push(new Date(startOfMonth)); datesArray.push(new Date(startOfMonth))
startOfMonth.setDate(startOfMonth.getDate() + 1); startOfMonth.setDate(startOfMonth.getDate() + 1)
} }
return datesArray; return datesArray
}); })
const getGridPosition = (event) => { const getGridPosition = (event) => {
const firstDayOfMonth = new Date(pageDate.value.getFullYear(), pageDate.value.getMonth(), 1).getDay()
const firstDayOfMonth = new Date(pageDate.value.getFullYear(), pageDate.value.getMonth(), 1).getDay(); const startDate = new Date(event.from_date_time)
const startDayIndex = startDate.getDate() - 1 + firstDayOfMonth
const startDate = new Date(event.from_date_time); const endDate = new Date(event.to_date_time)
const startDayIndex = startDate.getDate() - 1 + firstDayOfMonth; const endDayIndex = endDate.getDate() - 1 + firstDayOfMonth
const endDate = new Date(event.to_date_time);
const endDayIndex = endDate.getDate() - 1 + firstDayOfMonth;
const startRow = Math.floor(startDayIndex / 7) + 1; const startRow = Math.floor(startDayIndex / 7) + 1
let endRow = Math.floor(endDayIndex / 7) + 1; let endRow = Math.floor(endDayIndex / 7) + 1
if (endDate.getMonth() !== pageDate.value.getMonth()) { if (endDate.getMonth() !== pageDate.value.getMonth()) {
endRow = Math.ceil((new Date(pageDate.value.getFullYear(), pageDate.value.getMonth() + 1, 0).getDate() + firstDayOfMonth) / 7); endRow = Math.ceil((new Date(pageDate.value.getFullYear(), pageDate.value.getMonth() + 1, 0).getDate() + firstDayOfMonth) / 7)
} }
const startCol = startDayIndex % 7 + 1; const startCol = (startDayIndex % 7) + 1
let endCol = endDayIndex % 7 + 1; let endCol = (endDayIndex % 7) + 1
if (endCol === 1) { if (endCol === 1) {
endRow++; endRow++
endCol = 8; endCol = 8
} }
return { return {
colStart: startCol, colStart: startCol,
colEnd: endCol, colEnd: endCol,
rowStart: startRow, rowStart: startRow,
rowEnd: endRow rowEnd: endRow,
}; }
}; }
const selectDate = (date: Date) => { const selectDate = (date: Date) => {
if (!date) return; if (!date) return
selectedDate.value = date; selectedDate.value = date
} }
const isSameDate = (date1: Date, date2: Date) => { const isSameDate = (date1: Date, date2: Date) => {
if(!date1 || !date2) return false; if (!date1 || !date2) return false
return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate(); return (
date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate()
)
} }
const isDateSelected = (date: Date) => { const isDateSelected = (date: Date) => {
if (!selectedDate.value) return false; if (!selectedDate.value) return false
const propDate = new Date(selectedDate.value); const propDate = new Date(selectedDate.value)
return isSameDate(propDate, date); return isSameDate(propDate, date)
} }
const handleScroll = (event) => { const handleScroll = (event) => {
if (event.deltaY > 0) { if (event.deltaY > 0) {
pageDate.value.setMonth(pageDate.value.getMonth() + 1); pageDate.value.setMonth(pageDate.value.getMonth() + 1)
} else { } else {
pageDate.value.setMonth(pageDate.value.getMonth() - 1); pageDate.value.setMonth(pageDate.value.getMonth() - 1)
} }
} }
</script> </script>
<template> <template>
<div class="h-full" @="handleScroll"> <div class="h-full" @="handleScroll">
<div class="grid grid-cols-7"> <div class="grid grid-cols-7">
<div v-for="day in days" :key="day" 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-800"> <div
v-for="day in days"
:key="day"
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-800"
>
{{ day }} {{ day }}
</div> </div>
</div> </div>
<div class="grid relative grid-cols-7 h-full" > <div class="grid relative grid-cols-7 h-full">
<div v-for="date in dates" :key="date" :class="{ <div
'!border-x-2 !border-y-2 border-brand-500': isDateSelected(date), v-for="date in dates"
'!bg-gray-50 !text-gray-400': !isDayInPagedMonth(date), :key="date"
:class="{
}" class="text-right !h-[100%] group grid-cell py-1 text-sm border-b-1 border-r-1 bg-white last:border-r-0 border-gray-200 font-semibold text-gray-800" '!border-x-2 !border-y-2 border-brand-500': isDateSelected(date),
@click="selectDate(date)" '!bg-gray-50 !text-gray-400': !isDayInPagedMonth(date),
}"
class="text-right !h-[100%] group grid-cell py-1 text-sm border-b-1 border-r-1 bg-white last:border-r-0 border-gray-200 font-semibold text-gray-800"
@click="selectDate(date)"
> >
<div class="h-full"> <div class="h-full">
<div class="flex justify-between p-1"> <div class="flex justify-between p-1">
<span :class="{ <span
'block': !isDateSelected(date), :class="{
'hidden': isDateSelected(date), block: !isDateSelected(date),
}" class="group-hover:hidden"></span> hidden: isDateSelected(date),
<NcButton type="secondary" size="small" :class="{ }"
'!block': isDateSelected(date), class="group-hover:hidden"
'!hidden': !isDateSelected(date), ></span>
}" class="!group-hover:block"> <NcButton
<component :is="iconMap.plus" class="h-4 w-4"/> type="secondary"
</NcButton> size="small"
<span class="px-1 py-2">{{ date.getDate() }}</span> :class="{
</div> '!block': isDateSelected(date),
'!hidden': !isDateSelected(date),
}"
class="!group-hover:block"
>
<component :is="iconMap.plus" class="h-4 w-4" />
</NcButton>
<span class="px-1 py-2">{{ date.getDate() }}</span>
</div>
</div> </div>
</div> </div>
<div v-for="event in events" :key="event.Id" :class="[ <div
'absolute w-full mt-16 px-2', v-for="event in events"
`!col-start-[${getGridPosition(event, pageDate).colStart}]`, :key="event.Id"
`!col-span-[${getGridPosition(event, pageDate).colEnd - getGridPosition(event, pageDate).colStart}]`, :class="[
`!row-start-[${getGridPosition(event, pageDate).rowStart}]`, `!col-start-[${getGridPosition(event, pageDate).colStart}]`,
`!row-span-[${getGridPosition(event, pageDate).rowEnd - getGridPosition(event, pageDate).rowStart}]` `!col-span-[${getGridPosition(event, pageDate).colEnd - getGridPosition(event, pageDate).colStart}]`,
]" class="event-display"> `!row-start-[${getGridPosition(event, pageDate).rowStart}]`,
<LazySmartsheetCalendarRecordCard :name="event.Title" :date="event.from_date_time" color="blue" /> `!row-span-[${getGridPosition(event, pageDate).rowEnd - getGridPosition(event, pageDate).rowStart}]`,
]"
class="event-display absolute w-full mt-16 px-2"
>
<LazySmartsheetCalendarRecordCard :name="event.Title" :date="event.from_date_time" color="blue" />
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss"></style>
</style>

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

@ -1,11 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
interface Props { interface Props {
name: string; name: string
date?: string; date?: string
color?: string; color?: string
size? : 'small' | 'medium' | 'large'; size?: 'small' | 'medium' | 'large'
showDate?: boolean; showDate?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -14,13 +13,13 @@ const props = withDefaults(defineProps<Props>(), {
color: 'blue', color: 'blue',
size: 'small', size: 'small',
showDate: true, showDate: true,
}); })
</script> </script>
<template> <template>
<div :class="{ <div
'bg-maroon-50': props.color === 'maroon', :class="{
'bg-maroon-50': props.color === 'maroon',
'bg-blue-50': props.color === 'blue', 'bg-blue-50': props.color === 'blue',
'bg-green-50': props.color === 'green', 'bg-green-50': props.color === 'green',
'bg-yellow-50': props.color === 'yellow', 'bg-yellow-50': props.color === 'yellow',
@ -29,22 +28,25 @@ const props = withDefaults(defineProps<Props>(), {
'h-12': props.size === 'medium', 'h-12': props.size === 'medium',
'h-16': props.size === 'large', 'h-16': props.size === 'large',
'h-8': props.size === 'small', 'h-8': props.size === 'small',
}" class="flex items-center w-full border-1 cursor-pointer border-gray-200 gap-2 items-center px-2 py-3 rounded-lg"> }"
<span :class="{ class="flex items-center w-full border-1 cursor-pointer border-gray-200 gap-2 items-center px-2 py-3 rounded-lg"
'bg-maroon-500': props.color === 'maroon', >
'bg-blue-500': props.color === 'blue', <span
'bg-green-500': props.color === 'green', :class="{
'bg-yellow-500': props.color === 'yellow', 'bg-maroon-500': props.color === 'maroon',
'bg-pink-500': props.color === 'pink', 'bg-blue-500': props.color === 'blue',
'bg-purple-500': props.color === 'purple', 'bg-green-500': props.color === 'green',
}" class="block h-5 w-1 rounded"></span> 'bg-yellow-500': props.color === 'yellow',
'bg-pink-500': props.color === 'pink',
'bg-purple-500': props.color === 'purple',
}"
class="block h-5 w-1 rounded"
></span>
<div class="flex flex-row items-baseline gap-2"> <div class="flex flex-row items-baseline gap-2">
<span class="text-sm font-bold text-gray-800">{{name}}</span> <span class="text-sm font-bold text-gray-800">{{ name }}</span>
<span v-if="showDate" class="text-xs text-gray-600">{{date}}</span> <span v-if="showDate" class="text-xs text-gray-600">{{ date }}</span>
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss"></style>
</style>

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

@ -1,91 +1,115 @@
<script setup lang="ts"> <script setup lang="ts">
import { CalendarViewTypeInj } from '#imports'
import { CalendarViewTypeInj } from "#imports";
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
}>() }>()
const activeCalendarView = inject(CalendarViewTypeInj, ref<'month' | 'day' | 'year' | 'week'>('month' as const)) const emit = defineEmits(['expand-record'])
const {t} = useI18n() const activeCalendarView = inject(CalendarViewTypeInj, ref<'month' | 'day' | 'year' | 'week'>('month' as const))
const emit = defineEmits(['expand-record']); const { t } = useI18n()
const options = computed(() => { const options = computed(() => {
switch (activeCalendarView.value) { switch (activeCalendarView.value) {
case 'day' as const: case 'day' as const:
return [ return [
{label: 'in this day', value: 'day'}, { label: 'in this day', value: 'day' },
{label: 'without dates', value: 'withoutDates'}, { label: 'without dates', value: 'withoutDates' },
{label: 'in selected hours', value: 'selectedHours'}, { label: 'in selected hours', value: 'selectedHours' },
{label: 'all records', value: 'allRecords'}, { label: 'all records', value: 'allRecords' },
] ]
case 'week' as const: case 'week' as const:
return [ return [
{label: 'in this week', value: 'week'}, { label: 'in this week', value: 'week' },
{label: 'without dates', value: 'withoutDates'}, { label: 'without dates', value: 'withoutDates' },
{label: 'in selected hours', value: 'selectedHours'}, { label: 'in selected hours', value: 'selectedHours' },
{label: 'all records', value: 'allRecords'}, { label: 'all records', value: 'allRecords' },
] ]
case 'month' as const: case 'month' as const:
return [ return [
{label: 'in this month', value: 'month'}, { label: 'in this month', value: 'month' },
{label: 'without dates', value: 'withoutDates'}, { label: 'without dates', value: 'withoutDates' },
{label: 'all records', value: 'allRecords'}, { label: 'all records', value: 'allRecords' },
{label: 'in selected date', value: 'selectedDate'}, { label: 'in selected date', value: 'selectedDate' },
] ]
case 'year' as const: case 'year' as const:
return [ return [
{label: 'in this year', value: 'year'}, { label: 'in this year', value: 'year' },
{label: 'without dates', value: 'withoutDates'}, { label: 'without dates', value: 'withoutDates' },
{label: 'all records', value: 'allRecords'}, { label: 'all records', value: 'allRecords' },
{label: 'in selected date', value: 'selectedDate'}, { label: 'in selected date', value: 'selectedDate' },
] ]
} }
}) })
const { pageDate, selectedDate, selectedDateRange } = useCalendarViewStoreOrThrow()
const { pageDate, selectedDate, selectedDateRange } = useCalendarViewStoreOrThrow()
const activeDates = ref([new Date()]) const activeDates = ref([new Date()])
</script> </script>
<template> <template>
<div :class="{ <div
'w-0': !props.visible, :class="{
'w-1/4 min-w-[22.1rem]': props.visible, 'w-0': !props.visible,
'transition-all': true, 'w-1/4 min-w-[22.1rem]': props.visible,
}" class="h-full border-l-1 border-gray-200"> }"
<NcDateWeekSelector v-if="activeCalendarView === ('day' as const)" v-model:page-date="pageDate" v-model:selected-date="selectedDate" :active-dates="activeDates" /> class="h-full border-l-1 border-gray-200 transition-all"
<NcDateWeekSelector v-else-if="activeCalendarView === ('week' as const)" v-model:page-date="pageDate" week-picker v-model:selected-week="selectedDateRange" /> >
<NcMonthYearSelector v-else-if="activeCalendarView === ('month' as const)" v-model:page-date="pageDate" v-model:selected-date="selectedDate" /> <NcDateWeekSelector
<NcMonthYearSelector v-else-if="activeCalendarView === ('year' as const)" year-picker v-model:page-date="pageDate" v-model:selected-date="selectedDate" /> v-if="activeCalendarView === ('day' as const)"
v-model:page-date="pageDate"
v-model:selected-date="selectedDate"
:active-dates="activeDates"
/>
<NcDateWeekSelector
v-else-if="activeCalendarView === ('week' as const)"
v-model:page-date="pageDate"
v-model:selected-week="selectedDateRange"
week-picker
/>
<NcMonthYearSelector
v-else-if="activeCalendarView === ('month' as const)"
v-model:page-date="pageDate"
v-model:selected-date="selectedDate"
/>
<NcMonthYearSelector
v-else-if="activeCalendarView === ('year' as const)"
v-model:page-date="pageDate"
v-model:selected-date="selectedDate"
year-picker
/>
<div class="px-4 flex flex-col gap-y-6 pt-4"> <div class="px-4 flex flex-col gap-y-6 pt-4">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-2xl font-bold">{{ t('objects.Records') }}</span> <span class="text-2xl font-bold">{{ t('objects.Records') }}</span>
<NcSelect :options="options" value="all records"/> <NcSelect :options="options" value="all records" />
</div> </div>
<a-input class="!rounded-lg !border-gray-200 !px-4 !py-2" placeholder="Search your records"> <a-input class="!rounded-lg !border-gray-200 !px-4 !py-2" placeholder="Search your records">
<template #prefix> <template #prefix>
<component :is="iconMap.search" class="h-4 w-4 mr-1 text-gray-500"/> <component :is="iconMap.search" class="h-4 w-4 mr-1 text-gray-500" />
</template> </template>
</a-input> </a-input>
<div :class="{ <div
:class="{
'h-[calc(100vh-40rem)]': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const), 'h-[calc(100vh-40rem)]': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const),
'h-[calc(100vh-29rem)]': activeCalendarView === ('month' as const) || activeCalendarView === ('year' as const), 'h-[calc(100vh-29rem)]': activeCalendarView === ('month' as const) || activeCalendarView === ('year' as const),
}" class="gap-2 flex flex-col nc-scrollbar-md overflow-y-auto nc-calendar-top-height"> }"
<LazySmartsheetCalendarSideRecordCard v-for="(x, id) in Array(50)" :color="x%2 === 0 ? 'maroon': 'blue'" class="gap-2 flex flex-col nc-scrollbar-md overflow-y-auto nc-calendar-top-height"
date="27 April 2003" name="Saturday HackNight" @click="emit('expand-record', id)"/> >
<LazySmartsheetCalendarSideRecordCard
v-for="(x, id) in Array(50)"
:key="id"
:color="x % 2 === 0 ? 'maroon' : 'blue'"
date="27 April 2003"
name="Saturday HackNight"
@click="emit('expand-record', id)"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss"></style>
</style>

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

@ -1,10 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
interface Props { interface Props {
name: string; name: string
date?: string; date?: string
color?: string; color?: string
showDate?: boolean; showDate?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -12,27 +11,27 @@ const props = withDefaults(defineProps<Props>(), {
date: '', date: '',
color: 'blue', color: 'blue',
showDate: true, showDate: true,
}); })
</script> </script>
<template> <template>
<div class="flex border-1 cursor-pointer border-gray-200 items-center px-2 py-3 rounded-lg"> <div class="flex border-1 cursor-pointer border-gray-200 items-center px-2 py-3 rounded-lg">
<span :class="{ <span
'bg-maroon-500': props.color === 'maroon', :class="{
'bg-blue-500': props.color === 'blue', 'bg-maroon-500': props.color === 'maroon',
'bg-green-500': props.color === 'green', 'bg-blue-500': props.color === 'blue',
'bg-yellow-500': props.color === 'yellow', 'bg-green-500': props.color === 'green',
'bg-pink-500': props.color === 'pink', 'bg-yellow-500': props.color === 'yellow',
'bg-purple-500': props.color === 'purple', 'bg-pink-500': props.color === 'pink',
}" class="block h-10 w-1 rounded"></span> 'bg-purple-500': props.color === 'purple',
}"
class="block h-10 w-1 rounded"
></span>
<div class="flex flex-col gap-1 ml-3"> <div class="flex flex-col gap-1 ml-3">
<span class="text-sm font-bold text-gray-700">{{name}}</span> <span class="text-sm font-bold text-gray-700">{{ name }}</span>
<span v-if="showDate" class="text-xs text-gray-500">{{date}}</span> <span v-if="showDate" class="text-xs text-gray-500">{{ date }}</span>
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss"></style>
</style>

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

@ -1,8 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
const {t} = useI18n()
const { pageDate, selectedDate, selectedDateRange } = useCalendarViewStoreOrThrow() const { pageDate, selectedDate, selectedDateRange } = useCalendarViewStoreOrThrow()
const months = computed(() => { const months = computed(() => {
@ -13,17 +9,19 @@ const months = computed(() => {
} }
return months return months
}) })
</script> </script>
<template> <template>
<div class="grid justify-items-center gap-6 grid-cols-3 overflow-auto nc-scrollbar-md"> <div class="grid justify-items-center gap-6 grid-cols-3 overflow-auto nc-scrollbar-md">
<NcDateWeekSelector v-for="(month, index) in months" disable-pagination :key="month" v-model:page-date="months[index]" v-model:selected-date="selectedDate" class="max-w-[350px]"/> <NcDateWeekSelector
v-for="(month, index) in months"
:key="month"
v-model:page-date="months[index]"
v-model:selected-date="selectedDate"
disable-pagination
class="max-w-[350px]"
/>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss"></style>
</style>

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

@ -1,20 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import dayjs from 'dayjs'
import { import {
ActiveViewInj, ActiveViewInj,
CalendarViewTypeInj, CalendarViewTypeInj,
inject,
IsCalendarInj, IsCalendarInj,
IsFormInj, IsFormInj,
IsGalleryInj, IsGalleryInj,
IsGridInj, IsGridInj,
IsKanbanInj, IsKanbanInj,
MetaInj, MetaInj,
inject,
provide, provide,
ref, ref,
useI18n, useI18n,
} from '#imports' } from '#imports'
import dayjs from "dayjs";
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
@ -22,7 +21,7 @@ const view = inject(ActiveViewInj, ref())
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const {t} = useI18n() const { t } = useI18n()
provide(IsFormInj, ref(false)) provide(IsFormInj, ref(false))
@ -44,17 +43,16 @@ const {
activeCalendarView, activeCalendarView,
addEmptyRow, addEmptyRow,
paginationData, paginationData,
paginateCalendarView paginateCalendarView,
} = useCalendarViewStoreOrThrow() } = useCalendarViewStoreOrThrow()
provide(CalendarViewTypeInj, activeCalendarView) provide(CalendarViewTypeInj, activeCalendarView)
const showSideMenu = ref(true) const showSideMenu = ref(true)
const isExpanded = ref(false) const isExpanded = ref(false)
const expandedRecordId = ref<string | null>(null); const expandedRecordId = ref<string | null>(null)
const expandRecord = (id: string) => { const expandRecord = (id: string) => {
isExpanded.value = true isExpanded.value = true
@ -66,52 +64,56 @@ const headerText = computed(() => {
case 'day': case 'day':
return dayjs(selectedDate.value).format('D MMMM YYYY') return dayjs(selectedDate.value).format('D MMMM YYYY')
case 'week': case 'week':
return dayjs(selectedDateRange.value.start).format('D MMMM YYYY') + ' - ' + dayjs(selectedDateRange.value.end).format('D MMMM YYYY') return `${dayjs(selectedDateRange.value.start).format('D MMMM YYYY')} - ${dayjs(selectedDateRange.value.end).format(
'D MMMM YYYY',
)}`
case 'month': case 'month':
return dayjs(selectedDate.value).format('MMMM YYYY') return dayjs(selectedDate.value).format('MMMM YYYY')
case 'year': case 'year':
return dayjs(selectedDate.value).format('YYYY') return dayjs(selectedDate.value).format('YYYY')
} }
}) })
</script> </script>
<template> <template>
<div class="flex h-full flex-row"> <div class="flex h-full flex-row">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class="flex justify-between p-3 items-center border-b-1 border-gray-200"> <div class="flex justify-between p-3 items-center border-b-1 border-gray-200">
<div class="flex justify-start gap-3 items-center"> <div class="flex justify-start gap-3 items-center">
<NcButton size="small" type="secondary" @click="paginateCalendarView('prev')"> <NcButton size="small" type="secondary" @click="paginateCalendarView('prev')">
<component :is="iconMap.doubleLeftArrow" class="h-4 w-4"/> <component :is="iconMap.doubleLeftArrow" class="h-4 w-4" />
</NcButton> </NcButton>
<span class="font-bold text-gray-700">{{headerText}}</span> <span class="font-bold text-gray-700">{{ headerText }}</span>
<NcButton size="small" type="secondary" @click="paginateCalendarView('next')" > <NcButton size="small" type="secondary" @click="paginateCalendarView('next')">
<component :is="iconMap.doubleRightArrow" class="h-4 w-4"/> <component :is="iconMap.doubleRightArrow" class="h-4 w-4" />
</NcButton> </NcButton>
</div> </div>
<NcButton v-if="!isMobileMode" size="small" type="secondary" @click="showSideMenu = !showSideMenu"> <NcButton v-if="!isMobileMode" size="small" type="secondary" @click="showSideMenu = !showSideMenu">
<component :is="iconMap.sidebarMinimise" :class="{ <component
'transform rotate-180': showSideMenu, :is="iconMap.sidebarMinimise"
}" class="h-4 w-4 transition-all"/> :class="{
'transform rotate-180': showSideMenu,
}"
class="h-4 w-4 transition-all"
/>
</NcButton> </NcButton>
</div> </div>
<LazySmartsheetCalendarYearView v-if="activeCalendarView === 'year'" class="flex-grow-1" /> <LazySmartsheetCalendarYearView v-if="activeCalendarView === 'year'" class="flex-grow-1" />
<LazySmartsheetCalendarMonthView v-else-if="activeCalendarView === 'month'" class="flex-grow-1" /> <LazySmartsheetCalendarMonthView v-else-if="activeCalendarView === 'month'" class="flex-grow-1" />
<LazySmartsheetCalendarDayView v-else-if="activeCalendarView === 'day'" class="flex-grow-1" /> <LazySmartsheetCalendarDayView v-else-if="activeCalendarView === 'day'" class="flex-grow-1" />
</div> </div>
<LazySmartsheetCalendarSideMenu v-if="!isMobileMode" :visible="showSideMenu" @expand-record="expandRecord"/> <LazySmartsheetCalendarSideMenu v-if="!isMobileMode" :visible="showSideMenu" @expand-record="expandRecord" />
</div> </div>
<LazySmartsheetExpandedForm v-model="isExpanded" :view="view" :row="{ <LazySmartsheetExpandedForm
row: {}, v-model="isExpanded"
rowMeta: { :view="view"
new: !expandedRecordId, :row="{
}, row: {},
rowMeta: {
}" :meta="meta" /> new: !expandedRecordId,
},
}"
</template> :meta="meta"
/>
</template>

2
packages/nc-gui/components/smartsheet/grid/index.vue

@ -5,8 +5,8 @@ import GroupBy from './GroupBy.vue'
import { import {
ActiveViewInj, ActiveViewInj,
FieldsInj, FieldsInj,
IsFormInj,
IsCalendarInj, IsCalendarInj,
IsFormInj,
IsGalleryInj, IsGalleryInj,
IsGridInj, IsGridInj,
MetaInj, MetaInj,

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

@ -1,36 +1,33 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useCalendarViewStoreOrThrow } from '#imports'
import { useCalendarViewStoreOrThrow } from "#imports"; const { changeCalendarView, activeCalendarView } = useCalendarViewStoreOrThrow()
const highlightStyle = ref({ left: '0px' });
const highlightStyle = ref({ left: '0px' })
const setActiveCalendarMode = (mode: 'day' | 'week' | 'month' | 'year', event: MouseEvent) => { const setActiveCalendarMode = (mode: 'day' | 'week' | 'month' | 'year', event: MouseEvent) => {
changeCalendarView(mode); changeCalendarView(mode)
const tabElement = event.target as HTMLElement; const tabElement = event.target as HTMLElement
highlightStyle.value.left = `${tabElement.offsetLeft}px`; highlightStyle.value.left = `${tabElement.offsetLeft}px`
}; }
const { changeCalendarView, activeCalendarView } = useCalendarViewStoreOrThrow()
</script> </script>
<template> <template>
<div class="flex flex-row relative p-1 mx-3 mt-3 mb-3 bg-gray-100 rounded-lg gap-x-0.5 nc-calendar-mode-tab"> <div class="flex flex-row relative p-1 mx-3 mt-3 mb-3 bg-gray-100 rounded-lg gap-x-0.5 nc-calendar-mode-tab">
<div class="highlight" :style="highlightStyle"></div> <div class="highlight" :style="highlightStyle"></div>
<div v-for="mode in ['day', 'week', 'month', 'year']" :key="mode" <div
:class="{ active: activeCalendarView === mode }" v-for="mode in ['day', 'week', 'month', 'year']"
class="tab" :key="mode"
@click="setActiveCalendarMode(mode, $event)" :class="{ active: activeCalendarView === mode }"
class="tab"
@click="setActiveCalendarMode(mode, $event)"
> >
<div class="tab-title nc-tab">{{ $t(`objects.${mode}`) }}</div> <div class="tab-title nc-tab">{{ $t(`objects.${mode}`) }}</div>
</div>
</div> </div>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.highlight { .highlight {
@apply absolute h-7.5 w-20 shadow bg-white rounded-lg transition-all duration-300; @apply absolute h-7.5 w-20 shadow bg-white rounded-lg transition-all duration-300;
z-index: 0; z-index: 0;
@ -55,6 +52,4 @@ const { changeCalendarView, activeCalendarView } = useCalendarViewStoreOrThrow()
.nc-calendar-mode-tab { .nc-calendar-mode-tab {
@apply -ml-120 relative; @apply -ml-120 relative;
} }
</style>
</style>

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

@ -1,25 +1,23 @@
<script lang="ts" setup> <script lang="ts" setup>
import {isSystemColumn, UITypes} from 'nocodb-sdk' import { UITypes, isSystemColumn } from 'nocodb-sdk'
import type {SelectProps} from 'ant-design-vue' import type { SelectProps } from 'ant-design-vue'
import { import {
ActiveViewInj, ActiveViewInj,
computed,
iconMap,
inject,
IsLockedInj, IsLockedInj,
IsPublicInj, IsPublicInj,
MetaInj, MetaInj,
computed,
iconMap,
inject,
ref, ref,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useViewColumnsOrThrow, useViewColumnsOrThrow,
watch, watch,
} from '#imports' } from '#imports'
const {eventBus} = useSmartsheetStoreOrThrow()
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const {$e, $api} = useNuxtApp() const { $api } = useNuxtApp()
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
@ -27,25 +25,24 @@ const isLocked = inject(IsLockedInj, ref(false))
const IsPublic = inject(IsPublicInj, ref(false)) const IsPublic = inject(IsPublicInj, ref(false))
const {fields, loadViewColumns, metaColumnById} = useViewColumnsOrThrow() const { fields, loadViewColumns } = useViewColumnsOrThrow()
const calendarRangeDropdown = ref(false) const calendarRangeDropdown = ref(false)
watch( watch(
() => activeView.value?.id, () => activeView.value?.id,
async (newVal, oldVal) => { async (newVal, oldVal) => {
if (newVal !== oldVal && meta.value) { if (newVal !== oldVal && meta.value) {
await loadViewColumns() await loadViewColumns()
} }
}, },
{immediate: true}, { immediate: true },
) )
const calendarRange = computed<{ fk_from_column_id: string; fk_to_column_id: string | null }[]>(() => { const calendarRange = computed<{ fk_from_column_id: string; fk_to_column_id: string | null }[]>(() => {
const tempCalendarRange: { fk_from_column_id: string; fk_to_column_id: string | null }[] = []; const tempCalendarRange: { fk_from_column_id: string; fk_to_column_id: string | null }[] = []
if (!activeView.value || !activeView.value.view) return tempCalendarRange; if (!activeView.value || !activeView.value.view) return tempCalendarRange
activeView.value.view.calendar_range?.forEach((range) => { activeView.value.view.calendar_range?.forEach((range) => {
tempCalendarRange.push({ tempCalendarRange.push({
fk_from_column_id: range.fk_from_column_id, fk_from_column_id: range.fk_from_column_id,
@ -59,23 +56,21 @@ const calendarRange = computed<{ fk_from_column_id: string; fk_to_column_id: str
const _calendar_ranges = ref<{ fk_from_column_id: string; fk_to_column_id: string | null }[]>(calendarRange.value) const _calendar_ranges = ref<{ fk_from_column_id: string; fk_to_column_id: string | null }[]>(calendarRange.value)
const saveCalendarRanges = async () => { const saveCalendarRanges = async () => {
if(activeView.value) { if (activeView.value) {
try { try {
const calRanges = _calendar_ranges.value.map((range) => { const calRanges = _calendar_ranges.value
if (range.fk_from_column_id) { .filter((range) => range.fk_from_column_id)
return { .map((range) => ({
fk_from_column_id: range.fk_from_column_id, fk_from_column_id: range.fk_from_column_id,
fk_to_column_id: range.fk_to_column_id, fk_to_column_id: range.fk_to_column_id,
} }))
} await $api.dbView.calendarUpdate(activeView.value?.id as string, {
}) calendar_range: calRanges as { fk_from_column_id: string; fk_to_column_id: string | null }[],
await $api.dbView.calendarUpdate(activeView.value?.id as string, { })
calendar_range: calRanges as { fk_from_column_id: string; fk_to_column_id: string | null }[], } catch (e) {
}) console.log(e)
} catch (e) { message.error('There was an error while updating view!')
console.log(e) }
message.error('There was an error while updating view!')
}
} else { } else {
message.error('Please select a view first') message.error('Please select a view first')
} }
@ -90,27 +85,23 @@ const addCalendarRange = async () => {
} }
const dateFieldOptions = computed<SelectProps['options']>(() => { const dateFieldOptions = computed<SelectProps['options']>(() => {
return meta.value?.columns return (
?.filter((c) => { meta.value?.columns
if (c.uidt === UITypes.Date || c.uidt === UITypes.DateTime && !isSystemColumn(c)) { ?.filter((c) => c.uidt === UITypes.Date || (c.uidt === UITypes.DateTime && !isSystemColumn(c)))
return true
}
})
.map((c) => ({ .map((c) => ({
label: c.title, label: c.title,
value: c.id, value: c.id,
})) ?? [] })) ?? []
)
}) })
</script> </script>
<template> <template>
<NcDropdown v-if="!IsPublic" v-model:visible="calendarRangeDropdown" :trigger="['click']" class="!xs:hidden"> <NcDropdown v-if="!IsPublic" v-model:visible="calendarRangeDropdown" :trigger="['click']" class="!xs:hidden">
<div class="nc-calendar-btn"> <div class="nc-calendar-btn">
<a-button v-e="['c:calendar:change-calendar-range']" :disabled="isLocked" <a-button v-e="['c:calendar:change-calendar-range']" :disabled="isLocked" class="nc-calendar-range-btn nc-toolbar-btn">
class="nc-calendar-range-btn nc-toolbar-btn">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<component :is="iconMap.calendar" class="h-4 w-4"/> <component :is="iconMap.calendar" class="h-4 w-4" />
<span class="text-capitalize !text-sm font-medium"> <span class="text-capitalize !text-sm font-medium">
{{ $t('activity.viewSettings') }} {{ $t('activity.viewSettings') }}
</span> </span>
@ -118,55 +109,54 @@ const dateFieldOptions = computed<SelectProps['options']>(() => {
</a-button> </a-button>
</div> </div>
<template #overlay> <template #overlay>
<div <div v-if="calendarRangeDropdown" class="flex flex-col w-full p-6 w-[40rem]" @click.stop>
v-if="calendarRangeDropdown"
class="flex flex-col w-full p-6 w-[40rem]"
@click.stop
>
<div> <div>
<span class="font-bold"> {{ $t('activity.calendar') + $t('activity.viewSettings') }}</span> <span class="font-bold"> {{ $t('activity.calendar') + $t('activity.viewSettings') }}</span>
<a-divider class="!my-2"/> <a-divider class="!my-2" />
</div> </div>
<div v-for="cal in _calendar_ranges" class="flex w-full gap-3"> <div v-for="(cal, id) in _calendar_ranges" :key="id" class="flex w-full gap-3">
<div class="flex flex-col gap-2 w-1/2"> <div class="flex flex-col gap-2 w-1/2">
<span> <span>
{{ $t('labels.organizeRecordsBy') }} {{ $t('labels.organizeRecordsBy') }}
</span> </span>
<NcSelect <NcSelect
v-model:value="cal.fk_from_column_id" v-model:value="cal.fk_from_column_id"
:options="dateFieldOptions" :options="dateFieldOptions"
class="w-full" class="w-full"
@click.stop @click.stop
@change="saveCalendarRanges" @change="saveCalendarRanges"
/> />
</div> </div>
<div v-if="cal.fk_to_column_id === null && isEeUI" class="flex flex-col justify-end w-1/2"> <div v-if="cal.fk_to_column_id === null && isEeUI" class="flex flex-col justify-end w-1/2">
<div class="cursor-pointer flex items-center font-medium gap-1 mb-1" <div class="cursor-pointer flex items-center font-medium gap-1 mb-1" @click="cal.fk_to_column_id = ''">
@click="cal.fk_to_column_id = ''"> <component :is="iconMap.plus" class="h-4 w-4" />
<component :is="iconMap.plus" class="h-4 w-4"/>
{{ $t('activity.setEndDate') }} {{ $t('activity.setEndDate') }}
</div> </div>
</div> </div>
<div v-else-if="isEeUI" class="flex flex-col gap-2 w-1/2"> <div v-else-if="isEeUI" class="flex flex-col gap-2 w-1/2">
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<span> <span>
{{ $t('labels.endDateField') }} {{ $t('labels.endDateField') }}
</span> </span>
<component :is="iconMap.delete" class="h-4 w-4 cursor-pointer text-red-500" <component
@click="() => { :is="iconMap.delete"
cal.fk_to_column_id = null class="h-4 w-4 cursor-pointer text-red-500"
saveCalendarRanges() @click="
}"/> () => {
cal.fk_to_column_id = null
saveCalendarRanges()
}
"
/>
</div> </div>
<NcSelect <NcSelect
v-model:value="cal.fk_to_column_id" v-model:value="cal.fk_to_column_id"
:options="dateFieldOptions" :options="dateFieldOptions"
:disabled="!cal.fk_from_column_id" :disabled="!cal.fk_from_column_id"
:placeholder="$t('placeholder.notSelected')" :placeholder="$t('placeholder.notSelected')"
class="w-full" class="w-full"
@click.stop @click.stop
@change="saveCalendarRanges" @change="saveCalendarRanges"
/> />
</div> </div>
</div> </div>

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

@ -1,17 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import {CalendarType, ColumnType, GalleryType, isVirtualCol, KanbanType, UITypes, ViewTypes} from 'nocodb-sdk' import type { CalendarType, ColumnType, GalleryType, KanbanType } from 'nocodb-sdk'
import { UITypes, ViewTypes, isVirtualCol } from 'nocodb-sdk'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import type {SelectProps} from 'ant-design-vue' import type { SelectProps } from 'ant-design-vue'
import { import {
ActiveViewInj, ActiveViewInj,
computed,
FieldsInj, FieldsInj,
iconMap,
inject,
IsLockedInj, IsLockedInj,
IsPublicInj, IsPublicInj,
computed,
iconMap,
inject,
ref, ref,
resolveComponent, resolveComponent,
useMenuCloseOnEsc, useMenuCloseOnEsc,
@ -150,7 +151,9 @@ const coverOptions = computed<SelectProps['options']>(() => {
const updateCoverImage = async (val?: string | null) => { const updateCoverImage = async (val?: string | null) => {
if ( if (
(activeView.value?.type === ViewTypes.GALLERY || activeView.value?.type === ViewTypes.KANBAN || activeView.value?.type === ViewTypes.CALENDAR) && (activeView.value?.type === ViewTypes.GALLERY ||
activeView.value?.type === ViewTypes.KANBAN ||
activeView.value?.type === ViewTypes.CALENDAR) &&
activeView.value?.id && activeView.value?.id &&
activeView.value?.view activeView.value?.view
) { ) {
@ -177,7 +180,10 @@ const updateCoverImage = async (val?: string | null) => {
const coverImageColumnId = computed({ const coverImageColumnId = computed({
get: () => { get: () => {
const fk_cover_image_col_id = const fk_cover_image_col_id =
(activeView.value?.type === ViewTypes.GALLERY || activeView.value?.type === ViewTypes.KANBAN || activeView.value?.type === ViewTypes.CALENDAR) && activeView.value?.view (activeView.value?.type === ViewTypes.GALLERY ||
activeView.value?.type === ViewTypes.KANBAN ||
activeView.value?.type === ViewTypes.CALENDAR) &&
activeView.value?.view
? (activeView.value?.view as GalleryType).fk_cover_image_col_id ? (activeView.value?.view as GalleryType).fk_cover_image_col_id
: undefined : undefined
// check if `fk_cover_image_col_id` is in `coverOptions` // check if `fk_cover_image_col_id` is in `coverOptions`
@ -301,7 +307,11 @@ useMenuCloseOnEsc(open)
<a-button v-e="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked"> <a-button v-e="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<GeneralIcon <GeneralIcon
v-if="activeView?.type === ViewTypes.KANBAN || activeView?.type === ViewTypes.GALLERY || activeView?.type === ViewTypes.CALENDAR" v-if="
activeView?.type === ViewTypes.KANBAN ||
activeView?.type === ViewTypes.GALLERY ||
activeView?.type === ViewTypes.CALENDAR
"
icon="creditCard" icon="creditCard"
class="h-4 w-4" class="h-4 w-4"
/> />
@ -310,7 +320,12 @@ useMenuCloseOnEsc(open)
<!-- Fields --> <!-- Fields -->
<span v-if="!isMobileMode" class="text-capitalize text-sm font-medium"> <span v-if="!isMobileMode" class="text-capitalize text-sm font-medium">
<template <template
v-if="activeView?.type === ViewTypes.KANBAN || activeView?.type === ViewTypes.GALLERY || activeView?.type === ViewTypes.CALENDAR"> v-if="
activeView?.type === ViewTypes.KANBAN ||
activeView?.type === ViewTypes.GALLERY ||
activeView?.type === ViewTypes.CALENDAR
"
>
{{ $t('title.editCards') }} {{ $t('title.editCards') }}
</template> </template>
<template v-else> <template v-else>
@ -327,7 +342,13 @@ useMenuCloseOnEsc(open)
<template #overlay> <template #overlay>
<div class="p-4 pr-0 bg-white w-90 rounded-2xl nc-table-toolbar-menu" data-testid="nc-fields-menu" @click.stop> <div class="p-4 pr-0 bg-white w-90 rounded-2xl nc-table-toolbar-menu" data-testid="nc-fields-menu" @click.stop>
<div <div
v-if="!filterQuery && !isPublic && (activeView?.type === ViewTypes.GALLERY || activeView?.type === ViewTypes.KANBAN || activeView?.type === ViewTypes.CALENDAR)" v-if="
!filterQuery &&
!isPublic &&
(activeView?.type === ViewTypes.GALLERY ||
activeView?.type === ViewTypes.KANBAN ||
activeView?.type === ViewTypes.CALENDAR)
"
class="flex flex-col gap-y-2 pr-4 mb-6" class="flex flex-col gap-y-2 pr-4 mb-6"
> >
<div class="flex text-sm select-none">Select cover image field</div> <div class="flex text-sm select-none">Select cover image field</div>

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

@ -1,23 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import type {ColumnType, LinkToAnotherRecordType, TableType} from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import {isLinksOrLTAR, UITypes} from 'nocodb-sdk' import { UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import type {TabItem} from '#imports' import type { TabItem } from '#imports'
import { import {
ActiveViewInj, ActiveViewInj,
computed,
createEventHook,
FieldsInj, FieldsInj,
IsFormInj, IsFormInj,
IsLockedInj, IsLockedInj,
MetaInj, MetaInj,
OpenNewRecordFormHookInj, OpenNewRecordFormHookInj,
provide,
ReadonlyInj, ReadonlyInj,
ref,
ReloadViewDataHookInj, ReloadViewDataHookInj,
ReloadViewMetaHookInj, ReloadViewMetaHookInj,
TabMetaInj, TabMetaInj,
computed,
createEventHook,
provide,
ref,
toRef, toRef,
useMetas, useMetas,
useProvideCalendarViewStore, useProvideCalendarViewStore,

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

@ -1,162 +1,169 @@
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import {ColumnType, CalendarType, PaginatedType, TableType, ViewType, UITypes} from 'nocodb-sdk' import type { CalendarType, PaginatedType, ViewType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { addDays, addMonths, addYears } from '../utils'
import { IsPublicInj, ref, storeToRefs, useBase, useInjectionState, useMetas } from '#imports' import { IsPublicInj, ref, storeToRefs, useBase, useInjectionState, useMetas } from '#imports'
import type { Row } from '#imports' import type { Row } from '#imports'
import {addDays, addMonths, addWeeks, addYears} from "../utils";
const formatData = (list: Record<string, any>[]) => const formatData = (list: Record<string, any>[]) =>
list.map( list.map(
(row) => (row) =>
({ ({
row: { ...row }, row: { ...row },
oldRow: { ...row }, oldRow: { ...row },
rowMeta: {}, rowMeta: {},
} as Row), } as Row),
) )
const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState( const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
( (
meta: Ref<(CalendarType & { id: string }) | undefined>, meta: Ref<(CalendarType & { id: string }) | undefined>,
viewMeta: Ref<(ViewType | CalendarType | undefined) & { id: string }> | ComputedRef<(ViewType & { id: string }) | undefined>, viewMeta: Ref<(ViewType | CalendarType | undefined) & { id: string }> | ComputedRef<(ViewType & { id: string }) | undefined>,
shared = false, shared = false,
where?: ComputedRef<string | undefined>, where?: ComputedRef<string | undefined>,
) => { ) => {
if (!meta) { if (!meta) {
throw new Error('Table meta is not available') throw new Error('Table meta is not available')
} }
const pageDate = ref<Date> (new Date()) const pageDate = ref<Date>(new Date())
const activeCalendarView = ref<'month' | 'year' | 'day' | 'week'>('month') const activeCalendarView = ref<'month' | 'year' | 'day' | 'week'>('month')
const calDataType = ref<UITypes.Date | UITypes.DateTime>(UITypes.Date) const calDataType = ref<UITypes.Date | UITypes.DateTime>(UITypes.Date)
const selectedDate = ref<Date>(new Date()) const selectedDate = ref<Date>(new Date())
const selectedDateRange = ref<{ const selectedDateRange = ref<{
start: Date | null start: Date | null
end: Date | null end: Date | null
}>({start: new Date(), end: null}) }>({ start: new Date(), end: null })
const defaultPageSize = 1000 const defaultPageSize = 1000
const formattedData = ref<Row[]>([]) const formattedData = ref<Row[]>([])
const { api } = useApi() const { api } = useApi()
const { base } = storeToRefs(useBase()) const { base } = storeToRefs(useBase())
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const isPublic = ref(shared) || inject(IsPublicInj, ref(false)) const isPublic = ref(shared) || inject(IsPublicInj, ref(false))
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow() const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()
const { sharedView, fetchSharedViewData } = useSharedView() const { sharedView, fetchSharedViewData } = useSharedView()
const calendarMetaData = ref<CalendarType>({}) const calendarMetaData = ref<CalendarType>({})
// Not Required in Calendar View TODO: Remove // Not Required in Calendar View TODO: Remove
// const geoDataFieldColumn = ref<ColumnType | undefined>() // const geoDataFieldColumn = ref<ColumnType | undefined>()
const paginationData = ref<PaginatedType>({ page: 1, pageSize: defaultPageSize }) const paginationData = ref<PaginatedType>({ page: 1, pageSize: defaultPageSize })
const queryParams = computed(() => ({ const queryParams = computed(() => ({
limit: paginationData.value.pageSize ?? defaultPageSize, limit: paginationData.value.pageSize ?? defaultPageSize,
where: where?.value ?? '', where: where?.value ?? '',
})) }))
const changeCalendarView = (view: 'month' | 'year' | 'day' | 'week') => { const changeCalendarView = (view: 'month' | 'year' | 'day' | 'week') => {
activeCalendarView.value = view activeCalendarView.value = view
} }
async function loadCalendarMeta() { async function loadCalendarMeta() {
if (!viewMeta?.value?.id || !meta?.value?.columns) return if (!viewMeta?.value?.id || !meta?.value?.columns) return
// TODO: Fetch Calendar Meta // TODO: Fetch Calendar Meta
// calendarMetaData.value = isPublic.value ? (sharedView.value?.view as CalendarType) : await $api.dbView.calendarRead(viewMeta.value.id) // calendarMetaData.value = isPublic.value ? (sharedView.value?.view as CalendarType) : await $api.dbView.calendarRead(viewMeta.value.id)
calendarMetaData.value = isPublic.value ? (sharedView.value?.view as CalendarType) : await $api.dbView.mapRead(viewMeta.value.id) calendarMetaData.value = isPublic.value
? (sharedView.value?.view as CalendarType)
: await $api.dbView.mapRead(viewMeta.value.id)
/*geoDataFieldColumn.value = /* geoDataFieldColumn.value =
(meta.value.columns as ColumnType[]).filter((f) => f.id === mapMetaData.value.fk_geo_data_col_id)[0] || {} (meta.value.columns as ColumnType[]).filter((f) => f.id === mapMetaData.value.fk_geo_data_col_id)[0] || {}
*/ } */
}
async function loadCalendarData() {
if ((!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic?.value) return async function loadCalendarData() {
if ((!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic?.value) return
const res = !isPublic.value
? await api.dbViewRow.list('noco', base.value.id!, meta.value!.id!, viewMeta.value!.id!, { const res = !isPublic.value
...queryParams.value, ? await api.dbViewRow.list('noco', base.value.id!, meta.value!.id!, viewMeta.value!.id!, {
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), ...queryParams.value,
where: where?.value, ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
}) where: where?.value,
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value }) })
formattedData.value = formatData(res!.list) : await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value })
} formattedData.value = formatData(res!.list)
}
async function updateCalendarMeta(updateObj: Partial<CalendarType>) {
if (!viewMeta?.value?.id || !isUIAllowed('dataEdit')) return async function updateCalendarMeta(updateObj: Partial<CalendarType>) {
// await $api.dbView.calendarUpdate(viewMeta.value.id, updateObj) if (!viewMeta?.value?.id || !isUIAllowed('dataEdit')) return
await $api.dbView.mapUpdate(viewMeta.value.id, updateObj) // await $api.dbView.calendarUpdate(viewMeta.value.id, updateObj)
} await $api.dbView.mapUpdate(viewMeta.value.id, updateObj)
}
const paginateCalendarView = (action: 'next' | 'prev') => {
switch (activeCalendarView.value) { const paginateCalendarView = (action: 'next' | 'prev') => {
case 'month': switch (activeCalendarView.value) {
selectedDate.value = action === 'next' ? addMonths(selectedDate.value, 1) : addMonths(selectedDate.value, -1) case 'month':
if(pageDate.value.getFullYear() !== selectedDate.value.getFullYear()) { selectedDate.value = action === 'next' ? addMonths(selectedDate.value, 1) : addMonths(selectedDate.value, -1)
pageDate.value = selectedDate.value if (pageDate.value.getFullYear() !== selectedDate.value.getFullYear()) {
} pageDate.value = selectedDate.value
break }
case 'year': break
selectedDate.value = action === 'next' ? addYears(selectedDate.value, 1) : addYears(selectedDate.value, -1) case 'year':
if(pageDate.value.getFullYear() !== selectedDate.value.getFullYear()) { selectedDate.value = action === 'next' ? addYears(selectedDate.value, 1) : addYears(selectedDate.value, -1)
pageDate.value = selectedDate.value if (pageDate.value.getFullYear() !== selectedDate.value.getFullYear()) {
} pageDate.value = selectedDate.value
break }
case 'day': break
selectedDate.value = action === 'next' ? addDays(selectedDate.value, 1) : addDays(selectedDate.value, -1) case 'day':
if(pageDate.value.getFullYear() !== selectedDate.value.getFullYear()) { selectedDate.value = action === 'next' ? addDays(selectedDate.value, 1) : addDays(selectedDate.value, -1)
pageDate.value = selectedDate.value if (pageDate.value.getFullYear() !== selectedDate.value.getFullYear()) {
} else if(pageDate.value.getMonth() !== selectedDate.value.getMonth()) { pageDate.value = selectedDate.value
pageDate.value = selectedDate.value } else if (pageDate.value.getMonth() !== selectedDate.value.getMonth()) {
} pageDate.value = selectedDate.value
break }
case 'week': break
selectedDateRange.value = action === 'next' ? { case 'week':
start: addDays(selectedDateRange.value.start!, 7), selectedDateRange.value =
end: addDays(selectedDateRange.value.end!, 7) action === 'next'
} : { ? {
start: addDays(selectedDateRange.value.start!, -7), start: addDays(selectedDateRange.value.start!, 7),
end: addDays(selectedDateRange.value.end!, -7) end: addDays(selectedDateRange.value.end!, 7),
} }
break : {
} start: addDays(selectedDateRange.value.start!, -7),
} end: addDays(selectedDateRange.value.end!, -7),
}
return { break
formattedData, }
changeCalendarView, }
calDataType,
loadCalendarMeta, return {
updateCalendarMeta, formattedData,
calendarMetaData, changeCalendarView,
activeCalendarView, calDataType,
pageDate, loadCalendarMeta,
paginationData, updateCalendarMeta,
selectedDate, calendarMetaData,
selectedDateRange, activeCalendarView,
paginateCalendarView, pageDate,
} paginationData,
}, selectedDate,
selectedDateRange,
paginateCalendarView,
}
},
) )
export { useProvideCalendarViewStore } export { useProvideCalendarViewStore }
export function useCalendarViewStoreOrThrow() { export function useCalendarViewStoreOrThrow() {
const calendarViewStore = useCalendarViewStore() const calendarViewStore = useCalendarViewStore()
if (calendarViewStore == null) throw new Error('Please call `useProvideCalendarViewStore` on the appropriate parent component') if (calendarViewStore == null) throw new Error('Please call `useProvideCalendarViewStore` on the appropriate parent component')
return calendarViewStore return calendarViewStore
} }

2
packages/nc-gui/utils/dateTimeUtils.ts

@ -110,4 +110,4 @@ export function addYears(date: Date, years: number) {
const d = new Date(date) const d = new Date(date)
d.setFullYear(d.getFullYear() + years) d.setFullYear(d.getFullYear() + years)
return d return d
} }

22
packages/nocodb/src/controllers/calendars.controller.spec.ts

@ -4,18 +4,18 @@ import { CalendarsController } from './calendars.controller';
import type { TestingModule } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing';
describe('CalendarsController', () => { describe('CalendarsController', () => {
let controller: CalendarsController; let controller: CalendarsController;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
controllers: [CalendarsController], controllers: [CalendarsController],
providers: [CalendarsService], providers: [CalendarsService],
}).compile(); }).compile();
controller = module.get<CalendarsController>(CalendarsController); controller = module.get<CalendarsController>(CalendarsController);
}); });
it('should be defined', () => { it('should be defined', () => {
expect(controller).toBeDefined(); expect(controller).toBeDefined();
}); });
}); });

110
packages/nocodb/src/controllers/calendars.controller.ts

@ -1,13 +1,13 @@
import { import {
Body, Body,
Controller, Controller,
Get, Get,
HttpCode, HttpCode,
Param, Param,
Patch, Patch,
Post, Post,
Req, Req,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { Request } from 'express'; import { Request } from 'express';
import { ViewCreateReqType } from 'nocodb-sdk'; import { ViewCreateReqType } from 'nocodb-sdk';
@ -19,54 +19,54 @@ import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(MetaApiLimiterGuard, GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class CalendarsController { export class CalendarsController {
constructor(private readonly calendarsService: CalendarsService) {} constructor(private readonly calendarsService: CalendarsService) {}
@Get([ @Get([
'/api/v1/db/meta/calendars/:calendarViewId', '/api/v1/db/meta/calendars/:calendarViewId',
'/api/v2/meta/calendars/:calendarViewId', '/api/v2/meta/calendars/:calendarViewId',
]) ])
@Acl('calendarViewGet') @Acl('calendarViewGet')
async calendarViewGet(@Param('calendarViewId') calendarViewId: string) { async calendarViewGet(@Param('calendarViewId') calendarViewId: string) {
return await this.calendarsService.calendarViewGet({ return await this.calendarsService.calendarViewGet({
calendarViewId, calendarViewId,
}); });
} }
@Post([ @Post([
'/api/v1/db/meta/tables/:tableId/calendars', '/api/v1/db/meta/tables/:tableId/calendars',
'/api/v2/meta/tables/:tableId/calendars', '/api/v2/meta/tables/:tableId/calendars',
]) ])
@HttpCode(200) @HttpCode(200)
@Acl('calendarViewCreate') @Acl('calendarViewCreate')
async calendarViewCreate( async calendarViewCreate(
@Param('tableId') tableId: string, @Param('tableId') tableId: string,
@Body() body: ViewCreateReqType, @Body() body: ViewCreateReqType,
@Req() req: Request, @Req() req: Request,
) { ) {
return await this.calendarsService.calendarViewCreate({ return await this.calendarsService.calendarViewCreate({
tableId, tableId,
calendar: body, calendar: body,
user: req.user, user: req.user,
req, req,
}); });
} }
@Patch([ @Patch([
'/api/v1/db/meta/calendars/:calendarViewId', '/api/v1/db/meta/calendars/:calendarViewId',
'/api/v2/meta/calendars/:calendarViewId', '/api/v2/meta/calendars/:calendarViewId',
]) ])
// #TODO Enable ACL Later // #TODO Enable ACL Later
// @Acl('calendarViewUpdate') // @Acl('calendarViewUpdate')
async calendarViewUpdate( async calendarViewUpdate(
@Param('calendarViewId') calendarViewId: string, @Param('calendarViewId') calendarViewId: string,
@Body() body, @Body() body,
@Req() req: Request, @Req() req: Request,
) { ) {
return await this.calendarsService.calendarViewUpdate({ return await this.calendarsService.calendarViewUpdate({
calendarViewId, calendarViewId,
calendar: body, calendar: body,
req, req,
}); });
} }
} }

12
packages/nocodb/src/meta/meta.service.ts

@ -1,16 +1,16 @@
import {Injectable, Optional} from '@nestjs/common'; import { Injectable, Optional } from '@nestjs/common';
import {customAlphabet} from 'nanoid'; import { customAlphabet } from 'nanoid';
import CryptoJS from 'crypto-js'; import CryptoJS from 'crypto-js';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone'; import timezone from 'dayjs/plugin/timezone';
import type * as knex from 'knex'; import type * as knex from 'knex';
import type {Knex} from 'knex'; import type { Knex } from 'knex';
import XcMigrationSource from '~/meta/migrations/XcMigrationSource'; import XcMigrationSource from '~/meta/migrations/XcMigrationSource';
import XcMigrationSourcev2 from '~/meta/migrations/XcMigrationSourcev2'; import XcMigrationSourcev2 from '~/meta/migrations/XcMigrationSourcev2';
import {XKnex} from '~/db/CustomKnex'; import { XKnex } from '~/db/CustomKnex';
import {NcConfig} from '~/utils/nc-config'; import { NcConfig } from '~/utils/nc-config';
import {MetaTable} from '~/utils/globals'; import { MetaTable } from '~/utils/globals';
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);

4
packages/nocodb/src/meta/migrations/v2/nc_041_calander_view.ts

@ -1,5 +1,5 @@
import type {Knex} from 'knex'; import type { Knex } from 'knex';
import {MetaTable} from '~/utils/globals'; import { MetaTable } from '~/utils/globals';
const up = async (knex: Knex) => { const up = async (knex: Knex) => {
await knex.schema.createTable(MetaTable.CALENDAR_VIEW, (table) => { await knex.schema.createTable(MetaTable.CALENDAR_VIEW, (table) => {

265
packages/nocodb/src/models/CalendarRange.ts

@ -1,145 +1,146 @@
import type {CalendarRangeType} from 'nocodb-sdk'; import type { CalendarRangeType } from 'nocodb-sdk';
import Noco from '~/Noco'; import Noco from '~/Noco';
import NocoCache from '~/cache/NocoCache'; import NocoCache from '~/cache/NocoCache';
import {extractProps} from '~/helpers/extractProps'; import { extractProps } from '~/helpers/extractProps';
import {CacheGetType, CacheScope, MetaTable} from '~/utils/globals'; import { CacheGetType, CacheScope, MetaTable } from '~/utils/globals';
export default class CalendarRange implements CalendarRangeType { export default class CalendarRange implements CalendarRangeType {
id?: string; id?: string;
fk_from_column_id?: string; fk_from_column_id?: string;
fk_to_column_id?: string | null; fk_to_column_id?: string | null;
fk_view_id?: string; fk_view_id?: string;
constructor(data: Partial<CalendarRange>) { constructor(data: Partial<CalendarRange>) {
Object.assign(this, data); Object.assign(this, data);
}
public static async insert(
data: Partial<CalendarRange>,
ncMeta = Noco.ncMeta,
) {
const insertObj = extractProps(data, [
'fk_from_column_id',
'fk_to_column_id',
'fk_view_id',
]);
const { id } = await ncMeta.metaInsert2(
null,
null,
MetaTable.CALENDAR_VIEW_RANGE,
insertObj,
);
await NocoCache.appendToList(
CacheScope.CALENDAR_VIEW_RANGE,
[data.fk_view_id],
`${CacheScope.CALENDAR_VIEW_RANGE}:${id}`,
);
return this.get(id, ncMeta);
}
public static async bulkInsert(
data: Partial<CalendarRange>[],
ncMeta = Noco.ncMeta,
) {
const insertObj = [];
for (const d of data) {
const tempObj = extractProps(d, [
'fk_from_column_id',
'fk_to_column_id',
'fk_view_id',
]);
insertObj.push(tempObj);
} }
public static async insert( const bulkData = await ncMeta.bulkMetaInsert(
data: Partial<CalendarRange>, null,
ncMeta = Noco.ncMeta, null,
) { MetaTable.CALENDAR_VIEW_RANGE,
const insertObj = extractProps(data, [ insertObj,
'fk_from_column_id', );
'fk_to_column_id',
'fk_view_id', for (const d of bulkData) {
]); await NocoCache.appendToList(
CacheScope.CALENDAR_VIEW_RANGE,
const { id } = await ncMeta.metaInsert2( [d.fk_view_id],
null, `${CacheScope.CALENDAR_VIEW_RANGE}:${d.id}`,
null, );
MetaTable.CALENDAR_VIEW_RANGE, await NocoCache.set(`${CacheScope.CALENDAR_VIEW_RANGE}:${d.id}`, d);
insertObj,
);
await NocoCache.appendToList(
CacheScope.CALENDAR_VIEW_RANGE,
[data.fk_view_id],
`${CacheScope.CALENDAR_VIEW_RANGE}:${id}`,
);
return this.get(id, ncMeta);
} }
public static async bulkInsert( return true;
data: Partial<CalendarRange>[], }
ncMeta = Noco.ncMeta,
) { public static async get(
const insertObj = []; calendarRangeId: string,
ncMeta = Noco.ncMeta,
for (const d of data) { ): Promise<CalendarRange> {
const tempObj = extractProps(d, [ let data =
'fk_from_column_id', calendarRangeId &&
'fk_to_column_id', (await NocoCache.get(
'fk_view_id', `${CacheScope.CALENDAR_VIEW_RANGE}:${calendarRangeId}`,
]); CacheGetType.TYPE_OBJECT,
insertObj.push(tempObj); ));
} if (!data) {
data = await ncMeta.metaGet2(
const bulkData = await ncMeta.bulkMetaInsert( null,
null, null,
null, MetaTable.CALENDAR_VIEW_RANGE,
MetaTable.CALENDAR_VIEW_RANGE, calendarRangeId,
insertObj, );
); await NocoCache.set(
`${CacheScope.CALENDAR_VIEW_RANGE}:${calendarRangeId}`,
for (const d of bulkData) { data,
await NocoCache.appendToList( );
CacheScope.CALENDAR_VIEW_RANGE,
[d.fk_view_id],
`${CacheScope.CALENDAR_VIEW_RANGE}:${d.id}`,
);
await NocoCache.set(`${CacheScope.CALENDAR_VIEW_RANGE}:${d.id}`, d);
}
return true;
} }
return data && new CalendarRange(data);
public static async get( }
calendarRangeId: string,
ncMeta = Noco.ncMeta, public static async read(fk_view_id: string, ncMeta = Noco.ncMeta) {
): Promise<CalendarRange> { const cachedList = await NocoCache.getList(CacheScope.CALENDAR_VIEW_RANGE, [
let data = fk_view_id,
calendarRangeId && ]);
(await NocoCache.get( let { list: ranges } = cachedList;
`${CacheScope.CALENDAR_VIEW_RANGE}:${calendarRangeId}`, const { isNoneList } = cachedList;
CacheGetType.TYPE_OBJECT, if (!isNoneList && !ranges.length) {
)); ranges = await ncMeta.metaList2(
if (!data) { null, //,
data = await ncMeta.metaGet2( null, //model.db_alias,
null, MetaTable.CALENDAR_VIEW_RANGE,
null, { condition: { fk_view_id } },
MetaTable.CALENDAR_VIEW_RANGE, );
calendarRangeId, await NocoCache.setList(
); CacheScope.CALENDAR_VIEW_RANGE,
await NocoCache.set( [fk_view_id],
`${CacheScope.CALENDAR_VIEW_RANGE}:${calendarRangeId}`, ranges.map(({ created_at, updated_at, ...others }) => others),
data, );
);
}
return data && new CalendarRange(data);
} }
public static async read(fk_view_id: string, ncMeta = Noco.ncMeta) { return ranges?.length
const cachedList = await NocoCache.getList(CacheScope.CALENDAR_VIEW_RANGE, [ ? {
fk_view_id, ranges: ranges.map(
]); ({ created_at, updated_at, ...c }) => new CalendarRange(c),
let { list: ranges } = cachedList; ),
const { isNoneList } = cachedList;
if (!isNoneList && !ranges.length) {
ranges = await ncMeta.metaList2(
null, //,
null, //model.db_alias,
MetaTable.CALENDAR_VIEW_RANGE,
{ condition: { fk_view_id } },
);
await NocoCache.setList(
CacheScope.CALENDAR_VIEW_RANGE,
[fk_view_id],
ranges.map(({ created_at, updated_at, ...others }) => others),
);
} }
: null;
return ranges?.length }
? {
ranges: ranges public static async find(
.map(({ created_at, updated_at, ...c }) => new CalendarRange(c)) fk_view_id: string,
} ncMeta = Noco.ncMeta,
: null; ): Promise<CalendarRange> {
} const data = await ncMeta.metaGet2(
null,
public static async find( null,
fk_view_id: string, MetaTable.CALENDAR_VIEW_RANGE,
ncMeta = Noco.ncMeta, {
): Promise<CalendarRange> { fk_view_id,
const data = await ncMeta.metaGet2( },
null, );
null,
MetaTable.CALENDAR_VIEW_RANGE, return data && new CalendarRange(data);
{ }
fk_view_id,
},
);
return data && new CalendarRange(data);
}
} }

208
packages/nocodb/src/models/CalendarView.ts

@ -1,115 +1,135 @@
import type {CalendarType} from 'nocodb-sdk'; import type { BoolType, MetaType } from 'nocodb-sdk';
import {BoolType, MetaType} from 'nocodb-sdk'; import type { CalendarType } from 'nocodb-sdk';
import View from '~/models/View'; import View from '~/models/View';
import {extractProps} from '~/helpers/extractProps'; import { extractProps } from '~/helpers/extractProps';
import NocoCache from '~/cache/NocoCache'; import NocoCache from '~/cache/NocoCache';
import Noco from '~/Noco'; import Noco from '~/Noco';
import {CacheGetType, CacheScope, MetaTable} from '~/utils/globals'; import { CacheGetType, CacheScope, MetaTable } from '~/utils/globals';
import CalendarRange from "~/models/CalendarRange"; import CalendarRange from '~/models/CalendarRange';
export default class CalendarView implements CalendarType { export default class CalendarView implements CalendarType {
fk_view_id: string; fk_view_id: string;
title: string; title: string;
base_id?: string; base_id?: string;
source_id?: string; source_id?: string;
meta?: MetaType; meta?: MetaType;
calendar_range?: Array<Partial<CalendarRange>> calendar_range?: Array<Partial<CalendarRange>>;
fk_cover_image_col_id?: string; fk_cover_image_col_id?: string;
// below fields are not in use at this moment // below fields are not in use at this moment
// keep them for time being // keep them for time being
show?: BoolType; show?: BoolType;
public?: BoolType; public?: BoolType;
password?: string; password?: string;
show_all_fields?: BoolType; show_all_fields?: BoolType;
constructor(data: CalendarView) { constructor(data: CalendarView) {
Object.assign(this, data); Object.assign(this, data);
} }
public static async get(viewId: string, ncMeta = Noco.ncMeta) {
let view =
viewId &&
(await NocoCache.get(
`${CacheScope.CALENDAR_VIEW}:${viewId}`,
CacheGetType.TYPE_OBJECT,
));
if (view) {
const calendarRange = await CalendarRange.read(viewId, ncMeta);
if (calendarRange) {
view.calendar_range = calendarRange.ranges;
} else {
view.calendar_range = [];
}
} else {
view = await ncMeta.metaGet2(null, null, MetaTable.CALENDAR_VIEW, {
fk_view_id: viewId,
});
const calendarRange = await CalendarRange.read(viewId);
if (calendarRange) {
view.calendar_range = calendarRange.ranges;
}
await NocoCache.set(`${CacheScope.CALENDAR_VIEW}:${viewId}`, view);
}
return view && new CalendarView(view); public static async get(viewId: string, ncMeta = Noco.ncMeta) {
let view =
viewId &&
(await NocoCache.get(
`${CacheScope.CALENDAR_VIEW}:${viewId}`,
CacheGetType.TYPE_OBJECT,
));
if (view) {
const calendarRange = await CalendarRange.read(viewId, ncMeta);
if (calendarRange) {
view.calendar_range = calendarRange.ranges;
} else {
view.calendar_range = [];
}
} else {
view = await ncMeta.metaGet2(null, null, MetaTable.CALENDAR_VIEW, {
fk_view_id: viewId,
});
const calendarRange = await CalendarRange.read(viewId);
if (calendarRange) {
view.calendar_range = calendarRange.ranges;
}
await NocoCache.set(`${CacheScope.CALENDAR_VIEW}:${viewId}`, view);
} }
static async insert(view: Partial<CalendarView>, ncMeta = Noco.ncMeta) { return view && new CalendarView(view);
const insertObj = { }
base_id: view.base_id,
source_id: view.source_id,
fk_view_id: view.fk_view_id,
meta: view.meta,
};
const viewRef = await View.get(view.fk_view_id); static async insert(view: Partial<CalendarView>, ncMeta = Noco.ncMeta) {
const insertObj = {
base_id: view.base_id,
source_id: view.source_id,
fk_view_id: view.fk_view_id,
meta: view.meta,
};
if (!(view.base_id && view.source_id)) { const viewRef = await View.get(view.fk_view_id);
insertObj.base_id = viewRef.base_id;
insertObj.source_id = viewRef.source_id;
}
await ncMeta.metaInsert2(null, null, MetaTable.CALENDAR_VIEW, insertObj, true); if (!(view.base_id && view.source_id)) {
insertObj.base_id = viewRef.base_id;
return this.get(view.fk_view_id, ncMeta); insertObj.source_id = viewRef.source_id;
} }
static async update( await ncMeta.metaInsert2(
calendarId: string, null,
body: Partial<CalendarView>, null,
ncMeta = Noco.ncMeta, MetaTable.CALENDAR_VIEW,
) { insertObj,
// get existing cache true,
const key = `${CacheScope.CALENDAR_VIEW_COLUMN}:${calendarId}`; );
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
return this.get(view.fk_view_id, ncMeta);
}
const updateObj = extractProps(body, ['fk_cover_image_col_id', 'meta']); static async update(
calendarId: string,
body: Partial<CalendarView>,
ncMeta = Noco.ncMeta,
) {
// get existing cache
const key = `${CacheScope.CALENDAR_VIEW_COLUMN}:${calendarId}`;
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
if (updateObj.meta && typeof updateObj.meta === 'object') { const updateObj = extractProps(body, ['fk_cover_image_col_id', 'meta']);
updateObj.meta = JSON.stringify(updateObj.meta ?? {});
}
if (o) { if (updateObj.meta && typeof updateObj.meta === 'object') {
o = { ...o, ...updateObj }; updateObj.meta = JSON.stringify(updateObj.meta ?? {});
// set cache }
await NocoCache.set(key, o);
} if (o) {
o = { ...o, ...updateObj };
// set cache
await NocoCache.set(key, o);
}
if (body.calendar_range) { if (body.calendar_range) {
await NocoCache.del(`${CacheScope.CALENDAR_VIEW_RANGE}:${calendarId}`); await NocoCache.del(`${CacheScope.CALENDAR_VIEW_RANGE}:${calendarId}`);
await ncMeta.metaDelete(null, null, MetaTable.CALENDAR_VIEW_RANGE, {}, { await ncMeta.metaDelete(
fk_view_id: calendarId, null,
}); null,
await CalendarRange.bulkInsert(body.calendar_range.map((range) => { MetaTable.CALENDAR_VIEW_RANGE,
return { {},
fk_view_id: calendarId, {
...range fk_view_id: calendarId,
} },
})); );
} await CalendarRange.bulkInsert(
// update meta body.calendar_range.map((range) => {
return await ncMeta.metaUpdate(null, null, MetaTable.CALENDAR_VIEW, updateObj, { return {
fk_view_id: calendarId, fk_view_id: calendarId,
}); ...range,
};
}),
);
} }
// update meta
return await ncMeta.metaUpdate(
null,
null,
MetaTable.CALENDAR_VIEW,
updateObj,
{
fk_view_id: calendarId,
},
);
}
} }

331
packages/nocodb/src/models/CalendarViewColumn.ts

@ -1,173 +1,180 @@
import type {BoolType, MetaType,} from 'nocodb-sdk'; import type { BoolType, MetaType } from 'nocodb-sdk';
import View from '~/models/View'; import View from '~/models/View';
import Noco from '~/Noco'; import Noco from '~/Noco';
import NocoCache from '~/cache/NocoCache'; import NocoCache from '~/cache/NocoCache';
import {extractProps} from '~/helpers/extractProps'; import { extractProps } from '~/helpers/extractProps';
import {deserializeJSON} from '~/utils/serialize'; import { deserializeJSON } from '~/utils/serialize';
import {CacheGetType, CacheScope, MetaTable} from '~/utils/globals'; import { CacheGetType, CacheScope, MetaTable } from '~/utils/globals';
export default class CalendarViewColumn { export default class CalendarViewColumn {
id?: string; id?: string;
fk_view_id?: string; fk_view_id?: string;
fk_column_id?: string; fk_column_id?: string;
base_id?: string; base_id?: string;
source_id?: string; source_id?: string;
show?: BoolType; show?: BoolType;
underline?: BoolType; underline?: BoolType;
bold?: BoolType; bold?: BoolType;
italic?: BoolType; italic?: BoolType;
order?: number; order?: number;
meta?: MetaType; meta?: MetaType;
constructor(data: CalendarViewColumn) { constructor(data: CalendarViewColumn) {
Object.assign(this, data); Object.assign(this, data);
}
public static async get(calendarViewColumnId: string, ncMeta = Noco.ncMeta) {
let viewColumn =
calendarViewColumnId &&
(await NocoCache.get(
`${CacheScope.CALENDAR_VIEW_COLUMN}:${calendarViewColumnId}`,
CacheGetType.TYPE_OBJECT,
));
if (!viewColumn) {
viewColumn = await ncMeta.metaGet2(
null,
null,
MetaTable.CALENDAR_VIEW_COLUMNS,
calendarViewColumnId,
);
viewColumn.meta =
viewColumn.meta && typeof viewColumn.meta === 'string'
? JSON.parse(viewColumn.meta)
: viewColumn.meta;
} }
await NocoCache.set(
public static async get(calendarViewColumnId: string, ncMeta = Noco.ncMeta) { `${CacheScope.CALENDAR_VIEW_COLUMN}:${calendarViewColumnId}`,
let viewColumn = viewColumn,
calendarViewColumnId && );
(await NocoCache.get(
`${CacheScope.CALENDAR_VIEW_COLUMN}:${calendarViewColumnId}`, return viewColumn && new CalendarViewColumn(viewColumn);
CacheGetType.TYPE_OBJECT, }
));
if (!viewColumn) { static async insert(
viewColumn = await ncMeta.metaGet2( column: Partial<CalendarViewColumn>,
null, ncMeta = Noco.ncMeta,
null, ) {
MetaTable.CALENDAR_VIEW_COLUMNS, const insertObj = extractProps(column, [
calendarViewColumnId, 'fk_view_id',
); 'fk_column_id',
viewColumn.meta = 'show',
viewColumn.meta && typeof viewColumn.meta === 'string' 'base_id',
? JSON.parse(viewColumn.meta) 'source_id',
: viewColumn.meta; 'underline',
} 'bold',
await NocoCache.set( 'italic',
`${CacheScope.CALENDAR_VIEW_COLUMN}:${calendarViewColumnId}`, ]);
viewColumn,
); insertObj.order = await ncMeta.metaGetNextOrder(
MetaTable.CALENDAR_VIEW_COLUMNS,
return viewColumn && new CalendarViewColumn(viewColumn); {
} fk_view_id: insertObj.fk_view_id,
},
static async insert(column: Partial<CalendarViewColumn>, ncMeta = Noco.ncMeta) { );
const insertObj = extractProps(column, [
'fk_view_id', if (!(insertObj.base_id && insertObj.source_id)) {
'fk_column_id', const viewRef = await View.get(insertObj.fk_view_id, ncMeta);
'show', insertObj.base_id = viewRef.base_id;
'base_id', insertObj.source_id = viewRef.source_id;
'source_id',
'underline',
'bold',
'italic',
]);
insertObj.order = await ncMeta.metaGetNextOrder(
MetaTable.CALENDAR_VIEW_COLUMNS,
{
fk_view_id: insertObj.fk_view_id,
},
);
if (!(insertObj.base_id && insertObj.source_id)) {
const viewRef = await View.get(insertObj.fk_view_id, ncMeta);
insertObj.base_id = viewRef.base_id;
insertObj.source_id = viewRef.source_id;
}
const { id, fk_column_id } = await ncMeta.metaInsert2(
null,
null,
MetaTable.CALENDAR_VIEW_COLUMNS,
insertObj,
);
await NocoCache.set(`${CacheScope.CALENDAR_VIEW_COLUMN}:${fk_column_id}`, id);
// if cache is not present skip pushing it into the list to avoid unexpected behaviour
const { list } = await NocoCache.getList(CacheScope.CALENDAR_VIEW_COLUMN, [
column.fk_view_id,
]);
if (list?.length)
await NocoCache.appendToList(
CacheScope.CALENDAR_VIEW_COLUMN,
[column.fk_view_id],
`${CacheScope.CALENDAR_VIEW_COLUMN}:${id}`,
);
return this.get(id, ncMeta);
} }
public static async list( const { id, fk_column_id } = await ncMeta.metaInsert2(
viewId: string, null,
ncMeta = Noco.ncMeta, null,
): Promise<CalendarViewColumn[]> { MetaTable.CALENDAR_VIEW_COLUMNS,
const cachedList = await NocoCache.getList(CacheScope.CALENDAR_VIEW_COLUMN, [ insertObj,
viewId, );
]);
let { list: viewColumns } = cachedList; await NocoCache.set(
const { isNoneList } = cachedList; `${CacheScope.CALENDAR_VIEW_COLUMN}:${fk_column_id}`,
if (!isNoneList && !viewColumns.length) { id,
viewColumns = await ncMeta.metaList2( );
null,
null, // if cache is not present skip pushing it into the list to avoid unexpected behaviour
MetaTable.CALENDAR_VIEW_COLUMNS, const { list } = await NocoCache.getList(CacheScope.CALENDAR_VIEW_COLUMN, [
{ column.fk_view_id,
condition: { ]);
fk_view_id: viewId,
}, if (list?.length)
orderBy: { await NocoCache.appendToList(
order: 'asc', CacheScope.CALENDAR_VIEW_COLUMN,
}, [column.fk_view_id],
}, `${CacheScope.CALENDAR_VIEW_COLUMN}:${id}`,
); );
return this.get(id, ncMeta);
for (const viewColumn of viewColumns) { }
viewColumn.meta = deserializeJSON(viewColumn.meta);
} public static async list(
viewId: string,
await NocoCache.setList( ncMeta = Noco.ncMeta,
CacheScope.CALENDAR_VIEW_COLUMN, ): Promise<CalendarViewColumn[]> {
[viewId], const cachedList = await NocoCache.getList(
viewColumns, CacheScope.CALENDAR_VIEW_COLUMN,
); [viewId],
} );
viewColumns.sort( let { list: viewColumns } = cachedList;
(a, b) => const { isNoneList } = cachedList;
(a.order != null ? a.order : Infinity) - if (!isNoneList && !viewColumns.length) {
(b.order != null ? b.order : Infinity), viewColumns = await ncMeta.metaList2(
); null,
return viewColumns?.map((v) => new CalendarViewColumn(v)); null,
MetaTable.CALENDAR_VIEW_COLUMNS,
{
condition: {
fk_view_id: viewId,
},
orderBy: {
order: 'asc',
},
},
);
for (const viewColumn of viewColumns) {
viewColumn.meta = deserializeJSON(viewColumn.meta);
}
await NocoCache.setList(
CacheScope.CALENDAR_VIEW_COLUMN,
[viewId],
viewColumns,
);
} }
viewColumns.sort(
static async update( (a, b) =>
columnId: string, (a.order != null ? a.order : Infinity) -
body: Partial<CalendarViewColumn>, (b.order != null ? b.order : Infinity),
ncMeta = Noco.ncMeta, );
) { return viewColumns?.map((v) => new CalendarViewColumn(v));
const updateObj = extractProps(body, [ }
'show',
'order', static async update(
'underline', columnId: string,
'bold', body: Partial<CalendarViewColumn>,
'italic', ncMeta = Noco.ncMeta,
]); ) {
const updateObj = extractProps(body, [
// get existing cache 'show',
const key = `${CacheScope.CALENDAR_VIEW_COLUMN}:${columnId}`; 'order',
const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT); 'underline',
if (o) { 'bold',
Object.assign(o, updateObj); 'italic',
// set cache ]);
await NocoCache.set(key, o);
} // get existing cache
// update meta const key = `${CacheScope.CALENDAR_VIEW_COLUMN}:${columnId}`;
return await ncMeta.metaUpdate( const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
null, if (o) {
null, Object.assign(o, updateObj);
MetaTable.CALENDAR_VIEW_COLUMNS, // set cache
updateObj, await NocoCache.set(key, o);
columnId,
);
} }
// update meta
return await ncMeta.metaUpdate(
null,
null,
MetaTable.CALENDAR_VIEW_COLUMNS,
updateObj,
columnId,
);
}
} }

80
packages/nocodb/src/models/View.ts

@ -1,14 +1,14 @@
import type {BoolType, ColumnReqType, ViewType} from 'nocodb-sdk'; import { isSystemColumn, UITypes, ViewTypes } from 'nocodb-sdk';
import {isSystemColumn, UITypes, ViewTypes} from 'nocodb-sdk'; import type { BoolType, ColumnReqType, ViewType } from 'nocodb-sdk';
import Model from '~/models/Model'; import Model from '~/models/Model';
import FormView from '~/models/FormView'; import FormView from '~/models/FormView';
import GridView from '~/models/GridView'; import GridView from '~/models/GridView';
import KanbanView from '~/models/KanbanView'; import KanbanView from '~/models/KanbanView';
import GalleryView from '~/models/GalleryView'; import GalleryView from '~/models/GalleryView';
import CalendarView from "~/models/CalendarView"; import CalendarView from '~/models/CalendarView';
import GridViewColumn from '~/models/GridViewColumn'; import GridViewColumn from '~/models/GridViewColumn';
import CalendarViewColumn from '~/models/CalendarViewColumn'; import CalendarViewColumn from '~/models/CalendarViewColumn';
import CalendarRange from "~/models/CalendarRange"; import CalendarRange from '~/models/CalendarRange';
import Sort from '~/models/Sort'; import Sort from '~/models/Sort';
import Filter from '~/models/Filter'; import Filter from '~/models/Filter';
import GalleryViewColumn from '~/models/GalleryViewColumn'; import GalleryViewColumn from '~/models/GalleryViewColumn';
@ -17,11 +17,16 @@ import KanbanViewColumn from '~/models/KanbanViewColumn';
import Column from '~/models/Column'; import Column from '~/models/Column';
import MapView from '~/models/MapView'; import MapView from '~/models/MapView';
import MapViewColumn from '~/models/MapViewColumn'; import MapViewColumn from '~/models/MapViewColumn';
import {extractProps} from '~/helpers/extractProps'; import { extractProps } from '~/helpers/extractProps';
import NocoCache from '~/cache/NocoCache'; import NocoCache from '~/cache/NocoCache';
import {CacheDelDirection, CacheGetType, CacheScope, MetaTable,} from '~/utils/globals'; import {
CacheDelDirection,
CacheGetType,
CacheScope,
MetaTable,
} from '~/utils/globals';
import Noco from '~/Noco'; import Noco from '~/Noco';
import {parseMetaProp, stringifyMetaProp} from '~/utils/modelUtils'; import { parseMetaProp, stringifyMetaProp } from '~/utils/modelUtils';
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
@ -54,14 +59,20 @@ export default class View implements ViewType {
fk_model_id: string; fk_model_id: string;
model?: Model; model?: Model;
view?: FormView | GridView | KanbanView | GalleryView | MapView | CalendarView; view?:
| FormView
| GridView
| KanbanView
| GalleryView
| MapView
| CalendarView;
columns?: Array< columns?: Array<
| FormViewColumn | FormViewColumn
| GridViewColumn | GridViewColumn
| GalleryViewColumn | GalleryViewColumn
| KanbanViewColumn | KanbanViewColumn
| MapViewColumn | MapViewColumn
| CalendarViewColumn | CalendarViewColumn
>; >;
sorts: Sort[]; sorts: Sort[];
@ -277,7 +288,9 @@ export default class View implements ViewType {
static async insert( static async insert(
view: Partial<View> & view: Partial<View> &
Partial<FormView | GridView | GalleryView | KanbanView | MapView | CalendarView> & { Partial<
FormView | GridView | GalleryView | KanbanView | MapView | CalendarView
> & {
copy_from_id?: string; copy_from_id?: string;
fk_grp_col_id?: string; fk_grp_col_id?: string;
calendar_range?: Partial<CalendarRange>[]; calendar_range?: Partial<CalendarRange>[];
@ -386,18 +399,21 @@ export default class View implements ViewType {
); );
break; break;
case ViewTypes.CALENDAR: case ViewTypes.CALENDAR:
const obj = extractProps(view, ["calendar_range"]) const obj = extractProps(view, ['calendar_range']);
if (!obj.calendar_range) break; if (!obj.calendar_range) break;
const calendarRange = obj.calendar_range as Partial<CalendarRange>[]; const calendarRange = obj.calendar_range as Partial<CalendarRange>[];
calendarRange.forEach((range) => { calendarRange.forEach((range) => {
range.fk_view_id = view_id; range.fk_view_id = view_id;
}) });
await CalendarView.insert({ await CalendarView.insert(
...(copyFromView?.view || {}), {
...view, ...(copyFromView?.view || {}),
fk_view_id: view_id, ...view,
}, ncMeta,) fk_view_id: view_id,
},
ncMeta,
);
await CalendarRange.bulkInsert(calendarRange, ncMeta); await CalendarRange.bulkInsert(calendarRange, ncMeta);
} }
@ -456,12 +472,12 @@ export default class View implements ViewType {
if (view.type === ViewTypes.CALENDAR) { if (view.type === ViewTypes.CALENDAR) {
const calRange = await CalendarRange.read(view_id, ncMeta); const calRange = await CalendarRange.read(view_id, ncMeta);
if (calRange) { if (calRange) {
const calIds: Set<string> = new Set() const calIds: Set<string> = new Set();
calRange.ranges.forEach((range) => { calRange.ranges.forEach((range) => {
calIds.add(range.fk_from_column_id); calIds.add(range.fk_from_column_id);
if (!range.fk_to_column_id) return; if (!range.fk_to_column_id) return;
calIds.add(range.fk_to_column_id); calIds.add(range.fk_to_column_id);
}) });
calendarRanges = Array.from(calIds) as Array<string>; calendarRanges = Array.from(calIds) as Array<string>;
} }
} }
@ -491,9 +507,9 @@ export default class View implements ViewType {
for (const vCol of columns) { for (const vCol of columns) {
let show = 'show' in vCol ? vCol.show : true; let show = 'show' in vCol ? vCol.show : true;
let underline = false; const underline = false;
let bold = false; const bold = false;
let italic = false; const italic = false;
if (view.type === ViewTypes.GALLERY) { if (view.type === ViewTypes.GALLERY) {
const galleryView = await GalleryView.get(view_id, ncMeta); const galleryView = await GalleryView.get(view_id, ncMeta);
@ -644,7 +660,8 @@ export default class View implements ViewType {
italic?; italic?;
fk_column_id; fk_column_id;
id?: string; id?: string;
} & Partial<FormViewColumn> & Partial<CalendarViewColumn>, } & Partial<FormViewColumn> &
Partial<CalendarViewColumn>,
ncMeta = Noco.ncMeta, ncMeta = Noco.ncMeta,
) { ) {
const view = await this.get(param.view_id, ncMeta); const view = await this.get(param.view_id, ncMeta);
@ -706,8 +723,9 @@ export default class View implements ViewType {
); );
} }
break; break;
case ViewTypes.CALENDAR: { case ViewTypes.CALENDAR:
col = await CalendarViewColumn.insert( {
col = await CalendarViewColumn.insert(
{ {
...param, ...param,
fk_view_id: view.id, fk_view_id: view.id,
@ -739,7 +757,7 @@ export default class View implements ViewType {
| GalleryViewColumn | GalleryViewColumn
| KanbanViewColumn | KanbanViewColumn
| MapViewColumn | MapViewColumn
| CalendarViewColumn | CalendarViewColumn
> >
> { > {
let columns: Array<GridViewColumn | any> = []; let columns: Array<GridViewColumn | any> = [];
@ -1002,7 +1020,7 @@ export default class View implements ViewType {
show: colData.show, show: colData.show,
}); });
case ViewTypes.CALENDAR: case ViewTypes.CALENDAR:
// todo: calendar view column // todo: calendar view column
} }
return await ncMeta.metaInsert2(view.base_id, view.source_id, table, { return await ncMeta.metaInsert2(view.base_id, view.source_id, table, {
fk_view_id: viewId, fk_view_id: viewId,
@ -1223,8 +1241,12 @@ export default class View implements ViewType {
if (view.type === ViewTypes.CALENDAR) { if (view.type === ViewTypes.CALENDAR) {
await ncMeta.metaDelete(null, null, MetaTable.CALENDAR_VIEW_RANGE, { await ncMeta.metaDelete(null, null, MetaTable.CALENDAR_VIEW_RANGE, {
fk_view_id: viewId, fk_view_id: viewId,
}) });
await NocoCache.deepDel(CacheScope.CALENDAR_VIEW_RANGE, `${CacheScope.CALENDAR_VIEW_RANGE}:${viewId}`, CacheDelDirection.CHILD_TO_PARENT) await NocoCache.deepDel(
CacheScope.CALENDAR_VIEW_RANGE,
`${CacheScope.CALENDAR_VIEW_RANGE}:${viewId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
} }
await NocoCache.deepDel( await NocoCache.deepDel(
`${columnTableScope}:${viewId}`, `${columnTableScope}:${viewId}`,

137
packages/nocodb/src/services/calendars.service.ts

@ -1,85 +1,86 @@
import {Injectable} from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import type {CalendarUpdateReqType, UserType, ViewCreateReqType,} from 'nocodb-sdk'; import { AppEvents, ViewTypes } from 'nocodb-sdk';
import {AppEvents, ViewTypes} from 'nocodb-sdk'; import type {
import type {NcRequest} from '~/interface/config'; CalendarUpdateReqType,
import {AppHooksService} from '~/services/app-hooks/app-hooks.service'; UserType,
import {validatePayload} from '~/helpers'; ViewCreateReqType,
import {NcError} from '~/helpers/catchError'; } from 'nocodb-sdk';
import {CalendarView, View} from '~/models'; import type { NcRequest } from '~/interface/config';
import CalendarRange from "~/models/CalendarRange"; import { AppHooksService } from '~/services/app-hooks/app-hooks.service';
import { validatePayload } from '~/helpers';
import { NcError } from '~/helpers/catchError';
import { CalendarView, View } from '~/models';
import CalendarRange from '~/models/CalendarRange';
@Injectable() @Injectable()
export class CalendarsService { export class CalendarsService {
constructor(private readonly appHooksService: AppHooksService) { constructor(private readonly appHooksService: AppHooksService) {}
}
async calendarViewGet(param: { calendarViewId: string }) {
const calendarView = await CalendarView.get(param.calendarViewId);
if (!calendarView) {
NcError.badRequest('Calendar view not found');
}
const calendarRanges = await CalendarRange.read(param.calendarViewId)
return {
...calendarView,
calendar_range: calendarRanges
};
async calendarViewGet(param: { calendarViewId: string }) {
const calendarView = await CalendarView.get(param.calendarViewId);
if (!calendarView) {
NcError.badRequest('Calendar view not found');
} }
const calendarRanges = await CalendarRange.read(param.calendarViewId);
return {
...calendarView,
calendar_range: calendarRanges,
};
}
async calendarViewCreate(param: { async calendarViewCreate(param: {
tableId: string; tableId: string;
calendar: ViewCreateReqType; calendar: ViewCreateReqType;
user: UserType; user: UserType;
req: NcRequest; req: NcRequest;
}) { }) {
- -validatePayload(
validatePayload( 'swagger.json#/components/schemas/ViewCreateReq',
'swagger.json#/components/schemas/ViewCreateReq', param.calendar,
param.calendar, );
);
const view = await View.insert({ const view = await View.insert({
...param.calendar, ...param.calendar,
// todo: sanitize // todo: sanitize
fk_model_id: param.tableId, fk_model_id: param.tableId,
type: ViewTypes.CALENDAR, type: ViewTypes.CALENDAR,
}); });
this.appHooksService.emit(AppEvents.VIEW_CREATE, { this.appHooksService.emit(AppEvents.VIEW_CREATE, {
view, view,
showAs: 'calendar', showAs: 'calendar',
user: param.user, user: param.user,
req: param.req, req: param.req,
}); });
return view; return view;
} }
async calendarViewUpdate(param: { async calendarViewUpdate(param: {
calendarViewId: string; calendarViewId: string;
calendar: CalendarUpdateReqType; calendar: CalendarUpdateReqType;
req: NcRequest; req: NcRequest;
}) { }) {
validatePayload( validatePayload(
'swagger.json#/components/schemas/CalendarUpdateReq', 'swagger.json#/components/schemas/CalendarUpdateReq',
param.calendar, param.calendar,
); );
const view = await View.get(param.calendarViewId); const view = await View.get(param.calendarViewId);
if (!view) { if (!view) {
NcError.badRequest('View not found'); NcError.badRequest('View not found');
} }
const res = await CalendarView.update(param.calendarViewId, param.calendar); const res = await CalendarView.update(param.calendarViewId, param.calendar);
this.appHooksService.emit(AppEvents.VIEW_UPDATE, { this.appHooksService.emit(AppEvents.VIEW_UPDATE, {
view, view,
showAs: 'calendar', showAs: 'calendar',
req: param.req, req: param.req,
}); });
return res; return res;
} }
} }

Loading…
Cancel
Save