diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index aca8f76d..a1e4f8e2 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -44,7 +44,7 @@ import { TreeViewSettings, ValidationMap, } from './types'; -import { validateSelect } from './validationFunction'; +import { validateOptions, validateRequired } from './validationFunction'; export class Doc extends Observable { name?: string; @@ -308,11 +308,15 @@ export class Doc extends Observable { } async _validateField(field: Field, value: DocValue) { - if (field.fieldtype == 'Select') { - validateSelect(field as OptionField, value as string); + if ( + field.fieldtype === FieldTypeEnum.Select || + field.fieldtype === FieldTypeEnum.AutoComplete + ) { + validateOptions(field as OptionField, value as string, this); } - if (value === null || value === undefined) { + validateRequired(field, value, this); + if (getIsNullOrUndef(value)) { return; } diff --git a/fyo/model/types.ts b/fyo/model/types.ts index 89d92f49..1b2346df 100644 --- a/fyo/model/types.ts +++ b/fyo/model/types.ts @@ -1,6 +1,6 @@ import { DocValue, DocValueMap } from 'fyo/core/types'; import SystemSettings from 'fyo/models/SystemSettings'; -import { FieldType } from 'schemas/types'; +import { FieldType, SelectOption } from 'schemas/types'; import { QueryFilter } from 'utils/db/types'; import { Router } from 'vue-router'; import { Doc } from './doc'; @@ -55,7 +55,7 @@ export type FiltersMap = Record; export type EmptyMessageFunction = (doc: Doc) => string; export type EmptyMessageMap = Record; -export type ListFunction = (doc?: Doc) => string[]; +export type ListFunction = (doc?: Doc) => string[] | SelectOption[]; export type ListsMap = Record; export interface Action { diff --git a/fyo/model/validationFunction.ts b/fyo/model/validationFunction.ts index 9c51726c..8471dd2b 100644 --- a/fyo/model/validationFunction.ts +++ b/fyo/model/validationFunction.ts @@ -1,7 +1,10 @@ import { DocValue } from 'fyo/core/types'; +import { getOptionList } from 'fyo/utils'; import { ValidationError, ValueError } from 'fyo/utils/errors'; import { t } from 'fyo/utils/translation'; -import { OptionField } from 'schemas/types'; +import { Field, OptionField } from 'schemas/types'; +import { getIsNullOrUndef } from 'utils'; +import { Doc } from './doc'; export function validateEmail(value: DocValue) { const isValid = /(.+)@(.+){2,}\.(.+){2,}/.test(value as string); @@ -17,9 +20,9 @@ export function validatePhoneNumber(value: DocValue) { } } -export function validateSelect(field: OptionField, value: string) { - const options = field.options; - if (!options) { +export function validateOptions(field: OptionField, value: string, doc: Doc) { + const options = getOptionList(field, doc); + if (!options.length) { return; } @@ -29,12 +32,25 @@ export function validateSelect(field: OptionField, value: string) { const validValues = options.map((o) => o.value); - if (validValues.includes(value)) { + if (validValues.includes(value) || field.allowCustom) { return; } const labels = options.map((o) => o.label).join(', '); - throw new ValueError( - t`Invalid value ${value} for ${field.label}. Must be one of ${labels}` - ); + throw new ValueError(t`Invalid value ${value} for ${field.label}`); +} + +export function validateRequired(field: Field, value: DocValue, doc: Doc) { + if (!getIsNullOrUndef(value)) { + return; + } + + if (field.required) { + throw new ValidationError(`${field.label} is required`); + } + + const requiredFunction = doc.required[field.fieldname]; + if (requiredFunction && requiredFunction()) { + throw new ValidationError(`${field.label} is required`); + } } diff --git a/fyo/models/SystemSettings.ts b/fyo/models/SystemSettings.ts index ad4a5b10..7eb5c327 100644 --- a/fyo/models/SystemSettings.ts +++ b/fyo/models/SystemSettings.ts @@ -1,8 +1,10 @@ import { DocValue } from 'fyo/core/types'; import { Doc } from 'fyo/model/doc'; -import { ValidationMap } from 'fyo/model/types'; +import { ListsMap, ValidationMap } from 'fyo/model/types'; import { ValidationError } from 'fyo/utils/errors'; import { t } from 'fyo/utils/translation'; +import { SelectOption } from 'schemas/types'; +import { getCountryInfo } from 'utils/misc'; export default class SystemSettings extends Doc { validations: ValidationMap = { @@ -16,4 +18,26 @@ export default class SystemSettings extends Doc { ); }, }; + + static lists: ListsMap = { + locale() { + const countryInfo = getCountryInfo(); + return Object.keys(countryInfo) + .filter((c) => !!countryInfo[c]?.locale) + .map( + (c) => + ({ + value: countryInfo[c]?.locale, + label: `${c} (${countryInfo[c]?.locale})`, + } as SelectOption) + ); + }, + currency() { + const countryInfo = getCountryInfo(); + const currencies = Object.values(countryInfo) + .map((ci) => ci?.currency as string) + .filter(Boolean); + return [...new Set(currencies)]; + }, + }; } diff --git a/fyo/utils/index.ts b/fyo/utils/index.ts index 9ba16f4a..f59170df 100644 --- a/fyo/utils/index.ts +++ b/fyo/utils/index.ts @@ -2,6 +2,7 @@ import { Fyo } from 'fyo'; import { Doc } from 'fyo/model/doc'; import { Action } from 'fyo/model/types'; import { pesa } from 'pesa'; +import { Field, OptionField, SelectOption } from 'schemas/types'; export function slug(str: string) { return str @@ -73,3 +74,43 @@ export async function getSingleValue( return singleValue.value; } + +export function getOptionList( + field: Field, + doc: Doc | undefined +): SelectOption[] { + const list = getRawOptionList(field, doc); + return list.map((option) => { + if (typeof option === 'string') { + return { + label: option, + value: option, + }; + } + + return option; + }); +} + +function getRawOptionList(field: Field, doc: Doc | undefined) { + const options = (field as OptionField).options; + if (options && options.length > 0) { + return (field as OptionField).options; + } + + if (doc === undefined) { + return []; + } + + const Model = doc.fyo.models[doc.schemaName]; + if (Model === undefined) { + return []; + } + + const getList = Model.lists[field.fieldname]; + if (getList === undefined) { + return []; + } + + return getList(doc); +} diff --git a/schemas/app/AccountingSettings.json b/schemas/app/AccountingSettings.json index d215d6b7..a3ce4dce 100644 --- a/schemas/app/AccountingSettings.json +++ b/schemas/app/AccountingSettings.json @@ -9,6 +9,7 @@ "label": "Company Name", "fieldname": "companyName", "fieldtype": "Data", + "readOnly": true, "required": true }, { @@ -49,6 +50,7 @@ "fieldname": "bankName", "label": "Bank Name", "fieldtype": "Data", + "readOnly": true, "required": true }, { diff --git a/schemas/core/SystemSettings.json b/schemas/core/SystemSettings.json index 317928de..c9683496 100644 --- a/schemas/core/SystemSettings.json +++ b/schemas/core/SystemSettings.json @@ -7,7 +7,7 @@ { "fieldname": "dateFormat", "label": "Date Format", - "fieldtype": "Select", + "fieldtype": "AutoComplete", "options": [ { "label": "23/03/2022", @@ -40,13 +40,15 @@ ], "default": "MMM d, y", "required": true, + "allowCustom": true, "description": "Sets the app-wide date display format." }, { "fieldname": "locale", "label": "Locale", - "fieldtype": "Data", + "fieldtype": "AutoComplete", "default": "en-IN", + "required": true, "description": "Set the local code. This is used for number formatting." }, { @@ -84,9 +86,8 @@ { "fieldname": "currency", "label": "Currency", - "fieldtype": "Data", - "readOnly": true, - "required": false + "fieldtype": "AutoComplete", + "required": true } ], "quickEditFields": [ diff --git a/schemas/types.ts b/schemas/types.ts index 2d59ccd1..5c0b019a 100644 --- a/schemas/types.ts +++ b/schemas/types.ts @@ -42,6 +42,7 @@ export interface OptionField extends BaseField { | FieldTypeEnum.AutoComplete | FieldTypeEnum.Color; options: SelectOption[]; + allowCustom?: boolean; } export interface TargetField extends BaseField { diff --git a/src/components/Controls/AutoComplete.vue b/src/components/Controls/AutoComplete.vue index 13879be4..09563b3b 100644 --- a/src/components/Controls/AutoComplete.vue +++ b/src/components/Controls/AutoComplete.vue @@ -11,30 +11,57 @@
{{ df.label }}
- +
+ + + + +