Browse Source

Merge pull request #7717 from nocodb/nc-feat/form-view-tbd-1

Nc feat/form view tbd 1
1-command-setup
Raju Udava 7 months ago committed by GitHub
parent
commit
4a7b70bbf2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 15
      packages/nc-gui/assets/nc-icons/crop.svg
  2. 17
      packages/nc-gui/components/general/FormBanner.vue
  3. 148
      packages/nc-gui/components/general/ImageCropper.vue
  4. 304
      packages/nc-gui/components/smartsheet/Form.vue
  5. 1
      packages/nc-gui/composables/useViewData.ts
  6. 10
      packages/nc-gui/lang/en.json
  7. 1
      packages/nc-gui/package.json
  8. 2
      packages/nc-gui/utils/iconUtils.ts
  9. 2
      packages/nocodb/src/controllers/form-columns.controller.ts
  10. 15
      packages/nocodb/src/models/View.ts
  11. 4
      packages/nocodb/src/schema/swagger-v2.json
  12. 4
      packages/nocodb/src/schema/swagger.json
  13. 2
      packages/nocodb/src/utils/acl.ts
  14. 27
      pnpm-lock.yaml

15
packages/nc-gui/assets/nc-icons/crop.svg

@ -0,0 +1,15 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_184_5665)">
<path
d="M4.08667 0.666626L4 10.6666C4 11.0202 4.14048 11.3594 4.39052 11.6094C4.64057 11.8595 4.97971 12 5.33333 12H15.3333"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M0.666672 4.08667L10.6667 4C11.0203 4 11.3594 4.14048 11.6095 4.39052C11.8595 4.64057 12 4.97971 12 5.33333V15.3333"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</g>
<defs>
<clipPath id="clip0_184_5665">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 797 B

17
packages/nc-gui/components/general/FormBanner.vue

@ -3,19 +3,22 @@ interface Props {
bannerImageUrl?: string | null
}
const { bannerImageUrl } = defineProps<Props>()
const { getPossibleAttachmentSrc } = useAttachment()
</script>
<template>
<div
class="w-full max-w-screen-xl mx-auto bg-white border-1 border-gray-200 rounded-3xl overflow-hidden"
:style="
bannerImageUrl
? { 'background-image': `url(${bannerImageUrl})`, 'background-size': 'cover', 'background-position': 'center' }
: {}
"
class="nc-form-banner-wrapper w-full mx-auto bg-white border-1 border-gray-200 rounded-2xl overflow-hidden"
:style="{ aspectRatio: 4 / 1 }"
>
<LazyCellAttachmentImage
v-if="bannerImageUrl"
:srcs="getPossibleAttachmentSrc(parseProp(bannerImageUrl))"
class="nc-form-banner-image object-cover w-full"
/>
<!-- Todo: aspect ratio and cover image uploader and image cropper to crop image in fixed aspect ratio -->
<div v-if="!bannerImageUrl" class="h-full flex items-stretch justify-between">
<div v-else class="h-full flex items-stretch justify-between">
<div class="flex">
<img src="~assets/img/form-banner-left.png" alt="form-banner-left'" />
</div>

148
packages/nc-gui/components/general/ImageCropper.vue

@ -0,0 +1,148 @@
<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,
}
: {}
"
image-restriction="none"
/>
<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>

304
packages/nc-gui/components/smartsheet/Form.vue

