import type { ComputedRef, Ref } from 'vue' import type { Api, CalendarRangeType, CalendarType, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk' import dayjs from 'dayjs' const formatData = (list: Record[]) => list.map( (row) => ({ row: { ...row }, oldRow: { ...row }, rowMeta: {}, } as Row), ) const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState( ( meta: Ref, viewMeta: | Ref<(ViewType | CalendarType | undefined) & { id: string }> | ComputedRef< | (ViewType & { id: string }) | undefined >, shared = false, where?: ComputedRef, ) => { if (!meta) { throw new Error('Table meta is not available') } const pageDate = ref(dayjs()) const { isUIAllowed } = useRoles() const { isMobileMode } = useGlobal() const displayField = computed(() => meta.value?.columns?.find((c) => c.pv)) const activeCalendarView = ref<'month' | 'year' | 'day' | 'week'>() const searchQuery = reactive({ value: '', field: '', }) const selectedDate = ref(dayjs()) const selectedTime = ref(dayjs()) const selectedMonth = ref(dayjs()) const isCalendarDataLoading = ref(false) const isCalendarMetaLoading = ref(false) const showSideMenu = ref(!isMobileMode.value) const selectedDateRange = ref<{ start: dayjs.Dayjs end: dayjs.Dayjs }>({ start: dayjs(selectedDate.value).startOf('week'), // This will be the previous Monday end: dayjs(selectedDate.value).startOf('week').add(6, 'day'), // This will be the following Sunday }) const defaultPageSize = 25 const formattedData = ref([]) const formattedSideBarData = ref([]) const isSidebarLoading = ref(false) const activeDates = ref([]) const sideBarFilterOption = ref(activeCalendarView.value ?? 'allRecords') const { api } = useApi() const { base } = storeToRefs(useBase()) const { $api, $e } = useNuxtApp() const { t } = useI18n() const { addUndo, clone, defineViewScope } = useUndoRedo() const isPublic = ref(shared) || inject(IsPublicInj, ref(false)) const { sorts, nestedFilters } = useSmartsheetStoreOrThrow() const { sharedView, fetchSharedViewData, fetchSharedViewActiveDate, fetchSharedCalendarViewData } = useSharedView() const calendarMetaData = ref({}) const paginationData = ref({ page: 1, pageSize: defaultPageSize }) const queryParams = computed(() => ({ limit: paginationData.value.pageSize ?? defaultPageSize, where: where?.value ?? '', })) const calendarRange = ref< Array<{ fk_from_col: ColumnType fk_to_col?: ColumnType | null id: string }> >([]) const calDataType = computed(() => { if (!calendarRange.value || !calendarRange.value[0]) return null return calendarRange.value[0]?.fk_from_col?.uidt }) const viewMetaProperties = computed<{ active_view: string hide_weekend: boolean }>(() => { let meta = calendarMetaData.value?.meta ?? {} if (typeof meta === 'string') { try { meta = JSON.parse(meta) } catch (e) {} } return meta as { active_view: string hide_weekend: boolean } }) const sideBarFilter = computed(() => { let combinedFilters: any = [] if (!calendarRange.value) return [] if (sideBarFilterOption.value === 'allRecords') { combinedFilters = [] } else if (sideBarFilterOption.value === 'withoutDates') { calendarRange.value.forEach((range) => { const fromCol = range.fk_from_col const toCol = range.fk_to_col if (!fromCol) return const filters = [] if (fromCol) { filters.push({ fk_column_id: fromCol.id, logical_op: 'or', comparison_op: 'blank', }) } if (toCol) { filters.push({ fk_column_id: toCol.id, logical_op: 'or', comparison_op: 'blank', }) } // Combine the filters for this range with the overall filter array combinedFilters = [...combinedFilters, ...filters] }) // Wrap the combined filters in a group combinedFilters = [ { is_group: true, logical_op: 'or', children: combinedFilters, }, ] } else if ( sideBarFilterOption.value === 'week' || sideBarFilterOption.value === 'month' || sideBarFilterOption.value === 'day' || sideBarFilterOption.value === 'year' || sideBarFilterOption.value === 'selectedDate' || sideBarFilterOption.value === 'selectedHours' ) { let prevDate: string | null | dayjs.Dayjs = null let fromDate: string | null | dayjs.Dayjs = null let toDate: string | null | dayjs.Dayjs = null let nextDate: string | null | dayjs.Dayjs = null switch (sideBarFilterOption.value) { case 'day': fromDate = selectedDate.value.startOf('day') toDate = selectedDate.value.endOf('day') prevDate = selectedDate.value.subtract(1, 'day').endOf('day') nextDate = selectedDate.value.add(1, 'day').startOf('day') break case 'week': fromDate = selectedDateRange.value.start.startOf('day') toDate = selectedDateRange.value.end.endOf('day') prevDate = selectedDateRange.value.start.subtract(1, 'day').endOf('day') nextDate = selectedDateRange.value.end.add(1, 'day').startOf('day') break case 'month': { const startOfMonth = selectedMonth.value.startOf('month') const endOfMonth = selectedMonth.value.endOf('month') const daysToDisplay = Math.max(endOfMonth.diff(startOfMonth, 'day') + 1, 35) fromDate = startOfMonth.subtract((startOfMonth.day() + 7) % 7, 'day').add(1, 'day') toDate = fromDate.add(daysToDisplay, 'day').endOf('day') prevDate = fromDate.subtract(1, 'day').endOf('day') nextDate = toDate.add(1, 'day').startOf('day') break } case 'year': fromDate = selectedDate.value.startOf('year') toDate = selectedDate.value.endOf('year') prevDate = fromDate.subtract(1, 'day').endOf('day') nextDate = toDate.add(1, 'day').startOf('day') break case 'selectedDate': fromDate = selectedDate.value.startOf('day') toDate = selectedDate.value.endOf('day') prevDate = selectedDate.value.subtract(1, 'day').endOf('day') nextDate = selectedDate.value.add(1, 'day').startOf('day') break case 'selectedHours': fromDate = (selectedTime.value ?? dayjs()).startOf('hour') toDate = (selectedTime.value ?? dayjs()).endOf('hour') prevDate = fromDate?.subtract(1, 'hour').endOf('hour') nextDate = toDate?.add(1, 'hour').startOf('hour') break } fromDate = fromDate!.format('YYYY-MM-DD HH:mm:ssZ') prevDate = prevDate!.format('YYYY-MM-DD HH:mm:ssZ') nextDate = nextDate!.format('YYYY-MM-DD HH:mm:ssZ') calendarRange.value.forEach((range) => { const fromCol = range.fk_from_col const toCol = range.fk_to_col let rangeFilter: any = [] if (fromCol && toCol) { rangeFilter = [ { is_group: true, logical_op: 'and', children: [ { fk_column_id: fromCol.id, comparison_op: 'lt', comparison_sub_op: 'exactDate', value: nextDate, }, { fk_column_id: toCol.id, comparison_op: 'gt', comparison_sub_op: 'exactDate', value: prevDate, }, ], }, { fk_column_id: fromCol.id, comparison_op: 'eq', logical_op: 'or', comparison_sub_op: 'exactDate', value: fromDate, }, ] combinedFilters.push({ is_group: true, logical_op: 'or', children: rangeFilter, }) } else if (fromCol) { rangeFilter = [ { fk_column_id: fromCol.id, comparison_op: 'lt', comparison_sub_op: 'exactDate', value: nextDate, }, { fk_column_id: fromCol.id, comparison_op: 'gt', comparison_sub_op: 'exactDate', value: prevDate, }, ] combinedFilters.push({ is_group: true, logical_op: 'and', children: rangeFilter, }) } }) } if (displayField.value && searchQuery.value) { if (combinedFilters.length > 0) { combinedFilters = [ { is_group: true, logical_op: 'and', children: [ ...combinedFilters, { fk_column_id: displayField.value.id, comparison_op: 'like', value: searchQuery.value, }, ], }, ] } else { combinedFilters.push({ fk_column_id: displayField.value.id, comparison_op: 'like', value: searchQuery.value, }) } } return combinedFilters }) async function loadMoreSidebarData(params: Parameters['dbViewRow']['list']>[4] = {}) { if (((!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic.value) || !calendarRange.value?.length) return if (isSidebarLoading.value) return try { const response = !isPublic.value ? await api.dbViewRow.list('noco', base.value.id!, meta.value!.id!, viewMeta.value!.id, { ...params, offset: params.offset, ...{}, ...{}, ...(isUIAllowed('filterSync') ? { filterArrJson: JSON.stringify([...sideBarFilter.value]) } : { filterArrJson: JSON.stringify([nestedFilters.value, ...sideBarFilter.value]) }), }) : await fetchSharedViewData({ ...params, sortsArr: sorts.value, filtersArr: [...nestedFilters.value, ...sideBarFilter.value], offset: params.offset, }) formattedSideBarData.value = [...formattedSideBarData.value, ...formatData(response!.list)] } catch (e) { console.log(e) } } const fetchActiveDates = async () => { if (!base?.value?.id || !meta.value?.id || !viewMeta.value?.id || !calendarRange.value?.length) return let prevDate: dayjs.Dayjs | string | null = null let nextDate: dayjs.Dayjs | string | null = null let fromDate: dayjs.Dayjs | string | null = null if (activeCalendarView.value === 'week' || activeCalendarView.value === 'day' || activeCalendarView.value === 'month') { const startOfMonth = pageDate.value.startOf('month') const endOfMonth = pageDate.value.endOf('month') const daysToDisplay = Math.max(endOfMonth.diff(startOfMonth, 'day') + 1, 35) fromDate = startOfMonth.subtract((startOfMonth.day() + 7) % 7, 'day') const toDate = fromDate.add(daysToDisplay, 'day') prevDate = fromDate.subtract(1, 'day').endOf('day') nextDate = toDate.add(1, 'day').startOf('day') } else if (activeCalendarView.value === 'year') { prevDate = selectedDate.value.startOf('year').subtract(1, 'day').endOf('day') nextDate = selectedDate.value.endOf('year').add(1, 'day').startOf('day') } prevDate = prevDate!.format('YYYY-MM-DD HH:mm:ssZ') nextDate = nextDate!.format('YYYY-MM-DD HH:mm:ssZ') if (!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) return try { const res = !isPublic.value ? await api.dbCalendarViewRowCount.dbCalendarViewRowCount('noco', base.value.id!, meta.value!.id!, viewMeta.value.id, { ...queryParams.value, from_date: prevDate, to_date: nextDate, }) : await fetchSharedViewActiveDate({ from_date: prevDate, to_date: nextDate, sortsArr: sorts.value, filtersArr: nestedFilters.value, }) activeDates.value = res.dates.map((dateObj: unknown) => dayjs(dateObj as string)) if (res.count > 3000 && activeCalendarView.value !== 'year') { message.warning( 'This current date range has more than 3000 records. Some records may not be displayed. To get complete records, contact support', ) } } catch (e) { activeDates.value = [] message.error( `${t('msg.error.fetchingActiveDates')} ${await extractSdkResponseErrorMsg( e as Error & { response: { data: { message: string } } }, )}`, ) console.log(e) } } const changeCalendarView = async (view: 'month' | 'year' | 'day' | 'week') => { $e('c:calendar:change-calendar-view', view) try { activeCalendarView.value = view await updateCalendarMeta({ meta: { ...(typeof calendarMetaData.value.meta === 'string' ? JSON.parse(calendarMetaData.value.meta) : calendarMetaData.value.meta), active_view: view, }, }) if (activeCalendarView.value === 'week') { selectedTime.value = null } } catch (e) { message.error('Error changing calendar view') console.log(e) } } async function loadCalendarMeta() { if (!viewMeta?.value?.id || !meta?.value?.columns) return isCalendarMetaLoading.value = true try { const res = isPublic.value ? (sharedView.value?.view as CalendarType) : await $api.dbView.calendarRead(viewMeta.value.id) calendarMetaData.value = res const calMeta = typeof res.meta === 'string' ? JSON.parse(res.meta) : res.meta activeCalendarView.value = calMeta?.active_view if (!activeCalendarView.value) activeCalendarView.value = 'month' calendarRange.value = res?.calendar_range?.map( ( range: CalendarRangeType & { id?: string }, ) => { return { id: range?.id, fk_from_col: meta.value?.columns?.find((col) => col.id === range.fk_from_column_id), fk_to_col: range.fk_to_column_id ? meta.value?.columns?.find((col) => col.id === range.fk_to_column_id) : null, } }, ) as any } catch (e: unknown) { message.error( `Error loading calendar meta ${await extractSdkResponseErrorMsg( e as Error & { response: { data: { message: string } } }, )}`, ) } finally { isCalendarMetaLoading.value = false } } async function loadCalendarData(showLoading = true) { if (((!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic?.value) || !calendarRange.value?.length) return if (activeCalendarView.value === 'year') { return } let prevDate: string | null | dayjs.Dayjs = null let fromDate: dayjs.Dayjs | null | string = null let toDate: dayjs.Dayjs | null | string = null let nextDate: string | null | dayjs.Dayjs = null switch (activeCalendarView.value) { case 'week': fromDate = selectedDateRange.value.start.startOf('day') toDate = selectedDateRange.value.end.endOf('day') prevDate = selectedDateRange.value.start.subtract(1, 'day').endOf('day') nextDate = selectedDateRange.value.end.add(1, 'day').startOf('day') // Hide weekends if (viewMetaProperties.value?.hide_weekend) { toDate = toDate.subtract(2, 'day') nextDate = nextDate.subtract(2, 'day') } break case 'month': { const startOfMonth = selectedMonth.value.startOf('month') const endOfMonth = selectedMonth.value.endOf('month') const daysToDisplay = Math.max(endOfMonth.diff(startOfMonth, 'day') + 1, 35) fromDate = startOfMonth.subtract((startOfMonth.day() + 7) % 7, 'day') toDate = fromDate.add(daysToDisplay, 'day') prevDate = fromDate.subtract(1, 'day').endOf('day') nextDate = toDate.add(1, 'day').startOf('day') break } case 'day': fromDate = selectedDate.value.startOf('day') toDate = selectedDate.value.endOf('day') prevDate = selectedDate.value.subtract(1, 'day').endOf('day') nextDate = selectedDate.value.add(1, 'day').startOf('day') break } prevDate = prevDate!.format('YYYY-MM-DD HH:mm:ssZ') nextDate = nextDate!.format('YYYY-MM-DD HH:mm:ssZ') try { if (showLoading) isCalendarDataLoading.value = true const res = !isPublic.value ? await api.dbCalendarViewRow.list( 'noco', base.value.id!, meta.value!.id!, viewMeta.value!.id!, { from_date: prevDate, to_date: nextDate, }, { ...queryParams.value, ...(isUIAllowed('filterSync') ? { filterArrJson: [] } : { filterArrJson: JSON.stringify([nestedFilters.value]) }), where: where?.value ?? '', filterArrJson: JSON.stringify([...nestedFilters.value]), }, ) : await fetchSharedCalendarViewData({ sortsArr: sorts.value, from_date: prevDate, to_date: nextDate, filtersArr: nestedFilters.value, }) formattedData.value = formatData(res!.list) } catch (e) { message.error( `${t('msg.error.fetchingCalendarData')} ${await extractSdkResponseErrorMsg( e as Error & { response: { data: { message: string } } }, )}`, ) console.log(e) } finally { isCalendarDataLoading.value = false } } async function updateCalendarMeta(updateObj: Partial) { if (!viewMeta?.value?.id || !isUIAllowed('dataEdit') || isPublic.value) return const updateValue = { ...(typeof calendarMetaData.value.meta === 'string' ? JSON.parse(calendarMetaData.value.meta) : calendarMetaData.value.meta), ...(typeof updateObj.meta === 'string' ? JSON.parse(updateObj.meta) : updateObj.meta), } try { await $api.dbView.calendarUpdate(viewMeta.value.id, { ...updateObj, meta: JSON.stringify(updateValue), }) calendarMetaData.value = { ...calendarMetaData.value, ...updateObj, meta: updateValue, } } catch (e) { message.error('Error updating changes') console.log(e) } } function findRowInState(rowData: Record) { const pk: Record = rowPkData(rowData, meta?.value?.columns as ColumnType[]) for (const row of formattedData.value) { if (Object.keys(pk).every((k) => pk[k] === row.row[k])) { return row } } } const paginateCalendarView = async (action: 'next' | 'prev') => { switch (activeCalendarView.value) { case 'month': selectedMonth.value = action === 'next' ? selectedMonth.value.add(1, 'month') : selectedMonth.value.subtract(1, 'month') pageDate.value = action === 'next' ? pageDate.value.add(1, 'month') : pageDate.value.subtract(1, 'month') // selectedDate.value = action === 'next' ? addMonths(selectedDate.value, 1) : addMonths(selectedDate.value, -1) if (pageDate.value.year() !== selectedDate.value.year()) { pageDate.value = selectedDate.value } break case 'year': selectedDate.value = action === 'next' ? selectedDate.value.add(1, 'year') : selectedDate.value.subtract(1, 'year') if (pageDate.value.year() !== selectedDate.value.year()) { pageDate.value = selectedDate.value } break case 'day': selectedDate.value = action === 'next' ? selectedDate.value.add(1, 'day') : selectedDate.value.subtract(1, 'day') selectedTime.value = selectedDate.value if (pageDate.value.year() !== selectedDate.value.year()) { pageDate.value = selectedDate.value } else if (pageDate.value.month() !== selectedDate.value.month()) { pageDate.value = selectedDate.value } break case 'week': selectedDateRange.value = action === 'next' ? { start: selectedDateRange.value.start.add(7, 'day'), end: selectedDateRange.value.end.add(7, 'day'), } : { start: selectedDateRange.value.start.subtract(7, 'day'), end: selectedDateRange.value.end.subtract(7, 'day'), } if (pageDate.value.month() !== selectedDateRange.value.end.month()) { pageDate.value = selectedDateRange.value.start } break } } const loadSidebarData = async (showLoading = true) => { if (!base?.value?.id || !meta.value?.id || !viewMeta.value?.id || !calendarRange.value?.length) return try { if (showLoading) isSidebarLoading.value = true const res = !isPublic.value ? await api.dbViewRow.list('noco', base.value.id!, meta.value!.id!, viewMeta.value.id, { ...queryParams.value, ...{}, ...{}, ...{ filterArrJson: JSON.stringify([...sideBarFilter.value]) }, }) : await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: [...nestedFilters.value, ...sideBarFilter.value], }) formattedSideBarData.value = formatData(res!.list) } catch (e) { message.error( `${t('msg.error.fetchingCalendarData')} ${await extractSdkResponseErrorMsg( e as Error & { response: { data: { message: string } } }, )}`, ) console.log(e) } finally { isSidebarLoading.value = false } } async function updateRowProperty(toUpdate: Row, property: string[], undo = false) { try { const id = extractPkFromRow(toUpdate.row, meta?.value?.columns as ColumnType[]) const updateObj = property.reduce( ( acc: { [x: string]: string }, curr, ) => { acc[curr] = toUpdate.row[curr] return acc }, {}, ) const updatedRowData = await $api.dbViewRow.update( NOCO, base?.value.id as string, meta.value?.id as string, viewMeta?.value?.id as string, id, updateObj, // todo: // { // query: { ignoreWebhook: !saved } // } ) if (!undo) { addUndo({ redo: { fn: async (toUpdate: Row, property: string[]) => { const updatedRow = await updateRowProperty(toUpdate, property, true) const row = findRowInState(toUpdate.row) if (row) { Object.assign(row.row, updatedRow) } Object.assign(row?.oldRow, updatedRow) }, args: [clone(toUpdate), property], }, undo: { fn: async (toUpdate: Row, property: string[]) => { const updatedData = await updateRowProperty( { row: toUpdate.oldRow, oldRow: toUpdate.row, rowMeta: toUpdate.rowMeta }, property, true, ) const row = findRowInState(toUpdate.row) if (row) { Object.assign(row.row, updatedData) } Object.assign(row!.oldRow, updatedData) }, args: [clone(toUpdate), property], }, scope: defineViewScope({ view: viewMeta.value as ViewType }), }) Object.assign(toUpdate.row, updatedRowData) Object.assign(toUpdate.oldRow, updatedRowData) } await fetchActiveDates() return updatedRowData } catch (e: any) { message.error(`${t('msg.error.rowUpdateFailed')} ${await extractSdkResponseErrorMsg(e)}`) } } watch(selectedDate, async (value, oldValue) => { if (activeCalendarView.value === 'month' || activeCalendarView.value === 'week') { if (sideBarFilterOption.value === 'selectedDate' && showSideMenu.value) { await loadSidebarData() } } else if (activeCalendarView.value === 'year') { if (value.year() !== oldValue.year()) { await Promise.all([loadCalendarData(), loadSidebarData(), await fetchActiveDates()]) } else if (sideBarFilterOption.value === 'selectedDate' && showSideMenu.value) { await loadSidebarData() } } else { if (showSideMenu.value) { await Promise.all([loadSidebarData(), loadCalendarData()]) } else { await Promise.all([loadCalendarData()]) } } if (activeCalendarView.value === 'year' && value.year() !== oldValue.year()) { await fetchActiveDates() } }) watch(selectedTime, async () => { if (calDataType.value !== UITypes.Date && showSideMenu.value && sideBarFilterOption.value === 'selectedHours') { await loadSidebarData() } }) watch(selectedMonth, async () => { if (activeCalendarView.value !== 'month') return await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()]) }) watch(selectedDateRange, async () => { if (activeCalendarView.value !== 'week') return await Promise.all([loadCalendarData(), loadSidebarData()]) }) watch(activeCalendarView, async (value, oldValue) => { if (oldValue === 'week') { pageDate.value = selectedDate.value selectedMonth.value = selectedTime.value ?? selectedDate.value ?? selectedDateRange.value.start selectedDate.value = selectedTime.value ?? selectedDateRange.value.start selectedTime.value = selectedDate.value ?? selectedDateRange.value.start } else if (oldValue === 'month') { selectedDate.value = selectedMonth.value pageDate.value = selectedDate.value selectedTime.value = selectedDate.value selectedDateRange.value = { start: selectedDate.value.startOf('week'), end: selectedDate.value.endOf('week'), } } else if (oldValue === 'day') { pageDate.value = selectedDate.value selectedTime.value = selectedDate.value selectedMonth.value = selectedDate.value selectedDateRange.value = { start: selectedDate.value.startOf('week'), end: selectedDate.value.endOf('week'), } } else if (oldValue === 'year') { selectedMonth.value = selectedDate.value selectedTime.value = selectedDate.value pageDate.value = selectedDate.value selectedDateRange.value = { start: selectedDate.value.startOf('week'), end: selectedDate.value.endOf('week'), } } sideBarFilterOption.value = activeCalendarView.value ?? 'allRecords' if (activeCalendarView.value === 'year') { await Promise.all([loadSidebarData(), fetchActiveDates()]) } else { await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()]) } }) watch(showSideMenu, async (val) => { if (val) await loadSidebarData() }) watch(sideBarFilterOption, async () => { $e('a:calendar:sidebar-filter', sideBarFilterOption.value) await loadSidebarData() }) watch(searchQuery, async () => { await loadSidebarData() }) watch(pageDate, async () => { if (activeCalendarView.value === 'year') return await fetchActiveDates() }) watch( () => viewMetaProperties.value.hide_weekend, async () => { if (activeCalendarView.value === 'week') { await loadCalendarData() } }, ) return { fetchActiveDates, formattedSideBarData, loadMoreSidebarData, updateCalendarMeta, loadSidebarData, displayField, sideBarFilterOption, searchQuery, activeDates, isCalendarDataLoading, isCalendarMetaLoading, changeCalendarView, calDataType, loadCalendarMeta, calendarRange, loadCalendarData, formattedData, isSidebarLoading, showSideMenu, selectedTime, calendarMetaData, updateRowProperty, activeCalendarView, pageDate, paginationData, selectedDate, selectedMonth, selectedDateRange, paginateCalendarView, viewMetaProperties, } }, ) export { useProvideCalendarViewStore } export function useCalendarViewStoreOrThrow() { const calendarViewStore = useCalendarViewStore() if (calendarViewStore == null) throw new Error('Please call `useProvideCalendarViewStore` on the appropriate parent component') return calendarViewStore }