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:
parent
024687c1b9
commit
371cda82b3
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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`);
|
||||
}
|
||||
}
|
||||
|
@ -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)];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
},
|
||||
{
|
||||
|
@ -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": [
|
||||
|
@ -42,6 +42,7 @@ export interface OptionField extends BaseField {
|
||||
| FieldTypeEnum.AutoComplete
|
||||
| FieldTypeEnum.Color;
|
||||
options: SelectOption[];
|
||||
allowCustom?: boolean;
|
||||
}
|
||||
|
||||
export interface TargetField extends BaseField {
|
||||
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>`}
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -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: {
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user