2
0
mirror of https://github.com/frappe/books.git synced 2025-02-03 04:28:32 +00:00

Merge pull request #567 from frappe/fix-inline-fields

fix(ux): inline fieldtype
This commit is contained in:
Alan 2023-03-02 00:16:55 -08:00 committed by GitHub
commit 0f2f923bd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 163 additions and 185 deletions

View File

@ -41,12 +41,12 @@ export class AccountingLedgerEntry extends Doc {
static getListViewSettings(): ListViewSettings {
return {
columns: [
'date',
'account',
'party',
'debit',
'credit',
'referenceName',
'reverted',
],
};
}

View File

@ -1,6 +1,11 @@
import { t } from 'fyo';
import { Fyo, t } from 'fyo';
import { Doc } from 'fyo/model/doc';
import { EmptyMessageMap, FormulaMap, ListsMap } from 'fyo/model/types';
import {
EmptyMessageMap,
FormulaMap,
ListsMap,
ListViewSettings,
} from 'fyo/model/types';
import { codeStateMap } from 'regional/in';
import { getCountryInfo } from 'utils/misc';
@ -54,4 +59,10 @@ export class Address extends Doc {
return t`Enter Country to load States`;
},
};
static override getListViewSettings(): ListViewSettings {
return {
columns: ['name', 'addressLine1', 'city', 'state', 'country'],
};
}
}

View File

@ -1,7 +1,7 @@
import { Doc } from 'fyo/model/doc';
import { ListViewSettings } from 'fyo/model/types';
import { Money } from 'pesa';
export class StockLedgerEntry extends Doc {
date?: Date;
item?: string;
@ -11,4 +11,17 @@ export class StockLedgerEntry extends Doc {
referenceName?: string;
referenceType?: string;
batch?: string;
static override getListViewSettings(): ListViewSettings {
return {
columns: [
'date',
'item',
'location',
'rate',
'quantity',
'referenceName',
],
};
}
}

View File

@ -1,8 +1,15 @@
{
"name": "Address",
"label": "Address",
"naming": "manual",
"isSingle": false,
"fields": [
{
"fieldname": "name",
"label": "Address Name",
"fieldtype": "Data",
"required": true
},
{
"fieldname": "addressLine1",
"label": "Address Line 1",
@ -69,6 +76,7 @@
}
],
"quickEditFields": [
"name",
"addressLine1",
"addressLine2",
"city",
@ -76,5 +84,5 @@
"country",
"postalCode"
],
"inlineEditDisplayField": "addressDisplay"
"linkDisplayField": "addressDisplay"
}

View File

@ -74,7 +74,7 @@
"label": "Address",
"fieldtype": "Link",
"target": "Address",
"inline": true
"create": true
},
{
"fieldname": "taxId",

View File

@ -34,7 +34,7 @@
"label": "Address",
"fieldtype": "Link",
"target": "Address",
"inline": true,
"create": true,
"section": "Contacts"
},
{

View File

@ -16,7 +16,7 @@
"label": "Address",
"fieldtype": "Link",
"target": "Address",
"inline": true
"create": true
}
],
"quickEditFields": ["item", "address"]

View File

@ -50,8 +50,8 @@ function removeFields(schemaMap: SchemaMap): SchemaMap {
(fn) => fn !== fieldname
);
if (schema.inlineEditDisplayField === fieldname) {
delete schema.inlineEditDisplayField;
if (schema.linkDisplayField === fieldname) {
delete schema.linkDisplayField;
}
}

View File