@ -4,7 +4,7 @@ import Draggable from 'vuedraggable'
import tinycolor from 'tinycolor2'
import { Pane, Splitpanes } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
import { RelationTypes, UITypes, ViewTypes, getSystemColumns, isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk'
import { ProjectRoles, RelationTypes, UITypes, ViewTypes, getSystemColumns, isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk'
import type { Permission } from '#imports'
import {
ActiveViewInj,
@ -26,6 +26,7 @@ import {
ref,
useDebounceFn,
useEventListener,
useFileDialog,
useGlobal,
useI18n,
useNuxtApp,
@ -62,6 +63,10 @@ const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useRoles()
const { base } = storeToRefs(useBase())
const { getPossibleAttachmentSrc } = useAttachment()
let formState = reactive<Record<string, any>>({})
const secondsRemain = ref(0)
@ -85,7 +90,7 @@ reloadEventHook.on(async () => {
setFormData()
})
const { fields, showAll, hideAll, saveOrUpdate } = useViewColumnsOrThrow()
const { fields, showAll, hideAll } = useViewColumnsOrThrow()
const { state, row } = useProvideSmartsheetRowStore(
meta,
@ -118,6 +123,34 @@ const isTabPressed = ref(false)
const isLoadingFormView = ref(false)
const showCropper = ref(false)
const imageCropperData = ref<{
imageConfig: {
src: string
type: string
name: string
}
cropperConfig: {
aspectRatio?: number
}
uploadConfig?: {
path?: string
}
cropFor: 'banner' | 'logo'
}>({
imageConfig: {
src: '',
type: '',
name: '',
},
cropperConfig: {},
uploadConfig: {
path: '',
},
cropFor: 'banner',
})
const focusLabel: VNodeRef = (el) => {
return (el as HTMLInputElement)?.focus()
}
@ -128,6 +161,11 @@ const { t } = useI18n()
const { betaFeatureToggleState } = useBetaFeatureToggle()
const { open, onChange: onChangeFile } = useFileDialog({
accept: 'image/*',
multiple: false,
})
const visibleColumns = computed(() => localColumns.value.filter((f) => f.show).sort((a, b) => a.order - b.order))
const updateView = useDebounceFn(
@ -139,6 +177,8 @@ const updateView = useDebounceFn(
)
async function submitForm() {
if (isLocked.value || !isUIAllowed('dataInsert')) return
try {
await formRef.value?.validateFields()
} catch (e: any) {
@ -158,6 +198,8 @@ async function submitForm() {
}
async function clearForm() {
if (isLocked.value || !isUIAllowed('dataInsert')) return
formState = reactive<Record<string, any>>({})
state.value = {}
await formRef.value.clearValidate()
@ -205,14 +247,15 @@ function onMoveCallback(event: any) {
}
}
// Todo: reorder visible form fields
function onMove(event: any, isVisibleFormFields = false) {
async function onMove(event: any, isVisibleFormFields = false) {
if (isLocked.value || !isEditable) return
const { oldIndex } = event.moved
let { newIndex, element } = event.moved
const fieldIndex = fields.value?.findIndex((f) => f?.fk_column_id === element.fk_column_id)
if (fieldIndex === -1 || fieldIndex === undefined) return
if (fieldIndex === -1 || fieldIndex === undefined || !fields.value?.[fieldIndex]) return
if (isVisibleFormFields) {
element = localColumns.value[localColumns.value?.findIndex((c) => c.fk_column_id === element.fk_column_id)]
@ -233,28 +276,38 @@ function onMove(event: any, isVisibleFormFields = false) {
element.order = ((localColumns.value[newIndex - 1]?.order || 0) + (localColumns.value[newIndex + 1].order || 0)) / 2
}
saveOrUpdate(element, fieldIndex)
await $api.dbView.formColumnUpdate(element.id, element)
fields.value[fieldIndex] = element as any
// saveOrUpdate(element, fieldIndex)
$e('a:form-view:reorder')
}
async function showOrHideColumn(column: Record<string, any>, show: boolean, isSidePannel = false) {
if (isLocked.value || !isEditable) return
if (shouldSkipColumn(column)) {
// Required field can't be moved
!isSidePannel && message.info(t('msg.info.requriedFieldsCantBeMoved'))
return
}
const fieldIndex = fields.value?.findIndex((f) => f?.fk_column_id === column.fk_column_id)
if (fieldIndex !== -1 && fieldIndex !== undefined) {
await saveOrUpdate(
{
...column,
show,
},
fieldIndex,
)
if (fieldIndex !== -1 && fieldIndex !== undefined && fields.value?.[fieldIndex]) {
console.log('column', column)
column.show = show
await $api.dbView.formColumnUpdate(column.id, column)
fields.value[fieldIndex] = column as any
// await saveOrUpdate(
// {
// ...column,
// show,
// },
// fieldIndex,
// )
reloadEventHook.trigger()
@ -273,6 +326,8 @@ function shouldSkipColumn(col: Record<string, any>) {
}
async function handleAddOrRemoveAllColumns(value: boolean) {
if (isLocked.value || !isEditable) return
if (value) {
for (const col of (localColumns as Record<string, any>)?.value) {
col.show = true
@ -310,6 +365,8 @@ function setFormData() {
systemFieldsIds.value = getSystemColumns(col).map((c) => c.fk_column_id)
formViewData.value = {
banner_image_url: '',
logo_url: '',
...formViewData.value,
submit_another_form: !!(formViewData.value?.submit_another_form ?? 0),
show_blank_form: !!(formViewData.value?.show_blank_form ?? 0),
@ -383,11 +440,14 @@ const columnSupportsScanning = (elementType: UITypes) =>
const onFormItemClick = (element: any) => {
if (isLocked.value || !isEditable) return
activeRow.value = element.title
isTabPressed.value = false
}
const handleChangeBackground = (color: string) => {
if (isLocked.value || !isEditable) return
const tcolor = tinycolor(color)
if (tcolor.isValid()) {
;(formViewData.value?.meta as Record<string, any>).background_color = color
@ -395,12 +455,66 @@ const handleChangeBackground = (color: string) => {
}
}
const openUploadImage = (isUploadBanner: boolean) => {
if (!isEditable) return
imageCropperData.value.uploadConfig = {
path: [NOCO, base.value.id, meta.value?.id, formViewData.value?.id].join('/'),
}
if (isUploadBanner) {
imageCropperData.value.cropperConfig = {
aspectRatio: 4 / 1,
}
imageCropperData.value.cropFor = 'banner'
} else {
imageCropperData.value.cropperConfig = {
aspectRatio: undefined,
}
imageCropperData.value.cropFor = 'logo'
}
open()
}
onChangeFile((files) => {
if (files && files[0]) {
// 1. Revoke the object URL, to allow the garbage collector to destroy the uploaded before file
if (imageCropperData.value.imageConfig.src) {
URL.revokeObjectURL(imageCropperData.value.imageConfig.src)
}
// 2. Create the blob link to the file to optimize performance:
const blob = URL.createObjectURL(files[0])
// 3. Update the image. The type will be derived from the extension
imageCropperData.value.imageConfig = {
src: blob,
type: files[0].type,
name: files[0].name,
}
showCropper.value = true
}
})
const handleOnUploadImage = (data: Record<string, any> = {}) => {
if (imageCropperData.value.cropFor === 'banner') {
formViewData.value!.banner_image_url = stringifyProp(data) ?? ''
} else {
formViewData.value!.logo_url = stringifyProp(data) ?? ''
}
updateView()
}
onClickOutside(draggableRef, () => {
activeRow.value = ''
isTabPressed.value = false
})
onMounted(async () => {
if (imageCropperData.value.src) {
URL.revokeObjectURL(imageCropperData.value.imageConfig.src)
}
isLoadingFormView.value = true
await loadFormView()
setFormData()
@ -480,7 +594,7 @@ useEventListener(
</script>
<template>
<div class="h-full">
<div class="h-full relative">
<template v-if="isMobileMode">
<div class="pl-6 pr-[120px] py-6 bg-white flex-col justify-start items-start gap-2.5 inline-flex">
<div class="text-gray-500 text-5xl font-semibold leading-16">
@ -550,18 +664,61 @@ useEventListener(
class="flex-1 h-full overflow-auto nc-form-scrollbar p-6"
:style="{background:(formViewData?.meta as Record<string,any>).background_color || '#F9F9FA'}"
>
<div :class="isEditable ? 'min-w-[616px] overflow-x-auto nc-form-scrollbar' : ''">
<!-- for future implementation of cover image -->
<!-- Todo: cover image uploader and image cropper to crop image in fixed aspect ratio -->
<GeneralFormBanner
v-if="
formViewData.banner_image_url || !(parseProp(formViewData?.meta).hide_branding && !formViewData.banner_image_url)
"
:banner-image-url="formViewData.banner_image_url"
/>
<div class="min-w-[616px] overflow-x-auto nc-form-scrollbar">
<GeneralImageCropper
v-if="isEditable"
v-model:show-cropper="showCropper"
:image-config="imageCropperData.imageConfig"
:cropper-config="imageCropperData.cropperConfig"
:upload-config="imageCropperData.uploadConfig"
@submit="handleOnUploadImage"
></GeneralImageCropper>
<!-- cover image -->
<div class="relative max-w-[max(33%,688px)] mx-auto">
<GeneralFormBanner :banner-image-url="formViewData.banner_image_url" />
<div class="absolute bottom-0 right-0">
<div class="flex items-center space-x-1 m-2">
<NcButton
type="secondary"
size="small"
class="nc-form-upload-banner-btn"
data-testid="nc-form-upload-banner-btn"
@click="openUploadImage(true)"
>
<div class="flex gap-2 items-center">
<component :is="iconMap.upload" class="w-4 h-4" />
<span>
{{ formViewData.banner_image_url ? $t('general.replace') : $t('general.upload') }}
{{ $t('general.banner') }}
</span>
</div>
</NcButton>
<NcTooltip v-if="formViewData.banner_image_url">
<template #title> {{ $t('general.delete') }} {{ $t('general.banner') }} </template>
<NcButton
type="secondary"
size="small"
class="nc-form-delete-banner-btn"
data-testid="nc-form-delete-banner-btn"
@click="
() => {
if (isEditable) {
formViewData!.banner_image_url = ''
updateView()
}
}
"
>
<div class="flex gap-2 items-center">
<component :is="iconMap.delete" class="w-4 h-4" />
</div>
</NcButton>
</NcTooltip>
</div>
</div>
</div>
<a-card
class="!py-8 !lg:py-12 !border-gray-200 !rounded-3xl !mt-6 max-w-[688px] !mx-auto"
class="!py-8 !lg:py-12 !border-gray-200 !rounded-3xl !mt-6 !max-w-[max(33%,688px)] !mx-auto"
:body-style="{
margin: '0 auto',
padding: '0px !important',
@ -571,16 +728,59 @@ useEventListener(
<!-- form header -->
<div class="flex flex-col px-4 lg:px-6">
<!-- Form logo -->
<!-- <div v-if="isEditable">
<div class="inline-block rounded-xl bg-gray-100 p-3">
<NcButton type="secondary" size="small" class="nc-form-upload-logo" data-testid="nc-form-upload-log">
<div class="flex gap-2 items-center">
<component :is="iconMap.upload" class="w-4 h-4" />
<span> Upload Logo </span>
</div>
</NcButton>
<div class="mb-4">
<div
class="nc-form-logo-wrapper mx-6 group relative rounded-xl inline-block h-56px max-w-189px overflow-hidden"
:class="formViewData.logo_url ? 'hover:(w-full bg-gray-100)' : 'bg-gray-100'"
style="transition: all 0.3s ease-in"
>
<LazyCellAttachmentImage
v-if="formViewData.logo_url"
:srcs="getPossibleAttachmentSrc(parseProp(formViewData.logo_url))"
class="nc-form-logo !object-contain object-left max-h-full max-w-full !m-0 rounded-xl"
/>
<div
class="items-center space-x-1 flex-nowrap m-3"
:class="formViewData.logo_url ? 'hidden absolute top-0 left-0 group-hover:flex' : 'flex'"
>
<NcButton
v-if="isEditable"
type="secondary"
size="small"
class="nc-form-upload-logo-btn"
data-testid="nc-form-upload-log-btn"
@click="openUploadImage(false)"
>
<div class="flex gap-2 items-center">
<component :is="iconMap.upload" class="w-4 h-4" />
<span> {{ formViewData.logo_url ? $t('general.replace') : $t('general.upload') }} Logo</span>
</div>
</NcButton>
<NcTooltip v-if="formViewData.logo_url">
<template #title> {{ $t('general.delete') }} {{ $t('general.logo') }} </template>
<NcButton
type="secondary"
size="small"
class="nc-form-delete-logo-btn"
data-testid="nc-form-delete-logo-btn"
@click="
() => {
if (isEditable) {
formViewData!.logo_url = ''
updateView()
}
}
"
>
<div class="flex gap-2 items-center">
<component :is="iconMap.delete" class="w-4 h-4" />
</div>
</NcButton>
</NcTooltip>
</div>
</div>
</div> -->
</div>
<!-- form title -->
<div
class="border-transparent px-4 lg:px-6"
@ -944,7 +1144,7 @@ useEventListener(
</a-card>
</div>
</div>
<div v-if="isEditable" class="h-full flex-1 max-w-[384px] nc-form-left-drawer border-l border-gray-200">
<div class="h-full flex-1 max-w-[384px] nc-form-left-drawer border-l border-gray-200">
<Splitpanes horizontal class="w-full nc-form-right-splitpane">
<Pane min-size="30" size="50" class="nc-form-right-splitpane-item p-4 flex flex-col space-y-4 !min-h-200px">
<div class="flex flex-wrap justify-between items-center gap-2">
@ -958,6 +1158,7 @@ useEventListener(
</div>
<a-dropdown
v-if="isUIAllowed('fieldAdd')"
v-model:visible="showColumnDropdown"
:trigger="['click']"
overlay-class-name="nc-dropdown-form-add-column"
@ -1035,6 +1236,7 @@ useEventListener(
item-key="id"
ghost-class="nc-form-field-ghost"
:style="{ height: 'calc(100% - 64px)' }"
:disabled="isLocked || !isEditable"
@change="onMove($event)"
@start="drag = true"
@end="drag = false"
@ -1080,7 +1282,7 @@ useEventListener(
<span v-if="isRequired(field, field.required)" class="text-red-500 text-sm align-top">&nbsp;*</span>
</div>
</div>
<NcSwitch :checked="!!field.show" :disabled="field.required" />
<NcSwitch :checked="!!field.show" :disabled="field.required || isLocked || !isEditable" />
</div>
</div>
</template>
@ -1097,7 +1299,7 @@ useEventListener(
</div>
</Pane>
<Pane
v-if="isEditable && !isLocked && formViewData"
v-if="formViewData"
min-size="20"
size="50"
class="nc-form-right-splitpane-item !overflow-y-auto nc-form-scrollbar"
@ -1106,7 +1308,7 @@ useEventListener(
<!-- Appearance Settings -->
<div class="text-base font-bold text-gray-900">{{ $t('labels.appearanceSettings') }}</div>
<div>
<div :class="isLocked || !isEditable ? 'pointer-events-none' : ''">
<div class="text-gray-800">{{ $t('labels.backgroundColor') }}</div>
<div class="flex justify-start">
<LazyGeneralColorPicker
@ -1149,8 +1351,11 @@ useEventListener(
size="small"
class="nc-form-hide-branding"
data-testid="nc-form-hide-branding"
:disabled="isLocked || !isEditable"
@change="
(value) => {
if (isLocked || !isEditable) return
(formViewData!.meta as Record<string,any>).hide_branding = value
updateView()
}
@ -1183,6 +1388,7 @@ useEventListener(
hide-details
class="nc-form-after-submit-msg !rounded-lg !px-3 !py-1"
data-testid="nc-form-after-submit-msg"
:disabled="isLocked || !isEditable"
@change="updateView"
/>
</div>
@ -1197,6 +1403,7 @@ useEventListener(
size="small"
class="nc-form-checkbox-submit-another-form"
data-testid="nc-form-checkbox-submit-another-form"
:disabled="isLocked || !isEditable"
@change="updateView"
/>
<span class="ml-4">{{ $t('msg.info.submitAnotherForm') }}</span>
@ -1210,6 +1417,7 @@ useEventListener(
size="small"
class="nc-form-checkbox-show-blank-form"
data-testid="nc-form-checkbox-show-blank-form"
:disabled="isLocked || !isEditable"
@change="updateView"
/>
@ -1223,6 +1431,7 @@ useEventListener(
size="small"
class="nc-form-checkbox-send-email"
data-testid="nc-form-checkbox-send-email"
:disabled="isLocked || !isEditable"
@change="onEmailChange"
/>
@ -1239,6 +1448,21 @@ useEventListener(
</div>
</div>
</template>
<div
v-if="user?.base_roles?.viewer || user?.base_roles?.commenter"
class="absolute inset-0 bg-black/40 z-500 grid place-items-center"
>
<div class="text-center bg-white px-6 py-8 rounded-xl max-w-lg">
<div class="text-2xl text-gray-800 font-bold">
{{ $t('msg.info.yourCurrentRoleIs') }}
'<span class="capitalize"> {{ Object.keys(user.base_roles)?.[0] ?? ProjectRoles.NO_ACCESS }}</span
>'.
</div>
<div class="text-sm text-gray-700 pt-6">
{{ $t('msg.info.pleaseRequestAccessForView', { viewName: 'form view' }) }}
</div>
</div>
</div>
</div>
</template>

1
packages/nc-gui/composables/useViewData.ts

@ -297,6 +297,7 @@ export function useViewData(
fk_column_id: c.id,
fk_view_id: viewMeta.value?.id,
...(fieldById[c.id!] ? fieldById[c.id!] : {}),
meta: { ...parseProp(c.meta), ...parseProp(fieldById[c.id!]?.meta) }, // TODO: discuss with @pranav
order: (fieldById[c.id!] && fieldById[c.id!].order) || order++,
id: fieldById[c.id!] && fieldById[c.id!].id,
}))

10
packages/nc-gui/lang/en.json

@ -192,7 +192,10 @@
"enter": "Enter",
"seconds": "Seconds",
"paste": "Paste",
"restore": "Restore"
"restore": "Restore",
"replace": "Replace",
"banner": "Banner",
"logo": "Logo"
},
"objects": {
"day": "Day",
@ -1294,7 +1297,10 @@
"notAvailableAtTheMoment": "Not available at the moment",
"groupPasteIsNotSupportedOnLinksColumn": "Group paste operation is not supported on Links/LinkToAnotherRecord column",
"groupClearIsNotSupportedOnLinksColumn": "Group clear operation is not supported on Links/LinkToAnotherRecord column",
"upgradeToEnterpriseEdition": "Upgrade to Enterprise Edition {extraInfo}"
"upgradeToEnterpriseEdition": "Upgrade to Enterprise Edition {extraInfo}",
"yourCurrentRoleIs": "Your current role is",
"pleaseRequestAccessForView": "Please request for higher permission from the Admin / Base owner / Workspace owner to get access to this {viewName}"
},
"error": {
"fetchingCalendarData": "Error fetching calendar data",

1
packages/nc-gui/package.json

@ -88,6 +88,7 @@
"unique-names-generator": "^4.7.1",
"v3-infinite-loading": "^1.3.1",
"validator": "^13.11.0",
"vue-advanced-cropper": "^2.8.8",
"vue-barcode-reader": "^1.0.3",
"vue-chartjs": "^5.3.0",
"vue-dompurify-html": "^3.1.2",

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

@ -123,6 +123,7 @@ 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 NcCrop from '~icons/nc-icons/crop'
// keep it for reference
// todo: remove it after all icons are migrated
@ -483,6 +484,7 @@ export const iconMap = {
ncArrowUp: NcArrowUp,
ncArrowDown: NcArrowDown,
phoneCall: NcPhoneCall,
crop: NcCrop,
}
export const getMdiIcon = (type: string): any => {

2
packages/nocodb/src/controllers/form-columns.controller.ts

@ -16,7 +16,7 @@ export class FormColumnsController {
'/api/v1/db/meta/form-columns/:formViewColumnId',
'/api/v2/meta/form-columns/:formViewColumnId',
])
@Acl('columnUpdate')
@Acl('formViewUpdate')
async columnUpdate(
@Param('formViewColumnId') formViewColumnId: string,
@Body() formViewColumnbody: FormColumnUpdateReqType,

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

@ -1554,10 +1554,16 @@ export default class View implements ViewType {
'base_id',
'source_id',
'order',
'width',
'group_by',
'group_by_order',
'group_by_sort',
...(view.type === ViewTypes.FORM
? [
'label',
'help',
'description',
'required',
'enable_scanner',
'meta',
]
: ['width', 'group_by', 'group_by_order', 'group_by_sort']),
]),
fk_view_id: view.id,
base_id: view.base_id,
@ -1760,6 +1766,7 @@ export default class View implements ViewType {
const copyFromView =
view.copy_from_id && (await View.get(view.copy_from_id, ncMeta));
await copyFromView?.getView();
// get base and base id if missing
if (!(view.base_id && view.source_id)) {

4
packages/nocodb/src/schema/swagger-v2.json

@ -13024,7 +13024,7 @@
}
},
"banner_image_url": {
"$ref": "#/components/schemas/TextOrNull",
"$ref": "#/components/schemas/StringOrNull",
"description": "Banner Image URL. Not in use currently."
},
"columns": {
@ -13067,7 +13067,7 @@
"example": "collaborative"
},
"logo_url": {
"$ref": "#/components/schemas/TextOrNull",
"$ref": "#/components/schemas/StringOrNull",
"description": "Logo URL. Not in use currently."
},
"meta": {

4
packages/nocodb/src/schema/swagger.json

@ -18772,7 +18772,7 @@
"type": "object",
"properties": {
"banner_image_url": {
"$ref": "#/components/schemas/TextOrNull",
"$ref": "#/components/schemas/StringOrNull",
"description": "Banner Image URL. Not in use currently."
},
"email": {
@ -18786,7 +18786,7 @@
"type": "string"
},
"logo_url": {
"$ref": "#/components/schemas/TextOrNull",
"$ref": "#/components/schemas/StringOrNull",
"description": "Logo URL. Not in use currently."
},
"meta": {

2
packages/nocodb/src/utils/acl.ts

@ -70,6 +70,7 @@ const permissionScopes = {
'galleryViewGet',
'kanbanViewGet',
'gridViewUpdate',
'formViewUpdate',
'calendarViewGet',
'groupedDataList',
'mmList',
@ -208,6 +209,7 @@ const rolePermissions:
dataInsert: true,
viewColumnUpdate: true,
gridViewUpdate: true,
formViewUpdate: true,
sortCreate: true,
sortUpdate: true,
sortDelete: true,

27
pnpm-lock.yaml

@ -183,6 +183,9 @@ importers:
validator:
specifier: ^13.11.0
version: 13.11.0
vue-advanced-cropper:
specifier: ^2.8.8
version: 2.8.8(vue@3.3.13)
vue-barcode-reader:
specifier: ^1.0.3
version: 1.0.3
@ -12515,6 +12518,10 @@ packages:
resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==}
dev: true
/classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
dev: false
/clean-regexp@1.0.0:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
engines: {node: '>=4'}
@ -13614,6 +13621,10 @@ packages:
resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==}
dev: false
/debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
dev: false
/debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
@ -14042,6 +14053,10 @@ packages:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: true
/easy-bem@1.1.1:
resolution: {integrity: sha512-GJRqdiy2h+EXy6a8E6R+ubmqUM08BK0FWNq41k24fup6045biQ8NXxoXimiwegMQvFFV3t1emADdGNL1TlS61A==}
dev: false
/ecc-jsbn@0.1.2:
resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
dependencies:
@ -26568,6 +26583,18 @@ packages:
resolution: {integrity: sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==}
dev: true
/vue-advanced-cropper@2.8.8(vue@3.3.13):
resolution: {integrity: sha512-yDM7Jb/gnxcs//JdbOogBUoHr1bhCQSto7/ohgETKAe4wvRpmqIkKSppMm1huVQr+GP1YoVlX/fkjKxvYzwwDQ==}
engines: {node: '>=8', npm: '>=5'}
peerDependencies:
vue: 3.3.13
dependencies:
classnames: 2.5.1
debounce: 1.2.1
easy-bem: 1.1.1
vue: 3.3.13
dev: false
/vue-barcode-reader@1.0.3:
resolution: {integrity: sha512-z4mv7+ai/8vECppBTb00tHnyFMMx6W1rAaQe+v214ihoaWK9iGrn8ZZsmgSxf3lwnrtGaibLdkonTtMrGsO+dA==}
dependencies:

Loading…
Cancel
Save