mirror of https://github.com/nocodb/nocodb
github-actions[bot]
7 months ago
committed by
GitHub
90 changed files with 3663 additions and 2008 deletions
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,146 @@ |
|||||||
|
<script lang="ts"> |
||||||
|
// modified version of default NuxtErrorBoundary component - https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/components/nuxt-error-boundary.ts |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { computed, onErrorCaptured, ref, useCopy, useNuxtApp } from '#imports' |
||||||
|
|
||||||
|
export default { |
||||||
|
emits: { |
||||||
|
error(_error: unknown) { |
||||||
|
return true |
||||||
|
}, |
||||||
|
}, |
||||||
|
setup(_props, { emit }) { |
||||||
|
const nuxtApp = useNuxtApp() |
||||||
|
const error = ref() |
||||||
|
const prevError = ref() |
||||||
|
const errModal = computed(() => !!error.value) |
||||||
|
const key = ref(0) |
||||||
|
const isErrorExpanded = ref(false) |
||||||
|
const { copy } = useCopy() |
||||||
|
|
||||||
|
onErrorCaptured((err) => { |
||||||
|
if (import.meta.client && (!nuxtApp.isHydrating || !nuxtApp.payload.serverRendered)) { |
||||||
|
console.log('UI Error :', err) |
||||||
|
emit('error', err) |
||||||
|
error.value = err |
||||||
|
return false |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const copyError = async () => { |
||||||
|
try { |
||||||
|
if (error.value) await copy(`message: ${error.value.message}\n\n${error.value.stack}`) |
||||||
|
message.info('Error message copied to clipboard.') |
||||||
|
} catch (e) { |
||||||
|
message.error('Something went wrong while copying to clipboard, please copy from browser console.') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const reload = () => { |
||||||
|
prevError.value = error.value |
||||||
|
error.value = null |
||||||
|
key.value++ |
||||||
|
} |
||||||
|
|
||||||
|
const navigateToHome = () => { |
||||||
|
prevError.value = error.value |
||||||
|
error.value = null |
||||||
|
location.hash = '/' |
||||||
|
location.reload() |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
errModal, |
||||||
|
error, |
||||||
|
key, |
||||||
|
isErrorExpanded, |
||||||
|
prevError, |
||||||
|
copyError, |
||||||
|
reload, |
||||||
|
navigateToHome, |
||||||
|
} |
||||||
|
}, |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<slot :key="key"></slot> |
||||||
|
<slot name="error"> |
||||||
|
<NcModal v-model:visible="errModal" :class="{ active: errModal }" :centered="true" :closable="false" :footer="null"> |
||||||
|
<div class="w-full flex flex-col gap-1"> |
||||||
|
<h2 class="text-xl font-semibold">Oops! Something unexpected happened :/</h2> |
||||||
|
|
||||||
|
<p class="mb-0"> |
||||||
|
<span |
||||||
|
>Please report this error in our |
||||||
|
<a href="https://discord.gg/8jX2GQn" target="_blank" rel="noopener noreferrer">Discord channel</a>. You can copy the |
||||||
|
error message by clicking the "Copy" button below.</span |
||||||
|
> |
||||||
|
</p> |
||||||
|
|
||||||
|
<span class="cursor-pointer" @click="isErrorExpanded = !isErrorExpanded" |
||||||
|
>{{ isErrorExpanded ? 'Hide' : 'Show' }} details |
||||||
|
<GeneralIcon |
||||||
|
icon="arrowDown" |
||||||
|
class="transition-transform transform duration-300" |
||||||
|
:class="{ |
||||||
|
'rotate-180': isErrorExpanded, |
||||||
|
}" |
||||||
|
/></span> |
||||||
|
<div |
||||||
|
class="nc-error" |
||||||
|
:class="{ |
||||||
|
active: isErrorExpanded, |
||||||
|
}" |
||||||
|
> |
||||||
|
<div class="nc-left-vertical-bar"></div> |
||||||
|
<div class="nc-error-content"> |
||||||
|
<span class="font-weight-bold">Message: {{ error.message }}</span> |
||||||
|
<br /> |
||||||
|
<div class="text-gray-500 mt-2">{{ error.stack }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="flex justify-end gap-2"> |
||||||
|
<NcButton size="small" type="secondary" @click="copyError"> |
||||||
|
<div class="flex items-center gap-1"> |
||||||
|
<GeneralIcon icon="copy" /> |
||||||
|
Copy Error |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
<NcButton v-if="!prevError || error.message !== prevError.message" size="small" @click="reload"> |
||||||
|
<div class="flex items-center gap-1"> |
||||||
|
<GeneralIcon icon="reload" /> |
||||||
|
Reload |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
<NcButton v-else size="small" @click="navigateToHome"> |
||||||
|
<div class="flex items-center gap-1"> |
||||||
|
<GeneralIcon icon="link" /> |
||||||
|
Home |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</NcModal> |
||||||
|
</slot> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.nc-error { |
||||||
|
@apply flex gap-2 mb-2 max-h-0; |
||||||
|
white-space: pre; |
||||||
|
transition: max-height 300ms linear; |
||||||
|
|
||||||
|
&.active { |
||||||
|
max-height: 250px; |
||||||
|
} |
||||||
|
|
||||||
|
.nc-left-vertical-bar { |
||||||
|
@apply w-6px min-w-6px rounded min-h-full bg-gray-300; |
||||||
|
} |
||||||
|
|
||||||
|
.nc-error-content { |
||||||
|
@apply min-w-0 overflow-auto pl-2 flex-shrink; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 212 KiB |
After Width: | Height: | Size: 190 KiB |
@ -0,0 +1,103 @@ |
|||||||
|
import { |
||||||
|
Controller, |
||||||
|
Get, |
||||||
|
Param, |
||||||
|
Query, |
||||||
|
Req, |
||||||
|
Res, |
||||||
|
UseGuards, |
||||||
|
} from '@nestjs/common'; |
||||||
|
import { Request, Response } from 'express'; |
||||||
|
import { GlobalGuard } from '~/guards/global/global.guard'; |
||||||
|
import { DataApiLimiterGuard } from '~/guards/data-api-limiter.guard'; |
||||||
|
import { CalendarDatasService } from '~/services/calendar-datas.service'; |
||||||
|
import { parseHrtimeToMilliSeconds } from '~/helpers'; |
||||||
|
|
||||||
|
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; |
||||||
|
|
||||||
|
@Controller() |
||||||
|
@UseGuards(DataApiLimiterGuard, GlobalGuard) |
||||||
|
export class CalendarDatasController { |
||||||
|
constructor(private readonly calendarDatasService: CalendarDatasService) {} |
||||||
|
|
||||||
|
@Get(['/api/v1/db/calendar-data/:orgs/:baseName/:tableName/views/:viewName']) |
||||||
|
@Acl('dataList') |
||||||
|
async dataList( |
||||||
|
@Req() req: Request, |
||||||
|
@Param('viewName') viewId: string, |
||||||
|
@Query('from_date') fromDate: string, |
||||||
|
@Query('to_date') toDate: string, |
||||||
|
) { |
||||||
|
return await this.calendarDatasService.getCalendarDataList({ |
||||||
|
viewId: viewId, |
||||||
|
query: req.query, |
||||||
|
from_date: fromDate, |
||||||
|
to_date: toDate, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Get([ |
||||||
|
'/api/v1/db/calendar-data/:orgs/:baseName/:tableName/views/:viewName/countByDate/', |
||||||
|
]) |
||||||
|
@Acl('dataList') |
||||||
|
async calendarDataCount( |
||||||
|
@Req() req: Request, |
||||||
|
@Res() res: Response, |
||||||
|
@Param('baseName') baseName: string, |
||||||
|
@Param('tableName') tableName: string, |
||||||
|
@Param('viewName') viewName: string, |
||||||
|
@Query('from_date') fromDate: string, |
||||||
|
@Query('to_date') toDate: string, |
||||||
|
) { |
||||||
|
const startTime = process.hrtime(); |
||||||
|
|
||||||
|
const data = await this.calendarDatasService.getCalendarRecordCount({ |
||||||
|
query: req.query, |
||||||
|
viewId: viewName, |
||||||
|
from_date: fromDate, |
||||||
|
to_date: toDate, |
||||||
|
}); |
||||||
|
|
||||||
|
const elapsedSeconds = parseHrtimeToMilliSeconds(process.hrtime(startTime)); |
||||||
|
res.setHeader('xc-db-response', elapsedSeconds); |
||||||
|
res.json(data); |
||||||
|
} |
||||||
|
|
||||||
|
@Get([ |
||||||
|
'/api/v1/db/public/calendar-view/:sharedViewUuid/countByDate', |
||||||
|
'/api/v2/public/calendar-view/:sharedViewUuid/countByDate', |
||||||
|
]) |
||||||
|
async countByDate( |
||||||
|
@Req() req: Request, |
||||||
|
@Param('sharedViewUuid') sharedViewUuid: string, |
||||||
|
@Query('from_date') fromDate: string, |
||||||
|
@Query('to_date') toDate: string, |
||||||
|
) { |
||||||
|
return await this.calendarDatasService.getPublicCalendarRecordCount({ |
||||||
|
query: req.query, |
||||||
|
password: req.headers?.['xc-password'] as string, |
||||||
|
sharedViewUuid, |
||||||
|
from_date: fromDate, |
||||||
|
to_date: toDate, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Get([ |
||||||
|
'/api/v1/db/public/calendar-view/:sharedViewUuid', |
||||||
|
'/api/v2/public/calendar-view/:sharedViewUuid', |
||||||
|
]) |
||||||
|
async getPublicCalendarDataList( |
||||||
|
@Req() req: Request, |
||||||
|
@Param('sharedViewUuid') sharedViewUuid: string, |
||||||
|
@Query('from_date') fromDate: string, |
||||||
|
@Query('to_date') toDate: string, |
||||||
|
) { |
||||||
|
return await this.calendarDatasService.getPublicCalendarDataList({ |
||||||
|
query: req.query, |
||||||
|
password: req.headers?.['xc-password'] as string, |
||||||
|
sharedViewUuid, |
||||||
|
from_date: fromDate, |
||||||
|
to_date: toDate, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,230 @@ |
|||||||
|
import { Injectable, Logger } from '@nestjs/common'; |
||||||
|
import { ErrorMessages, ViewTypes } from 'nocodb-sdk'; |
||||||
|
import dayjs from 'dayjs'; |
||||||
|
import type { CalendarRangeType, FilterType } from 'nocodb-sdk'; |
||||||
|
import { CalendarRange, Model, View } from '~/models'; |
||||||
|
import { NcError } from '~/helpers/catchError'; |
||||||
|
import { DatasService } from '~/services/datas.service'; |
||||||
|
|
||||||
|
@Injectable() |
||||||
|
export class CalendarDatasService { |
||||||
|
protected logger = new Logger(CalendarDatasService.name); |
||||||
|
|
||||||
|
constructor(protected datasService: DatasService) {} |
||||||
|
|
||||||
|
async getCalendarDataList(param: { |
||||||
|
viewId: string; |
||||||
|
query: any; |
||||||
|
from_date: string; |
||||||
|
to_date: string; |
||||||
|
}) { |
||||||
|
const { viewId, query, from_date, to_date } = param; |
||||||
|
|
||||||
|
if (!from_date || !to_date) |
||||||
|
NcError.badRequest('from_date and to_date are required'); |
||||||
|
|
||||||
|
if (dayjs(to_date).diff(dayjs(from_date), 'days') > 42) { |
||||||
|
NcError.badRequest('Date range should not exceed 42 days'); |
||||||
|
} |
||||||
|
|
||||||
|
const view = await View.get(viewId); |
||||||
|
|
||||||
|
if (!view) NcError.notFound('View not found'); |
||||||
|
|
||||||
|
if (view.type !== ViewTypes.CALENDAR) |
||||||
|
NcError.badRequest('View is not a calendar view'); |
||||||
|
|
||||||
|
const calendarRange = await CalendarRange.read(view.id); |
||||||
|
|
||||||
|
if (!calendarRange?.ranges?.length) NcError.badRequest('No ranges found'); |
||||||
|
|
||||||
|
const filterArr = await this.buildFilterArr({ |
||||||
|
viewId, |
||||||
|
from_date, |
||||||
|
to_date, |
||||||
|
}); |
||||||
|
|
||||||
|
query.filterArr = [...(query.filterArr ? query.filterArr : []), filterArr]; |
||||||
|
|
||||||
|
const model = await Model.getByIdOrName({ |
||||||
|
id: view.fk_model_id, |
||||||
|
}); |
||||||
|
|
||||||
|
return await this.datasService.dataList({ |
||||||
|
...param, |
||||||
|
...query, |
||||||
|
baseName: model.base_id, |
||||||
|
tableName: model.id, |
||||||
|
calendarLimitOverride: 3000, // TODO: make this configurable in env
|
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async getPublicCalendarRecordCount(param: { |
||||||
|
password: string; |
||||||
|
query: any; |
||||||
|
sharedViewUuid: string; |
||||||
|
from_date: string; |
||||||
|
to_date: string; |
||||||
|
}) { |
||||||
|
const { sharedViewUuid, password, query = {} } = param; |
||||||
|
const view = await View.getByUUID(sharedViewUuid); |
||||||
|
|
||||||
|
if (!view) NcError.notFound('Not found'); |
||||||
|
if (view.type !== ViewTypes.CALENDAR) { |
||||||
|
NcError.notFound('Not found'); |
||||||
|
} |
||||||
|
|
||||||
|
if (view.password && view.password !== password) { |
||||||
|
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); |
||||||
|
} |
||||||
|
|
||||||
|
return this.getCalendarRecordCount({ |
||||||
|
viewId: view.id, |
||||||
|
query, |
||||||
|
from_date: param.from_date, |
||||||
|
to_date: param.to_date, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async getPublicCalendarDataList(param: { |
||||||
|
password: string; |
||||||
|
query: any; |
||||||
|
sharedViewUuid: string; |
||||||
|
from_date: string; |
||||||
|
to_date: string; |
||||||
|
}) { |
||||||
|
const { sharedViewUuid, password, query = {} } = param; |
||||||
|
const view = await View.getByUUID(sharedViewUuid); |
||||||
|
|
||||||
|
if (!view) NcError.notFound('Not found'); |
||||||
|
if (view.type !== ViewTypes.CALENDAR) { |
||||||
|
NcError.notFound('Not found'); |
||||||
|
} |
||||||
|
|
||||||
|
if (view.password && view.password !== password) { |
||||||
|
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); |
||||||
|
} |
||||||
|
|
||||||
|
return this.getCalendarDataList({ |
||||||
|
viewId: view.id, |
||||||
|
query, |
||||||
|
from_date: param.from_date, |
||||||
|
to_date: param.to_date, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async getCalendarRecordCount(param: { |
||||||
|
viewId: string; |
||||||
|
query: any; |
||||||
|
from_date: string; |
||||||
|
to_date: string; |
||||||
|
}) { |
||||||
|
const { viewId, query, from_date, to_date } = param; |
||||||
|
|
||||||
|
if (!from_date || !to_date) |
||||||
|
NcError.badRequest('from_date and to_date are required'); |
||||||
|
|
||||||
|
if (dayjs(to_date).diff(dayjs(from_date), 'days') > 395) { |
||||||
|
NcError.badRequest('Date range should not exceed 395 days'); |
||||||
|
} |
||||||
|
|
||||||
|
const view = await View.get(viewId); |
||||||
|
|
||||||
|
if (!view) NcError.notFound('View not found'); |
||||||
|
|
||||||
|
if (view.type !== ViewTypes.CALENDAR) |
||||||
|
NcError.badRequest('View is not a calendar view'); |
||||||
|
|
||||||
|
const { ranges } = await CalendarRange.read(view.id); |
||||||
|
|
||||||
|
if (!ranges.length) NcError.badRequest('No ranges found'); |
||||||
|
|
||||||
|
const filterArr = await this.buildFilterArr({ |
||||||
|
viewId, |
||||||
|
from_date, |
||||||
|
to_date, |
||||||
|
}); |
||||||
|
|
||||||
|
query.filterArr = [...(query.filterArr ? query.filterArr : []), filterArr]; |
||||||
|
|
||||||
|
const model = await Model.getByIdOrName({ |
||||||
|
id: view.fk_model_id, |
||||||
|
}); |
||||||
|
|
||||||
|
const data = await this.datasService.dataList({ |
||||||
|
...param, |
||||||
|
baseName: model.base_id, |
||||||
|
tableName: model.id, |
||||||
|
ignorePagination: true, |
||||||
|
}); |
||||||
|
|
||||||
|
if (!data) NcError.notFound('Data not found'); |
||||||
|
|
||||||
|
const dates: Array<string> = []; |
||||||
|
|
||||||
|
const columns = await model.getColumns(); |
||||||
|
|
||||||
|
ranges.forEach((range: CalendarRangeType) => { |
||||||
|
const fromCol = columns.find( |
||||||
|
(c) => c.id === range.fk_from_column_id, |
||||||
|
)?.title; |
||||||
|
|
||||||
|
data.list.forEach((date) => { |
||||||
|
const fromDt = dayjs(date[fromCol]); |
||||||
|
|
||||||
|
if (fromCol && fromDt.isValid()) { |
||||||
|
dates.push(fromDt.format('YYYY-MM-DD HH:mm:ssZ')); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
return { |
||||||
|
count: dates.length, |
||||||
|
dates: Array.from(new Set(dates)), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
async buildFilterArr({ |
||||||
|
viewId, |
||||||
|
from_date, |
||||||
|
to_date, |
||||||
|
}: { |
||||||
|
viewId: string; |
||||||
|
from_date: string; |
||||||
|
to_date: string; |
||||||
|
}) { |
||||||
|
const calendarRange = await CalendarRange.read(viewId); |
||||||
|
if (!calendarRange?.ranges?.length) NcError.badRequest('No ranges found'); |
||||||
|
|
||||||
|
const filterArr: FilterType = { |
||||||
|
is_group: true, |
||||||
|
logical_op: 'and', |
||||||
|
children: [], |
||||||
|
}; |
||||||
|
|
||||||
|
calendarRange.ranges.forEach((range: CalendarRange) => { |
||||||
|
const fromColumn = range.fk_from_column_id; |
||||||
|
let rangeFilter: any = []; |
||||||
|
if (fromColumn) { |
||||||
|
rangeFilter = [ |
||||||
|
{ |
||||||
|
fk_column_id: fromColumn, |
||||||
|
comparison_op: 'lt', |
||||||
|
comparison_sub_op: 'exactDate', |
||||||
|
value: to_date as string, |
||||||
|
}, |
||||||
|
{ |
||||||
|
fk_column_id: fromColumn, |
||||||
|
comparison_op: 'gt', |
||||||
|
comparison_sub_op: 'exactDate', |
||||||
|
value: from_date as string, |
||||||
|
}, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
if (rangeFilter.length > 0) filterArr.children.push(rangeFilter); |
||||||
|
}); |
||||||
|
|
||||||
|
return filterArr; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue