2
0
mirror of https://github.com/frappe/books.git synced 2024-11-08 14:50:56 +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;
type?: 'primary' | 'secondary';
component?: {
template?: string;
template: string;
};
}

View File

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

View File

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

View File

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

View File

@ -30,11 +30,10 @@
</div>
</template>
<script lang="ts">
// @ts-nocheck
import { isPesa } from 'fyo/utils';
import { Money } from 'pesa';
import { fyo } from 'src/initFyo';
import { safeParseFloat } from 'utils/index';
import { safeParsePesa } from 'utils/index';
import { defineComponent, nextTick } from 'vue';
import Float from './Float.vue';
@ -49,8 +48,13 @@ export default defineComponent({
};
},
methods: {
onFocus(e) {
e.target.select();
onFocus(e: FocusEvent) {
const target = e.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
target.select();
this.showInput = true;
this.$emit('focus', e);
},
@ -66,23 +70,7 @@ export default defineComponent({
return fyo.pesa(0).round();
},
parse(value: unknown): Money {
if (isPesa(value)) {
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);
return safeParsePesa(value, this.fyo);
},
onBlur(e: FocusEvent) {
const target = e.target;
@ -106,7 +94,8 @@ export default defineComponent({
},
computed: {
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"
@change="onChange"
:border="true"
:input-class="'rounded py-1.5'"
input-class="rounded py-1.5"
/>
</template>
<script>
<script lang="ts">
import { DEFAULT_LANGUAGE } from 'fyo/utils/consts';
import { fyo } from 'src/initFyo';
import { languageCodeMap, setLanguageMap } from 'src/utils/language';
import { defineComponent } from 'vue';
import FormControl from './FormControl.vue';
export default {
methods: {
setLanguageMap,
},
export default defineComponent({
props: {
dontReload: {
type: Boolean,
@ -25,7 +23,11 @@ export default {
},
components: { FormControl },
methods: {
onChange(value) {
onChange(value: unknown) {
if (typeof value !== 'string') {
return;
}
if (languageCodeMap[value] === undefined) {
return;
}
@ -48,5 +50,5 @@ export default {
};
},
},
};
});
</script>

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
<Dropdown
v-if="actions && actions.length"
class="text-xs"
:items="actions"
:items="items"
:doc="doc"
right
>
@ -16,23 +16,46 @@
</Dropdown>
</template>
<script>
import Button from 'src/components/Button';
import Dropdown from 'src/components/Dropdown';
<script lang="ts">
import { Doc } from 'fyo/model/doc';
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',
props: {
actions: { default: [] },
actions: { type: Array as PropType<Action[]>, default: () => [] },
type: { type: String, default: 'secondary' },
icon: { type: Boolean, default: true },
},
inject: {
doc: { default: null },
injectedDoc: { from: 'doc' },
},
components: {
Dropdown,
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,8 +9,7 @@ import {
DuplicateEntryError,
LinkValidationError,
} from 'fyo/utils/errors';
import { Money } from 'pesa';
import { Field, FieldType, FieldTypeEnum } from 'schemas/types';
import { Field, FieldType, FieldTypeEnum, NumberField } from 'schemas/types';
import { fyo } from 'src/initFyo';
export function stringifyCircular(
@ -112,7 +111,13 @@ export function getErrorMessage(e: Error, doc?: Doc): string {
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') {
fieldtype = fieldtype?.fieldtype;
}

View File

@ -90,6 +90,15 @@ export type ActionGroup = {
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 ExportFormat = 'csv' | 'json';
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.
*/
@ -187,6 +190,30 @@ export function safeParseInt(value: unknown): number {
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>(
listA: A[],
listB: B[],