Browse Source

Merge pull request #7673 from nocodb/nc-calendar/field-followup

feat(nc-gui): calendar field customization
pull/7723/head
Raju Udava 6 months ago committed by GitHub
parent
commit
291c8a61cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      packages/nc-gui/assets/nc-icons/bold.svg
  2. 5
      packages/nc-gui/assets/nc-icons/italic.svg
  3. 6
      packages/nc-gui/assets/nc-icons/underline.svg
  4. 2
      packages/nc-gui/components/nc/Select.vue
  5. 371
      packages/nc-gui/components/smartsheet/calendar/Cell.vue
  6. 58
      packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue
  7. 61
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  8. 91
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  9. 13
      packages/nc-gui/components/smartsheet/calendar/RecordCard.vue
  10. 21
      packages/nc-gui/components/smartsheet/calendar/VRecordCard.vue
  11. 68
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  12. 65
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  13. 6
      packages/nc-gui/components/smartsheet/calendar/index.vue
  14. 37
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  15. 12
      packages/nc-gui/composables/useViewColumns.ts
  16. 3
      packages/nc-gui/lib/types.ts
  17. 6
      packages/nc-gui/utils/iconUtils.ts
  18. 13
      packages/nocodb/src/models/View.ts

4
packages/nc-gui/assets/nc-icons/bold.svg

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 8H10C10.7072 8 11.3855 8.28095 11.8856 8.78105C12.3857 9.28115 12.6667 9.95942 12.6667 10.6667C12.6667 11.3739 12.3857 12.0522 11.8856 12.5523C11.3855 13.0524 10.7072 13.3333 10 13.3333H4V8Z" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 2.66663H9.33333C10.0406 2.66663 10.7189 2.94758 11.219 3.44767C11.719 3.94777 12 4.62605 12 5.33329C12 6.04054 11.719 6.71881 11.219 7.21891C10.7189 7.71901 10.0406 7.99996 9.33333 7.99996H4V2.66663Z" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 672 B

5
packages/nc-gui/assets/nc-icons/italic.svg

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.33325 13.3334H3.33325" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.6667 2.66663H6.66675" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 2.66663L6 13.3333" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 433 B

6
packages/nc-gui/assets/nc-icons/underline.svg

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="underline">
<path id="Vector" d="M2.66675 14H13.3334" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M4 2V6.66667C4 7.72753 4.42143 8.74495 5.17157 9.49509C5.92172 10.2452 6.93913 10.6667 8 10.6667C9.06087 10.6667 10.0783 10.2452 10.8284 9.49509C11.5786 8.74495 12 7.72753 12 6.66667V2" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 547 B

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

@ -82,7 +82,7 @@ const onChange = (value: string) => {
height: fit-content;
.ant-select-selector {
box-shadow: 0px 5px 3px -2px rgba(0, 0, 0, 0.02), 0px 3px 1px -2px rgba(0, 0, 0, 0.06);
@apply border-1 border-gray-200 !rounded-lg;
@apply border-1 border-gray-200 rounded-lg;
}
.ant-select-selection-item {

371
packages/nc-gui/components/smartsheet/calendar/Cell.vue

@ -0,0 +1,371 @@
<script lang="ts" setup>
import {
type BoolType,
type ColumnType,
type LookupType,
type RollupType,
dateFormats,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
timeFormats,
} from 'nocodb-sdk'
import dayjs from 'dayjs'
import {
computed,
isBoolean,
isDate,
isDateTime,
isInt,
parseProp,
ref,
storeToRefs,
useAttachment,
useBase,
useMetas,
} from '#imports'
interface Props {
column: ColumnType
modelValue: any
bold?: BoolType
italic?: BoolType
underline?: BoolType
}
const props = defineProps<Props>()
const meta = inject(MetaInj)
const { t } = useI18n()
const { metas } = useMetas()
const column = toRef(props, 'column')
const { sqlUis } = storeToRefs(useBase())
const basesStore = useBases()
const { basesUser } = storeToRefs(basesStore)
const { isXcdbBase, isMssql, isMysql } = useBase()
const { getPossibleAttachmentSrc } = useAttachment()
const sqlUi = ref(column.value?.source_id ? sqlUis.value[column.value?.source_id] : Object.values(sqlUis.value)[0])
const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value))
const getCheckBoxValue = (modelValue: boolean | string | number | '0' | '1') => {
return !!modelValue && modelValue !== '0' && modelValue !== 0 && modelValue !== 'false'
}
const getMultiSelectValue = (modelValue: any, col: ColumnType): string => {
if (!modelValue) {
return ''
}
return modelValue
? Array.isArray(modelValue)
? modelValue.join(', ')
: modelValue.toString()
: isMysql(col.source_id)
? modelValue.toString().split(',').join(', ')
: modelValue.split(', ')
}
const getDateValue = (modelValue: string | null | number, col: ColumnType, isSystemCol?: boolean) => {
const dateFormat = !isSystemCol ? parseProp(col.meta)?.date_format ?? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'
if (!modelValue || !dayjs(modelValue).isValid()) {
return ''
} else {
return dayjs(/^\d+$/.test(String(modelValue)) ? +modelValue : modelValue).format(dateFormat)
}
}
const getYearValue = (modelValue: string | null) => {
if (!modelValue) {
return ''
} else if (!dayjs(modelValue).isValid()) {
return ''
} else {
return dayjs(modelValue.toString(), 'YYYY').format('YYYY')
}
}
const getDateTimeValue = (modelValue: string | null, col: ColumnType) => {
if (!modelValue || !dayjs(modelValue).isValid()) {
return ''
}
const dateFormat = parseProp(col?.meta)?.date_format ?? dateFormats[0]
const timeFormat = parseProp(col?.meta)?.time_format ?? timeFormats[0]
const dateTimeFormat = `${dateFormat} ${timeFormat}`
const isXcDB = isXcdbBase(col.source_id)
if (!isXcDB) {
return dayjs(/^\d+$/.test(modelValue) ? +modelValue : modelValue, dateTimeFormat).format(dateTimeFormat)
}
if (isMssql(col.source_id)) {
// e.g. 2023-04-29T11:41:53.000Z
return dayjs(modelValue, dateTimeFormat).format(dateTimeFormat)
} else {
return dayjs(modelValue).utc().local().format(dateTimeFormat)
}
}
const getTimeValue = (modelValue: string | null) => {
if (!modelValue) {
return ''
}
let dateTime = dayjs(modelValue)
if (!dateTime.isValid()) {
dateTime = dayjs(modelValue, 'HH:mm:ss')
}
if (!dateTime.isValid()) {
dateTime = dayjs(`1999-01-01 ${modelValue}`)
}
if (!dateTime.isValid()) {
return ''
}
return dateTime.format('HH:mm')
}
const getDurationValue = (modelValue: string | null, col: ColumnType) => {
const durationType = parseProp(col.meta)?.duration || 0
return convertMS2Duration(modelValue, durationType)
}
const getPercentValue = (modelValue: string | null) => {
return modelValue ? `${modelValue}%` : ''
}
const getCurrencyValue = (modelValue: string | number | null | undefined, col: ColumnType): string => {
const currencyMeta = {
currency_locale: 'en-US',
currency_code: 'USD',
...parseProp(col.meta),
}
try {
if (modelValue === null || modelValue === undefined || isNaN(modelValue)) {
return modelValue === null || modelValue === undefined ? '' : (modelValue as string)
}
return new Intl.NumberFormat(currencyMeta.currency_locale || 'en-US', {
style: 'currency',
currency: currencyMeta.currency_code || 'USD',
}).format(+modelValue)
} catch (e) {
return modelValue as string
}
}
const getUserValue = (modelValue: string | string[] | null | Array<any>) => {
if (!modelValue) {
return ''
}
const baseUsers = meta?.value.base_id ? basesUser.value.get(meta?.value.base_id) || [] : []
if (typeof modelValue === 'string') {
const idsOrMails = modelValue.split(',')
return idsOrMails
.map((idOrMail) => {
const user = baseUsers.find((u) => u.id === idOrMail || u.email === idOrMail)
return user ? user.display_name || user.email : idOrMail.id
})
.join(', ')
} else {
if (Array.isArray(modelValue)) {
return modelValue
.map((idOrMail) => {
const user = baseUsers.find((u) => u.id === idOrMail.id || u.email === idOrMail.email)
return user ? user.display_name || user.email : idOrMail.id
})
.join(', ')
} else {
return modelValue ? modelValue.display_name || modelValue.email : ''
}
}
}
const getDecimalValue = (modelValue: string | null | number, col: ColumnType) => {
if (!modelValue || isNaN(Number(modelValue))) {
return ''
}
const columnMeta = parseProp(col.meta)
return Number(modelValue).toFixed(columnMeta?.precision ?? 1)
}
const getIntValue = (modelValue: string | null | number) => {
if (!modelValue || isNaN(Number(modelValue))) {
return ''
}
return Number(modelValue) as unknown as string
}
const getTextAreaValue = (modelValue: string | null, col: ColumnType) => {
const isRichMode = typeof col.meta === 'string' ? JSON.parse(col.meta).richMode : col.meta?.richMode
if (isRichMode) {
return modelValue?.replace(/[*_~\[\]]|<\/?[^>]+(>|$)/g, '') || ''
}
return modelValue || ''
}
const getRollupValue = (modelValue: string | null | number, col: ColumnType) => {
const colOptions = col.colOptions as RollupType
const fns = ['count', 'avg', 'sum', 'countDistinct', 'sumDistinct', 'avgDistinct']
if (fns.includes(colOptions.rollup_function!)) {
return modelValue as string
} else {
const relationColumnOptions = colOptions.fk_relation_column_id
? meta?.value.columns?.find((c) => c.id === colOptions.fk_relation_column_id)?.colOptions
: null
const relatedTableMeta =
relationColumnOptions?.fk_related_model_id && metas.value?.[relationColumnOptions.fk_related_model_id as string]
const childColumn = relatedTableMeta?.columns.find((c: ColumnType) => c.id === colOptions.fk_rollup_column_id)
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return parseValue(modelValue, childColumn) as string
}
}
const getLookupValue = (modelValue: string | null | number | Array<any>, col: ColumnType) => {
const colOptions = col.colOptions as LookupType
const relationColumnOptions = colOptions.fk_relation_column_id
? meta?.value.columns?.find((c) => c.id === colOptions.fk_relation_column_id)?.colOptions
: null
const relatedTableMeta =
relationColumnOptions?.fk_related_model_id && metas.value?.[relationColumnOptions.fk_related_model_id as string]
const childColumn = relatedTableMeta?.columns.find((c: ColumnType) => c.id === colOptions.fk_lookup_column_id) as
| ColumnType
| undefined
if (Array.isArray(modelValue)) {
return modelValue
.map((v) => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return parseValue(v, childColumn!)
})
.join(', ')
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return parseValue(modelValue, childColumn!)
}
const getAttachmentValue = (modelValue: string | null | number | Array<any>) => {
console.log(modelValue)
if (Array.isArray(modelValue)) {
return modelValue.map((v) => `${v.title} (${getPossibleAttachmentSrc(v).join(', ')})`).join(', ')
}
return modelValue as string
}
const getLinksValue = (modelValue: string, col: ColumnType) => {
if (typeof col.meta === 'string') {
col.meta = JSON.parse(col.meta)
}
const parsedValue = +modelValue || 0
if (!parsedValue) {
return ''
} else if (parsedValue === 1) {
return `1 ${col?.meta?.singular || t('general.link')}`
} else {
return `${parsedValue} ${col?.meta?.plural || t('general.links')}`
}
}
const parseValue = (value: any, col: ColumnType): string => {
if (!col) {
return ''
}
if (isGeoData(col)) {
const [latitude, longitude] = ((value as string) || '').split(';')
return latitude && longitude ? `${latitude}; ${longitude}` : value
}
if (isTextArea(col)) {
return getTextAreaValue(value, col)
}
if (isBoolean(col, abstractType)) {
return getCheckBoxValue(value) ? 'Checked' : 'Unchecked'
}
if (isMultiSelect(col)) {
return getMultiSelectValue(value, col)
}
if (isDate(col, abstractType)) {
return getDateValue(value, col)
}
if (isYear(col, abstractType)) {
return getYearValue(value)
}
if (isDateTime(col, abstractType)) {
return getDateTimeValue(value, col)
}
if (isTime(col, abstractType)) {
return getTimeValue(value)
}
if (isDuration(col)) {
return getDurationValue(value, col)
}
if (isPercent(col)) {
return getPercentValue(value)
}
if (isCurrency(col)) {
return getCurrencyValue(value, col)
}
if (isUser(col)) {
return getUserValue(value)
}
if (isDecimal(col)) {
return getDecimalValue(value, col)
}
if (isInt(col, abstractType)) {
return getIntValue(value)
}
if (isJSON(col)) {
return JSON.stringify(value, null, 2)
}
if (isRollup(col)) {
return getRollupValue(value, col)
}
if (isLookup(col)) {
return getLookupValue(value, col)
}
if (isCreatedOrLastModifiedTimeCol(col)) {
return getDateValue(value, col, true)
}
if (isCreatedOrLastModifiedByCol(col)) {
return getUserValue(value)
}
if (isAttachment(col)) {
return getAttachmentValue(value)
}
if (isLink(col)) {
return getLinksValue(value, col)
}
return value as unknown as string
}
</script>
<template>
<span
:class="{
'font-bold': bold,
'italic': italic,
'underline': underline,
}"
data-testid="nc-calendar-cell"
>
{{ parseValue(modelValue, column) }}
</span>
</template>
<style lang="scss" scoped></style>

