2
0
mirror of https://github.com/frappe/books.git synced 2025-01-22 14:48:25 +00:00

fix: get system settings to function and display

- improve autocomplete display and func
This commit is contained in:
18alantom 2022-04-27 14:07:06 +05:30
parent 024687c1b9
commit 371cda82b3
15 changed files with 245 additions and 118 deletions

View File

@ -44,7 +44,7 @@ import {
TreeViewSettings,
ValidationMap,
} from './types';
import { validateSelect } from './validationFunction';
import { validateOptions, validateRequired } from './validationFunction';
export class Doc extends Observable<DocValue | Doc[]> {
name?: string;
@ -308,11 +308,15 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
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;
}

View File

@ -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<string, FilterFunction>;
export type EmptyMessageFunction = (doc: Doc) => string;
export type EmptyMessageMap = Record<string, EmptyMessageFunction>;
export type ListFunction = (doc?: Doc) => string[];
export type ListFunction = (doc?: Doc) => string[] | SelectOption[];
export type ListsMap = Record<string, ListFunction | undefined>;
export interface Action {

View File

@ -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`);
}
}

View File

@ -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)];
},
};
}

View File

@ -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);
}

View File

@ -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
},
{

View File

@ -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": [

View File

@ -42,6 +42,7 @@ export interface OptionField extends BaseField {
| FieldTypeEnum.AutoComplete
| FieldTypeEnum.Color;
options: SelectOption[];
allowCustom?: boolean;
}
export interface TargetField extends BaseField {

View File

@ -11,30 +11,57 @@
<div class="text-gray-600 text-sm mb-1" v-if="showLabel">
{{ df.label }}
</div>
<input
ref="input"
:class="inputClasses"
type="text"
:value="linkValue"
:placeholder="inputPlaceholder"
:readonly="isReadOnly"
@focus="(e) => onFocus(e, toggleDropdown)"
@blur="(e) => onBlur(e.target.value)"
@input="onInput"
@keydown.up="highlightItemUp"
@keydown.down="highlightItemDown"
@keydown.enter="selectHighlightedItem"
@keydown.tab="toggleDropdown(false)"
@keydown.esc="toggleDropdown(false)"
/>
<div
class="
flex
items-center
justify-between
bg-white
focus-within:bg-gray-200
pr-2
rounded
"
>
<input
ref="input"
:class="inputClasses"
type="text"
:value="linkValue"
:placeholder="inputPlaceholder"
:readonly="isReadOnly"
@focus="(e) => onFocus(e, toggleDropdown)"
@blur="(e) => onBlur(e.target.value)"
@input="onInput"
@keydown.up="highlightItemUp"
@keydown.down="highlightItemDown"
@keydown.enter="selectHighlightedItem"
@keydown.tab="toggleDropdown(false)"
@keydown.esc="toggleDropdown(false)"
/>
<svg
class="w-3 h-3"
style="background: inherit; margin-right: -3px"
viewBox="0 0 5 10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 2.636L2.636 1l1.637 1.636M1 7.364L2.636 9l1.637-1.636"
stroke="#404040"
fill="none"
fill-rule="evenodd"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
</template>
</Dropdown>
</template>
<script>
import { getOptionList } from 'fyo/utils';
import Dropdown from 'src/components/Dropdown.vue';
import { fuzzyMatch } from 'src/utils';
import { getOptionList } from 'src/utils/doc';
import Base from './Base.vue';
export default {
@ -57,47 +84,81 @@ export default {
value: {
immediate: true,
handler(newValue) {
this.linkValue = newValue;
this.linkValue = this.getLabel(newValue);
},
},
},
inject: {
doc: { default: null },
},
computed: {},
mounted() {
this.linkValue = this.getLabel(this.linkValue || this.value);
},
computed: {
options() {
if (!this.df) {
return [];
}
return getOptionList(this.df, this.doc);
},
},
methods: {
async updateSuggestions(e) {
let keyword;
if (e) {
keyword = e.target.value;
getLabel(value) {
const oldValue = this.linkValue;
let option = this.options.find((o) => o.value === value);
if (option === undefined) {
option = this.options.find((o) => o.label === value);
}
return option?.label ?? oldValue;
},
getValue(label) {
let option = this.options.find((o) => o.label === label);
if (option === undefined) {
option = this.options.find((o) => o.value === label);
}
return option?.value ?? label;
},
async updateSuggestions(keyword) {
if (typeof keyword === 'string') {
this.linkValue = keyword;
}
this.isLoading = true;
let suggestions = await this.getSuggestions(keyword);
this.suggestions = suggestions.map((d) => {
if (!d.action) {
d.action = () => this.setSuggestion(d);
}
return d;
});
const suggestions = await this.getSuggestions(keyword);
this.suggestions = this.setSetSuggestionAction(suggestions);
this.isLoading = false;
},
async getSuggestions(keyword = '') {
const options = getOptionList(this.df, this.doc);
keyword = keyword.toLowerCase();
if (!keyword) {
return options;
setSetSuggestionAction(suggestions) {
for (const option of suggestions) {
if (option.action) {
continue;
}
option.action = () => {
this.setSuggestion(option);
};
}
return options
.map((item) => ({ ...fuzzyMatch(keyword, item.value), item }))
return suggestions;
},
async getSuggestions(keyword = '') {
keyword = keyword.toLowerCase();
if (!keyword) {
return this.options;
}
return this.options
.map((item) => ({ ...fuzzyMatch(keyword, item.label), item }))
.filter(({ isMatch }) => isMatch)
.sort((a, b) => a.distance - b.distance)
.map(({ item }) => item);
},
setSuggestion(suggestion) {
this.linkValue = suggestion.value;
this.linkValue = suggestion.label;
this.triggerChange(suggestion.value);
this.toggleDropdown(false);
},
@ -107,34 +168,28 @@ export default {
this.updateSuggestions();
this.$emit('focus', e);
},
async onBlur(value) {
if (value === '' || value == null) {
async onBlur(label) {
if (!label) {
this.triggerChange('');
return;
}
if (value && this.suggestions.length === 0) {
this.triggerChange(value);
if (label && this.suggestions.length === 0) {
this.triggerChange(label);
return;
}
if (
value &&
!this.suggestions.map(({ value }) => value).includes(value)
label &&
!this.suggestions.map(({ label }) => label).includes(label)
) {
const suggestion = await this.getSuggestions(value);
if (suggestion.length < 2) {
this.linkValue = '';
this.triggerChange('');
} else {
this.setSuggestion(suggestion[0]);
}
const suggestions = await this.getSuggestions(label);
this.setSuggestion(suggestions[0]);
}
},
onInput(e) {
this.toggleDropdown(true);
this.updateSuggestions(e);
this.updateSuggestions(e.target.value);
},
},
};

View File

@ -2,7 +2,7 @@
<FormControl
:df="languageDf"
:value="value"
@change="(v) => setLanguageMap(v, dontReload)"
@change="onChange"
:input-class="'focus:outline-none rounded ' + inputClass"
/>
</template>
@ -28,6 +28,15 @@ export default {
},
},
components: { FormControl },
methods: {
onChange(value) {
if (languageCodeMap[value] === undefined) {
return;
}
setLanguageMap(value, this.dontReload);
},
},
computed: {
value() {
return fyo.config.get('language') ?? DEFAULT_LANGUAGE;

View File

@ -9,6 +9,11 @@ export default {
name: 'Link',
extends: AutoComplete,
emits: ['new-doc'],
mounted() {
if (this.value) {
this.linkValue = this.value;
}
},
methods: {
async getSuggestions(keyword = '') {
const schemaName = this.df.target;

View File

@ -177,11 +177,11 @@ export default {
}
const oldValue = this.doc.get(df.fieldname);
this.errors[df.fieldname] = null;
if (oldValue === value) {
return;
}
this.errors[df.fieldname] = null;
if (this.emitChange) {
this.$emit('change', df, value, oldValue);
}
@ -251,7 +251,7 @@ export default {
},
async stopInlineEditing() {
if (this.inlineEditDoc?.dirty) {
await this.inlineEditDoc.load()
await this.inlineEditDoc.load();
}
this.inlineEditDoc = null;
this.inlineEditField = null;

View File

@ -60,6 +60,7 @@ import WindowControls from 'src/components/WindowControls.vue';
import { showToast } from 'src/utils/ui';
import { IPC_MESSAGES } from 'utils/messages';
import { h, markRaw } from 'vue';
import TabGeneral from './TabGeneral.vue';
import TabInvoice from './TabInvoice.vue';
import TabSystem from './TabSystem.vue';
@ -88,15 +89,13 @@ export default {
key: 'General',
label: t`General`,
icon: 'general',
// component: markRaw(TabGeneral),
component: {template: `<h1>General</h1>`}
component: markRaw(TabGeneral),
},
{
key: 'System',
label: t`System`,
icon: 'system',
component: markRaw(TabSystem),
component: {template: `<h1>System</h1>`}
},
],
};

View File

@ -40,12 +40,14 @@
<script>
import { ConfigKeys } from 'fyo/core/types';
import { getTelemetryOptions } from 'fyo/telemetry/helpers';
import { TelemetrySetting } from 'fyo/telemetry/types';
import { NounEnum, TelemetrySetting, Verb } from 'fyo/telemetry/types';
import { ModelNameEnum } from 'models/types';
import FormControl from 'src/components/Controls/FormControl.vue';
import LanguageSelector from 'src/components/Controls/LanguageSelector.vue';
import TwoColumnForm from 'src/components/TwoColumnForm';
import { fyo } from 'src/initFyo';
import { checkForUpdates } from 'src/utils';
import { checkForUpdates } from 'src/utils/ipcCalls';
import { getCountryInfo } from 'utils/misc';
export default {
name: 'TabSystem',
@ -65,6 +67,7 @@ export default {
this.doc = fyo.singles.SystemSettings;
this.companyName = fyo.singles.AccountingSettings.companyName;
this.telemetry = fyo.config.get(ConfigKeys.Telemetry);
window.gci = getCountryInfo
},
computed: {
df() {
@ -81,8 +84,9 @@ export default {
};
},
fields() {
let meta = fyo.getMeta('SystemSettings');
return meta.getQuickEditFields();
return fyo.schemaMap.SystemSettings.quickEditFields.map((f) =>
fyo.getField(ModelNameEnum.SystemSettings, f)
);
},
},
methods: {

View File

@ -1,6 +1,5 @@
import { Doc } from 'fyo/model/doc';
import { Field, OptionField, SelectOption } from 'schemas/types';
import { fyo } from 'src/initFyo';
import { Field } from 'schemas/types';
export function evaluateReadOnly(field: Field, doc: Doc) {
if (field.readOnly !== undefined) {
@ -27,36 +26,3 @@ export function evaluateHidden(field: Field, doc: Doc) {
return false;
}
export function getOptionList(field: Field, doc: Doc): SelectOption[] {
const list = _getOptionList(field, doc);
return list.map((option) => {
if (typeof option === 'string') {
return {
label: option,
value: option,
};
}
return option;
});
}
function _getOptionList(field: Field, doc: Doc) {
const options = (field as OptionField).options;
if (options && options.length > 0) {
return (field as OptionField).options;
}
const Model = fyo.models[doc.schemaName];
if (Model === undefined) {
return [];
}
const getList = Model.lists[field.fieldname];
if (getList === undefined) {
return [];
}
return getList(doc);
}