Files
nocodb/packages/nc-gui/components/virtual-cell/QrCode.vue
mertmit 69a29568c7 chore: sync
Signed-off-by: mertmit <mertmit99@gmail.com>
2026-01-10 00:21:02 +03:00

259 lines
7.8 KiB
Vue

<script setup lang="ts">
import { useQRCode } from '@vueuse/integrations/useQRCode'
import type QRCode from 'qrcode'
import { IsCanvasInjectionInj } from '../../context'
import { base64ToBlob, copyPNGToClipboard } from '~/utils/svgToPng'
const { t } = useI18n()
const { isMobileMode } = useGlobal()
const isCanvasInjected = inject(IsCanvasInjectionInj, false)
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const maxNumberOfAllowedCharsForQrValue = 2000
const cellValue = inject(CellValueInj)
const column = inject(ColumnInj)
const qrValue = computed(() => String(cellValue?.value ?? ''))
const _isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
const isLinkRecordDropdown = inject(IsLinkRecordDropdownInj, ref(false))
const isExpandedFormOpen = computed(() => {
return _isExpandedFormOpen.value && !isLinkRecordDropdown.value
})
const tooManyCharsForQrCode = computed(() => qrValue?.value.length > maxNumberOfAllowedCharsForQrValue)
const showQrCode = computed(() => qrValue?.value?.length > 0 && !tooManyCharsForQrCode.value && qrValue?.value !== 'ERR!')
const compressedQrValue = computed(() => {
if (qrValue.value.length > maxNumberOfAllowedCharsForQrValue) {
return qrValue?.value?.slice(0, maxNumberOfAllowedCharsForQrValue)
}
return qrValue.value
})
const qrCodeOptions: QRCode.QRCodeToDataURLOptions = {
errorCorrectionLevel: 'M',
margin: 1,
rendererOpts: {
quality: 1,
},
}
const rowHeight = inject(RowHeightInj, ref(undefined))
const qrCode = useQRCode(compressedQrValue, {
...qrCodeOptions,
width: 150,
})
const qrCodeLarge = useQRCode(compressedQrValue, {
...qrCodeOptions,
width: 600,
})
const modalVisible = ref(false)
const showQrModal = (ev?: Event) => {
if (!showQrCode.value) return
ev?.stopPropagation()
modalVisible.value = true
}
const handleModalOkClick = () => (modalVisible.value = false)
const meta = inject(MetaInj)
const valueFieldId = computed(() => column?.value.colOptions?.fk_qr_value_column_id)
const { showClearNonEditableFieldWarning } = useShowNotEditableWarning({ onEnter: showQrModal })
const { isCopied, performCopy } = useIsCopied()
const copyAsPng = async () => {
if (!qrCodeLarge.value) return
const blob = await base64ToBlob(qrCodeLarge.value)
const success = await copyPNGToClipboard(blob)
if (!success) throw new Error(t('msg.error.notSupported'))
}
const height = computed(() => {
if (isExpandedFormOpen.value) {
return undefined
}
if (!rowHeight.value) {
return '1.8rem'
}
return `${rowHeight.value === 1 ? rowHeightInPx['1']! - 4 : rowHeightInPx[`${rowHeight.value}`]! - 20}px`
})
onMounted(() => {
if (isCanvasInjected && !isUnderLookup.value && !isExpandedFormOpen.value && !isLinkRecordDropdown.value) {
if (showQrCode.value) {
modalVisible.value = true
}
}
})
</script>
<template>
<a-modal
v-model:visible="modalVisible"
:class="{ active: modalVisible }"
wrap-class-name="nc-qr-code-large qrcode-modal"
:body-style="{ padding: '0px', display: 'flex', justifyContent: 'center', flexDirection: 'column', alignItems: 'center' }"
:closable="false"
:centered="isMobileMode"
@ok="handleModalOkClick"
>
<template #title>
<div class="flex gap-2 items-center w-full">
<h1 class="font-weight-700 m-0">{{ column?.title }}</h1>
<div class="h-5 px-1 bg-nc-bg-gray-medium text-nc-content-gray-subtle2 rounded-md justify-center items-center flex">
<SmartsheetHeaderIcon
v-if="meta?.columnsById?.[valueFieldId]"
:column="meta?.columnsById?.[valueFieldId]"
class="h-4"
/>
<div class="text-sm font-medium">{{ meta?.columnsById?.[valueFieldId]?.title }}</div>
</div>
<div class="flex-1"></div>
<NcButton class="nc-qrcode-close !px-1" type="text" size="xs" @click="modalVisible = false">
<GeneralIcon class="text-md text-nc-content-gray-subtle h-4 w-4" icon="close" />
</NcButton>
</div>
</template>
<template #footer>
<div class="flex flex-row items-center justify-end">
<div class="flex flex-row flex-grow mr-2 !overflow-y-auto py-2 hidden" data-testid="nc-qr-code-large-value-label">
{{ qrValue }}
</div>
<div v-if="showQrCode" class="flex gap-2">
<NcTooltip>
<template #title>
{{ $t('labels.clickToCopy') }}
</template>
<NcButton size="small" type="secondary" @click="performCopy(copyAsPng)">
<template #icon>
<div class="flex children:flex-none relative h-4 w-4">
<Transition name="icon-fade" :duration="200">
<GeneralIcon v-if="isCopied" icon="check" class="h-4 w-4 opacity-80" />
<GeneralIcon v-else icon="copy" class="h-4 w-4 opacity-80" />
</Transition>
</div>
</template>
{{ isCopied ? $t('general.copied') : $t('general.copy') }}
</NcButton>
</NcTooltip>
<a :href="qrCodeLarge" :download="`${qrValue}.png`">
<NcTooltip>
<template #title>
{{ $t('labels.clickToDownload') }}
</template>
<NcButton size="small" type="secondary">
<template #icon>
<GeneralIcon icon="download" class="w-4 h-4" />
</template>
{{ $t('general.download') }}
</NcButton>
</NcTooltip>
</a>
</div>
</div>
</template>
<div v-if="showQrCode" class="w-full px-4">
<img :src="qrCodeLarge" :alt="$t('title.qrCode')" class="h-[156px] mx-auto mt-8 mb-4" />
<div class="bg-nc-bg-gray-light px-3 py-2 rounded-lg">
<NcTooltip show-on-truncate-only class="truncate">
<template #title>
{{ qrValue }}
</template>
{{ qrValue }}
</NcTooltip>
</div>
</div>
</a-modal>
<div
v-if="showQrCode"
class="nc-qrcode-container w-full flex"
:class="{
'flex-start h-20': isExpandedFormOpen,
'justify-center': !isExpandedFormOpen && !isLinkRecordDropdown,
}"
>
<img
v-if="rowHeight"
:style="{ height }"
:class="{
'border-1': isExpandedFormOpen,
}"
:src="qrCode"
:alt="$t('title.qrCode')"
class="min-w-[1.4em]"
@click="showQrModal"
/>
<img
v-else
class="flex-none min-w-[1.4em]"
:class="!isExpandedFormOpen && !isLinkRecordDropdown ? 'mx-auto' : ''"
:src="qrCode"
:alt="$t('title.qrCode')"
@click="showQrModal"
/>
</div>
<div v-if="tooManyCharsForQrCode" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('labels.qrCodeValueTooLong') }}
</div>
<div v-if="showClearNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.warning.nonEditableFields.qrFieldsCannotBeDirectlyChanged') }}
</div>
<a-tooltip v-else-if="!showQrCode && qrValue === 'ERR!'" placement="bottom" class="text-nc-content-orange-dark">
<template #title>
<span class="font-bold">Please select a target field!</span>
</template>
<span>ERR!</span>
</a-tooltip>
</template>
<style lang="scss">
.qrcode-modal .ant-modal-content {
padding: 0 !important;
.ant-modal-header {
@apply border-b-1 border-b-nc-border-gray-medium;
position: relative;
padding: 8px 16px;
border-top-left-radius: 1em;
border-top-right-radius: 1em;
.ant-modal-title {
height: 30px;
display: flex;
align-items: center;
}
}
.ant-modal-footer {
padding: 8px 12px;
border: none;
}
}
.nc-data-cell {
&:has(.nc-virtual-cell-qrcode) {
@apply !border-none;
box-shadow: none !important;
&:focus-within:not(.nc-readonly-div-data-cell):not(.nc-system-field) {
box-shadow: none !important;
}
}
}
</style>