58
packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { type Row, computed, ref } from '#imports'
import type { ColumnType } from 'nocodb-sdk'
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { isRowEmpty } from '~/utils'
const emit = defineEmits(['expand-record', 'new-record'])
@ -15,6 +15,22 @@ const { isUIAllowed } = useRoles()
const { selectedDate, formattedData, formattedSideBarData, calendarRange, updateRowProperty, displayField } =
useCalendarViewStoreOrThrow()
const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType) => {
const fi = _fields.value.find((f) => f.title === field.title)
return {
underline: fi.underline,
bold: fi.bold,
italic: fi.italic,
}
}
const fieldsWithoutDisplay = computed(() => fields.value.filter((f) => !isPrimary(f)))
// We loop through all the records and calculate the position of each record based on the range
// We only need to calculate the top, of the record since there is no overlap in the day view of date Field
const recordsAcrossAllRange = computed<Row[]>(() => {
@ -196,27 +212,23 @@ const dropEvent = (event: DragEvent) => {
@click="emit('expand-record', record)"
>
<template v-if="!isRowEmpty(record, displayField)">
<div
:class="{
'!mt-1.5 ml-1': displayField.uidt === UITypes.SingleLineText,
'!mt-1': displayField.uidt === UITypes.MultiSelect || displayField.uidt === UITypes.SingleSelect,
}"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField!)"
v-model="record.row[displayField!.title!]"
:column="displayField"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[displayField!.title!]"
:column="displayField"
:edit-enabled="false"
:read-only="true"
/>
</div>
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField!).bold"
:column="displayField!"
:italic="getFieldStyle(displayField!).italic"
:underline="getFieldStyle(displayField!).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
:italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline"
/>
</template>
</LazySmartsheetCalendarRecordCard>
</LazySmartsheetRow>

61
packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue

@ -1,12 +1,13 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { type Row, computed, ref } from '#imports'
import type { ColumnType } from 'nocodb-sdk'
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emit = defineEmits(['expandRecord', 'new-record'])
const {
activeCalendarView,
selectedDate,
selectedTime,
formattedData,
@ -24,6 +25,23 @@ const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref())
const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType) => {
if (!_fields.value) return { underline: false, bold: false, italic: false }
const fi = _fields.value.find((f) => f.title === field.title)
return {
underline: fi?.underline,
bold: fi?.bold,
italic: fi?.italic,
}
}
const fieldsWithoutDisplay = computed(() => fields.value?.filter((f) => !isPrimary(f)))
const hours = computed(() => {
const hours: Array<dayjs.Dayjs> = []
const _selectedDate = dayjs(selectedDate.value)
@ -691,6 +709,7 @@ const viewMore = (hour: dayjs.Dayjs) => {
>
<LazySmartsheetRow :row="record">
<LazySmartsheetCalendarVRecordCard
:view="activeCalendarView"
:hover="hoverRecord === record.rowMeta.id"
:position="record.rowMeta!.position"
:record="record"
@ -699,27 +718,23 @@ const viewMore = (hour: dayjs.Dayjs) => {
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
<div
:class="{
'!mt-2': displayField!.uidt === UITypes.SingleLineText,
'!mt-1': displayField!.uidt === UITypes.MultiSelect || displayField!.uidt === UITypes.SingleSelect,
}"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField!)"
v-model="record.row[displayField!.title!]"
:column="displayField"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[displayField!.title!]"
:column="displayField"
:edit-enabled="false"
:read-only="true"
/>
</div>
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField!).bold"
:column="displayField"
:italic="getFieldStyle(displayField!).italic"
:underline="getFieldStyle(displayField!).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
:italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline"
/>
</template>
</LazySmartsheetCalendarVRecordCard>
</LazySmartsheetRow>

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

@ -1,8 +1,7 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Row } from '#imports'
import type { ColumnType } from 'nocodb-sdk'
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emit = defineEmits(['new-record', 'expandRecord'])
@ -61,6 +60,23 @@ const resizeDirection = ref<'right' | 'left'>()
const resizeRecord = ref<Row>()
const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType | undefined) => {
if (!field) return { underline: false, bold: false, italic: false }
const fi = _fields.value?.find((f) => f.title === field.title)
return {
underline: fi?.underline,
bold: fi?.bold,
italic: fi?.italic,
}
}
const fieldsWithoutDisplay = computed(() => fields.value?.filter((f) => !isPrimary(f)))
const dates = computed(() => {
const startOfMonth = selectedMonth.value.startOf('month')
const endOfMonth = selectedMonth.value.endOf('month')
@ -331,8 +347,8 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
const percentY = (event.clientY - top - window.scrollY) / height
const percentX = (event.clientX - left - window.scrollX) / width
const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
const toCol = dragRecord.value.rowMeta.range?.fk_to_col
const fromCol = dragRecord.value?.rowMeta.range?.fk_from_col
const toCol = dragRecord.value?.rowMeta.range?.fk_to_col
const week = Math.floor(percentY * dates.value.length)
const day = Math.floor(percentX * 7)
@ -345,7 +361,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
const newRow = {
...dragRecord.value,
row: {
...dragRecord.value.row,
...dragRecord.value?.row,
[fromCol!.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
@ -353,8 +369,8 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
const updateProperty = [fromCol!.title!]
if (toCol) {
const fromDate = dragRecord.value.row[fromCol!.title!] ? dayjs(dragRecord.value.row[fromCol!.title!]) : null
const toDate = dragRecord.value.row[toCol!.title!] ? dayjs(dragRecord.value.row[toCol!.title!]) : null
const fromDate = dragRecord.value?.row[fromCol!.title!] ? dayjs(dragRecord.value.row[fromCol!.title!]) : null
const toDate = dragRecord.value?.row[toCol!.title!] ? dayjs(dragRecord.value?.row[toCol!.title!]) : null
if (fromDate && toDate) {
endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
@ -366,7 +382,6 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
endDate = newStartDate.clone()
}
dragRecord.value = undefined
newRow.row[toCol!.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
updateProperty.push(toCol!.title!)
}
@ -440,7 +455,7 @@ const onResize = (event: MouseEvent) => {
[toCol!.title!]: dayjs(newEndDate).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
} else if (resizeDirection.value === 'left') {
} else {
let newStartDate = dates.value[week] ? dayjs(dates.value[week][day]) : null
updateProperty = [fromCol!.title!]
@ -465,7 +480,9 @@ const onResize = (event: MouseEvent) => {
return pk === newPk ? newRow : r
})
useDebouncedRowUpdate(newRow, updateProperty, false)
if (newRow) {
useDebouncedRowUpdate(newRow, updateProperty, false)
}
}
const onResizeEnd = () => {
@ -490,11 +507,14 @@ const onResizeStart = (direction: 'right' | 'left', event: MouseEvent, record: R
}
const stopDrag = (event: MouseEvent) => {
clearTimeout(dragTimeout.value)
console.log('stopDrag')
console.log('stopDrag', dragRecord.value, isDragging.value)
if (!isUIAllowed('dataEdit') || !dragRecord.value || !isDragging.value) return
console.log('stopDrag')
event.preventDefault()
clearTimeout(dragTimeout.value)
dragElement.value!.style.boxShadow = 'none'
const { newRow, updateProperty } = calculateNewRow(event, false)
@ -769,30 +789,26 @@ const isDateSelected = (date: dayjs.Dayjs) => {
: false
"
@resize-start="onResizeStart"
@dblclick.stop="emit('expand-record', record)"
@dblclick.stop="emit('expandRecord', record)"
>
<template v-if="!isRowEmpty(record, displayField)">
<div
:class="{
'mt-1.4': displayField!.uidt === UITypes.SingleLineText,
'mt-1': displayField!.uidt === UITypes.MultiSelect || displayField!.uidt === UITypes.SingleSelect,
}"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField!)"
v-model="record.row[displayField!.title!]"
:column="displayField"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[displayField!.title!]"
:column="displayField"
:edit-enabled="false"
:read-only="true"
/>
</div>
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField).bold"
:column="displayField"
:italic="getFieldStyle(displayField).italic"
:underline="getFieldStyle(displayField).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
:italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline"
/>
</template>
</LazySmartsheetCalendarRecordCard>
</LazySmartsheetRow>
@ -802,11 +818,6 @@ const isDateSelected = (date: dayjs.Dayjs) => {
</template>
<style lang="scss" scoped>
.hide {
transition: 0.01s;
transform: translateX(-9999px);
}
.prevent-select {
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */

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

@ -56,7 +56,7 @@ const emit = defineEmits(['resize-start'])
class="block h-full min-h-5 w-1 rounded"
></div>
<div v-if="(position === 'leftRounded' || position === 'rounded') && resize" class="mt-0.1 h-7.1 absolute -left-4 resize">
<div v-if="(position === 'leftRounded' || position === 'rounded') && resize" class="mt-0.7 h-7.1 absolute -left-4 resize">
<NcButton
:class="{
'!block z-1 !border-brand-500': selected || hover,
@ -70,15 +70,20 @@ const emit = defineEmits(['resize-start'])
</NcButton>
</div>
<div class="overflow-hidden ml-2 h-8 absolute">
<div class="overflow-hidden flex w-full ml-2 h-8 absolute">
<span v-if="position === 'rightRounded' || position === 'none'" class="mr-1"> .... </span>
<span class="text-sm !w-[80%] text-gray-800">
<span
:class="{
'pr-7': position === 'leftRounded',
}"
class="text-sm pt-1.5 pr-3 mr-3 break-word space-x-2 whitespace-nowrap gap-2 overflow-hidden text-ellipsis w-full truncate text-gray-800"
>
<slot />
</span>
<span v-if="position === 'leftRounded' || position === 'none'" class="absolute my-0 right-5"> .... </span>
</div>
<div v-if="(position === 'rightRounded' || position === 'rounded') && resize" class="absolute mt-0.1 z-1 -right-4 resize">
<div v-if="(position === 'rightRounded' || position === 'rounded') && resize" class="absolute mt-0.3 z-1 -right-4 resize">
<NcButton
:class="{
'!block !border-brand-500': selected || hover,

21
packages/nc-gui/components/smartsheet/calendar/VRecordCard.vue

@ -26,14 +26,14 @@ const emit = defineEmits(['resize-start'])
>
<NcButton
:class="{
'!flex border-1 rounded-lg border-brand-500': selected || hover,
'!flex rounded-lg border-brand-500': selected || hover,
}"
class="!group-hover:(border-brand-500) !border-1 cursor-ns-resize"
class="!group-hover:(border-brand-500) !border-1 text-gray-400 cursor-ns-resize"
size="xsmall"
type="secondary"
@mousedown.stop="emit('resize-start', 'left', $event, record)"
>
<component :is="iconMap.drag" class="text-gray-400"></component>
<component :is="iconMap.drag" class="mt-0.5" />
</NcButton>
</div>
<div
@ -69,11 +69,12 @@ const emit = defineEmits(['resize-start'])
<div v-if="position === 'bottomRounded' || position === 'none'" class="ml-3">....</div>
<div class="ml-3 pr-3 text-ellipsis overflow-hidden w-full h-8 absolute">
<span class="text-sm text-gray-800">
<slot />
</span>
</div>
<span
class="mt-1.5 pl-4 pr-1 text-sm h-[80%] text-gray-800 leading-7 space-x-2 break-all whitespace-normal truncate w-full overflow-y-hidden absolute"
>
<slot />
</span>
<div v-if="position === 'topRounded' || position === 'none'" class="h-full pb-7 flex items-end ml-3">....</div>
</div>
<div
@ -84,12 +85,12 @@ const emit = defineEmits(['resize-start'])
:class="{
'!flex border-1 rounded-lg z-1 cursor-ns-resize border-brand-500': selected || hover,
}"
class="!group-hover:(border-brand-500) !border-1"
class="!group-hover:(border-brand-500) text-gray-400 !border-1"
size="xsmall"
type="secondary"
@mousedown.stop="emit('resize-start', 'right', $event, record)"
>
<component :is="iconMap.drag" class="text-gray-400"></component>
<component :is="iconMap.drag" class="mt-0.5" />
</NcButton>
</div>
</template>

68
packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue

@ -1,8 +1,8 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { type ColumnType } from 'nocodb-sdk'
import type { Row } from '~/lib'
import { ref } from '#imports'
import { computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emits = defineEmits(['expandRecord'])
@ -18,14 +18,30 @@ const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref())
const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType | undefined) => {
const fi = _fields.value?.find((f) => f.title === field.title)
return {
underline: fi?.underline,
bold: fi?.bold,
italic: fi?.italic,
}
}
const fieldsWithoutDisplay = computed(() => fields.value?.filter((f) => !isPrimary(f)))
// Calculate the dates of the week
const weekDates = computed(() => {
const startOfWeek = new Date(selectedDateRange.value.start!)
const endOfWeek = new Date(selectedDateRange.value.end!)
let startOfWeek = dayjs(selectedDateRange.value.start)
const endOfWeek = dayjs(selectedDateRange.value.end)
const datesArray = []
while (startOfWeek.getTime() <= endOfWeek.getTime()) {
datesArray.push(new Date(startOfWeek))
startOfWeek.setDate(startOfWeek.getDate() + 1)
while (startOfWeek.isBefore(endOfWeek) || startOfWeek.isSame(endOfWeek, 'day')) {
datesArray.push(dayjs(startOfWeek))
startOfWeek = startOfWeek.add(1, 'day')
}
return datesArray
})
@ -567,27 +583,23 @@ const dropEvent = (event: DragEvent) => {
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
<div
:class="{
'mt-2': displayField.uidt === UITypes.SingleLineText,
'mt-1': displayField.uidt === UITypes.MultiSelect || displayField.uidt === UITypes.SingleSelect,
}"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField)"
v-model="record.row[displayField.title]"
:column="displayField"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[displayField.title]"
:column="displayField"
:edit-enabled="false"
:read-only="true"
/>
</div>
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField).bold"
:column="displayField"
:italic="getFieldStyle(displayField).italic"
:underline="getFieldStyle(displayField).underline"
/>
</template>
<template v-for="(field, index) in fieldsWithoutDisplay" :key="index">
<LazySmartsheetCalendarCell
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
:italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline"
/>
</template>
</LazySmartsheetCalendarRecordCard>
</LazySmartsheetRow>

65
packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue

@ -1,8 +1,8 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { type ColumnType } from 'nocodb-sdk'
import type { Row } from '~/lib'
import { computed, ref } from '#imports'
import { computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emits = defineEmits(['expandRecord'])
@ -30,6 +30,23 @@ const { isUIAllowed } = useRoles()
const meta = inject(MetaInj, ref())
const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType | undefined) => {
if (!field) return { underline: false, bold: false, italic: false }
const fi = _fields.value?.find((f) => f.title === field.title)
return {
underline: fi?.underline,
bold: fi?.bold,
italic: fi?.italic,
}
}
const fieldsWithoutDisplay = computed(() => fields.value?.filter((f) => !isPrimary(f)))
// Since it is a datetime Week view, we need to create a 2D array of dayjs objects to represent the hours in a day for each day in the week
const datesHours = computed(() => {
const datesHours: Array<Array<dayjs.Dayjs>> = []
@ -68,7 +85,11 @@ const recordsAcrossAllRange = computed<{
}
}
}>(() => {
if (!formattedData.value || !calendarRange.value || !container.value) return { records: [], count: {} }
if (!formattedData.value || !calendarRange.value || !container.value || !scrollContainer.value)
return {
records: [],
count: {},
}
const { scrollHeight } = scrollContainer.value
@ -746,27 +767,23 @@ const viewMore = (hour: dayjs.Dayjs) => {
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
<div
:class="{
'!mt-2': displayField!.uidt === UITypes.SingleLineText,
'!mt-1': displayField!.uidt === UITypes.MultiSelect || displayField!.uidt === UITypes.SingleSelect,
}"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField!)"
v-model="record.row[displayField!.title!]"
:column="displayField"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[displayField!.title!]"
:column="displayField"
:edit-enabled="false"
:read-only="true"
/>
</div>
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField).bold"
:column="displayField"
:italic="getFieldStyle(displayField).italic"
:underline="getFieldStyle(displayField).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<LazySmartsheetCalendarCell
v-model="record.row[field!.title!]"
:bold="getFieldStyle(field).bold"
:column="field"
:italic="getFieldStyle(field).italic"
:underline="getFieldStyle(field).underline"
/>
</template>
</LazySmartsheetCalendarVRecordCard>
</LazySmartsheetRow>

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

@ -41,6 +41,7 @@ provide(IsKanbanInj, ref(false))
provide(IsCalendarInj, ref(true))
const {
activeCalendarView,
calendarRange,
calDataType,
loadCalendarMeta,
@ -53,7 +54,6 @@ const {
pageDate,
showSideMenu,
selectedDateRange,
activeCalendarView,
paginateCalendarView,
} = useCalendarViewStoreOrThrow()
@ -181,8 +181,8 @@ const headerText = computed(() => {
</NcTooltip>
<NcDropdown v-model:visible="calendarRangeDropdown" :auto-close="false" :trigger="['click']">
<NcButton :class="{ '!w-24': activeCalendarView === 'year' }" class="w-45" full-width size="small" type="secondary">
<div class="flex w-full px-3 py-1 w-full items-center justify-between">
<NcButton :class="{ '!w-22': activeCalendarView === 'year' }" class="w-45" full-width size="small" type="secondary">
<div class="flex px-2 w-full items-center justify-between">
<span class="font-bold text-center text-brand-500" data-testid="nc-calendar-active-date">{{ headerText }}</span>
<component :is="iconMap.arrowDown" class="h-4 w-4 text-gray-700" />
</div>

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

@ -50,6 +50,7 @@ const {
saveOrUpdate,
metaColumnById,
loadViewColumns,
toggleFieldStyles,
toggleFieldVisibility,
} = useViewColumnsOrThrow()
@ -396,7 +397,41 @@ useMenuCloseOnEsc(open)
</template>
<template #default>{{ field.title }}</template>
</NcTooltip>
<div v-if="activeView.type === ViewTypes.CALENDAR" class="flex mr-2">
<NcButton
:class="{
'!bg-gray-800 !text-white': field.bold,
}"
class="!rounded-r-none"
size="xxsmall"
type="secondary"
@click.stop="toggleFieldStyles(field, 'bold', !field.bold)"
>
<component :is="iconMap.bold" />
</NcButton>
<NcButton
:class="{
'!bg-gray-800 !text-white': field.italic,
}"
class="!rounded-x-none !border-x-0"
size="xxsmall"
type="secondary"
@click.stop="toggleFieldStyles(field, 'italic', !field.italic)"
>
<component :is="iconMap.italic" />
</NcButton>
<NcButton
:class="{
'!bg-gray-800 !text-white': field.underline,
}"
class="!rounded-l-none"
size="xxsmall"
type="secondary"
@click.stop="toggleFieldStyles(field, 'underline', !field.underline)"
>
<component :is="iconMap.underline" />
</NcButton>
</div>
<NcSwitch :checked="field.show" :disabled="field.isViewEssentialField" @change="$t('a:fields:show-hide')" />
</div>

12
packages/nc-gui/composables/useViewColumns.ts

@ -1,8 +1,8 @@
import { ViewTypes, isHiddenCol, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, GridColumnReqType, GridColumnType, MapType, TableType, ViewType } from 'nocodb-sdk'
import { ViewTypes, isHiddenCol, isSystemColumn } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { computed, ref, storeToRefs, useBase, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports'
import type { Field } from '#imports'
import { computed, ref, storeToRefs, useBase, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports'
const [useProvideViewColumns, useViewColumns] = useInjectionState(
(
@ -278,6 +278,13 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
saveOrUpdate(field, fieldIndex)
}
const toggleFieldStyles = (field: any, style: 'underline' | 'bold' | 'italic', status: boolean) => {
const fieldIndex = fields.value?.findIndex((f) => f.fk_column_id === field.fk_column_id)
if (!fieldIndex && fieldIndex !== 0) return
field[style] = status
saveOrUpdate(field, fieldIndex, true)
}
// reload view columns when active view changes
// or when columns changes(delete/add)
watch(
@ -351,6 +358,7 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
showSystemFields,
metaColumnById,
toggleFieldVisibility,
toggleFieldStyles,
isViewColumnsLoading,
updateGridViewColumn,
gridViewCols,

3
packages/nc-gui/lib/types.ts

@ -32,6 +32,9 @@ interface ProjectMetaInfo {
interface Field {
order: number
show: number | boolean
bold: boolean | number
italic: boolean | number
underline: boolean | number
title: string
fk_column_id?: string
system?: boolean

6
packages/nc-gui/utils/iconUtils.ts

@ -124,6 +124,9 @@ import NcUpload from '~icons/nc-icons/upload'
import NcDownload from '~icons/nc-icons/download'
// import NcProjectGray from '~icons/nc-icons/project-gray'
import NcPhoneCall from '~icons/nc-icons/phone-call'
import NcItalic from '~icons/nc-icons/italic'
import NcBold from '~icons/nc-icons/bold'
import NcUnderline from '~icons/nc-icons/underline'
import NcCrop from '~icons/nc-icons/crop'
// keep it for reference
@ -484,6 +487,9 @@ export const iconMap = {
ncEdit: NcEdit,
ncArrowUp: NcArrowUp,
ncArrowDown: NcArrowDown,
underline: NcUnderline,
bold: NcBold,
italic: NcItalic,
phoneCall: NcPhoneCall,
crop: NcCrop,
}

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

@ -802,6 +802,9 @@ export default class View implements ViewType {
colData: {
order?: number;
show?: BoolType;
underline?: BoolType;
bold?: BoolType;
italic?: BoolType;
},
ncMeta = Noco.ncMeta,
) {
@ -833,10 +836,10 @@ export default class View implements ViewType {
table = MetaTable.CALENDAR_VIEW_COLUMNS;
cacheScope = CacheScope.CALENDAR_VIEW_COLUMN;
}
const updateObj = extractProps(colData, ['order', 'show']);
let updateObj = extractProps(colData, ['order', 'show']);
// keep primary_value_column always visible and first in grid view
if (view.type === ViewTypes.GRID || view.type === ViewTypes.CALENDAR) {
if (view.type === ViewTypes.GRID) {
const primary_value_column_meta = await ncMeta.metaGet2(
null,
null,
@ -862,6 +865,12 @@ export default class View implements ViewType {
updateObj.show = true;
}
}
if (view.type === ViewTypes.CALENDAR) {
updateObj = {
...updateObj,
...extractProps(colData, ['underline', 'bold', 'italic']),
};
}
// set meta
const res = await ncMeta.metaUpdate(null, null, table, updateObj, colId);

Loading…
Cancel
Save