多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

675 lines
24 KiB

<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { UITypes } from 'nocodb-sdk'
import dayjs from 'dayjs'
const props = defineProps<{
visible: boolean
}>()
const emit = defineEmits(['expandRecord', 'newRecord'])
interface Attachment {
url: string
}
const INFINITY_SCROLL_THRESHOLD = 100
const { isUIAllowed } = useRoles()
const { $e } = useNuxtApp()
const { appInfo, isMobileMode } = useGlobal()
const { height } = useWindowSize()
const meta = inject(MetaInj, ref())
const { fields } = useViewColumnsOrThrow()
const { getPossibleAttachmentSrc } = useAttachment()
const { t } = useI18n()
const {
pageDate,
displayField,
selectedDate,
selectedTime,
selectedMonth,
calendarRange,
selectedDateRange,
activeDates,
activeCalendarView,
isSidebarLoading,
isCalendarMetaLoading,
formattedSideBarData,
calDataType,
loadMoreSidebarData,
searchQuery,
sideBarFilterOption,
showSideMenu,
} = useCalendarViewStoreOrThrow()
const sideBarListRef = ref<VNodeRef | null>(null)
const coverImageColumns: any = computed(() => {
if (!fields.value || !meta.value?.columns) return
return meta.value.columns.find((c) => c.uidt === UITypes.Attachment && fields.value?.find((f) => f.fk_column_id === c.id).show)
})
const attachments = (record: any): Attachment[] => {
const col = coverImageColumns.value
try {
if (col?.title && record.row[col.title]) {
return typeof record.row[col.title] === 'string' ? JSON.parse(record.row[col.title]) : record.row[col.title]
}
return []
} catch (e) {
return []
}
}
const pushToArray = (arr: Array<Row>, record: Row, range) => {
arr.push({
...record,
rowMeta: {
...record.rowMeta,
range,
},
})
}
const dragElement = ref<HTMLElement | null>(null)
const dragStart = (event: DragEvent, record: Row) => {
dragElement.value = event.target as HTMLElement
const eventRect = dragElement.value.getBoundingClientRect()
const initialClickOffsetX = event.clientX - eventRect.left
const initialClickOffsetY = event.clientY - eventRect.top
event.dataTransfer?.setData(
'text/plain',
JSON.stringify({
record,
isWithoutDates: sideBarFilterOption.value === 'withoutDates',
initialClickOffsetY,
initialClickOffsetX,
}),
)
}
const renderData = computed<Array<Row>>(() => {
if (!calendarRange.value) return []
const rangedData: Array<Row> = []
calendarRange.value.forEach((range) => {
const fromCol = range.fk_from_col
const toCol = range.fk_to_col
formattedSideBarData.value.forEach((record) => {
if (fromCol && toCol) {
const from = dayjs(record.row[fromCol.title!])
const to = dayjs(record.row[toCol.title!])
if (sideBarFilterOption.value === 'withoutDates') {
if (!from.isValid() || !to.isValid()) {
pushToArray(rangedData, record, range)
}
} else if (sideBarFilterOption.value === 'allRecords') {
pushToArray(rangedData, record, range)
} else if (
sideBarFilterOption.value === 'month' ||
sideBarFilterOption.value === 'year' ||
sideBarFilterOption.value === 'selectedDate' ||
sideBarFilterOption.value === 'selectedHours' ||
sideBarFilterOption.value === 'week' ||
sideBarFilterOption.value === 'day'
) {
let fromDate: dayjs.Dayjs | null = null
let toDate: dayjs.Dayjs | null = null
switch (sideBarFilterOption.value) {
case 'month':
fromDate = dayjs(selectedMonth.value).startOf('month')
toDate = dayjs(selectedMonth.value).endOf('month')
break
case 'year':
fromDate = dayjs(selectedDate.value).startOf('year')
toDate = dayjs(selectedDate.value).endOf('year')
break
case 'selectedDate':
fromDate = dayjs(selectedDate.value).startOf('day')
toDate = dayjs(selectedDate.value).endOf('day')
break
case 'week':
fromDate = dayjs(selectedDateRange.value.start).startOf('week')
toDate = dayjs(selectedDateRange.value.end).endOf('week')
break
case 'day':
fromDate = dayjs(selectedDate.value).startOf('day')
toDate = dayjs(selectedDate.value).endOf('day')
break
case 'selectedHours':
fromDate = dayjs(selectedTime.value).startOf('hour')
toDate = dayjs(selectedTime.value).endOf('hour')
break
}
if (from && to) {
if (
(from.isSameOrAfter(fromDate) && to.isSameOrBefore(toDate)) ||
(from.isSameOrBefore(fromDate) && to.isSameOrAfter(toDate)) ||
(from.isSameOrBefore(fromDate) && to.isSameOrAfter(fromDate)) ||
(from.isSameOrBefore(toDate) && to.isSameOrAfter(toDate))
) {
pushToArray(rangedData, record, range)
}
}
}
} else if (fromCol) {
const from = dayjs(record.row[fromCol.title!])
if (sideBarFilterOption.value === 'withoutDates') {
if (!from.isValid()) {
pushToArray(rangedData, record, range)
}
} else if (sideBarFilterOption.value === 'allRecords') {
pushToArray(rangedData, record, range)
} else if (sideBarFilterOption.value === 'selectedDate' || sideBarFilterOption.value === 'day') {
if (from.isSame(selectedDate.value, 'day')) {
pushToArray(rangedData, record, range)
}
} else if (sideBarFilterOption.value === 'selectedHours') {
if (from.isSame(selectedTime.value, 'hour')) {
pushToArray(rangedData, record, range)
}
} else if (
sideBarFilterOption.value === 'week' ||
sideBarFilterOption.value === 'month' ||
sideBarFilterOption.value === 'year'
) {
let fromDate: dayjs.Dayjs
let toDate: dayjs.Dayjs
switch (sideBarFilterOption.value) {
case 'week':
fromDate = dayjs(selectedDateRange.value.start).startOf('week')
toDate = dayjs(selectedDateRange.value.end).endOf('week')
break
case 'month':
fromDate = dayjs(selectedMonth.value).startOf('month')
toDate = dayjs(selectedMonth.value).endOf('month')
break
case 'year':
fromDate = dayjs(selectedDate.value).startOf('year')
toDate = dayjs(selectedDate.value).endOf('year')
break
}
if (from.isSameOrAfter(fromDate) && from.isSameOrBefore(toDate)) {
pushToArray(rangedData, record, range)
}
}
}
})
})
return rangedData
})
const options = computed(() => {
switch (activeCalendarView.value) {
case 'day' as const:
if (calDataType.value === UITypes.Date) {
return [
{ label: 'In this day', value: 'day' },
{ label: 'Without dates', value: 'withoutDates' },
{ label: 'All records', value: 'allRecords' },
]
} else {
return [
{ label: 'In this day', value: 'day' },
{ label: 'Without dates', value: 'withoutDates' },
{ label: 'All records', value: 'allRecords' },
{ label: 'In selected hours', value: 'selectedHours' },
]
}
case 'week' as const:
if (calDataType.value === UITypes.Date) {
return [
{ label: 'In selected date', value: 'selectedDate' },
{ label: 'Without dates', value: 'withoutDates' },
{ label: 'In selected week', value: 'week' },
{ label: 'All records', value: 'allRecords' },
]
} else {
return [
{ label: 'Without dates', value: 'withoutDates' },
{ label: 'All records', value: 'allRecords' },
{ label: 'In selected hours', value: 'selectedHours' },
{ label: 'In selected week', value: 'week' },
{ label: 'In selected date', value: 'selectedDate' },
]
}
case 'month' as const:
return [
{ label: 'In this month', value: 'month' },
{ label: 'Without dates', value: 'withoutDates' },
{ label: 'All records', value: 'allRecords' },
{ label: 'In selected date', value: 'selectedDate' },
]
case 'year' as const:
return [
{ label: 'In this year', value: 'year' },
{ label: 'Without dates', value: 'withoutDates' },
{ label: 'All records', value: 'allRecords' },
{ label: 'In selected date', value: 'selectedDate' },
]
}
})
const sideBarListScrollHandle = useDebounceFn(async (e: Event) => {
const target = e.target as HTMLElement
if (target.clientHeight + target.scrollTop + INFINITY_SCROLL_THRESHOLD >= target.scrollHeight) {
const pageSize = appInfo.value?.defaultLimit ?? 25
const page = Math.ceil(formattedSideBarData.value.length / pageSize)
await loadMoreSidebarData({
offset: page * pageSize,
})
}
})
const newRecord = () => {
const row = {
...rowDefaultData(meta.value?.columns),
}
if (activeCalendarView.value === 'day') {
row[calendarRange.value[0]!.fk_from_col!.title!] = selectedDate.value.format('YYYY-MM-DD HH:mm:ssZ')
} else if (activeCalendarView.value === 'week') {
row[calendarRange.value[0]!.fk_from_col!.title!] = selectedDateRange.value.start.format('YYYY-MM-DD HH:mm:ssZ')
} else if (activeCalendarView.value === 'month') {
row[calendarRange.value[0]!.fk_from_col!.title!] = (selectedDate.value ?? selectedMonth.value).format('YYYY-MM-DD HH:mm:ssZ')
} else if (activeCalendarView.value === 'year') {
row[calendarRange.value[0]!.fk_from_col!.title!] = selectedDate.value.format('YYYY-MM-DD HH:mm:ssZ')
}
emit('newRecord', { row, oldRow: {}, rowMeta: { new: true } })
}
const toggleSideMenu = () => {
$e('c:calendar:toggle-sidebar', showSideMenu.value)
showSideMenu.value = !showSideMenu.value
}
const showSearch = ref(false)
const searchRef = ref()
const clickSearch = () => {
showSearch.value = true
nextTick(() => {
searchRef.value?.focus()
})
}
const toggleSearch = () => {
if (!searchQuery.value.length) {
showSearch.value = false
} else {
searchRef.value?.blur()
}
}
useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
if (cmdOrCtrl) {
switch (e.key.toLowerCase()) {
case 'f':
e.preventDefault()
clickSearch()
break
}
}
})
onClickOutside(searchRef, toggleSearch)
</script>
<template>
<NcTooltip
:class="{
'!right-26 top-[-36px]': showSideMenu && isMobileMode,
'right-2': !showSideMenu,
'right-74': showSideMenu,
}"
class="absolute transition-all ease-in-out z-9 top-2"
hide-on-click
>
<template #title> {{ $t('activity.toggleSidebar') }}</template>
<NcButton data-testid="nc-calendar-side-bar-btn" size="small" type="secondary" @click="toggleSideMenu">
<component :is="iconMap.sidebar" class="h-4 w-4 text-gray-600 transition-all" />
</NcButton>
</NcTooltip>
<div
:class="{
'!min-w-[100svw]': props.visible && isMobileMode,
'!w-0 hidden': !props.visible,
'nc-calendar-side-menu-open block !min-w-[288px]': props.visible,
}"
class="h-full relative border-l-1 border-gray-200 transition-all"
data-testid="nc-calendar-side-menu"
>
<div class="flex min-w-[288px] flex-col">
<NcDateWeekSelector
v-if="activeCalendarView === ('day' as const)"
v-model:active-dates="activeDates"
v-model:page-date="pageDate"
v-model:selected-date="selectedDate"
size="medium"
:hide-calendar="height < 700"
/>
<NcDateWeekSelector
v-else-if="activeCalendarView === ('week' as const)"
v-model:active-dates="activeDates"
v-model:page-date="pageDate"
v-model:selected-week="selectedDateRange"
:hide-calendar="height < 700"
is-week-picker
size="medium"
/>
<NcMonthYearSelector
v-else-if="activeCalendarView === ('month' as const)"
v-model:page-date="pageDate"
v-model:selected-date="selectedMonth"
:hide-calendar="height < 700"
size="medium"
/>
<NcMonthYearSelector
v-else-if="activeCalendarView === ('year' as const)"
v-model:page-date="pageDate"
v-model:selected-date="selectedDate"
:hide-calendar="height < 700"
is-year-picker
size="medium"
/>
</div>
<div
:class="{
'!border-t-0 ': height < 700,
'pt-6': height >= 700,
}"
class="border-t-1 !pt-3 border-gray-200 relative flex flex-col gap-y-3"
>
<div class="flex px-4 items-center gap-3">
<span class="capitalize font-medium text-gray-700">{{ $t('objects.records') }}</span>
<NcSelect
v-model:value="sideBarFilterOption"
size="small"
class="w-full !h-7 !text-gray-600"
data-testid="nc-calendar-sidebar-filter"
>
<a-select-option v-for="option in options" :key="option.value" :value="option.value" class="!text-gray-600">
<div class="flex items-center h-7 w-full justify-between gap-2">
<div class="truncate">
<NcTooltip :title="option.label" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
{{ option.label }}
</NcTooltip>
</div>
<component
:is="iconMap.check"
v-if="sideBarFilterOption === option.value"
id="nc-selected-item-icon"
class="text-primary w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
</div>
<div
:class="{
hidden: !showSearch,
}"
class="mx-4"
>
<a-input
ref="searchRef"
v-model:value="searchQuery.value"
:class="{
'!border-brand-500': searchQuery.value.length > 0,
'!hidden': !showSearch,
}"
class="!rounded-lg !h-8 !placeholder:text-gray-500 !border-gray-200 !px-4"
data-testid="nc-calendar-sidebar-search"
placeholder="Search records"
@keydown.esc="toggleSearch"
>
<template #prefix>
<component :is="iconMap.search" class="h-4 w-4 mr-1 text-gray-500" />
</template>
</a-input>
</div>
<div class="mx-4 gap-2 flex items-center">
<NcButton
v-if="!showSearch"
data-testid="nc-calendar-sidebar-search-btn"
size="small"
class="!h-7 !rounded-md"
type="secondary"
@click="clickSearch"
>
<component :is="iconMap.search" />
</NcButton>
<LazySmartsheetToolbarSortListMenu />
<div class="flex-1" />
<NcButton
v-if="isUIAllowed('dataEdit') && props.visible"
v-e="['c:calendar:calendar-sidemenu-new-record-btn']"
data-testid="nc-calendar-side-menu-new-btn"
class="!h-7 !rounded-md"
size="small"
type="secondary"
@click="newRecord"
>
<div class="flex items-center gap-2">
<component :is="iconMap.plus" />
Record
</div>
</NcButton>
</div>
<div
v-if="calendarRange?.length && !isCalendarMetaLoading"
:ref="sideBarListRef"
:class="{
'!h-[calc(100svh-22.15rem)]':
height > 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && !showSearch,
'!h-[calc(100svh-24.9rem)]':
height > 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && showSearch,
'!h-[calc(100svh-13.85rem)]':
height <= 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && !showSearch,
'!h-[calc(100svh-16.61rem)]':
height <= 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && showSearch,
'!h-[calc(100svh-30.15rem)]':
height > 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && !showSearch,
' !h-[calc(100svh-32.9rem)]':
height > 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && showSearch,
'!h-[calc(100svh-13.8rem)]':
height <= 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && !showSearch,
'!h-[calc(100svh-16.6rem)]':
height <= 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && showSearch,
}"
class="nc-scrollbar-md pl-4 pr-4 overflow-y-auto"
data-testid="nc-calendar-side-menu-list"
@scroll="sideBarListScrollHandle"
>
<div v-if="renderData.length === 0 || isSidebarLoading" class="flex h-full items-center justify-center">
<GeneralLoader v-if="isSidebarLoading" size="large" />
<div v-else class="text-gray-500">
{{ t('msg.noRecordsFound') }}
</div>
</div>
<template v-else-if="renderData.length > 0">
<div class="gap-2 flex flex-col">
<LazySmartsheetRow v-for="(record, rowIndex) in renderData" :key="rowIndex" :row="record">
<LazySmartsheetCalendarSideRecordCard
:draggable="sideBarFilterOption === 'withoutDates' && activeCalendarView !== 'year'"
:from-date="
record.rowMeta.range?.fk_from_col
? calDataType === UITypes.Date
? dayjs(record.row[record.rowMeta.range.fk_from_col.title!]).format('D MMM')
: dayjs(record.row[record.rowMeta.range.fk_from_col.title!]).format('D MMM h:mm a')
: null
"
:invalid="
record.rowMeta.range!.fk_to_col &&
dayjs(record.row[record.rowMeta.range!.fk_from_col.title!]).isAfter(
dayjs(record.row[record.rowMeta.range!.fk_to_col.title!]),
)
"
:row="record"
:to-date="
record.rowMeta.range!.fk_to_col
? calDataType === UITypes.Date
? dayjs(record.row[record.rowMeta.range!.fk_to_col.title!]).format('DD MMM')
: dayjs(record.row[record.rowMeta.range!.fk_to_col.title!]).format('DD MMM HH:mm A')
: null
"
color="blue"
data-testid="nc-sidebar-record-card"
@click="emit('expandRecord', record)"
@dragstart="dragStart($event, record)"
@dragover.prevent
>
<template v-if="coverImageColumns" #image>
<a-carousel
v-if="attachments(record).length"
class="gallery-carousel rounded-md !border-1 !border-gray-200"
arrows
>
<template #customPaging>
<a>
<div class="pt-[12px]">
<div></div>
</div>
</a>
</template>
<template #prevArrow>
<div class="z-10 arrow">
<MdiChevronLeft
class="text-gray-700 w-6 h-6 absolute left-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
</div>
</template>
<template #nextArrow>
<div class="z-10 arrow">
<MdiChevronRight
class="text-gray-700 w-6 h-6 absolute right-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
</div>
</template>
<template v-for="(attachment, index) in attachments(record)">
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-10 !w-10 !object-contain"
:srcs="getPossibleAttachmentSrc(attachment)"
/>
</template>
</a-carousel>
<div v-else class="h-10 w-10 !flex flex-row !border-1 rounded-md !border-gray-200 items-center justify-center">
<img class="object-contain w-[40px] h-[40px]" src="~assets/icons/FileIconImageBox.png" />
</div>
</template>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetPlainCell v-model="record.row[displayField!.title!]" :column="displayField" />
</template>
</LazySmartsheetCalendarSideRecordCard>
</LazySmartsheetRow>
</div>
</template>
</div>
<template v-else-if="isCalendarMetaLoading">
<div
:class="{
'!h-[calc(100svh-22.15rem)]':
height > 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && !showSearch,
'!h-[calc(100svh-24.9rem)]':
height > 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && showSearch,
'!h-[calc(100svh-13.85rem)]':
height <= 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && !showSearch,
'!h-[calc(100svh-16.61rem)]':
height <= 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && showSearch,
'!h-[calc(100svh-30.15rem)]':
height > 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && !showSearch,
' !h-[calc(100svh-32.9rem)]':
height > 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && showSearch,
'!h-[calc(100svh-13.8rem)]':
height <= 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && !showSearch,
'!h-[calc(100svh-16.6rem)]':
height <= 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && showSearch,
}"
class="flex items-center justify-center h-full"
>
<GeneralLoader size="xlarge" />
</div>
</template>
<div
v-else
:class="{
'!h-[calc(100svh-22.15rem)]':
height > 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && !showSearch,
'!h-[calc(100svh-24.9rem)]':
height > 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && showSearch,
'!h-[calc(100svh-13.85rem)]':
height <= 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && !showSearch,
'!h-[calc(100svh-16.61rem)]':
height <= 700 && (activeCalendarView === 'month' || activeCalendarView === 'year') && showSearch,
'!h-[calc(100svh-30.15rem)]':
height > 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && !showSearch,
' !h-[calc(100svh-32.9rem)]':
height > 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && showSearch,
'!h-[calc(100svh-13.8rem)]':
height <= 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && !showSearch,
'!h-[calc(100svh-16.6rem)]':
height <= 700 && (activeCalendarView === 'day' || activeCalendarView === 'week') && showSearch,
}"
class="flex items-center justify-center h-full"
>
{{ $t('activity.noRange') }}
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
:deep(.nc-attachment-image) {
@apply rounded-md;
}
:deep(.ant-select-selector) {
@apply !h-7;
}
:deep(.nc-month-picker-pagination) {
@apply !border-b-0;
}
:deep(.nc-date-week-header) {
@apply !border-b-0;
}
</style>