@ -9,6 +9,7 @@
}
],
"quickEditFields": [
"name",
"addressLine1",
"addressLine2",
"city",

View File

@ -72,8 +72,7 @@
"fieldname": "address",
"label": "Address",
"fieldtype": "Link",
"target": "Address",
"inline": true
"target": "Address"
}
],
"quickEditFields": [

View File

@ -61,7 +61,6 @@ export interface BaseField {
placeholder?: string; // UI Facing config, form field placeholder
groupBy?: string; // UI Facing used in dropdowns fields
meta?: boolean; // Field is a meta field, i.e. only for the db, not UI
inline?: boolean; // UI Facing config, whether to display doc inline.
filter?: boolean; // UI Facing config, whether to be used to filter the List.
computed?: boolean; // Computed values are not stored in the database.
section?: string; // UI Facing config, for grouping by sections
@ -118,7 +117,7 @@ export interface Schema {
isSubmittable?: boolean; // For transactional types, values considered only after submit
keywordFields?: string[]; // Used to get fields that are to be used for search.
quickEditFields?: string[]; // Used to get fields for the quickEditForm
inlineEditDisplayField?:string;// Display field if inline editable
linkDisplayField?:string;// Display field if inline editable
naming?: Naming; // Used for assigning name, default is 'random' else 'numberSeries' if present
titleField?: string; // Main display field
removeFields?: string[]; // Used by the builder to remove fields.

View File

@ -25,6 +25,7 @@
:placeholder="inputPlaceholder"
:readonly="isReadOnly"
@focus="(e) => !isReadOnly && onFocus(e, toggleDropdown)"
@click="(e) => !isReadOnly && onFocus(e, toggleDropdown)"
@blur="(e) => !isReadOnly && onBlur(e.target.value)"
@input="onInput"
@keydown.up="highlightItemUp"
@ -55,7 +56,6 @@
</template>
</Dropdown>
</template>
<script>
import { getOptionList } from 'fyo/utils';
import Dropdown from 'src/components/Dropdown.vue';
@ -81,7 +81,7 @@ export default {
value: {
immediate: true,
handler(newValue) {
this.linkValue = this.getLinkValue(newValue);
this.setLinkValue(this.getLinkValue(newValue));
},
},
},
@ -89,7 +89,8 @@ export default {
doc: { default: null },
},
mounted() {
this.linkValue = this.getLinkValue(this.linkValue || this.value);
const value = this.linkValue || this.value;
this.setLinkValue(this.getLinkValue(value));
},
computed: {
options() {
@ -101,6 +102,9 @@ export default {
},
},
methods: {
setLinkValue(value) {
this.linkValue = value;
},
getLinkValue(value) {
const oldValue = this.linkValue;
let option = this.options.find((o) => o.value === value);
@ -116,7 +120,7 @@ export default {
},
async updateSuggestions(keyword) {
if (typeof keyword === 'string') {
this.linkValue = keyword;
this.setLinkValue(keyword, true);
}
this.isLoading = true;
@ -152,12 +156,12 @@ export default {
},
setSuggestion(suggestion) {
if (suggestion?.actionOnly) {
this.linkValue = this.value;
this.setLinkValue(this.value);
return;
}
if (suggestion) {
this.linkValue = suggestion.label;
this.setLinkValue(suggestion.label);
this.triggerChange(suggestion.value);
}

View File

@ -48,6 +48,12 @@ export default {
});
},
methods: {
clear() {
const input = this.$refs.control.$refs.input;
if (input instanceof HTMLInputElement) {
input.value = '';
}
},
focus() {
this.$refs.control.focus();
},

View File

@ -16,18 +16,42 @@ export default {
},
mounted() {
if (this.value) {
this.linkValue = this.value;
this.setLinkValue();
}
},
watch: {
value: {
immediate: true,
handler(newValue) {
this.linkValue = newValue;
this.setLinkValue(newValue);
},
},
},
methods: {
async setLinkValue(newValue, isInput) {
if (isInput) {
return (this.linkValue = newValue || '');
}
const value = newValue ?? this.value;
const { fieldname, target } = this.df ?? {};
const displayField = fyo.schemaMap[target ?? '']?.linkDisplayField;
if (!displayField) {
return (this.linkValue = value);
}
let displayValue = this.docs?.links?.[fieldname]?.get(displayField);
if (!displayValue) {
displayValue = await fyo.getValue(
target,
this.value ?? '',
displayField
);
}
this.linkValue = displayValue;
},
getTargetSchemaName() {
return this.df.target;
},
@ -104,10 +128,11 @@ export default {
'<Badge color="blue" class="ms-2" v-if="isNewValue">{{ linkValue }}</Badge>' +
'</div>',
computed: {
value: () => this.value,
linkValue: () => this.linkValue,
isNewValue: () => {
let values = this.suggestions.map((d) => d.value);
return this.linkValue && !values.includes(this.linkValue);
return this.value && !values.includes(this.value);
},
},
components: { Badge },
@ -134,6 +159,7 @@ export default {
this.$emit('new-doc', doc);
this.$router.back();
this.results = [];
this.triggerChange(doc.name);
});
},
async getCreateFilters() {

View File

@ -23,7 +23,12 @@
@change="(e) => triggerChange(e.target.value)"
@focus="(e) => $emit('focus', e)"
>
<option value="" disabled selected v-if="inputPlaceholder">
<option
value=""
disabled
selected
v-if="inputPlaceholder && !showLabel"
>
{{ inputPlaceholder }}
</option>
<option

View File

@ -165,7 +165,11 @@ export default {
methods: {
getRandomString,
addNewFilter() {
let df = this.fields[0];
const df = this.fields[0];
if (!df) {
return;
}
this.addFilter(df.fieldname, 'like', '', false);
},
addFilter(fieldname, condition, value, implicit) {

View File

@ -1,8 +1,8 @@
<template>
<div class="text-sm" :class="{ 'border-t': !noBorder }">
<div class="text-sm border-t">
<template v-for="df in formFields">
<!-- Table Field Form (Eg: PaymentFor) -->
<FormControl
<Table
v-if="df.fieldtype === 'Table'"
:key="`${df.fieldname}-table`"
ref="controls"
@ -13,53 +13,13 @@
:read-only="readOnly"
/>
<!-- Inline Field Form (Eg: Address) -->
<div
v-else-if="renderInline(df)"
class="border-b"
:key="`${df.fieldname}-inline`"
>
<TwoColumnForm
class="overflow-auto custom-scroll"
style="max-height: calc((var(--h-row-mid) + 1px) * 3 - 1px)"
ref="inlineEditForm"
:doc="inlineEditDoc"
:fields="getInlineEditFields(df)"
:column-ratio="columnRatio"
:no-border="true"
:focus-first-input="true"
:autosave="false"
:read-only="readOnly"
@error="(msg) => $emit('error', msg)"
/>
<div
class="flex px-4 py-4 justify-between items-center"
style="max-height: calc(var(--h-row-mid) + 1px)"
>
<Button class="text-gray-900 w-20" @click="stopInlineEditing">
{{ t`Cancel` }}
</Button>
<Button
type="primary"
class="text-white w-20"
@click="saveInlineEditDoc(df)"
>
{{ t`Save` }}
</Button>
</div>
</div>
<!-- Regular Field Form -->
<div
v-else
class="grid items-center"
:class="{
'border-b': !noBorder,
}"
class="grid items-center border-b"
:key="`${df.fieldname}-regular`"
:style="{
...style,
height: getFieldHeight(df),
}"
>
@ -69,7 +29,6 @@
<div
class="py-2 pe-4"
@click="activateInlineEditing(df)"
:class="{
'ps-2': df.fieldtype === 'AttachImage',
}"
@ -78,12 +37,11 @@
ref="controls"
size="small"
:df="df"
:value="getRegularValue(df)"
:value="doc[df.fieldname]"
:class="{ 'p-2': df.fieldtype === 'Check' }"
:read-only="readOnly"
:text-end="false"
@change="async (value) => await onChange(df, value)"
@focus="activateInlineEditing(df)"
@new-doc="async (newdoc) => await onChange(df, newdoc.name)"
/>
<div
@ -99,12 +57,12 @@
</template>
<script>
import { Doc } from 'fyo/model/doc';
import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl.vue';
import { handleErrorWithDialog } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import { getErrorMessage } from 'src/utils';
import { evaluateHidden } from 'src/utils/doc';
import Table from './Controls/Table.vue';
export default {
name: 'TwoColumnForm',
@ -121,7 +79,6 @@ export default {
type: Boolean,
default: false,
},
noBorder: Boolean,
focusFirstInput: Boolean,
readOnly: { type: [null, Boolean], default: null },
},
@ -132,8 +89,6 @@ export default {
},
data() {
return {
inlineEditDoc: null,
inlineEditField: null,
formFields: [],
errors: {},
};
@ -147,8 +102,7 @@ export default {
},
components: {
FormControl,
Button,
TwoColumnForm: () => TwoColumnForm,
Table,
},
mounted() {
this.setFormFields();
@ -172,51 +126,25 @@ export default {
return 'calc(var(--h-row-mid) + 1px)';
},
getRegularValue(df) {
if (!df.inline) {
return this.doc[df.fieldname];
}
async onChange(field, value) {
const { fieldname } = field;
delete this.errors[fieldname];
const link = this.doc.getLink(df.fieldname);
if (!link) {
return this.doc[df.fieldname];
}
const fieldname = link.schema.inlineEditDisplayField ?? 'name';
return link[fieldname];
},
renderInline(df) {
return (
this.inlineEditField?.fieldname === df?.fieldname && this.inlineEditDoc
);
},
async onChange(df, value) {
if (df.inline) {
return;
}
// handle rename
if (this.autosave && df.fieldname === 'name' && this.doc.inserted) {
return this.doc.rename(value);
}
const oldValue = this.doc.get(df.fieldname);
this.errors[df.fieldname] = null;
await this.onChangeCommon(df, value, oldValue);
},
async onChangeCommon(df, value, oldValue) {
let isSet = false;
const oldValue = this.doc.get(fieldname);
try {
isSet = await this.doc.set(df.fieldname, value);
isSet = await this.doc.set(fieldname, value);
} catch (err) {
this.errors[df.fieldname] = getErrorMessage(err, this.doc);
if (!(err instanceof Error)) {
return;
}
this.errors[fieldname] = getErrorMessage(err, this.doc);
}
if (!isSet) {
return;
if (isSet) {
await this.handlePostSet(field, value, oldValue);
}
await this.handlePostSet(df, value, oldValue);
},
async handlePostSet(df, value, oldValue) {
this.setFormFields();
@ -224,17 +152,16 @@ export default {
this.$emit('change', df, value, oldValue);
}
if (this.autosave && this.doc.dirty) {
if (df.fieldtype === 'Table') {
return;
}
await this.doc.sync();
if (df.fieldtype === 'Table' || !this.doc.dirty || !this.autosave) {
return;
}
await this.doc.sync();
},
async sync() {
try {
await this.doc.sync();
this.setFormFields();
} catch (err) {
await handleErrorWithDialog(err, this.doc);
}
@ -242,57 +169,11 @@ export default {
async submit() {
try {
await this.doc.submit();
this.setFormFields();
} catch (err) {
await handleErrorWithDialog(err, this.doc);
}
},
async activateInlineEditing(df) {
if (!df.inline) {
return;
}
this.inlineEditField = df;
if (!this.doc[df.fieldname]) {
this.inlineEditDoc = await fyo.doc.getNewDoc(df.target);
} else {
this.inlineEditDoc = this.doc.getLink(df.fieldname);
}
},
getInlineEditFields(df) {
const inlineEditFieldNames =
fyo.schemaMap[df.target].quickEditFields ?? [];
return inlineEditFieldNames.map((fieldname) =>
fyo.getField(df.target, fieldname)
);
},
async saveInlineEditDoc(df) {
if (!this.inlineEditDoc) {
return;
}
try {
await this.inlineEditDoc.sync();
} catch (error) {
return await handleErrorWithDialog(error, this.inlineEditDoc);
}
await this.onChangeCommon(df, this.inlineEditDoc.name);
await this.doc.loadLinks();
if (this.emitChange) {
this.$emit('change', this.inlineEditField);
}
await this.stopInlineEditing();
},
async stopInlineEditing() {
if (this.inlineEditDoc?.dirty && !this.inlineEditDoc?.notInserted) {
await this.inlineEditDoc.load();
}
this.inlineEditDoc = null;
this.inlineEditField = null;
},
setFormFields() {
let fieldList = this.fields;

View File

@ -66,15 +66,19 @@ export default defineComponent({
methods: {
focusOnNameField() {
const naming = this.fyo.schemaMap[this.doc.schemaName]?.naming;
if (naming !== 'manual') {
if (naming !== 'manual' || this.doc.inserted) {
return;
}
const nameField = (this.$refs.nameField as { focus: Function }[])?.[0];
const nameField = (
this.$refs.nameField as { focus: Function; clear: Function }[]
)?.[0];
if (!nameField) {
return;
}
nameField.clear();
nameField.focus();
},
},

View File

@ -12,12 +12,11 @@
class="sticky top-0 bg-white border-b"
>
</FormHeader>
<!-- Section Container -->
<!-- Section Container -->
<div class="overflow-auto custom-scroll" v-if="doc">
<CommonFormSection
v-for="([name, fields], idx) in activeGroup.entries()"
@editrow="(doc: Doc) => toggleQuickEditDoc(doc)"
:key="name + idx"
ref="section"
class="p-4"
@ -95,13 +94,11 @@ export default defineComponent({
canSave: false,
activeTab: ModelNameEnum.AccountingSettings,
groupedFields: null,
quickEditDoc: null,
} as {
errors: Record<string, string>;
canSave: boolean;
activeTab: string;
groupedFields: null | UIGroupedFields;
quickEditDoc: null | Doc;
};
},
provide() {
@ -118,10 +115,26 @@ export default defineComponent({
activated(): void {
docsPathRef.value = docsPathMap.Settings ?? '';
},
deactivated(): void {
async deactivated(): Promise<void> {
docsPathRef.value = '';
if (!this.canSave) {
return;
}
await this.reset();
},
methods: {
async reset() {
const resetableDocs = this.schemas
.map(({ name }) => this.fyo.singles[name])
.filter((doc) => doc?.dirty) as Doc[];
for (const doc of resetableDocs) {
await doc.load();
}
this.update();
},
async sync(): Promise<void> {
const syncableDocs = this.schemas
.map(({ name }) => this.fyo.singles[name])
@ -151,14 +164,6 @@ export default defineComponent({
await handleErrorWithDialog(err, doc);
}
},
async toggleQuickEditDoc(doc: Doc | null): Promise<void> {
if (this.quickEditDoc && doc) {
this.quickEditDoc = null;
await nextTick();
}
this.quickEditDoc = doc;
},
async onValueChange(field: Field, value: DocValue): Promise<void> {
const { fieldname } = field;
delete this.errors[fieldname];

View File

@ -2,7 +2,14 @@ import { Doc } from 'fyo/model/doc';
import { Field } from 'schemas/types';
export function evaluateReadOnly(field: Field, doc?: Doc) {
if (field.fieldname === 'numberSeries' && !doc?.notInserted) {
if (doc?.inserted && field.fieldname === 'numberSeries') {
return true;
}
if (
field.fieldname === 'name' &&
(doc?.inserted || doc?.schema.naming !== 'manual')
) {
return true;
}

View File

@ -223,6 +223,10 @@ function getListViewList(fyo: Fyo): SearchItem[] {
ModelNameEnum.PurchaseInvoice,
ModelNameEnum.SalesInvoice,
ModelNameEnum.Tax,
ModelNameEnum.UOM,
ModelNameEnum.Address,
ModelNameEnum.AccountingLedgerEntry,
ModelNameEnum.Currency,
];
const hasInventory = fyo.doc.singles.AccountingSettings?.enableInventory;
@ -231,7 +235,8 @@ function getListViewList(fyo: Fyo): SearchItem[] {
ModelNameEnum.StockMovement,
ModelNameEnum.Shipment,
ModelNameEnum.PurchaseReceipt,
ModelNameEnum.Location
ModelNameEnum.Location,
ModelNameEnum.StockLedgerEntry
);
}