@@ -1792,21 +1825,21 @@ useEventListener(
.nc-form-field-ghost {
@apply bg-gray-50;
}
-:deep(.nc-form-input-required + button):focus {
+:deep(.nc-form-input-required + button):focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
}
-:deep(.nc-form-switch-focus):focus {
+:deep(.nc-form-switch-focus):focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
}
.nc-form-field-layout {
@apply !flex !items-center w-full space-x-3;
:deep(.ant-radio-wrapper) {
- @apply border-1 border-gray-200 rounded-lg !py-2 !px-3 basis-full !mr-0 !items-center;
+ @apply border-1 border-gray-200 rounded-lg !py-2 !px-3 basis-full !mr-0 !items-center bg-white;
.ant-radio {
@apply !top-0;
- &:focus-within .ant-radio-inner {
+ .ant-radio-input:focus-visible + .ant-radio-inner {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
}
}
@@ -1814,12 +1847,9 @@ useEventListener(
}
.nc-form-wrapper {
- .ant-switch:focus,
- .ant-switch-checked:focus {
+ .ant-switch:focus-visible,
+ .ant-switch-checked:focus-visible {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
- &:hover {
- box-shadow: none;
- }
}
}
diff --git a/packages/nc-gui/composables/useSharedFormViewStore.ts b/packages/nc-gui/composables/useSharedFormViewStore.ts
index 4be41faebc..ee76defc11 100644
--- a/packages/nc-gui/composables/useSharedFormViewStore.ts
+++ b/packages/nc-gui/composables/useSharedFormViewStore.ts
@@ -1,5 +1,6 @@
import useVuelidate from '@vuelidate/core'
import { helpers, minLength, required } from '@vuelidate/validators'
+import dayjs from 'dayjs'
import type { Ref } from 'vue'
import type {
BoolType,
@@ -7,18 +8,22 @@ import type {
FormColumnType,
FormType,
LinkToAnotherRecordType,
+ SelectOptionsType,
StringOrNullType,
TableType,
} from 'nocodb-sdk'
-import { ErrorMessages, RelationTypes, UITypes, isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk'
+import { ErrorMessages, RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { isString } from '@vue/shared'
import { filterNullOrUndefinedObjectProperties } from '~/helpers/parsers/parserHelpers'
import {
+ PreFilledMode,
SharedViewPasswordInj,
computed,
createEventHook,
extractSdkResponseErrorMsg,
+ isNumericFieldType,
message,
+ parseProp,
provide,
ref,
storeToRefs,
@@ -48,8 +53,15 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const sharedFormView = ref
()
const meta = ref()
- const columns =
- ref<(ColumnType & { required?: BoolType; show?: BoolType; label?: StringOrNullType; enable_scanner?: BoolType })[]>()
+ const columns = ref<
+ (ColumnType & {
+ required?: BoolType
+ show?: BoolType
+ label?: StringOrNullType
+ enable_scanner?: BoolType
+ read_only?: boolean
+ })[]
+ >()
const sharedViewMeta = ref({})
const formResetHook = createEventHook()
@@ -58,15 +70,20 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const { metas, setMeta } = useMetas()
const baseStore = useBase()
- const { base } = storeToRefs(baseStore)
+ const { base, sqlUis } = storeToRefs(baseStore)
const basesStore = useBases()
+
const { basesUser } = storeToRefs(basesStore)
const { t } = useI18n()
+ const route = useRoute()
+
const formState = ref>({})
+ const preFilledformState = ref>({})
+
const { state: additionalState } = useProvideSmartsheetRowStore(
meta as Ref,
ref({
@@ -80,7 +97,11 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
helpers.withMessage(t('msg.error.fieldRequired', { value: fieldName }), required)
const formColumns = computed(() =>
- columns.value?.filter((c) => c.show).filter((col) => !isVirtualCol(col) || isLinksOrLTAR(col.uidt)),
+ columns.value
+ ?.filter((c) => c.show)
+ .filter(
+ (col) => !isSystemColumn(col) && col.uidt !== UITypes.SpecificDBType && (!isVirtualCol(col) || isLinksOrLTAR(col.uidt)),
+ ),
)
const loadSharedView = async () => {
@@ -107,11 +128,25 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
{} as Record,
)
- columns.value = viewMeta.model?.columns?.map((c) => ({
- ...c,
- meta: { ...parseProp(fieldById[c.id].meta), ...parseProp(c.meta) },
- description: fieldById[c.id].description,
- }))
+ columns.value = viewMeta.model?.columns?.map((c) => {
+ if (
+ !isSystemColumn(c) &&
+ !isVirtualCol(c) &&
+ !isAttachment(c) &&
+ c.uidt !== UITypes.SpecificDBType &&
+ c?.title &&
+ c?.cdf &&
+ !/^\w+\(\)|CURRENT_TIMESTAMP$/.test(c.cdf)
+ ) {
+ formState.value[c.title] = c.cdf
+ }
+
+ return {
+ ...c,
+ meta: { ...parseProp(fieldById[c.id].meta), ...parseProp(c.meta) },
+ description: fieldById[c.id].description,
+ }
+ })
const _sharedViewMeta = (viewMeta as any).meta
sharedViewMeta.value = isString(_sharedViewMeta) ? JSON.parse(_sharedViewMeta) : _sharedViewMeta
@@ -136,6 +171,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
if (viewMeta.users) {
basesUser.value.set(viewMeta.base_id, viewMeta.users)
}
+
+ handlePreFillForm()
} catch (e: any) {
if (e.response && e.response.status === 404) {
notFound.value = true
@@ -233,10 +270,203 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
formResetHook.trigger()
additionalState.value = {}
- formState.value = {}
+ formState.value = {
+ ...([PreFilledMode.Locked, PreFilledMode.Hidden].includes(sharedViewMeta.value.preFilledMode)
+ ? preFilledformState.value
+ : {}),
+ }
v$.value?.$reset()
}
+ function handlePreFillForm() {
+ if (Object.keys(route.query || {}).length && sharedViewMeta.value.preFillEnabled) {
+ columns.value = (columns.value || []).map((c) => {
+ const queryParam = route.query[c.title as string] || route.query[encodeURIComponent(c.title as string)]
+ if (
+ !c.title ||
+ !queryParam ||
+ isSystemColumn(c) ||
+ isVirtualCol(c) ||
+ isAttachment(c) ||
+ c.uidt === UITypes.SpecificDBType
+ ) {
+ return c
+ }
+
+ const preFillValue = getPreFillValue(c, decodeURIComponent(queryParam as string).trim())
+ if (preFillValue !== undefined) {
+ // Prefill form state
+ formState.value[c.title] = preFillValue
+ // preFilledformState will be used in clear for to fill the filled data
+ preFilledformState.value[c.title] = preFillValue
+
+ // Update column
+ switch (sharedViewMeta.value.preFilledMode) {
+ case PreFilledMode.Hidden: {
+ c.show = false
+ break
+ }
+ case PreFilledMode.Locked: {
+ c.read_only = true
+ break
+ }
+ }
+ }
+
+ return c
+ })
+ }
+ }
+
+ function getColAbstractType(c: ColumnType) {
+ return (c?.source_id ? sqlUis.value[c?.source_id] : Object.values(sqlUis.value)[0]).getAbstractType(c)
+ }
+
+ function getPreFillValue(c: ColumnType, value: string) {
+ let preFillValue: any
+ switch (c.uidt) {
+ case UITypes.SingleSelect:
+ case UITypes.MultiSelect:
+ case UITypes.User: {
+ const limitOptions = (parseProp(c.meta).isLimitOption ? parseProp(c.meta).limitOptions || [] : []).reduce((ac, op) => {
+ if (op?.id) {
+ ac[op.id] = op
+ }
+ return ac
+ }, {})
+
+ const queryOptions = value.split(',')
+
+ let options: string[] = []
+
+ if ([UITypes.SingleSelect, UITypes.MultiSelect].includes(c.uidt as UITypes)) {
+ options = ((c.colOptions as SelectOptionsType)?.options || [])
+ .filter((op) => {
+ if (
+ op?.id &&
+ op?.title &&
+ queryOptions.includes(op.title) &&
+ (limitOptions[op.id]
+ ? limitOptions[op.id]?.show
+ : parseProp(c.meta).isLimitOption
+ ? !(parseProp(c.meta).limitOptions || []).length
+ : true)
+ ) {
+ return true
+ }
+ return false
+ })
+ .map((op) => op.title as string)
+
+ if (options.length) {
+ preFillValue = c.uidt === UITypes.SingleSelect ? options[0] : options.join(',')
+ }
+ } else {
+ options = (meta.value?.base_id ? basesUser.value.get(meta.value.base_id) || [] : [])
+ .filter((user) => {
+ if (
+ user?.id &&
+ user?.email &&
+ (queryOptions.includes(user.email) || queryOptions.includes(user.id)) &&
+ (limitOptions[user.id]
+ ? limitOptions[user.id]?.show
+ : parseProp(c.meta).isLimitOption
+ ? !(parseProp(c.meta).limitOptions || []).length
+ : true)
+ ) {
+ return true
+ }
+ return false
+ })
+ .map((user) => user.email)
+
+ if (options.length) {
+ preFillValue = !parseProp(c.meta)?.is_multi ? options[0] : options.join(',')
+ }
+ }
+ break
+ }
+ case UITypes.Checkbox: {
+ if (['true', '1'].includes(value.toLowerCase())) {
+ preFillValue = true
+ } else if (['false', '0'].includes(value.toLowerCase())) {
+ preFillValue = false
+ }
+ break
+ }
+ case UITypes.Rating: {
+ if (!isNaN(Number(value))) {
+ preFillValue = Math.min(Math.max(Number(value), 0), parseProp(c.meta).max ?? 5)
+ }
+ break
+ }
+ case UITypes.URL: {
+ if (parseProp(c.meta).validate) {
+ if (isValidURL(value)) {
+ preFillValue = value
+ }
+ } else {
+ preFillValue = value
+ }
+ break
+ }
+ case UITypes.Year: {
+ if (/^\d+$/.test(value)) {
+ preFillValue = Number(value)
+ }
+ break
+ }
+ case UITypes.Date: {
+ const parsedDate = dayjs(value)
+ if ((parsedDate.isValid() && parsedDate.toISOString() === value) || dayjs(value, 'YYYY-MM-DD').isValid()) {
+ preFillValue = dayjs(value).format('YYYY-MM-DD')
+ }
+ break
+ }
+ case UITypes.DateTime: {
+ const parsedDateTime = dayjs(value)
+
+ if (
+ (parsedDateTime.isValid() && parsedDateTime.toISOString() === value) ||
+ dayjs(value, 'YYYY-MM-DD HH:mm:ss').isValid()
+ ) {
+ preFillValue = dayjs(value).utc().format('YYYY-MM-DD HH:mm:ssZ')
+ }
+ break
+ }
+ case UITypes.Time: {
+ let parsedTime = dayjs(value)
+
+ if (!parsedTime.isValid()) {
+ parsedTime = dayjs(value, 'HH:mm:ss')
+ }
+ if (!parsedTime.isValid()) {
+ parsedTime = dayjs(`1999-01-01 ${value}`)
+ }
+ if (parsedTime.isValid()) {
+ preFillValue = parsedTime.format(baseStore.isMysql(c.source_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ')
+ }
+ break
+ }
+ case UITypes.LinkToAnotherRecord:
+ case UITypes.Links: {
+ // Todo: create an api which will fetch query params records and then autofill records
+ break
+ }
+
+ default: {
+ if (isNumericFieldType(c, getColAbstractType(c))) {
+ if (!isNaN(Number(value))) {
+ preFillValue = Number(value)
+ }
+ } else {
+ preFillValue = value
+ }
+ }
+ }
+ return preFillValue
+ }
+
/** reset form if show_blank_form is true */
watch(submitted, (nextVal) => {
if (nextVal && sharedFormView.value?.show_blank_form) {
diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts
index 853f28a53e..8ce1235199 100644
--- a/packages/nc-gui/composables/useViewData.ts
+++ b/packages/nc-gui/composables/useViewData.ts
@@ -297,7 +297,7 @@ export function useViewData(
fk_column_id: c.id,
fk_view_id: viewMeta.value?.id,
...(fieldById[c.id!] ? fieldById[c.id!] : {}),
- meta: { ...parseProp(fieldById[c.id!]?.meta), ...parseProp(c.meta) }, // TODO: discuss with @pranav
+ meta: { ...parseProp(fieldById[c.id!]?.meta), ...parseProp(c.meta) },
order: (fieldById[c.id!] && fieldById[c.id!].order) || order++,
id: fieldById[c.id!] && fieldById[c.id!].id,
}))
diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json
index f7c5751ed5..f66b113cf4 100644
--- a/packages/nc-gui/lang/en.json
+++ b/packages/nc-gui/lang/en.json
@@ -926,7 +926,15 @@
"startCommenting": "Start commenting!",
"clearForm": "Clear Form",
"addFieldFromFormView": "Add Field",
- "selectAllFields": "Select all fields"
+ "selectAllFields": "Select all fields",
+ "preFilledFields": {
+ "title": "Enable Pre-fill",
+ "default": "Default",
+ "locked": "Lock pre-filled fields as read-only",
+ "hidden": "Hide pre-filled fields",
+ "lockedFieldTooltip": "Pre-filled value"
+ },
+ "getPreFilledLink": "Get Pre-filled Link"
},
"tooltip": {
"reachedSourceLimit": "Limited to only one data source for the moment",
@@ -956,7 +964,9 @@
"clearMetadata": "Clear all metadata from meta tables.",
"clientKey": "Select .key file",
"clientCert": "Select .cert file",
- "clientCA": "Select CA file"
+ "clientCA": "Select CA file",
+ "preFillFormInfo": "Generate share form URL with pre-filled field data. To get a pre-filled link, make sure you’ve filled the necessary fields in the form view builder.",
+ "surveyFormInfo": "Form mode with one field per page"
},
"placeholder": {
"selectSlackChannels": "Select Slack channels",
diff --git a/packages/nc-gui/lib/enums.ts b/packages/nc-gui/lib/enums.ts
index d490484794..f386e6a9df 100644
--- a/packages/nc-gui/lib/enums.ts
+++ b/packages/nc-gui/lib/enums.ts
@@ -136,3 +136,9 @@ export enum ImportSource {
URL = 'url',
STRING = 'string',
}
+
+export enum PreFilledMode {
+ Default = 'default',
+ Hidden = 'hidden',
+ Locked = 'locked',
+}
diff --git a/packages/nc-gui/lib/types.ts b/packages/nc-gui/lib/types.ts
index 7109e5e316..24c1d997f5 100644
--- a/packages/nc-gui/lib/types.ts
+++ b/packages/nc-gui/lib/types.ts
@@ -2,7 +2,7 @@ import type { BaseType, ColumnType, FilterType, MetaType, PaginatedType, Roles,
import type { I18n } from 'vue-i18n'
import type { Theme as AntTheme } from 'ant-design-vue/es/config-provider'
import type { UploadFile } from 'ant-design-vue'
-import type { ImportSource, ImportType, TabType } from './enums'
+import type { ImportSource, ImportType, PreFilledMode, TabType } from './enums'
import type { rolePermissions } from './acl'
interface User {
@@ -115,6 +115,8 @@ interface SharedViewMeta extends Record {
theme?: Partial
allowCSVDownload?: boolean
rtl?: boolean
+ preFillEnabled?: boolean
+ preFilledMode?: PreFilledMode
}
interface SharedView {
diff --git a/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue b/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
index fb2e45ff2c..6657d3b99f 100644
--- a/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
+++ b/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
@@ -87,6 +87,15 @@ p {
&:not(.layout-list) {
@apply rounded-lg border-solid border-1 border-gray-200 focus-within:border-brand-500 overflow-hidden;
+ &.readonly {
+ @apply bg-gray-50 cursor-not-allowed;
+
+ input,
+ textarea {
+ @apply !bg-transparent;
+ }
+ }
+
& > div {
@apply !bg-transparent;
}
@@ -107,11 +116,17 @@ p {
}
}
+ &:not(.readonly) {
+ input,
+ textarea,
+ &.nc-virtual-cell {
+ @apply bg-white !disabled:bg-transparent;
+ }
+ }
+
input,
textarea,
&.nc-virtual-cell {
- @apply bg-white dark:(bg-slate-500 text-white);
-
.ant-btn {
@apply dark:(bg-slate-300);
}
@@ -120,9 +135,7 @@ p {
@apply dark:(bg-slate-700 text-white);
}
}
- &:not(.layout-list) > div {
- @apply bg-white dark:(bg-slate-500 text-white);
- }
+
&.layout-list > div {
.ant-btn {
@apply dark:(bg-slate-300);
@@ -138,6 +151,9 @@ p {
& > div {
@apply w-full;
}
+ &.readonly > div {
+ @apply px-3 py-1;
+ }
textarea {
@apply px-3;
diff --git a/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue b/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue
index ac9aa25d17..4e8d4083ad 100644
--- a/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue
+++ b/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue
@@ -180,39 +180,44 @@ const onDecode = async (scannedCodeValue: string) => {
-
-
+
+ {{ $t('activity.preFilledFields.lockedFieldTooltip') }}
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue b/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue
index 0f104e3e9b..0724623a40 100644
--- a/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue
+++ b/packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue
@@ -280,37 +280,45 @@ onMounted(() => {
-
-
-
-
-
-
-
- {{ error.$message }}
+
+ {{ $t('activity.preFilledFields.lockedFieldTooltip') }}
+
+
+
+
+
+
+
+ {{ error.$message }}
+
+
+
+ {{ $t('general.shift') }} + {{ $t('general.enter') }}
+ {{ $t('msg.makeLineBreak') }}
+
-
-
- {{ $t('general.shift') }} + {{ $t('general.enter') }}
- {{ $t('msg.makeLineBreak') }}
-
-
-
+
+
diff --git a/packages/nc-gui/store/views.ts b/packages/nc-gui/store/views.ts
index f32ca96271..64e426f48f 100644
--- a/packages/nc-gui/store/views.ts
+++ b/packages/nc-gui/store/views.ts
@@ -116,6 +116,8 @@ export const useViewsStore = defineStore('viewsStore', () => {
// Used for Grid View Pagination
const isPaginationLoading = ref(true)
+ const preFillFormSearchParams = ref('')
+
const loadViews = async ({
tableId,
ignoreLoading,
@@ -322,6 +324,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
activeSorts,
activeNestedFilters,
isActiveViewLocked,
+ preFillFormSearchParams,
}
})
diff --git a/packages/nc-gui/utils/cell.ts b/packages/nc-gui/utils/cell.ts
index 4df66dab79..66e73c81a5 100644
--- a/packages/nc-gui/utils/cell.ts
+++ b/packages/nc-gui/utils/cell.ts
@@ -82,3 +82,14 @@ export const renderValue = (result?: any) => {
return dayjs(d).isValid() ? dayjs(d).format('YYYY-MM-DD HH:mm') : d
})
}
+
+export const isNumericFieldType = (column: ColumnType, abstractType: any) => {
+ return (
+ isInt(column, abstractType) ||
+ isFloat(column, abstractType) ||
+ isDecimal(column) ||
+ isCurrency(column) ||
+ isPercent(column) ||
+ isDuration(column)
+ )
+}
diff --git a/packages/nc-gui/utils/dataUtils.ts b/packages/nc-gui/utils/dataUtils.ts
index d5eb9a029e..939ce369f7 100644
--- a/packages/nc-gui/utils/dataUtils.ts
+++ b/packages/nc-gui/utils/dataUtils.ts
@@ -1,4 +1,4 @@
-import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
+import { RelationTypes, UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { isColumnRequiredAndNull } from './columnUtils'
import type { Row } from '~/lib'
@@ -102,9 +102,9 @@ export const rowDefaultData = (columns: ColumnType[] = []) => {
if (
!isSystemColumn(col) &&
!isVirtualCol(col) &&
- !isLinksOrLTAR({ uidt: col.uidt! }) &&
![UITypes.Rollup, UITypes.Lookup, UITypes.Formula, UITypes.Barcode, UITypes.QrCode].includes(col.uidt) &&
- col?.cdf
+ col?.cdf &&
+ !/^\w+\(\)|CURRENT_TIMESTAMP$/.test(col.cdf)
) {
const defaultValue = col.cdf
acc[col.title!] = typeof defaultValue === 'string' ? defaultValue.replace(/^'/, '').replace(/'$/, '') : defaultValue