mirror of
https://github.com/frappe/books.git
synced 2024-12-22 10:58:59 +00:00
fix(ui): make settings use common form format
This commit is contained in:
parent
ee495bb174
commit
469a7c932b
@ -177,11 +177,12 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
}
|
||||
|
||||
get canSave() {
|
||||
if (!!this.submitted) {
|
||||
const isSubmittable = this.schema.isSubmittable;
|
||||
if (isSubmittable && !!this.submitted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!!this.cancelled) {
|
||||
if (isSubmittable && !!this.cancelled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -189,10 +190,6 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.schema.isSingle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.schema.isChild) {
|
||||
return false;
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
ValidationMap,
|
||||
} from 'fyo/model/types';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { AccountRootTypeEnum, AccountTypeEnum } from '../Account/types';
|
||||
|
||||
|
@ -1,191 +1,256 @@
|
||||
<template>
|
||||
<FormContainer :title="t`Settings`" :searchborder="false">
|
||||
<FormContainer>
|
||||
<template #header>
|
||||
<Button v-if="canSave" type="primary" @click="sync">
|
||||
{{ t`Save` }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #body>
|
||||
<!-- Icon Tab Bar -->
|
||||
<div class="flex m-4 mb-0 gap-8">
|
||||
<button
|
||||
v-for="(tab, i) in tabs"
|
||||
:key="tab.label"
|
||||
class="
|
||||
hover:bg-white
|
||||
flex flex-col
|
||||
items-center
|
||||
justify-center
|
||||
cursor-pointer
|
||||
text-sm
|
||||
"
|
||||
:class="
|
||||
i === activeTab &&
|
||||
'text-blue-500 font-semibold border-b-2 border-blue-500'
|
||||
"
|
||||
:style="{
|
||||
paddingBottom: i === activeTab ? 'calc(1rem - 2px)' : '1rem',
|
||||
}"
|
||||
@click="activeTab = i"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
<FormHeader
|
||||
:form-title="tabLabels[activeTab] ?? ''"
|
||||
:form-sub-title="t`Settings`"
|
||||
class="sticky top-0 bg-white border-b"
|
||||
>
|
||||
</FormHeader>
|
||||
<!-- 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"
|
||||
:class="idx !== 0 && activeGroup.size > 1 ? 'border-t' : ''"
|
||||
:show-title="activeGroup.size > 1 && name !== t`Default`"
|
||||
:title="name"
|
||||
:fields="fields"
|
||||
:doc="doc"
|
||||
:errors="errors"
|
||||
@value-change="onValueChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Component -->
|
||||
<div class="flex-1 overflow-y-auto custom-scroll">
|
||||
<component
|
||||
:is="tabs[activeTab].component"
|
||||
:schema-name="tabs[activeTab].schemaName"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<!-- Tab Bar -->
|
||||
<div
|
||||
class="
|
||||
mt-auto
|
||||
px-4
|
||||
pb-4
|
||||
flex
|
||||
gap-8
|
||||
border-t
|
||||
flex-shrink-0
|
||||
sticky
|
||||
bottom-0
|
||||
bg-white
|
||||
"
|
||||
v-if="groupedFields && groupedFields.size > 1"
|
||||
>
|
||||
<div
|
||||
v-for="key of groupedFields.keys()"
|
||||
:key="key"
|
||||
@click="activeTab = key"
|
||||
class="text-sm cursor-pointer"
|
||||
:class="
|
||||
key === activeTab
|
||||
? 'text-blue-500 font-semibold border-t-2 border-blue-500'
|
||||
: ''
|
||||
"
|
||||
:style="{
|
||||
paddingTop: key === activeTab ? 'calc(1rem - 2px)' : '1rem',
|
||||
}"
|
||||
>
|
||||
{{ tabLabels[key] }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FormContainer>
|
||||
</template>
|
||||
<script>
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { t } from 'fyo';
|
||||
<script lang="ts">
|
||||
import { DocValue } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Field, Schema } from 'schemas/types';
|
||||
import Button from 'src/components/Button.vue';
|
||||
import FormContainer from 'src/components/FormContainer.vue';
|
||||
import Icon from 'src/components/Icon.vue';
|
||||
import PageHeader from 'src/components/PageHeader.vue';
|
||||
import Row from 'src/components/Row.vue';
|
||||
import StatusBadge from 'src/components/StatusBadge.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import { docsPathRef } from 'src/utils/refs';
|
||||
import FormHeader from 'src/components/FormHeader.vue';
|
||||
import { handleErrorWithDialog } from 'src/errorHandling';
|
||||
import { getErrorMessage } from 'src/utils';
|
||||
import { evaluateHidden } from 'src/utils/doc';
|
||||
import { reloadWindow } from 'src/utils/ipcCalls';
|
||||
import { UIGroupedFields } from 'src/utils/types';
|
||||
import { showToast } from 'src/utils/ui';
|
||||
import { IPC_MESSAGES } from 'utils/messages';
|
||||
import { h, markRaw } from 'vue';
|
||||
import TabBase from './TabBase.vue';
|
||||
import TabGeneral from './TabGeneral.vue';
|
||||
import TabInvoice from './TabInvoice.vue';
|
||||
import TabSystem from './TabSystem.vue';
|
||||
export default {
|
||||
name: 'Settings',
|
||||
components: {
|
||||
PageHeader,
|
||||
StatusBadge,
|
||||
Button,
|
||||
Row,
|
||||
FormContainer,
|
||||
},
|
||||
data() {
|
||||
const hasInventory = !!fyo.singles.AccountingSettings?.enableInventory;
|
||||
import { defineComponent, nextTick } from 'vue';
|
||||
import CommonFormSection from '../CommonForm/CommonFormSection.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { FormContainer, Button, FormHeader, CommonFormSection },
|
||||
data() {
|
||||
return {
|
||||
activeTab: 0,
|
||||
updated: false,
|
||||
fieldsChanged: [],
|
||||
tabs: [
|
||||
{
|
||||
key: 'Invoice',
|
||||
label: t`Invoice`,
|
||||
schemaName: 'PrintSettings',
|
||||
component: markRaw(TabInvoice),
|
||||
},
|
||||
{
|
||||
key: 'General',
|
||||
label: t`General`,
|
||||
schemaName: 'AccountingSettings',
|
||||
component: markRaw(TabGeneral),
|
||||
},
|
||||
{
|
||||
key: 'Defaults',
|
||||
label: t`Defaults`,
|
||||
schemaName: 'Defaults',
|
||||
component: markRaw(TabBase),
|
||||
},
|
||||
...(hasInventory
|
||||
? [
|
||||
{
|
||||
key: 'Inventory',
|
||||
label: t`Inventory`,
|
||||
schemaName: 'InventorySettings',
|
||||
component: markRaw(TabBase),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: 'System',
|
||||
label: t`System`,
|
||||
schemaName: 'SystemSettings',
|
||||
component: markRaw(TabSystem),
|
||||
},
|
||||
],
|
||||
errors: {},
|
||||
canSave: false,
|
||||
activeTab: ModelNameEnum.AccountingSettings,
|
||||
groupedFields: null,
|
||||
quickEditDoc: null,
|
||||
} as {
|
||||
errors: Record<string, string>;
|
||||
canSave: boolean;
|
||||
activeTab: string;
|
||||
groupedFields: null | UIGroupedFields;
|
||||
quickEditDoc: null | Doc;
|
||||
};
|
||||
},
|
||||
activated() {
|
||||
this.setActiveTab();
|
||||
docsPathRef.value = docsPathMap.Settings;
|
||||
},
|
||||
deactivated() {
|
||||
docsPathRef.value = '';
|
||||
if (this.fieldsChanged.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouleShowReload = this.fieldsChanged
|
||||
.map(({ fieldname }) => fieldname)
|
||||
.some((f) => {
|
||||
if (f.startsWith('enable')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (f === 'displayPrecision') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (f === 'hideGetStarted') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (shouleShowReload) {
|
||||
this.showReloadToast();
|
||||
mounted() {
|
||||
this.updateGroupedFields();
|
||||
if (this.fyo.store.isDevelopment) {
|
||||
// @ts-ignore
|
||||
window.settings = this;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showReloadToast() {
|
||||
showToast({
|
||||
message: t`Settings changes will be visible on reload`,
|
||||
actionText: t`Reload App`,
|
||||
type: 'info',
|
||||
action: async () => {
|
||||
ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
|
||||
},
|
||||
});
|
||||
},
|
||||
handleChange(df, newValue, oldValue) {
|
||||
if (!df) {
|
||||
return;
|
||||
async sync() {
|
||||
const syncableDocs = this.schemas
|
||||
.map(({ name }) => this.fyo.singles[name])
|
||||
.filter((doc) => doc?.canSave) as Doc[];
|
||||
|
||||
for (const doc of syncableDocs) {
|
||||
await this.syncDoc(doc);
|
||||
}
|
||||
|
||||
this.fieldsChanged.push(df);
|
||||
this.update();
|
||||
await showToast({
|
||||
message: this.t`Changes will be visible on reload`,
|
||||
actionText: this.t`Reload App`,
|
||||
type: 'info',
|
||||
action: reloadWindow,
|
||||
});
|
||||
},
|
||||
setActiveTab() {
|
||||
const { tab } = this.$route.query;
|
||||
const index = this.tabs.findIndex((i) => i.key === tab);
|
||||
if (index !== -1) {
|
||||
this.activeTab = index;
|
||||
} else {
|
||||
this.activeTab = 0;
|
||||
async syncDoc(doc: Doc) {
|
||||
try {
|
||||
await doc.sync();
|
||||
this.updateGroupedFields();
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await handleErrorWithDialog(err, doc);
|
||||
}
|
||||
},
|
||||
getIconComponent(tab) {
|
||||
return {
|
||||
render() {
|
||||
return h(Icon, {
|
||||
class: 'w-6 h-6',
|
||||
...Object.assign(
|
||||
{
|
||||
name: tab.icon,
|
||||
size: '24',
|
||||
},
|
||||
this.$attrs
|
||||
),
|
||||
});
|
||||
},
|
||||
};
|
||||
async toggleQuickEditDoc(doc: Doc | null) {
|
||||
if (this.quickEditDoc && doc) {
|
||||
this.quickEditDoc = null;
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
this.quickEditDoc = doc;
|
||||
},
|
||||
async onValueChange(field: Field, value: DocValue) {
|
||||
const { fieldname } = field;
|
||||
delete this.errors[fieldname];
|
||||
|
||||
try {
|
||||
await this.doc?.set(fieldname, value);
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.errors[fieldname] = getErrorMessage(err, this.doc ?? undefined);
|
||||
}
|
||||
|
||||
this.update();
|
||||
},
|
||||
update() {
|
||||
this.updateCanSave();
|
||||
this.updateGroupedFields();
|
||||
},
|
||||
updateCanSave() {
|
||||
this.canSave = this.schemas
|
||||
.map(({ name }) => this.fyo.singles[name]?.canSave)
|
||||
.some(Boolean);
|
||||
},
|
||||
updateGroupedFields() {
|
||||
const grouped: UIGroupedFields = new Map();
|
||||
const fields: Field[] = this.schemas.map((s) => s.fields).flat();
|
||||
|
||||
for (const field of fields) {
|
||||
const schemaName = field.schemaName!;
|
||||
if (!grouped.has(schemaName)) {
|
||||
grouped.set(schemaName, new Map());
|
||||
}
|
||||
|
||||
const tabbed = grouped.get(schemaName)!;
|
||||
const section = field.section ?? this.t`Misc`;
|
||||
if (!tabbed.has(section)) {
|
||||
tabbed.set(section, []);
|
||||
}
|
||||
|
||||
if (field.meta) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const doc = this.fyo.singles[schemaName];
|
||||
if (evaluateHidden(field, doc)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tabbed.get(section)!.push(field);
|
||||
}
|
||||
|
||||
this.groupedFields = grouped;
|
||||
},
|
||||
},
|
||||
};
|
||||
computed: {
|
||||
doc(): Doc | null {
|
||||
const doc = this.fyo.singles[this.activeTab];
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return doc;
|
||||
},
|
||||
tabLabels(): Record<string, string> {
|
||||
return {
|
||||
[ModelNameEnum.AccountingSettings]: this.t`Accounting`,
|
||||
[ModelNameEnum.PrintSettings]: this.t`Print`,
|
||||
[ModelNameEnum.InventorySettings]: this.t`Inventory`,
|
||||
[ModelNameEnum.Defaults]: this.t`Defaults`,
|
||||
[ModelNameEnum.SystemSettings]: this.t`System`,
|
||||
};
|
||||
},
|
||||
schemas(): Schema[] {
|
||||
const enableInventory =
|
||||
!!this.fyo.singles.AccountingSettings?.enableInventory;
|
||||
|
||||
return [
|
||||
ModelNameEnum.AccountingSettings,
|
||||
ModelNameEnum.PrintSettings,
|
||||
ModelNameEnum.InventorySettings,
|
||||
ModelNameEnum.Defaults,
|
||||
ModelNameEnum.SystemSettings,
|
||||
]
|
||||
.filter((s) =>
|
||||
s === ModelNameEnum.InventorySettings ? enableInventory : true
|
||||
)
|
||||
.map((s) => this.fyo.schemaMap[s]!);
|
||||
},
|
||||
activeGroup() {
|
||||
if (!this.groupedFields) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const group = this.groupedFields.get(this.activeTab);
|
||||
if (!group) {
|
||||
throw new ValidationError(
|
||||
`Tab group ${this.activeTab} has no value set`
|
||||
);
|
||||
}
|
||||
|
||||
return group;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -10,6 +10,10 @@ import { SelectFileOptions, SelectFileReturn } from 'utils/types';
|
||||
import { setLanguageMap } from './language';
|
||||
import { showMessageDialog, showToast } from './ui';
|
||||
|
||||
export function reloadWindow() {
|
||||
return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
|
||||
}
|
||||
|
||||
export async function selectFile(
|
||||
options: SelectFileOptions
|
||||
): Promise<SelectFileReturn> {
|
||||
|
@ -2,7 +2,8 @@ import { ipcRenderer } from 'electron';
|
||||
import { DEFAULT_LANGUAGE } from 'fyo/utils/consts';
|
||||
import { setLanguageMapOnTranslationString } from 'fyo/utils/translation';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
|
||||
import { IPC_ACTIONS } from 'utils/messages';
|
||||
import { reloadWindow } from './ipcCalls';
|
||||
import { systemLanguageRef } from './refs';
|
||||
import { showToast } from './ui';
|
||||
|
||||
@ -47,7 +48,7 @@ export async function setLanguageMap(
|
||||
}
|
||||
|
||||
if (!dontReload && success && initLanguage !== oldLanguage) {
|
||||
await ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
|
||||
reloadWindow();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user