多维表格
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.
 
 
 
 
 
 

339 lines
10 KiB

<script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk'
import { isSystemColumn } from 'nocodb-sdk'
import { NavigateDir } from '#imports'
interface Props {
column: ColumnType
modelValue: any
editEnabled: boolean
readOnly?: boolean
rowIndex?: number
active?: boolean
virtual?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'save', 'navigate', 'update:editEnabled', 'update:cdf'])
const column = toRef(props, 'column')
const meta = inject(MetaInj, ref())
const active = toRef(props, 'active', false)
const readOnly = toRef(props, 'readOnly', false)
provide(ColumnInj, column)
const editEnabled = useVModel(props, 'editEnabled', emit)
provide(EditModeInj, editEnabled)
provide(ActiveCellInj, active)
provide(ReadonlyInj, readOnly)
const isForm = inject(IsFormInj, ref(false))
const isGrid = inject(IsGridInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isCalendar = inject(IsCalendarInj, ref(false))
const isEditColumnMenu = inject(EditColumnInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
const { currentRow } = useSmartsheetRowStoreOrThrow()
const { sqlUis } = storeToRefs(useBase())
const { generatingRows, generatingColumns } = useNocoAi()
const pk = computed(() => {
if (!meta.value?.columns) return
return extractPkFromRow(currentRow.value?.row, meta.value.columns)
})
const isGenerating = computed(
() =>
pk.value && column.value.id && generatingRows.value.includes(pk.value) && generatingColumns.value.includes(column.value.id),
)
const sourceId = meta.value?.source_id || column.value?.source_id
const sqlUi = ref(sourceId && sqlUis.value[sourceId] ? sqlUis.value[sourceId] : Object.values(sqlUis.value)[0])
const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value))
const syncValue = useDebounceFn(
() => {
currentRow.value.rowMeta.changed = false
emit('save')
},
500,
{ maxWait: 2000 },
)
let saveTimer: number
const updateWhenEditCompleted = () => {
if (editEnabled.value) {
if (saveTimer) clearTimeout(saveTimer)
saveTimer = window.setTimeout(updateWhenEditCompleted, 500)
} else {
emit('save')
}
}
const vModel = computed({
get: () => {
return props.modelValue
},
set: (val) => {
if (isEditColumnMenu.value) {
emit('update:cdf', val)
} else if (val !== props.modelValue) {
currentRow.value.rowMeta.changed = true
emit('update:modelValue', val)
if (column.value.pk || column.value.unique) {
updateWhenEditCompleted()
} else if (isAutoSaved(column.value)) {
syncValue()
} else if (!isManualSaved(column.value)) {
emit('save')
}
}
},
})
const navigate = (dir: NavigateDir, e: KeyboardEvent) => {
if (isJSON(column.value)) return
if (currentRow.value.rowMeta.changed || currentRow.value.rowMeta.new) {
currentRow.value.rowMeta.changed = false
}
emit('navigate', dir)
if (!isForm.value) e.stopImmediatePropagation()
}
const isNumericField = computed(() => {
return isNumericFieldType(column.value, abstractType.value)
})
// disable contexxtmenu event propagation when cell is in
// editable state and typable (e.g. text area)
// this is to prevent the custom grid view context menu from opening
const onContextmenu = (e: MouseEvent) => {
if (props.editEnabled && isTypableInputColumn(column.value)) {
e.stopPropagation()
}
}
const showCurrentDateOption = computed(() => {
if (!isEditColumnMenu.value || (!isDate(column.value, abstractType.value) && !isDateTime(column.value, abstractType.value)))
return false
return sqlUi.value?.getCurrentDateDefault?.(column.value) ? true : 'disabled'
})
const currentDate = () => {
vModel.value = sqlUi.value?.getCurrentDateDefault?.(column.value)
}
const cellType = computed(() => {
if (isAI(column.value)) return 'ai'
if (isTextArea(column.value)) return 'textarea'
if (isGeoData(column.value)) return 'geoData'
if (isBoolean(column.value, abstractType.value)) return 'checkbox'
if (isAttachment(column.value)) return 'attachment'
if (isSingleSelect(column.value)) return 'singleSelect'
if (isMultiSelect(column.value)) return 'multiSelect'
if (isDate(column.value, abstractType.value)) return 'datePicker'
if (isYear(column.value, abstractType.value)) return 'yearPicker'
if (isDateTime(column.value, abstractType.value)) return 'dateTimePicker'
if (isTime(column.value, abstractType.value)) return 'timePicker'
if (isRating(column.value)) return 'rating'
if (isDuration(column.value)) return 'duration'
if (isEmail(column.value)) return 'email'
if (isURL(column.value)) return 'url'
if (isPhoneNumber(column.value)) return 'phoneNumber'
if (isPercent(column.value)) return 'percent'
if (isCurrency(column.value)) return 'currency'
if (isUser(column.value)) return 'user'
if (isDecimal(column.value)) return 'decimal'
if (isFloat(column.value, abstractType.value)) return 'float'
if (isString(column.value, abstractType.value)) return 'text'
if (isInt(column.value, abstractType.value)) return 'integer'
if (isJSON(column.value)) return 'json'
return 'text'
})
</script>
<template>
<div
:class="[
`nc-cell-${(column?.uidt || 'default').toLowerCase()}`,
{
'nc-display-value-cell': isPrimary(column) && !props.virtual && !isForm && !isCalendar,
'nc-grid-numeric-cell-right':
isGrid &&
isNumericField &&
!isEditColumnMenu &&
!isForm &&
!isExpandedFormOpen &&
!isRating(column) &&
!isYear(column, abstractType),
'h-10': !isEditColumnMenu && isForm && !isAttachment(column) && !isTextArea(column) && !isJSON(column) && !props.virtual,
'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu,
'!min-h-30': isTextArea(column) && (isForm || isSurveyForm),
'nc-cell-longtext-ai': cellType === 'ai',
},
]"
class="nc-cell w-full h-full relative"
@contextmenu="onContextmenu"
@keydown.enter.exact="navigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="navigate(NavigateDir.PREV, $event)"
>
<template v-if="column">
<div v-if="isGenerating" class="flex items-center gap-2 w-full">
<GeneralLoader />
<NcTooltip class="truncate max-w-[calc(100%_-_24px)]" show-on-truncate-only>
<template #title> {{ $t('general.generating') }} </template>
{{ $t('general.generating') }}
</NcTooltip>
</div>
<LazyCellAI v-else-if="cellType === 'ai'" v-model="vModel" @save="emit('save')" />
<LazyCellTextArea v-else-if="cellType === 'textarea'" v-model="vModel" :virtual="props.virtual" />
<LazyCellGeoData v-else-if="cellType === 'geoData'" v-model="vModel" />
<LazyCellCheckbox v-else-if="cellType === 'checkbox'" v-model="vModel" />
<LazyCellAttachment
v-else-if="cellType === 'attachment'"
ref="attachmentCell"
v-model="vModel"
:row-index="props.rowIndex"
/>
<LazyCellSingleSelect
v-else-if="cellType === 'singleSelect'"
v-model="vModel"
:disable-option-creation="!!isEditColumnMenu"
:row-index="props.rowIndex"
/>
<LazyCellMultiSelect
v-else-if="cellType === 'multiSelect'"
v-model="vModel"
:disable-option-creation="!!isEditColumnMenu"
:row-index="props.rowIndex"
/>
<LazyCellDatePicker
v-else-if="cellType === 'datePicker'"
v-model="vModel"
:is-pk="isPrimaryKey(column)"
:show-current-date-option="showCurrentDateOption"
@current-date="currentDate"
/>
<LazyCellYearPicker v-else-if="cellType === 'yearPicker'" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellDateTimePicker
v-else-if="cellType === 'dateTimePicker'"
v-model="vModel"
:is-pk="isPrimaryKey(column)"
:is-updated-from-copy-n-paste="currentRow.rowMeta.isUpdatedFromCopyNPaste"
:show-current-date-option="showCurrentDateOption"
@current-date="currentDate"
/>
<LazyCellTimePicker v-else-if="cellType === 'timePicker'" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellRating v-else-if="cellType === 'rating'" v-model="vModel" />
<LazyCellDuration v-else-if="cellType === 'duration'" v-model="vModel" />
<LazyCellEmail v-else-if="cellType === 'email'" v-model="vModel" />
<LazyCellUrl v-else-if="cellType === 'url'" v-model="vModel" />
<LazyCellPhoneNumber v-else-if="cellType === 'phoneNumber'" v-model="vModel" />
<LazyCellPercent v-else-if="cellType === 'percent'" v-model="vModel" />
<LazyCellCurrency v-else-if="cellType === 'currency'" v-model="vModel" @save="emit('save')" />
<LazyCellUser v-else-if="cellType === 'user'" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellDecimal v-else-if="cellType === 'decimal'" v-model="vModel" />
<LazyCellFloat v-else-if="cellType === 'float'" v-model="vModel" />
<LazyCellText v-else-if="cellType === 'text'" v-model="vModel" />
<LazyCellInteger v-else-if="cellType === 'integer'" v-model="vModel" />
<LazyCellJson v-else-if="cellType === 'json'" v-model="vModel" />
<LazyCellText v-else v-model="vModel" />
<div
v-if="
((isPublic && readOnly && !isForm) || isSystemColumn(column)) &&
!isAttachment(column) &&
!isTextArea(column) &&
!isAI(column)
"
class="nc-locked-overlay"
/>
</template>
</div>
</template>
<style lang="scss" scoped>
.nc-grid-numeric-cell-left {
text-align: left;
:deep(input) {
text-align: left;
}
}
.nc-grid-numeric-cell-right {
text-align: right;
:deep(input) {
text-align: right;
}
}
.nc-cell {
@apply text-sm text-gray-600;
font-weight: 500;
:deep(.nc-cell-field),
:deep(input),
:deep(textarea),
:deep(.nc-cell-field-link) {
@apply !text-sm;
font-weight: 500;
}
:deep(input::placeholder),
:deep(textarea::placeholder) {
@apply text-gray-400;
font-weight: 300;
}
&.nc-display-value-cell {
@apply !text-brand-500 !font-semibold;
:deep(.nc-cell-field),
:deep(input),
:deep(textarea),
:deep(.nc-cell-field-link) {
@apply !font-semibold;
}
}
&.nc-cell-longtext {
@apply leading-5;
}
:deep(.ant-picker-input) {
@apply text-sm leading-4;
font-weight: 500;
input {
@apply text-sm leading-4;
font-weight: 500;
}
}
:deep(.nc-cell-field) {
@apply px-0;
}
}
</style>