Browse Source

feat(nc-gui): calendar view (wip)

pull/7611/head
DarkPhoenix2704 9 months ago
parent
commit
4ff0993491
  1. 7
      packages/nc-gui/components/dlg/ViewCreate.vue
  2. 2
      packages/nc-gui/components/nc/DateWeekSelector.vue
  3. 90
      packages/nc-gui/components/smartsheet/calendar/DayView.vue
  4. 12
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  5. 42
      packages/nc-gui/components/smartsheet/calendar/WeekView.vue
  6. 102
      packages/nc-gui/components/smartsheet/calendar/index.vue
  7. 9
      packages/nc-gui/components/smartsheet/toolbar/CalendarMode.vue
  8. 119
      packages/nc-gui/composables/useCalendarViewStore.ts

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

@ -285,6 +285,7 @@ onMounted(async () => {
return { return {
value: field.id, value: field.id,
label: field.title, label: field.title,
uidt: field.uidt,
} }
}) })
@ -461,7 +462,11 @@ onMounted(async () => {
v-model:value="range.fk_to_column_id" v-model:value="range.fk_to_column_id"
:disabled="isMetaLoading" :disabled="isMetaLoading"
:loading="isMetaLoading" :loading="isMetaLoading"
:options="viewSelectFieldOptions" :options="
viewSelectFieldOptions.filter(
(el) => el.uidt === viewSelectFieldOptions.find((el) => el.id === range.fk_from_column_id),
)
"
:placeholder="$t('placeholder.notSelected')" :placeholder="$t('placeholder.notSelected')"
class="w-full" class="w-full"
/> />

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

@ -16,7 +16,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
selectedDate: null, selectedDate: null,
isDisabled: false, isDisabled: false,
isMondayFirst: false, isMondayFirst: true,
pageDate: new Date(), pageDate: new Date(),
weekPicker: false, weekPicker: false,
disablePagination: false, disablePagination: false,

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

@ -1,38 +1,76 @@
<script setup lang="ts"> <script lang="ts" setup>
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk';
import { computed, ref, type Row } from '#imports';
interface Props {
isEmbed?: boolean
data?: Row[] | null
}
const props = withDefaults(defineProps<Props>(), {
isEmbed: false,
data: null,
})
const emit = defineEmits(['expand-record']) const emit = defineEmits(['expand-record'])
const meta = inject(MetaInj, ref())
const fields = inject(FieldsInj, ref([]))
const data = toRefs(props).data
const displayField = computed(() => meta.value?.columns?.find((c) => c.pv && fields.value.includes(c)) ?? null)
const { pageDate, selectedDate, calDataType, formattedData, calendarRange } = useCalendarViewStoreOrThrow()
const hours = computed(() => {
const hours = []
for (let i = 0; i < 24; i++) {
hours.push(i)
}
return hours
})
const renderData = computed(() => {
console.log(data.value)
if (data.value) {
const { pageDate, selectedDate, calDataType } = useCalendarViewStoreOrThrow() return data.value
}
const events = ref([ return formattedData.value
{ })
Id: 1,
Title: 'Event 01',
from_date_time: '2023-12-15',
to_date_time: '2023-12-20',
},
{
Id: 2,
Title: 'Event 02',
from_date_time: '2023-12-20',
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"> <template v-if="formattedData.length">
<div
v-if="calDataType === UITypes.Date"
:class="{
'h-calc(100vh-10.8rem) overflow-y-auto nc-scrollbar-md': !props.isEmbed,
'border-r-1 h-full border-gray-200 hover:bg-gray-50 ': props.isEmbed,
}"
class="flex flex-col pt-3 gap-2 h-full w-full px-1"
>
<LazySmartSheetRow v-for="(record, rowIndex) in renderData" :row="record">
<LazySmartsheetCalendarRecordCard <LazySmartsheetCalendarRecordCard
v-for="event in events" :key="rowIndex"
:key="event.Id" :date="record.row[calendarRange[0].fk_from_col.title]"
:name="event.Title" :name="record.row[displayField.title]"
:date="event.from_date_time"
color="blue" color="blue"
size="medium" size="small"
@click="emit('expand-record', 'xxx')" @click="emit('expand-record', record)"
/> />
</LazySmartSheetRow>
</div> </div>
<div
v-else-if="calDataType === UITypes.DateTime"
:class="{
'h-calc(100vh-10.8rem) overflow-y-auto nc-scrollbar-md': !props.isEmbed,
'border-r-1 h-full border-gray-200 ': props.isEmbed,
}"
class="flex flex-col mt-3 gap-2 w-full px-1"
></div>
</template>
<div v-else-if="!data" class="w-full h-full flex text-md font-bold text-gray-500 items-center justify-center">No Records in this day</div>
</template> </template>
<style scoped lang="scss"></style> <style lang="scss" scoped></style>

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

@ -1,14 +1,10 @@
<script setup lang="ts"> <script lang="ts" setup>
import { CalendarViewTypeInj } from '#imports'
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
}>() }>()
const emit = defineEmits(['expand-record']) const emit = defineEmits(['expand-record'])
const activeCalendarView = inject(CalendarViewTypeInj, ref<'month' | 'day' | 'year' | 'week'>('month' as const))
const { t } = useI18n() const { t } = useI18n()
const options = computed(() => { const options = computed(() => {
@ -44,9 +40,7 @@ const options = computed(() => {
} }
}) })
const { pageDate, selectedDate, selectedDateRange } = useCalendarViewStoreOrThrow() const { pageDate, selectedDate, selectedDateRange, activeDates, activeCalendarView } = useCalendarViewStoreOrThrow()
const activeDates = ref([new Date()])
</script> </script>
<template> <template>
@ -112,4 +106,4 @@ const activeDates = ref([new Date()])
</div> </div>
</template> </template>
<style scoped lang="scss"></style> <style lang="scss" scoped></style>

42
packages/nc-gui/components/smartsheet/calendar/WeekView.vue

@ -0,0 +1,42 @@
<script lang="ts" setup>
import dayjs from 'dayjs';
const { selectedDateRange, formattedData, calendarRange } = useCalendarViewStoreOrThrow()
const weekDates = computed(() => {
const startOfWeek = new Date(selectedDateRange.value.start)
const endOfWeek = new Date(selectedDateRange.value.end)
const datesArray = []
while (startOfWeek <= endOfWeek) {
datesArray.push(new Date(startOfWeek))
startOfWeek.setDate(startOfWeek.getDate() + 1)
}
return datesArray
})
const getData = (date: Date) => {
const range = calendarRange.value[0]
return formattedData.value.filter((record) => dayjs(date).isSame(dayjs(record.row[range.fk_from_col.title])))
}
</script>
<template>
<div class="flex flex-col">
<div class="flex">
<div
v-for="date in weekDates"
:key="date"
class="w-1/7 text-center text-sm text-gray-500 w-full py-1 !last:mr-2.5 border-gray-200 border-b-1 border-r-1 bg-gray-50"
>
{{ dayjs(date).format('DD ddd') }}
</div>
</div>
<div class="flex overflow-auto nc-scrollbar-md h-[calc(100vh-12rem)]">
<div v-for="date in weekDates" :key="date" class="flex flex-col items-center w-1/7">
<LazySmartsheetCalendarDayView :data="getData(date)" :is-embed="true" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped></style>

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

@ -9,6 +9,11 @@ import {
IsGridInj, IsGridInj,
IsKanbanInj, IsKanbanInj,
MetaInj, MetaInj,
ReloadViewDataHookInj,
ReloadViewMetaHookInj,
type Row as RowType,
computed,
extractPkFromRow,
inject, inject,
provide, provide,
ref, ref,
@ -19,6 +24,9 @@ const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref()) const view = inject(ActiveViewInj, ref())
const reloadViewMetaHook = inject(ReloadViewMetaHookInj)
const reloadViewDataHook = inject(ReloadViewDataHookInj)
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const { t } = useI18n() const { t } = useI18n()
@ -36,6 +44,8 @@ provide(IsCalendarInj, ref(true))
const { const {
formattedData, formattedData,
loadCalendarMeta, loadCalendarMeta,
loadCalendarData,
isCalendarDataLoading,
updateCalendarMeta, updateCalendarMeta,
calendarMetaData, calendarMetaData,
selectedDate, selectedDate,
@ -50,15 +60,58 @@ provide(CalendarViewTypeInj, activeCalendarView)
const showSideMenu = ref(true) const showSideMenu = ref(true)
const isExpanded = ref(false) const expandedFormOnRowIdDlg = computed({
get() {
return !!route.query.rowId
},
set(val) {
if (!val)
router.push({
query: {
...route.query,
rowId: undefined,
},
})
},
})
const expandedFormDlg = ref(false)
const expandedFormRow = ref<RowType>()
const expandedFormRowState = ref<Record<string, any>>()
const expandedRecordId = ref<string | null>(null) const router = useRouter()
const route = useRoute()
const expandRecord = (id: string) => { const expandRecord = (row: RowType) => {
isExpanded.value = true const rowId = extractPkFromRow(row.row, meta.value!.columns!)
expandedRecordId.value = id
if (rowId) {
router.push({
query: {
...route.query,
rowId,
},
})
} else {
expandedFormRow.value = row
expandedFormRowState.value = state
expandedFormDlg.value = true
}
} }
onMounted(async () => {
await loadCalendarMeta()
await loadCalendarData()
})
reloadViewMetaHook?.on(async () => {
await loadCalendarMeta()
await loadCalendarData()
})
reloadViewDataHook?.on(async () => {
await loadCalendarData()
})
const headerText = computed(() => { const headerText = computed(() => {
switch (activeCalendarView.value) { switch (activeCalendarView.value) {
case 'day': case 'day':
@ -83,7 +136,7 @@ const headerText = computed(() => {
<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-center 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>
@ -98,22 +151,39 @@ const headerText = computed(() => {
/> />
</NcButton> </NcButton>
</div> </div>
<template v-if="!isCalendarDataLoading">
<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
<LazySmartsheetCalendarDayView v-else-if="activeCalendarView === 'day'" class="flex-grow-1" /> v-else-if="activeCalendarView === 'month'"
class="flex-grow-1"
@expand-record="expandRecord"
/>
<LazySmartsheetCalendarWeekView
v-else-if="activeCalendarView === 'week'"
class="flex-grow-1"
@expand-record="expandRecord"
/>
<LazySmartsheetCalendarDayView
v-else-if="activeCalendarView === 'day'"
class="flex-grow-1"
@expand-record="expandRecord"
/>
</template>
<div v-if="isCalendarDataLoading" class="flex w-full items-center h-full justify-center">
<GeneralLoader size="xlarge" />
</div>
</div> </div>
<LazySmartsheetCalendarSideMenu v-if="!isMobileMode" :visible="showSideMenu" @expand-record="expandRecord" /> <LazySmartsheetCalendarSideMenu v-if="!isMobileMode" :visible="showSideMenu" @expand-record="expandRecord" />
</div> </div>
<Suspense>
<LazySmartsheetExpandedForm <LazySmartsheetExpandedForm
v-model="isExpanded" v-if="expandedFormOnRowIdDlg"
:view="view" v-model="expandedFormOnRowIdDlg"
:row="{
row: {},
rowMeta: {
new: !expandedRecordId,
},
}"
:meta="meta" :meta="meta"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:row-id="route.query.rowId"
:view="view"
/> />
</Suspense>
</template> </template>

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

@ -10,11 +10,18 @@ const setActiveCalendarMode = (mode: 'day' | 'week' | 'month' | 'year', event: M
const tabElement = event.target as HTMLElement const tabElement = event.target as HTMLElement
highlightStyle.value.left = `${tabElement.offsetLeft}px` highlightStyle.value.left = `${tabElement.offsetLeft}px`
} }
onMounted(() => {
const activeTab = document.querySelector('.nc-calendar-mode-tab .tab.active') as HTMLElement
if (activeTab) {
highlightStyle.value.left = `${activeTab.offsetLeft}px`
}
})
</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 :style="highlightStyle" class="highlight"></div>
<div <div
v-for="mode in ['day', 'week', 'month', 'year']" v-for="mode in ['day', 'week', 'month', 'year']"
:key="mode" :key="mode"

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

@ -1,9 +1,8 @@
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import type { CalendarType, PaginatedType, ViewType } from 'nocodb-sdk' import type { CalendarType, PaginatedType, UITypes, ViewType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import dayjs from 'dayjs'
import { addDays, addMonths, addYears } from '../utils' import { addDays, addMonths, addYears } from '../utils'
import { IsPublicInj, ref, storeToRefs, useBase, useInjectionState, useMetas } from '#imports' import { IsPublicInj, type Row, ref, storeToRefs, useBase, useInjectionState } from '#imports'
import type { Row } from '#imports'
const formatData = (list: Record<string, any>[]) => const formatData = (list: Record<string, any>[]) =>
list.map( list.map(
@ -28,15 +27,21 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
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'>()
const calDataType = ref<UITypes.Date | UITypes.DateTime>(UITypes.Date) const calDataType = ref<UITypes.Date | UITypes.DateTime>()
const selectedDate = ref<Date>(new Date()) const selectedDate = ref<Date>(new Date())
const isCalendarDataLoading = ref<boolean>(false)
const selectedDateRange = ref<{ const selectedDateRange = ref<{
start: Date | null start: Date | null
end: Date | null end: Date | null
}>({ start: new Date(), end: null }) }>({
start: dayjs(selectedDate.value).startOf('week').toDate(), // This will be the previous Sunday
end: dayjs(selectedDate.value).startOf('week').add(6, 'day').toDate(), // This will be the following Saturday
})
const defaultPageSize = 1000 const defaultPageSize = 1000
@ -58,9 +63,6 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const calendarMetaData = ref<CalendarType>({}) const calendarMetaData = ref<CalendarType>({})
// Not Required in Calendar View TODO: Remove
// 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(() => ({
@ -68,43 +70,96 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
where: where?.value ?? '', where: where?.value ?? '',
})) }))
const changeCalendarView = (view: 'month' | 'year' | 'day' | 'week') => { const calendarRange = computed(() => {
if (!meta.value || !meta.value.columns || !calendarMetaData.value || !calendarMetaData.value.calendar_range) return []
return calendarMetaData.value.calendar_range.map((range) => {
// Get the column data for the calendar range
return {
fk_from_col: meta.value.columns.find((col) => col.id === range.fk_from_column_id),
fk_to_col: meta.value.columns.find((col) => col.id === range.fk_to_column_id),
}
})
})
const xWhere = computed(() => {
if (!meta.value || !meta.value.columns || !calendarMetaData.value || !calendarMetaData.value.calendar_range) return ''
console.log(meta.value.columns.find((col) => col.id === calendarMetaData.value.calendar_range[0].fk_from_column_id))
// If CalendarView, then we need to add the date filter to the where clause
let whereClause = where?.value ?? ''
if (whereClause.length > 0) {
whereClause += '~and('
}
if (activeCalendarView.value === 'week') {
whereClause += `(${
meta.value.columns.find((col) => col.id === calendarMetaData.value.calendar_range[0].fk_from_column_id).title
},gte,exactDate,${dayjs(selectedDateRange.value.start).format('YYYY-MM-DD')})`
whereClause += `~and(${
meta.value.columns.find((col) => col.id === calendarMetaData.value.calendar_range[0].fk_from_column_id).title
},lte,exactDate,${dayjs(selectedDateRange.value.end).format('YYYY-MM-DD')})`
return whereClause
} else if (activeCalendarView.value === 'day') {
return `(${
meta.value.columns.find((col) => col.id === calendarMetaData.value.calendar_range[0].fk_from_column_id).title
},eq,exactDate,${dayjs(selectedDate.value).format('YYYY-MM-DD')})`
}
})
// Set of Dates that have data
const activeDates = computed(() => {
const dates = new Set<Date>()
formattedData.value.forEach((row) => {
const date = dayjs(row.row[calendarMetaData.value.calendar_range![0].fk_from_column_id!]).toDate()
dates.add(date)
})
return Array.from(dates)
})
const changeCalendarView = async (view: 'month' | 'year' | 'day' | 'week') => {
try {
activeCalendarView.value = view activeCalendarView.value = view
await updateCalendarMeta({
meta: {
...JSON.parse(calendarMetaData.value?.meta ?? '{}'),
active_view: view,
},
})
} catch (e) {
message.error('Error changing calendar view')
console.log(e)
}
} }
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 calendarMetaData.value = isPublic.value
? (sharedView.value?.view as CalendarType) ? (sharedView.value?.view as CalendarType)
: await $api.dbView.mapRead(viewMeta.value.id) : await $api.dbView.calendarRead(viewMeta.value.id)
activeCalendarView.value = JSON.parse(calendarMetaData.value?.meta ?? '{}')?.active_view ?? 'month'
/* geoDataFieldColumn.value = calDataType.value = meta.value.columns.find((col) => col.id === calendarMetaData.value.calendar_range[0].fk_from_column_id)
(meta.value.columns as ColumnType[]).filter((f) => f.id === mapMetaData.value.fk_geo_data_col_id)[0] || {} ?.uidt as UITypes.Date | UITypes.DateTime
*/
} }
async function loadCalendarData() { async function loadCalendarData() {
if ((!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic?.value) return if ((!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic?.value) return
isCalendarDataLoading.value = true
const res = !isPublic.value const res = !isPublic.value
? await api.dbViewRow.list('noco', base.value.id!, meta.value!.id!, viewMeta.value!.id!, { ? await api.dbViewRow.list('noco', base.value.id!, meta.value!.id!, viewMeta.value!.id!, {
...queryParams.value, ...queryParams.value,
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
where: where?.value, where: xWhere?.value,
}) })
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value }) : await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value })
formattedData.value = formatData(res!.list) formattedData.value = formatData(res!.list)
isCalendarDataLoading.value = false
} }
async function updateCalendarMeta(updateObj: Partial<CalendarType>) { async function updateCalendarMeta(updateObj: Partial<CalendarType>) {
if (!viewMeta?.value?.id || !isUIAllowed('dataEdit')) return if (!viewMeta?.value?.id || !isUIAllowed('dataEdit')) return
// await $api.dbView.calendarUpdate(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') => { const paginateCalendarView = async (action: 'next' | 'prev') => {
switch (activeCalendarView.value) { switch (activeCalendarView.value) {
case 'month': case 'month':
selectedDate.value = action === 'next' ? addMonths(selectedDate.value, 1) : addMonths(selectedDate.value, -1) selectedDate.value = action === 'next' ? addMonths(selectedDate.value, 1) : addMonths(selectedDate.value, -1)
@ -137,15 +192,35 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
start: addDays(selectedDateRange.value.start!, -7), start: addDays(selectedDateRange.value.start!, -7),
end: addDays(selectedDateRange.value.end!, -7), end: addDays(selectedDateRange.value.end!, -7),
} }
if (pageDate.value.getMonth() !== selectedDateRange.value.end?.getMonth()) {
pageDate.value = selectedDateRange.value.start!
}
break break
} }
} }
watch(selectedDate, async () => {
await loadCalendarData()
})
watch(selectedDateRange, async () => {
if (activeCalendarView.value !== 'week') return
await loadCalendarData()
})
watch(activeCalendarView, async () => {
await loadCalendarData()
})
return { return {
formattedData, formattedData,
activeDates,
isCalendarDataLoading,
changeCalendarView, changeCalendarView,
calDataType, calDataType,
loadCalendarMeta, loadCalendarMeta,
calendarRange,
loadCalendarData,
updateCalendarMeta, updateCalendarMeta,
calendarMetaData, calendarMetaData,
activeCalendarView, activeCalendarView,

Loading…
Cancel
Save