mirror of https://github.com/nocodb/nocodb
Browse Source
* feat(nc-gui): custom validation setup * fix(nc-gui): custom validation table rounded issue * fix: add custom field validation type * fix(nc-gui): updated custom validator * feat(nc-gui): custom validation working state * fix(nc-gui): udpate default warning msg * chore(nc-gui): lint * fix(nc-gui): grayed out errors if input is focused * fix(nc-gui): input ring issue * fix(nc-gui): increase max height of validator select dropdown * fix(nc-gui): validator select dropdown item text color * fix(nc-gui): regex validation condition update * fix(nc-gui): add missing string validation types * fix(nc-gui): remove unwanted code * fix(nc-gui): move custom validation to ee * refacor(nc-gui): form view code * refactor(nc-gui): separate out formviewstore for ce & ee * fix(nc-gui): move all validations to another file * feat(nc-gui): add validation input component * feat(nc-gui): add time, month types * fix(nc-gui): add form field limit validations * fix(nc-gui): add limit link record validation * fix(nc-gui): add phonenumber & url validation type * feat(nc-gui): add email, url & phone number validators * fix(nc-gui): non working phone, email, url validation * chore(nc-giu): lint * feat(nc-gui): add attchment type validation * chore(nc-gui): lint * fix(nc-gui): add form field validation in shared form * fix(nc-gui): add form field validation in shared form oss * fix(nc-gui): oss validation conflict * fix(nc-gui): enter number validation function * fix(nc-gui): add config validators * fix(nc-gui): validation config error handling * fix(nc-gui): placeholder issue * fix(nc-gui): custom validation config error handling * fix(nc-gui): allow negative value validation * fix(nc-gui): add tooltip for required field switch * fix(nc-gui): refactor field validation from builder side * chore(nc-gui): lint * fix(nc-gui): update number validation logic * fix(nc-gui): rating field alignment issue * fix(nc-gui): small changes * fix(nc-gui): required field validation issue * fix(nc-gui): allow click on title to enable field config * feat(nc-gui): business email validation support * fix(nc-gui): add remove image btn in cell itself * fix(nc-gui): small changes * fix(nc-gui): survey form required field validation issue * fix(nc-gui): error field border issue * fix(nc-gui): currency validation input cell prefix issue * fix(nc-gui): remove console * chore(nc-gui): lint * fix: information text * fix(nc-gui): remove contains & doesn't contain option from phone number custom validation * fix(nc-gui): attachment merge conflict * fix(nc-gui): attachment cell expand btn size * fix(nc-gui): PR review changes * fix(nc-gui): lint * fix(nc-gui): updated form config heading text color * fix(nc-gui): small changes --------- Co-authored-by: Raju Udava <86527202+dstala@users.noreply.github.com>pull/8445/head
Ramesh Mane
6 months ago
committed by
GitHub
19 changed files with 935 additions and 565 deletions
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,3 @@
|
||||
<script setup lang="ts"></script> |
||||
|
||||
<template><div></div></template> |
@ -0,0 +1,116 @@
|
||||
<script setup lang="ts"> |
||||
import { UITypes, isSelectTypeCol } from 'nocodb-sdk' |
||||
|
||||
const { formState, activeField, updateColMeta, isRequired } = useFormViewStoreOrThrow() |
||||
|
||||
const { betaFeatureToggleState } = useBetaFeatureToggle() |
||||
|
||||
const updateSelectFieldLayout = (value: boolean) => { |
||||
if (!activeField.value) return |
||||
|
||||
activeField.value.meta.isList = value |
||||
updateColMeta(activeField.value) |
||||
} |
||||
|
||||
const columnSupportsScanning = (elementType: UITypes) => |
||||
betaFeatureToggleState.show && |
||||
[UITypes.SingleLineText, UITypes.Number, UITypes.Email, UITypes.URL, UITypes.LongText].includes(elementType) |
||||
</script> |
||||
|
||||
<template> |
||||
<!-- Field Settings --> |
||||
<template v-if="activeField"> |
||||
<div class="nc-form-field-settings p-4 flex flex-col gap-4 border-b border-gray-200"> |
||||
<div class="text-base font-bold">{{ $t('objects.field') }} {{ $t('activity.validations').toLowerCase() }}</div> |
||||
<div class="flex flex-col gap-6"> |
||||
<div class="flex items-center justify-between gap-3"> |
||||
<div |
||||
class="nc-form-input-required text-gray-800 font-medium" |
||||
@click=" |
||||
() => { |
||||
activeField.required = !activeField.required |
||||
updateColMeta(activeField) |
||||
} |
||||
" |
||||
> |
||||
{{ $t('general.required') }} {{ $t('objects.field').toLowerCase() }} |
||||
</div> |
||||
|
||||
<a-switch |
||||
v-model:checked="activeField.required" |
||||
v-e="['a:form-view:field:mark-required']" |
||||
size="small" |
||||
data-testid="nc-form-input-required" |
||||
@change="updateColMeta(activeField)" |
||||
/> |
||||
</div> |
||||
|
||||
<div v-if="columnSupportsScanning(activeField.uidt)" class="!my-0 nc-form-input-enable-scanner-form-item"> |
||||
<div class="flex items-center justify-between gap-3"> |
||||
<div class="nc-form-input-enable-scanner text-gray-800 font-medium"> |
||||
{{ $t('general.enableScanner') }} |
||||
</div> |
||||
<a-switch |
||||
v-model:checked="activeField.enable_scanner" |
||||
v-e="['a:form-view:field:mark-enable-scanner']" |
||||
data-testid="nc-form-input-enable-scanner" |
||||
size="small" |
||||
@change="updateColMeta(activeField)" |
||||
/> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Limit options --> |
||||
<div v-if="isSelectTypeCol(activeField.uidt)" class="w-full flex items-start justify-between gap-3"> |
||||
<div class="flex-1 max-w-[calc(100%_-_40px)]"> |
||||
<div class="font-medium text-gray-800">{{ $t('labels.limitOptions') }}</div> |
||||
<div class="text-gray-500 mt-2">{{ $t('labels.limitOptionsSubtext') }}.</div> |
||||
<div v-if="activeField.meta.isLimitOption" class="mt-3"> |
||||
<LazySmartsheetFormLimitOptions |
||||
v-model:model-value="activeField.meta.limitOptions" |
||||
:form-field-state="formState[activeField.title] || ''" |
||||
:column="activeField" |
||||
:is-required="isRequired(activeField, activeField.required)" |
||||
@update:model-value="updateColMeta(activeField)" |
||||
@update:form-field-state="(value)=>{ |
||||
formState[activeField!.title] = value |
||||
}" |
||||
></LazySmartsheetFormLimitOptions> |
||||
</div> |
||||
</div> |
||||
|
||||
<a-switch |
||||
v-model:checked="activeField.meta.isLimitOption" |
||||
v-e="['a:form-view:field:limit-options']" |
||||
size="small" |
||||
class="flex-none nc-form-switch-focus" |
||||
@change="updateColMeta(activeField)" |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Field Appearance Settings --> |
||||
<div |
||||
v-if="isSelectTypeCol(activeField.uidt)" |
||||
class="nc-form-field-appearance-settings p-4 flex flex-col gap-4 border-b border-gray-200" |
||||
> |
||||
<div class="text-base font-bold">{{ $t('general.appearance') }}</div> |
||||
<div class="flex flex-col gap-6"> |
||||
<!-- Select type field Options Layout --> |
||||
<div> |
||||
<div class="text-gray-800 font-medium">Options layout</div> |
||||
|
||||
<a-radio-group |
||||
:value="!!activeField.meta.isList" |
||||
class="nc-form-field-layout !mt-3 max-w-[calc(100%_-_40px)]" |
||||
@update:value="updateSelectFieldLayout" |
||||
> |
||||
<a-radio :value="false">{{ $t('general.dropdown') }}</a-radio> |
||||
<a-radio :value="true">{{ $t('general.list') }}</a-radio> |
||||
</a-radio-group> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</template> |
@ -0,0 +1,135 @@
|
||||
import type { Ref } from 'vue' |
||||
import type { RuleObject } from 'ant-design-vue/es/form' |
||||
import type { ColumnType, FormType, TableType, ViewType } from 'nocodb-sdk' |
||||
import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk' |
||||
|
||||
const useForm = Form.useForm |
||||
|
||||
const [useProvideFormViewStore, useFormViewStore] = useInjectionState( |
||||
( |
||||
_meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>, |
||||
viewMeta: Ref<ViewType | undefined> | ComputedRef<(ViewType & { id: string }) | undefined>, |
||||
formViewData: Ref<FormType | undefined>, |
||||
updateFormView: (view: FormType | undefined) => Promise<any>, |
||||
isEditable: boolean, |
||||
) => { |
||||
const { $api } = useNuxtApp() |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const formResetHook = createEventHook<void>() |
||||
|
||||
const formState = ref<Record<string, any>>({}) |
||||
|
||||
const localColumns = ref<Record<string, any>[]>([]) |
||||
|
||||
const activeRow = ref('') |
||||
|
||||
const visibleColumns = computed(() => localColumns.value.filter((f) => f.show).sort((a, b) => a.order - b.order)) |
||||
|
||||
const activeField = computed(() => visibleColumns.value.find((c) => c.id === activeRow.value) || null) |
||||
|
||||
const activeColumn = computed(() => { |
||||
if (_meta.value && activeField.value) { |
||||
if (_meta.value.columnsById && (_meta.value.columnsById as Record<string, ColumnType>)[activeField.value?.fk_column_id]) { |
||||
return (_meta.value.columnsById as Record<string, ColumnType>)[activeField.value.fk_column_id] |
||||
} else if (_meta.value.columns) { |
||||
return _meta.value.columns.find((c) => c.id === activeField.value?.fk_column_id) ?? null |
||||
} |
||||
} |
||||
return null |
||||
}) |
||||
|
||||
const validators = computed(() => { |
||||
const rulesObj: Record<string, RuleObject[]> = {} |
||||
|
||||
if (!visibleColumns.value) return rulesObj |
||||
|
||||
for (const column of visibleColumns.value) { |
||||
let rules: RuleObject[] = [ |
||||
{ |
||||
required: isRequired(column, column.required), |
||||
message: t('msg.error.fieldRequired'), |
||||
...(column.uidt === UITypes.Checkbox && isRequired(column, column.required) ? { type: 'enum', enum: [1, true] } : {}), |
||||
}, |
||||
] |
||||
|
||||
const additionalRules = extractFieldValidator(parseProp(column.meta).validators ?? [], column) |
||||
rules = [...rules, ...additionalRules] |
||||
|
||||
if (rules.length) { |
||||
rulesObj[column.title!] = rules |
||||
} |
||||
} |
||||
|
||||
return rulesObj |
||||
}) |
||||
|
||||
// Form field validation
|
||||
const { validate, validateInfos, clearValidate } = useForm(formState, validators) |
||||
|
||||
const validateActiveField = async (col: ColumnType) => { |
||||
try { |
||||
await validate(col.title) |
||||
} catch {} |
||||
} |
||||
|
||||
const updateView = useDebounceFn( |
||||
() => { |
||||
updateFormView(formViewData.value) |
||||
}, |
||||
300, |
||||
{ maxWait: 2000 }, |
||||
) |
||||
|
||||
const updateColMeta = useDebounceFn(async (col: Record<string, any>) => { |
||||
if (col.id && isEditable) { |
||||
validateActiveField(col) |
||||
|
||||
try { |
||||
await $api.dbView.formColumnUpdate(col.id, col) |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
}, 250) |
||||
|
||||
function isRequired(_columnObj: Record<string, any>, required = false) { |
||||
let columnObj = _columnObj |
||||
if (isLinksOrLTAR(columnObj.uidt) && columnObj.colOptions && columnObj.colOptions.type === RelationTypes.BELONGS_TO) { |
||||
columnObj = (_meta?.value?.columns || []).find( |
||||
(c: Record<string, any>) => c.id === columnObj.colOptions.fk_child_column_id, |
||||
) as Record<string, any> |
||||
} |
||||
|
||||
return required || (columnObj && columnObj.rqd && !columnObj.cdf) |
||||
} |
||||
|
||||
return { |
||||
onReset: formResetHook.on, |
||||
formState, |
||||
localColumns, |
||||
visibleColumns, |
||||
activeRow, |
||||
activeField, |
||||
activeColumn, |
||||
isRequired, |
||||
updateView, |
||||
updateColMeta, |
||||
validate, |
||||
validateInfos, |
||||
clearValidate, |
||||
} |
||||
}, |
||||
'form-view-store', |
||||
) |
||||
|
||||
export { useProvideFormViewStore } |
||||
|
||||
export function useFormViewStoreOrThrow() { |
||||
const sharedFormStore = useFormViewStore() |
||||
|
||||
if (sharedFormStore == null) throw new Error('Please call `useProvideFormViewStore` on the appropriate parent component') |
||||
|
||||
return sharedFormStore |
||||
} |
@ -0,0 +1,109 @@
|
||||
import type { RuleObject } from 'ant-design-vue/es/form' |
||||
import isMobilePhone from 'validator/lib/isMobilePhone' |
||||
import { StringValidationType, UITypes } from 'nocodb-sdk' |
||||
import type { ColumnType, Validation } from 'nocodb-sdk' |
||||
import { getI18n } from '../plugins/a.i18n' |
||||
|
||||
export const formEmailValidator = (val: Validation) => { |
||||
return { |
||||
validator: (_rule: RuleObject, value: any) => { |
||||
return new Promise((resolve, reject) => { |
||||
const { t } = getI18n().global |
||||
|
||||
if (value && !validateEmail(value)) { |
||||
return reject(val.message || t('msg.error.invalidEmail')) |
||||
} |
||||
return resolve(true) |
||||
}) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
export const formPhoneNumberValidator = (val: Validation) => { |
||||
return { |
||||
validator: (_rule: RuleObject, value: any) => { |
||||
return new Promise((resolve, reject) => { |
||||
const { t } = getI18n().global |
||||
|
||||
if (value && !isMobilePhone(value)) { |
||||
return reject(val.message || t('msg.invalidPhoneNumber')) |
||||
} |
||||
return resolve(true) |
||||
}) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
export const formUrlValidator = (val: Validation) => { |
||||
return { |
||||
validator: (_rule: RuleObject, value: any) => { |
||||
return new Promise((resolve, reject) => { |
||||
const { t } = getI18n().global |
||||
|
||||
if (value && !isValidURL(value)) { |
||||
return reject(val.message || t('msg.error.invalidURL')) |
||||
} |
||||
return resolve(true) |
||||
}) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
export const formNumberInputValidator = (cal: ColumnType) => { |
||||
return { |
||||
validator: (_rule: RuleObject, value: any) => { |
||||
return new Promise((resolve, reject) => { |
||||
const { t } = getI18n().global |
||||
|
||||
if (value && value !== '-' && !(cal.uidt === UITypes.Number ? /^-?\d+$/.test(value) : /^-?\d*\.?\d+$/.test(value))) { |
||||
return reject(t('msg.plsEnterANumber')) |
||||
} |
||||
return resolve(true) |
||||
}) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
export const extractFieldValidator = (_validators: Validation[], element: ColumnType) => { |
||||
const rules: RuleObject[] = [] |
||||
|
||||
// Add column default validators
|
||||
if ([UITypes.Number, UITypes.Currency, UITypes.Percent].includes(element.uidt)) { |
||||
rules.push(formNumberInputValidator(element)) |
||||
} |
||||
|
||||
switch (element.uidt) { |
||||
case UITypes.Email: { |
||||
if (parseProp(element.meta).validate) { |
||||
rules.push( |
||||
formEmailValidator({ |
||||
type: StringValidationType.Email, |
||||
}), |
||||
) |
||||
} |
||||
break |
||||
} |
||||
case UITypes.PhoneNumber: { |
||||
if (parseProp(element.meta).validate) { |
||||
rules.push( |
||||
formPhoneNumberValidator({ |
||||
type: StringValidationType.PhoneNumber, |
||||
}), |
||||
) |
||||
} |
||||
break |
||||
} |
||||
case UITypes.URL: { |
||||
if (parseProp(element.meta).validate) { |
||||
rules.push( |
||||
formUrlValidator({ |
||||
type: StringValidationType.Url, |
||||
}), |
||||
) |
||||
} |
||||
break |
||||
} |
||||
} |
||||
|
||||
return rules |
||||
} |
@ -0,0 +1,83 @@
|
||||
export interface Validation { |
||||
type: |
||||
| GenericValidationType |
||||
| StringValidationType |
||||
| NumberValidationType |
||||
| DateValidationType |
||||
| TimeValidationType |
||||
| YearValidationType |
||||
| SelectValidationType |
||||
| AttachmentValidationType |
||||
| null; |
||||
// Additional properties depending on the type of validation
|
||||
[key: string]: any; |
||||
} |
||||
|
||||
export type ValidationType = Exclude< |
||||
Validation['type'], |
||||
null | GenericValidationType |
||||
>; |
||||
|
||||
export enum GenericValidationType { |
||||
Required = 'required', |
||||
} |
||||
|
||||
export enum StringValidationType { |
||||
MinLength = 'minLength', |
||||
MaxLength = 'maxLength', |
||||
StartsWith = 'startsWith', |
||||
EndsWith = 'endsWith', |
||||
Includes = 'includes', |
||||
NotIncludes = 'notIncludes', |
||||
Regex = 'regex', |
||||
Email = 'email', |
||||
BusinessEmail = 'businessEmail', |
||||
PhoneNumber = 'phoneNumber', |
||||
Url = 'url', |
||||
} |
||||
|
||||
export enum NumberValidationType { |
||||
Min = 'min', |
||||
Max = 'max', |
||||
} |
||||
|
||||
export enum DateValidationType { |
||||
MinDate = 'minDate', |
||||
MaxDate = 'maxDate', |
||||
} |
||||
|
||||
export enum TimeValidationType { |
||||
MinTime = 'minTime', |
||||
MaxTime = 'maxTime', |
||||
} |
||||
|
||||
export enum YearValidationType { |
||||
MinYear = 'minYear', |
||||
MaxYear = 'maxYear', |
||||
} |
||||
|
||||
export enum SelectValidationType { |
||||
MinSelected = 'minSelected', |
||||
MaxSelected = 'maxSelected', |
||||
LimitOptions = 'limitOptions', |
||||
} |
||||
|
||||
export enum AttachmentValidationType { |
||||
FileTypes = 'fileTypes', |
||||
FileSize = 'fileSize', |
||||
FileCount = 'fileCount', |
||||
} |
||||
|
||||
export interface RequiredValidation extends Validation { |
||||
type: GenericValidationType.Required; |
||||
} |
||||
|
||||
export const oppositeValidationTypeMap = { |
||||
[StringValidationType.MaxLength]: StringValidationType.MinLength, |
||||
[StringValidationType.NotIncludes]: StringValidationType.Includes, |
||||
[NumberValidationType.Max]: NumberValidationType.Min, |
||||
[YearValidationType.MaxYear]: YearValidationType.MinYear, |
||||
[DateValidationType.MaxDate]: DateValidationType.MinDate, |
||||
[TimeValidationType.MaxTime]: TimeValidationType.MinTime, |
||||
[SelectValidationType.MaxSelected]: SelectValidationType.MinSelected, |
||||
}; |
Loading…
Reference in new issue