2
0
mirror of https://github.com/frappe/books.git synced 2025-02-08 23:18:31 +00:00

refactor: cleanup provide-injects

- type a bunch of form element .vue files
This commit is contained in:
18alantom 2023-03-31 14:00:37 +05:30 committed by Alan
parent 368714a84a
commit b618340cec
19 changed files with 226 additions and 160 deletions

View File

@ -68,7 +68,7 @@ export interface Action {
group?: string; group?: string;
type?: 'primary' | 'secondary'; type?: 'primary' | 'secondary';
component?: { component?: {
template?: string; template: string;
}; };
} }

View File

@ -96,9 +96,6 @@ export default {
}, },
}, },
}, },
inject: {
doc: { default: null },
},
mounted() { mounted() {
const value = this.linkValue || this.value; const value = this.linkValue || this.value;
this.setLinkValue(this.getLinkValue(value)); this.setLinkValue(this.getLinkValue(value));

View File

@ -14,10 +14,10 @@
:placeholder="inputPlaceholder" :placeholder="inputPlaceholder"
:readonly="isReadOnly" :readonly="isReadOnly"
:step="step" :step="step"
:max="df.maxvalue" :max="isNumeric(df) ? df.maxvalue : undefined"
:min="df.minvalue" :min="isNumeric(df) ? df.minvalue : undefined"
:style="containerStyles" :style="containerStyles"
@blur="(e) => !isReadOnly && triggerChange(e.target.value)" @blur="onBlur"
@focus="(e) => !isReadOnly && $emit('focus', e)" @focus="(e) => !isReadOnly && $emit('focus', e)"
@input="(e) => !isReadOnly && $emit('input', e)" @input="(e) => !isReadOnly && $emit('input', e)"
:tabindex="isReadOnly ? '-1' : '0'" :tabindex="isReadOnly ? '-1' : '0'"
@ -25,61 +25,71 @@
</div> </div>
</div> </div>
</template> </template>
<script lang="ts">
<script> import { Doc } from 'fyo/model/doc';
import { Field } from 'schemas/types';
import { isNumeric } from 'src/utils'; import { isNumeric } from 'src/utils';
import { evaluateReadOnly, evaluateRequired } from 'src/utils/doc'; import { evaluateReadOnly, evaluateRequired } from 'src/utils/doc';
import { getIsNullOrUndef } from 'utils/index'; import { getIsNullOrUndef } from 'utils/index';
import { defineComponent, PropType } from 'vue';
export default { export default defineComponent({
name: 'Base', name: 'Base',
props: { props: {
df: Object, df: { type: Object as PropType<Field>, required: true },
step: { type: Number, default: 1 }, step: { type: Number, default: 1 },
value: [String, Number, Boolean, Object], value: [String, Number, Boolean, Object],
inputClass: [Function, String, Object], inputClass: [String, Array] as PropType<string | string[]>,
border: { type: Boolean, default: false }, border: { type: Boolean, default: false },
size: { type: String, default: 'large' },
placeholder: String, placeholder: String,
size: String, showLabel: { type: Boolean, default: false },
showLabel: Boolean,
autofocus: Boolean,
containerStyles: { type: Object, default: () => ({}) }, containerStyles: { type: Object, default: () => ({}) },
textRight: { type: [null, Boolean], default: null }, textRight: {
readOnly: { type: [null, Boolean], default: null }, type: [null, Boolean] as PropType<boolean | null>,
required: { type: [null, Boolean], default: null }, default: null,
},
readOnly: {
type: [null, Boolean] as PropType<boolean | null>,
default: null,
},
required: {
type: [null, Boolean] as PropType<boolean | null>,
default: null,
},
}, },
emits: ['focus', 'input', 'change'], emits: ['focus', 'input', 'change'],
inject: { inject: {
schemaName: { injectedDoc: {
default: null, from: 'doc',
default: undefined,
}, },
name: {
default: null,
},
doc: {
default: null,
},
},
mounted() {
if (this.autofocus) {
this.focus();
}
}, },
computed: { computed: {
inputType() { doc(): Doc | undefined {
// @ts-ignore
const doc = this.injectedDoc;
if (doc instanceof Doc) {
return doc;
}
return undefined;
},
inputType(): string {
return 'text'; return 'text';
}, },
labelClasses() { labelClasses(): string {
return 'text-gray-600 text-sm mb-1'; return 'text-gray-600 text-sm mb-1';
}, },
inputClasses() { inputClasses(): string[] {
/** /**
* These classes will be used by components that extend Base * These classes will be used by components that extend Base
*/ */
const classes = []; const classes: string[] = [];
classes.push(this.baseInputClasses); classes.push(...this.baseInputClasses);
if (this.textRight ?? isNumeric(this.df)) { if (this.textRight ?? isNumeric(this.df)) {
classes.push('text-end'); classes.push('text-end');
} }
@ -89,7 +99,7 @@ export default {
return this.getInputClassesFromProp(classes).filter(Boolean); return this.getInputClassesFromProp(classes).filter(Boolean);
}, },
baseInputClasses() { baseInputClasses(): string[] {
return [ return [
'text-base', 'text-base',
'focus:outline-none', 'focus:outline-none',
@ -97,41 +107,41 @@ export default {
'placeholder-gray-500', 'placeholder-gray-500',
]; ];
}, },
sizeClasses() { sizeClasses(): string {
if (this.size === 'small') { if (this.size === 'small') {
return 'px-2 py-1'; return 'px-2 py-1';
} }
return 'px-3 py-2'; return 'px-3 py-2';
}, },
inputReadOnlyClasses() { inputReadOnlyClasses(): string {
if (this.isReadOnly) { if (this.isReadOnly) {
return 'text-gray-800 cursor-default'; return 'text-gray-800 cursor-default';
} }
return 'text-gray-900'; return 'text-gray-900';
}, },
containerClasses() { containerClasses(): string[] {
/** /**
* Used to accomodate extending compoents where the input is contained in * Used to accomodate extending compoents where the input is contained in
* a div eg AutoComplete * a div eg AutoComplete
*/ */
const classes = []; const classes: string[] = [];
classes.push(this.baseContainerClasses); classes.push(...this.baseContainerClasses);
classes.push(this.containerReadOnlyClasses); classes.push(this.containerReadOnlyClasses);
classes.push(this.borderClasses); classes.push(this.borderClasses);
return classes.filter(Boolean); return classes.filter(Boolean);
}, },
baseContainerClasses() { baseContainerClasses(): string[] {
return ['rounded']; return ['rounded'];
}, },
containerReadOnlyClasses() { containerReadOnlyClasses(): string {
if (!this.isReadOnly) { if (!this.isReadOnly) {
return 'focus-within:bg-gray-100'; return 'focus-within:bg-gray-100';
} }
return ''; return '';
}, },
borderClasses() { borderClasses(): string {
if (!this.border) { if (!this.border) {
return ''; return '';
} }
@ -144,13 +154,13 @@ export default {
return border + ' ' + background; return border + ' ' + background;
}, },
inputPlaceholder() { inputPlaceholder(): string {
return this.placeholder || this.df.placeholder || this.df.label; return this.placeholder || this.df.placeholder || this.df.label;
}, },
showMandatory() { showMandatory(): boolean {
return this.isEmpty && this.isRequired; return this.isEmpty && this.isRequired;
}, },
isEmpty() { isEmpty(): boolean {
if (Array.isArray(this.value) && !this.value.length) { if (Array.isArray(this.value) && !this.value.length) {
return true; return true;
} }
@ -165,14 +175,14 @@ export default {
return false; return false;
}, },
isReadOnly() { isReadOnly(): boolean {
if (typeof this.readOnly === 'boolean') { if (typeof this.readOnly === 'boolean') {
return this.readOnly; return this.readOnly;
} }
return evaluateReadOnly(this.df, this.doc); return evaluateReadOnly(this.df, this.doc);
}, },
isRequired() { isRequired(): boolean {
if (typeof this.required === 'boolean') { if (typeof this.required === 'boolean') {
return this.required; return this.required;
} }
@ -181,25 +191,40 @@ export default {
}, },
}, },
methods: { methods: {
getInputClassesFromProp(classes) { onBlur(e: FocusEvent) {
const target = e.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
if (this.isReadOnly) {
return;
}
this.triggerChange(target.value);
},
getInputClassesFromProp(classes: string[]) {
if (!this.inputClass) { if (!this.inputClass) {
return classes; return classes;
} }
if (typeof this.inputClass === 'function') { let inputClass = this.inputClass;
classes = this.inputClass(classes); if (typeof inputClass === 'string') {
} else { inputClass = [inputClass];
classes.push(this.inputClass);
} }
return classes; inputClass = inputClass.filter((i) => typeof i === 'string');
return [classes, inputClass].flat();
}, },
focus() { focus(): void {
if (this.$refs.input && this.$refs.input.focus) { const el = this.$refs.input;
this.$refs.input.focus();
if (el instanceof HTMLInputElement) {
el.focus();
} }
}, },
triggerChange(value) { triggerChange(value: unknown): void {
value = this.parse(value); value = this.parse(value);
if (value === '') { if (value === '') {
@ -208,10 +233,10 @@ export default {
this.$emit('change', value); this.$emit('change', value);
}, },
parse(value) { parse(value: unknown): unknown {
return value; return value;
}, },
isNumeric, isNumeric,
}, },
}; });
</script> </script>

View File

@ -12,7 +12,7 @@
:class="isReadOnly ? 'cursor-default' : 'cursor-pointer'" :class="isReadOnly ? 'cursor-default' : 'cursor-pointer'"
> >
<svg <svg
v-if="checked" v-if="value"
width="14" width="14"
height="14" height="14"
viewBox="0 0 14 14" viewBox="0 0 14 14"
@ -60,10 +60,10 @@
<input <input
ref="input" ref="input"
type="checkbox" type="checkbox"
:checked="value" :checked="getChecked(value)"
:readonly="isReadOnly" :readonly="isReadOnly"
:tabindex="isReadOnly ? '-1' : '0'" :tabindex="isReadOnly ? '-1' : '0'"
@change="(e) => !isReadOnly && triggerChange(e.target.checked)" @change="onChange"
@focus="(e) => $emit('focus', e)" @focus="(e) => $emit('focus', e)"
/> />
</div> </div>
@ -73,10 +73,11 @@
</label> </label>
</div> </div>
</template> </template>
<script> <script lang="ts">
import Base from './Base'; import { defineComponent } from 'vue';
import Base from './Base.vue';
export default { export default defineComponent({
name: 'Check', name: 'Check',
extends: Base, extends: Base,
emits: ['focus'], emits: ['focus'],
@ -106,11 +107,25 @@ export default {
return 'text-gray-600 text-base'; return 'text-gray-600 text-base';
}, },
checked() { },
return this.value; methods: {
getChecked(value: unknown) {
return Boolean(value);
},
onChange(e: Event) {
if (this.isReadOnly) {
return;
}
const target = e.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
this.triggerChange(target.checked);
}, },
}, },
}; });
</script> </script>
<style scoped> <style scoped>

View File

@ -30,11 +30,10 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
// @ts-nocheck
import { isPesa } from 'fyo/utils'; import { isPesa } from 'fyo/utils';
import { Money } from 'pesa'; import { Money } from 'pesa';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { safeParseFloat } from 'utils/index'; import { safeParsePesa } from 'utils/index';
import { defineComponent, nextTick } from 'vue'; import { defineComponent, nextTick } from 'vue';
import Float from './Float.vue'; import Float from './Float.vue';
@ -49,8 +48,13 @@ export default defineComponent({
}; };
}, },
methods: { methods: {
onFocus(e) { onFocus(e: FocusEvent) {
e.target.select(); const target = e.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
target.select();
this.showInput = true; this.showInput = true;
this.$emit('focus', e); this.$emit('focus', e);
}, },
@ -66,23 +70,7 @@ export default defineComponent({
return fyo.pesa(0).round(); return fyo.pesa(0).round();
}, },
parse(value: unknown): Money { parse(value: unknown): Money {
if (isPesa(value)) { return safeParsePesa(value, this.fyo);
return value;
}
if (typeof value === 'string') {
value = safeParseFloat(value);
}
if (typeof value === 'number') {
return fyo.pesa(value);
}
if (typeof value === 'bigint') {
return fyo.pesa(value);
}
return fyo.pesa(0);
}, },
onBlur(e: FocusEvent) { onBlur(e: FocusEvent) {
const target = e.target; const target = e.target;
@ -106,7 +94,8 @@ export default defineComponent({
}, },
computed: { computed: {
formattedValue() { formattedValue() {
return fyo.format(this.value ?? fyo.pesa(0), this.df, this.doc); const value = this.parse(this.value);
return fyo.format(value, this.df, this.doc);
}, },
}, },
}); });

View File

@ -4,19 +4,17 @@
:value="value" :value="value"
@change="onChange" @change="onChange"
:border="true" :border="true"
:input-class="'rounded py-1.5'" input-class="rounded py-1.5"
/> />
</template> </template>
<script> <script lang="ts">
import { DEFAULT_LANGUAGE } from 'fyo/utils/consts'; import { DEFAULT_LANGUAGE } from 'fyo/utils/consts';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { languageCodeMap, setLanguageMap } from 'src/utils/language'; import { languageCodeMap, setLanguageMap } from 'src/utils/language';
import { defineComponent } from 'vue';
import FormControl from './FormControl.vue'; import FormControl from './FormControl.vue';
export default { export default defineComponent({
methods: {
setLanguageMap,
},
props: { props: {
dontReload: { dontReload: {
type: Boolean, type: Boolean,
@ -25,7 +23,11 @@ export default {
}, },
components: { FormControl }, components: { FormControl },
methods: { methods: {
onChange(value) { onChange(value: unknown) {
if (typeof value !== 'string') {
return;
}
if (languageCodeMap[value] === undefined) { if (languageCodeMap[value] === undefined) {
return; return;
} }
@ -48,5 +50,5 @@ export default {
}; };
}, },
}, },
}; });
</script> </script>

View File

@ -20,7 +20,7 @@
'text-gray-500': !value, 'text-gray-500': !value,
}" }"
:value="value" :value="value"
@change="(e) => triggerChange(e.target.value)" @change="onChange"
@focus="(e) => $emit('focus', e)" @focus="(e) => $emit('focus', e)"
> >
<option <option
@ -61,31 +61,33 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import Base from './Base'; import Base from './Base.vue';
export default { import { defineComponent } from 'vue';
import { SelectOption } from 'schemas/types';
export default defineComponent({
name: 'Select', name: 'Select',
extends: Base, extends: Base,
emits: ['focus'], emits: ['focus'],
methods: { methods: {
map(v) { onChange(e: Event) {
if (this.df.map) { const target = e.target;
return this.df.map[v] ?? v; if (!(target instanceof HTMLInputElement)) {
return;
} }
return v;
this.triggerChange(target.value);
}, },
}, },
computed: { computed: {
options() { options(): SelectOption[] {
let options = this.df.options; if (this.df.fieldtype !== 'Select') {
return options.map((o) => { return [];
if (typeof o === 'string') {
return { label: this.map(o), value: o };
} }
return o;
return this.df.options;
},
},
}); });
},
},
};
</script> </script>

View File

@ -116,9 +116,6 @@ export default {
Row, Row,
TableRow, TableRow,
}, },
inject: {
doc: { default: null },
},
watch: { watch: {
value() { value() {
this.setMaxHeight(); this.setMaxHeight();

View File

@ -60,7 +60,7 @@
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import Row from 'src/components/Row.vue'; import Row from 'src/components/Row.vue';
import { getErrorMessage } from 'src/utils'; import { getErrorMessage } from 'src/utils';
import { nextTick } from 'vue'; import { computed, nextTick } from 'vue';
import Button from '../Button.vue'; import Button from '../Button.vue';
import FormControl from './FormControl.vue'; import FormControl from './FormControl.vue';
@ -90,9 +90,7 @@ export default {
}, },
provide() { provide() {
return { return {
schemaName: this.row.schemaName, doc: computed(() => this.row),
name: this.row.name,
doc: this.row,
}; };
}, },
computed: { computed: {

View File

@ -77,18 +77,10 @@
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { Field } from 'schemas/types'; import { Field } from 'schemas/types';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { DropdownItem } from 'src/utils/types';
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import Popover from './Popover.vue'; import Popover from './Popover.vue';
type DropdownItem = {
label: string;
value?: string;
action?: Function;
group?: string;
component?: { template: string };
isGroup?: boolean;
};
export default defineComponent({ export default defineComponent({
name: 'Dropdown', name: 'Dropdown',
props: { props: {

View File

@ -2,7 +2,7 @@
<Dropdown <Dropdown
v-if="actions && actions.length" v-if="actions && actions.length"
class="text-xs" class="text-xs"
:items="actions" :items="items"
:doc="doc" :doc="doc"
right right
> >
@ -16,23 +16,46 @@
</Dropdown> </Dropdown>
</template> </template>
<script> <script lang="ts">
import Button from 'src/components/Button'; import { Doc } from 'fyo/model/doc';
import Dropdown from 'src/components/Dropdown'; import { Action } from 'fyo/model/types';
import Button from 'src/components/Button.vue';
import Dropdown from 'src/components/Dropdown.vue';
import { DropdownItem } from 'src/utils/types';
import { defineComponent, PropType } from 'vue';
export default { export default defineComponent({
name: 'DropdownWithActions', name: 'DropdownWithActions',
props: { props: {
actions: { default: [] }, actions: { type: Array as PropType<Action[]>, default: () => [] },
type: { type: String, default: 'secondary' }, type: { type: String, default: 'secondary' },
icon: { type: Boolean, default: true }, icon: { type: Boolean, default: true },
}, },
inject: { inject: {
doc: { default: null }, injectedDoc: { from: 'doc' },
}, },
components: { components: {
Dropdown, Dropdown,
Button, Button,
}, },
}; computed: {
doc() {
// @ts-ignore
const doc = this.injectedDoc;
if (doc instanceof Doc) {
return doc;
}
return undefined;
},
items(): DropdownItem[] {
return this.actions.map(({ label, group, component, action }) => ({
label,
group,
action,
component,
}));
},
},
});
</script> </script>

View File

@ -81,13 +81,6 @@ export default {
errors: {}, errors: {},
}; };
}, },
provide() {
return {
schemaName: this.doc.schemaName,
name: this.doc.name,
doc: this.doc,
};
},
components: { components: {
FormControl, FormControl,
Table, Table,

View File

@ -163,8 +163,6 @@ export default defineComponent({
}, },
provide() { provide() {
return { return {
schemaName: computed(() => this.docOrNull?.schemaName),
name: computed(() => this.docOrNull?.name),
doc: computed(() => this.docOrNull), doc: computed(() => this.docOrNull),
}; };
}, },

View File

@ -362,8 +362,6 @@ export default {
}, },
provide() { provide() {
return { return {
schemaName: this.schemaName,
name: this.name,
doc: computed(() => this.doc), doc: computed(() => this.doc),
}; };
}, },

View File

@ -166,8 +166,6 @@ export default {
}, },
provide() { provide() {
return { return {
schemaName: this.schemaName,
name: this.name,
doc: computed(() => this.doc), doc: computed(() => this.doc),
}; };
}, },

View File

@ -98,8 +98,6 @@ export default defineComponent({
}, },
provide() { provide() {
return { return {
schemaName: computed(() => this.docOrNull?.schemaName),
name: computed(() => this.docOrNull?.name),
doc: computed(() => this.docOrNull), doc: computed(() => this.docOrNull),
}; };
}, },

View File

@ -9,8 +9,7 @@ import {
DuplicateEntryError, DuplicateEntryError,
LinkValidationError, LinkValidationError,
} from 'fyo/utils/errors'; } from 'fyo/utils/errors';
import { Money } from 'pesa'; import { Field, FieldType, FieldTypeEnum, NumberField } from 'schemas/types';
import { Field, FieldType, FieldTypeEnum } from 'schemas/types';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
export function stringifyCircular( export function stringifyCircular(
@ -112,7 +111,13 @@ export function getErrorMessage(e: Error, doc?: Doc): string {
return errorMessage; return errorMessage;
} }
export function isNumeric(fieldtype: Field | FieldType): boolean { export function isNumeric(
fieldtype: FieldType
): fieldtype is NumberField['fieldtype'];
export function isNumeric(fieldtype: Field): fieldtype is NumberField;
export function isNumeric(
fieldtype: Field | FieldType
): fieldtype is NumberField | NumberField['fieldtype'] {
if (typeof fieldtype !== 'string') { if (typeof fieldtype !== 'string') {
fieldtype = fieldtype?.fieldtype; fieldtype = fieldtype?.fieldtype;
} }

View File

@ -90,6 +90,15 @@ export type ActionGroup = {
actions: Action[]; actions: Action[];
}; };
export type DropdownItem = {
label: string;
value?: string;
action?: Function;
group?: string;
component?: { template: string };
isGroup?: boolean;
};
export type UIGroupedFields = Map<string, Map<string, Field[]>>; export type UIGroupedFields = Map<string, Map<string, Field[]>>;
export type ExportFormat = 'csv' | 'json'; export type ExportFormat = 'csv' | 'json';
export type PeriodKey = 'This Year' | 'This Quarter' | 'This Month'; export type PeriodKey = 'This Year' | 'This Quarter' | 'This Month';

View File

@ -1,3 +1,6 @@
import type { Fyo } from 'fyo';
import { Money } from 'pesa';
/** /**
* And so should not contain and platforma specific imports. * And so should not contain and platforma specific imports.
*/ */
@ -187,6 +190,30 @@ export function safeParseInt(value: unknown): number {
return safeParseNumber(value, (v: string) => Math.trunc(Number(v))); return safeParseNumber(value, (v: string) => Math.trunc(Number(v)));
} }
export function safeParsePesa(value: unknown, fyo: Fyo): Money {
if (value instanceof Money) {
return value;
}
if (typeof value === 'number') {
return fyo.pesa(value);
}
if (typeof value === 'bigint') {
return fyo.pesa(value);
}
if (typeof value !== 'string') {
return fyo.pesa(0);
}
try {
return fyo.pesa(value);
} catch {
return fyo.pesa(0);
}
}
export function joinMapLists<A, B>( export function joinMapLists<A, B>(
listA: A[], listA: A[],
listB: B[], listB: B[],