mirror of https://github.com/nocodb/nocodb
github-actions[bot]
8 months ago
committed by
GitHub
192 changed files with 6296 additions and 3616 deletions
@ -1,3 +1,3 @@ |
|||||||
engine-strict=true |
engine-strict=true |
||||||
shamefully-hoist=true |
shamefully-hoist=true |
||||||
use-node-version=18.19.0 |
use-node-version=18.19.1 |
||||||
|
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 634 B After Width: | Height: | Size: 675 B |
After Width: | Height: | Size: 433 B |
After Width: | Height: | Size: 547 B |
@ -0,0 +1,141 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import { Cropper } from 'vue-advanced-cropper' |
||||||
|
import 'vue-advanced-cropper/dist/style.css' |
||||||
|
import 'vue-advanced-cropper/dist/theme.classic.css' |
||||||
|
|
||||||
|
import type { AttachmentReqType } from 'nocodb-sdk' |
||||||
|
import { extractSdkResponseErrorMsg, useApi } from '#imports' |
||||||
|
interface Props { |
||||||
|
imageConfig: { |
||||||
|
src: string |
||||||
|
type: string |
||||||
|
name: string |
||||||
|
} |
||||||
|
cropperConfig: { |
||||||
|
aspectRatio?: number |
||||||
|
} |
||||||
|
uploadConfig?: { |
||||||
|
path?: string |
||||||
|
} |
||||||
|
showCropper: boolean |
||||||
|
} |
||||||
|
const { imageConfig, cropperConfig, uploadConfig, ...props } = defineProps<Props>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:showCropper', 'submit']) |
||||||
|
|
||||||
|
const showCropper = useVModel(props, 'showCropper', emit) |
||||||
|
|
||||||
|
const { api, isLoading } = useApi() |
||||||
|
|
||||||
|
const cropperRef = ref() |
||||||
|
|
||||||
|
const previewImage = ref({ |
||||||
|
canvas: {}, |
||||||
|
src: '', |
||||||
|
}) |
||||||
|
|
||||||
|
const handleCropImage = () => { |
||||||
|
const { canvas } = cropperRef.value.getResult() |
||||||
|
previewImage.value = { |
||||||
|
canvas, |
||||||
|
src: canvas.toDataURL(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleUploadImage = async (fileToUpload: AttachmentReqType[]) => { |
||||||
|
if (uploadConfig?.path) { |
||||||
|
try { |
||||||
|
const uploadResult = await api.storage.uploadByUrl( |
||||||
|
{ |
||||||
|
path: uploadConfig?.path as string, |
||||||
|
}, |
||||||
|
fileToUpload, |
||||||
|
) |
||||||
|
if (uploadResult?.[0]) { |
||||||
|
emit('submit', { |
||||||
|
...uploadResult[0], |
||||||
|
}) |
||||||
|
} else { |
||||||
|
emit('submit', fileToUpload[0]) |
||||||
|
} |
||||||
|
} catch (error: any) { |
||||||
|
console.error(error) |
||||||
|
message.error(await extractSdkResponseErrorMsg(error)) |
||||||
|
} |
||||||
|
} else { |
||||||
|
emit('submit', fileToUpload[0]) |
||||||
|
} |
||||||
|
|
||||||
|
showCropper.value = false |
||||||
|
} |
||||||
|
|
||||||
|
const handleSaveImage = async () => { |
||||||
|
if (previewImage.value.canvas) { |
||||||
|
;(previewImage.value.canvas as any).toBlob(async (blob: Blob) => { |
||||||
|
await handleUploadImage([ |
||||||
|
{ |
||||||
|
title: imageConfig.name, |
||||||
|
fileName: imageConfig.name, |
||||||
|
mimetype: imageConfig.type, |
||||||
|
size: blob.size, |
||||||
|
url: previewImage.value.src, |
||||||
|
}, |
||||||
|
]) |
||||||
|
}, imageConfig.type) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
watch(showCropper, () => { |
||||||
|
if (!showCropper.value) { |
||||||
|
previewImage.value = { |
||||||
|
canvas: {}, |
||||||
|
src: '', |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NcModal v-model:visible="showCropper" :mask-closable="false"> |
||||||
|
<div class="nc-image-cropper-wrapper relative"> |
||||||
|
<Cropper |
||||||
|
ref="cropperRef" |
||||||
|
class="nc-cropper relative" |
||||||
|
:src="imageConfig.src" |
||||||
|
:auto-zoom="true" |
||||||
|
:stencil-props="cropperConfig?.aspectRatio ? { aspectRatio: cropperConfig.aspectRatio } : {}" |
||||||
|
/> |
||||||
|
<div v-if="previewImage.src" class="result_preview"> |
||||||
|
<img :src="previewImage.src" alt="Preview Image" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="flex justify-between items-center space-x-4 mt-4"> |
||||||
|
<div class="flex items-center space-x-4"> |
||||||
|
<NcButton type="secondary" size="small" :disabled="isLoading" @click="showCropper = false"> Cancel </NcButton> |
||||||
|
</div> |
||||||
|
<div class="flex items-center space-x-4"> |
||||||
|
<NcButton type="secondary" size="small" :disabled="isLoading" @click="handleCropImage"> |
||||||
|
<GeneralIcon icon="crop"></GeneralIcon> |
||||||
|
<span class="ml-2">Crop</span> |
||||||
|
</NcButton> |
||||||
|
|
||||||
|
<NcButton size="small" :loading="isLoading" :disabled="!previewImage.src" @click="handleSaveImage"> Save </NcButton> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</NcModal> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.nc-cropper { |
||||||
|
min-height: 400px; |
||||||
|
max-height: 400px; |
||||||
|
} |
||||||
|
.nc-image-cropper-wrapper { |
||||||
|
.result_preview { |
||||||
|
@apply absolute right-4 bottom-4 border-1 border-dashed border-white/50 w-28 h-28 opacity-90 pointer-events-none; |
||||||
|
img { |
||||||
|
@apply w-full h-full object-contain; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,370 @@ |
|||||||
|
<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>) => { |
||||||
|
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> |
@ -0,0 +1,293 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import Draggable from 'vuedraggable' |
||||||
|
import tinycolor from 'tinycolor2' |
||||||
|
import type { ColumnType, SelectOptionType, SelectOptionsType, UserFieldRecordType } from 'nocodb-sdk' |
||||||
|
import { UITypes } from 'nocodb-sdk' |
||||||
|
import type { FormFieldsLimitOptionsType } from '~/lib' |
||||||
|
import { MetaInj, iconMap } from '#imports' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
modelValue: FormFieldsLimitOptionsType[] |
||||||
|
column: ColumnType |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const meta = inject(MetaInj)! |
||||||
|
|
||||||
|
const column = toRef(props, 'column') |
||||||
|
|
||||||
|
const basesStore = useBases() |
||||||
|
|
||||||
|
const { basesUser } = storeToRefs(basesStore) |
||||||
|
|
||||||
|
const baseUsers = computed(() => (meta.value.base_id ? basesUser.value.get(meta.value.base_id) || [] : [])) |
||||||
|
|
||||||
|
const searchQuery = ref('') |
||||||
|
|
||||||
|
const drag = ref(false) |
||||||
|
|
||||||
|
const vModel = computed({ |
||||||
|
get: () => { |
||||||
|
let order = 1 |
||||||
|
const limitOptionsById = |
||||||
|
(props.modelValue || []).reduce((o: Record<string, FormFieldsLimitOptionsType>, f: FormFieldsLimitOptionsType) => { |
||||||
|
if (order < (f?.order ?? 0)) { |
||||||
|
order = f.order |
||||||
|
} |
||||||
|
return { |
||||||
|
...o, |
||||||
|
[f.id]: f, |
||||||
|
} |
||||||
|
}, {} as Record<string, FormFieldsLimitOptionsType>) ?? {} |
||||||
|
|
||||||
|
if (UITypes.User === column.value.uidt) { |
||||||
|
const collaborators = ((baseUsers.value || []) as UserFieldRecordType[]) |
||||||
|
.filter((user) => !user?.deleted) |
||||||
|
.map((user: any) => ({ |
||||||
|
id: user.id, |
||||||
|
email: user.email, |
||||||
|
display_name: user.display_name, |
||||||
|
order: user.id && limitOptionsById[user.id] ? limitOptionsById[user.id]?.order ?? user.order : order++, |
||||||
|
show: user.id && limitOptionsById[user.id] ? limitOptionsById[user.id]?.show : !(props.modelValue || []).length, |
||||||
|
})) |
||||||
|
.sort((a, b) => a.order - b.order) |
||||||
|
|
||||||
|
if ((props.modelValue || []).length !== collaborators.length) { |
||||||
|
emit( |
||||||
|
'update:modelValue', |
||||||
|
collaborators.map((o) => ({ id: o.id, order: o.order, show: o.show })), |
||||||
|
) |
||||||
|
} |
||||||
|
return collaborators |
||||||
|
} else if ([UITypes.SingleSelect, UITypes.MultiSelect].includes(column.value.uidt as UITypes)) { |
||||||
|
const updateModelValue = ((column.value.colOptions as SelectOptionsType)?.options || []) |
||||||
|
.map((c) => { |
||||||
|
return { |
||||||
|
...c, |
||||||
|
order: c.id && limitOptionsById[c.id] ? limitOptionsById[c.id]?.order ?? c.order : order++, |
||||||
|
show: c.id && limitOptionsById[c.id] ? limitOptionsById[c.id]?.show : !(props.modelValue || []).length, |
||||||
|
} as SelectOptionType & { show?: boolean } |
||||||
|
}) |
||||||
|
.sort((a, b) => { |
||||||
|
if (a.order !== undefined && b.order !== undefined) { |
||||||
|
return a.order - b.order |
||||||
|
} |
||||||
|
return 0 |
||||||
|
}) |
||||||
|
|
||||||
|
if ((props.modelValue || []).length !== ((column.value.colOptions as SelectOptionsType)?.options || []).length) { |
||||||
|
emit( |
||||||
|
'update:modelValue', |
||||||
|
updateModelValue.map((o) => ({ id: o.id, order: o.order, show: o.show })), |
||||||
|
) |
||||||
|
} |
||||||
|
return updateModelValue |
||||||
|
} |
||||||
|
return [] |
||||||
|
}, |
||||||
|
set: (val) => { |
||||||
|
emit( |
||||||
|
'update:modelValue', |
||||||
|
val.map((o) => ({ id: o.id, order: o.order, show: o.show })), |
||||||
|
) |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
async function onMove(_event: { moved: { newIndex: number; oldIndex: number; element: any } }) { |
||||||
|
const { |
||||||
|
moved: { newIndex = 0, oldIndex = 0, element }, |
||||||
|
} = _event |
||||||
|
|
||||||
|
let nextOrder: number |
||||||
|
|
||||||
|
// set new order value based on the new order of the items |
||||||
|
if (!vModel.value.length || vModel.value.length === 1) { |
||||||
|
nextOrder = 1 |
||||||
|
} else if (vModel.value.length - 1 === newIndex) { |
||||||
|
// If moving to the end, set nextOrder greater than the maximum order in the list |
||||||
|
nextOrder = Math.max(...vModel.value.map((item) => item?.order ?? 0)) + 1 |
||||||
|
} else if (newIndex === 0) { |
||||||
|
// If moving to the beginning, set nextOrder smaller than the minimum order in the list |
||||||
|
nextOrder = Math.min(...vModel.value.map((item) => item?.order ?? 0)) / 2 |
||||||
|
} else { |
||||||
|
nextOrder = |
||||||
|
(parseFloat(String(vModel.value[newIndex - 1]?.order ?? 0)) + parseFloat(String(vModel.value[newIndex + 1]?.order ?? 0))) / |
||||||
|
2 |
||||||
|
} |
||||||
|
const _nextOrder = !isNaN(Number(nextOrder)) ? nextOrder : oldIndex |
||||||
|
|
||||||
|
element.order = _nextOrder |
||||||
|
|
||||||
|
vModel.value = [...vModel.value] |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="w-full h-full nc-col-select-option nc-form-scrollbar"> |
||||||
|
<div v-if="vModel.length > 12"> |
||||||
|
<a-input |
||||||
|
v-model:value="searchQuery" |
||||||
|
class="!h-9 !px-3 !py-1 !rounded-lg mb-2" |
||||||
|
:placeholder="`${$t('placeholder.searchOptions')}...`" |
||||||
|
name="nc-form-field-limit-option-search-input" |
||||||
|
data-testid="nc-form-field-limit-option-search-input" |
||||||
|
> |
||||||
|
<template #prefix> |
||||||
|
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500 group-hover:text-black" /> |
||||||
|
</template> |
||||||
|
<template #suffix> |
||||||
|
<GeneralIcon |
||||||
|
v-if="searchQuery.length > 0" |
||||||
|
icon="close" |
||||||
|
class="ml-2 h-4 w-4 text-gray-500 group-hover:text-black" |
||||||
|
data-testid="nc-form-field-clear-search" |
||||||
|
@click="searchQuery = ''" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
</a-input> |
||||||
|
</div> |
||||||
|
<Draggable |
||||||
|
v-if="vModel.length" |
||||||
|
:model-value="vModel" |
||||||
|
item-key="id" |
||||||
|
handle=".nc-child-draggable-icon" |
||||||
|
ghost-class="nc-form-field-limit-option-ghost" |
||||||
|
class="rounded-lg border-1 border-gray-200 !max-h-[224px] overflow-y-auto nc-form-scrollbar" |
||||||
|
@change="onMove($event)" |
||||||
|
@start="drag = true" |
||||||
|
@end="drag = false" |
||||||
|
> |
||||||
|
<template #item="{ element }"> |
||||||
|
<div |
||||||
|
v-if=" |
||||||
|
column.uidt === UITypes.User |
||||||
|
? (element?.display_name?.trim() || element?.email)?.toLowerCase().includes(searchQuery.toLowerCase()) |
||||||
|
: element.title?.toLowerCase().includes(searchQuery.toLowerCase()) |
||||||
|
" |
||||||
|
:key="element.id" |
||||||
|
class="w-full h-10 px-2 py-1.5 flex flex-row items-center gap-3 border-b-1 last:border-none border-gray-200" |
||||||
|
:class="[ |
||||||
|
`nc-form-field-${column.title?.replaceAll(' ', '')}-limit-option-${element.title?.replaceAll(' ', '')}`, |
||||||
|
`${element.show ? 'hover:bg-gray-50' : 'bg-gray-100'}`, |
||||||
|
]" |
||||||
|
:data-testid="`nc-form-field-${column.title?.replaceAll(' ', '')}-limit-option-${element.title?.replaceAll(' ', '')}`" |
||||||
|
> |
||||||
|
<component :is="iconMap.drag" class="nc-child-draggable-icon flex-none cursor-move !h-4 !w-4 text-gray-600" /> |
||||||
|
|
||||||
|
<div |
||||||
|
@click=" |
||||||
|
() => { |
||||||
|
element.show = !element.show |
||||||
|
vModel = [...vModel] |
||||||
|
} |
||||||
|
" |
||||||
|
> |
||||||
|
<component |
||||||
|
:is="element.show ? iconMap.eye : iconMap.eyeSlash" |
||||||
|
class="flex-none cursor-pointer !h-4 !w-4 text-gray-600" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<a-tag v-if="column.uidt === UITypes.User" class="rounded-tag max-w-[calc(100%_-_70px)] !pl-0" color="'#ccc'"> |
||||||
|
<span |
||||||
|
:style="{ |
||||||
|
'color': tinycolor.isReadable('#ccc' || '#ccc', '#fff', { level: 'AA', size: 'large' }) |
||||||
|
? '#fff' |
||||||
|
: tinycolor.mostReadable('#ccc' || '#ccc', ['#0b1d05', '#fff']).toHex8String(), |
||||||
|
'font-size': '13px', |
||||||
|
}" |
||||||
|
class="flex items-stretch gap-2" |
||||||
|
> |
||||||
|
<div> |
||||||
|
<GeneralUserIcon |
||||||
|
size="auto" |
||||||
|
:name="element.display_name?.trim() ? element.display_name?.trim() : ''" |
||||||
|
:email="element.email" |
||||||
|
class="!text-[0.65rem]" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<NcTooltip class="truncate max-w-full" show-on-truncate-only> |
||||||
|
<template #title> |
||||||
|
{{ element.display_name?.trim() || element?.email }} |
||||||
|
</template> |
||||||
|
<span |
||||||
|
class="text-ellipsis overflow-hidden" |
||||||
|
:style="{ |
||||||
|
wordBreak: 'keep-all', |
||||||
|
whiteSpace: 'nowrap', |
||||||
|
display: 'inline', |
||||||
|
}" |
||||||
|
> |
||||||
|
{{ element.display_name?.trim() || element?.email }} |
||||||
|
</span> |
||||||
|
</NcTooltip> |
||||||
|
</span> |
||||||
|
</a-tag> |
||||||
|
<a-tag v-else class="rounded-tag max-w-[calc(100%_-_70px)]" :color="element.color"> |
||||||
|
<span |
||||||
|
:style="{ |
||||||
|
'color': tinycolor.isReadable(element.color || '#ccc', '#fff', { level: 'AA', size: 'large' }) |
||||||
|
? '#fff' |
||||||
|
: tinycolor.mostReadable(element.color || '#ccc', ['#0b1d05', '#fff']).toHex8String(), |
||||||
|
'font-size': '13px', |
||||||
|
}" |
||||||
|
> |
||||||
|
<NcTooltip class="truncate max-w-full" show-on-truncate-only> |
||||||
|
<template #title> |
||||||
|
{{ element.title }} |
||||||
|
</template> |
||||||
|
<span |
||||||
|
class="text-ellipsis overflow-hidden" |
||||||
|
:style="{ |
||||||
|
wordBreak: 'keep-all', |
||||||
|
whiteSpace: 'nowrap', |
||||||
|
display: 'inline', |
||||||
|
}" |
||||||
|
> |
||||||
|
{{ element.title }} |
||||||
|
</span> |
||||||
|
</NcTooltip> |
||||||
|
</span> |
||||||
|
</a-tag> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template v-if="!vModel.length" #footer |
||||||
|
><div class="px-0.5 py-2 text-gray-500 text-center">{{ $t('title.noOptionsFound') }}</div></template |
||||||
|
> |
||||||
|
<template |
||||||
|
v-else-if=" |
||||||
|
vModel.length && |
||||||
|
searchQuery && |
||||||
|
!vModel?.filter((element) => { |
||||||
|
return column.uidt === UITypes.User |
||||||
|
? (element?.display_name?.trim() || element?.email)?.toLowerCase().includes(searchQuery.toLowerCase()) |
||||||
|
: element.title?.toLowerCase().includes(searchQuery.toLowerCase()) |
||||||
|
})?.length |
||||||
|
" |
||||||
|
#footer |
||||||
|
> |
||||||
|
<div class="px-0.5 py-2 text-gray-500 text-center">{{ $t('title.noOptionsFound') }} with title `{{ searchQuery }}`</div> |
||||||
|
</template> |
||||||
|
</Draggable> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.nc-form-scrollbar { |
||||||
|
@apply scrollbar scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent; |
||||||
|
&::-webkit-scrollbar-thumb:hover { |
||||||
|
@apply !scrollbar-thumb-gray-300; |
||||||
|
} |
||||||
|
} |
||||||
|
.rounded-tag { |
||||||
|
@apply py-0 px-[12px] rounded-[12px]; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-tag) { |
||||||
|
@apply rounded-tag my-[2px]; |
||||||
|
} |
||||||
|
.nc-form-field-limit-option-ghost { |
||||||
|
@apply bg-gray-50; |
||||||
|
} |
||||||
|
</style> |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue