2
0
mirror of https://github.com/frappe/books.git synced 2025-01-22 22:58:28 +00:00

fix(ui): use common form like ui for setup wizard

This commit is contained in:
18alantom 2023-03-15 12:48:10 +05:30
parent f94ae5c2d2
commit 703b1d2138
9 changed files with 231 additions and 202 deletions

View File

@ -8,18 +8,20 @@
{ {
"fieldname": "logo", "fieldname": "logo",
"label": "Company Logo", "label": "Company Logo",
"fieldtype": "AttachImage" "fieldtype": "AttachImage",
"section": "Default"
}, },
{ {
"fieldname": "country", "fieldname": "companyName",
"label": "Country", "label": "Company Name",
"fieldtype": "AutoComplete", "placeholder": "Company Name",
"placeholder": "Select Country", "fieldtype": "Data",
"required": true "required": true,
"section": "Default"
}, },
{ {
"fieldname": "fullname", "fieldname": "fullname",
"label": "Your Name", "label": "Full Name",
"fieldtype": "Data", "fieldtype": "Data",
"placeholder": "John Doe", "placeholder": "John Doe",
"required": true "required": true
@ -32,31 +34,11 @@
"required": true "required": true
}, },
{ {
"fieldname": "companyName", "fieldname": "country",
"label": "Company Name", "label": "Country",
"placeholder": "Company Name", "fieldtype": "AutoComplete",
"fieldtype": "Data", "placeholder": "Select Country",
"required": true "section": "Locale",
},
{
"fieldname": "bankName",
"label": "Bank Name",
"fieldtype": "Data",
"placeholder": "Prime Bank",
"required": true
},
{
"fieldname": "fiscalYearStart",
"label": "Fiscal Year Start Date",
"placeholder": "Fiscal Year Start Date",
"fieldtype": "Date",
"required": true
},
{
"fieldname": "fiscalYearEnd",
"label": "Fiscal Year End Date",
"placeholder": "Fiscal Year End Date",
"fieldtype": "Date",
"required": true "required": true
}, },
{ {
@ -64,6 +46,15 @@
"label": "Currency", "label": "Currency",
"fieldtype": "Data", "fieldtype": "Data",
"placeholder": "Currency", "placeholder": "Currency",
"section": "Locale",
"required": true
},
{
"fieldname": "bankName",
"label": "Bank Name",
"fieldtype": "Data",
"placeholder": "Prime Bank",
"section": "Accounting",
"required": true "required": true
}, },
{ {
@ -71,12 +62,30 @@
"label": "Chart of Accounts", "label": "Chart of Accounts",
"fieldtype": "AutoComplete", "fieldtype": "AutoComplete",
"placeholder": "Select CoA", "placeholder": "Select CoA",
"section": "Accounting",
"required": true
},
{
"fieldname": "fiscalYearStart",
"label": "Fiscal Year Start Date",
"placeholder": "Fiscal Year Start Date",
"fieldtype": "Date",
"section": "Accounting",
"required": true
},
{
"fieldname": "fiscalYearEnd",
"label": "Fiscal Year End Date",
"placeholder": "Fiscal Year End Date",
"fieldtype": "Date",
"section": "Accounting",
"required": true "required": true
}, },
{ {
"fieldname": "completed", "fieldname": "completed",
"label": "Completed", "label": "Completed",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": true,
"readOnly": true "readOnly": true
} }
], ],

View File

@ -13,6 +13,7 @@
'w-20 h-20': size !== 'small', 'w-20 h-20': size !== 'small',
'w-12 h-12': size === 'small', 'w-12 h-12': size === 'small',
}" }"
:title="df?.label"
> >
<img :src="value" v-if="value" /> <img :src="value" v-if="value" />
<div :class="[!isReadOnly ? 'group-hover:opacity-90' : '']" v-else> <div :class="[!isReadOnly ? 'group-hover:opacity-90' : '']" v-else>
@ -60,10 +61,11 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Field } from 'schemas/types';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { selectFile } from 'src/utils/ipcCalls'; import { selectFile } from 'src/utils/ipcCalls';
import { getDataURL } from 'src/utils/misc'; import { getDataURL } from 'src/utils/misc';
import { defineComponent } from 'vue'; import { defineComponent, PropType } from 'vue';
import FeatherIcon from '../FeatherIcon.vue'; import FeatherIcon from '../FeatherIcon.vue';
import Base from './Base.vue'; import Base from './Base.vue';
@ -73,6 +75,7 @@ export default defineComponent({
props: { props: {
letterPlaceholder: { type: String, default: '' }, letterPlaceholder: { type: String, default: '' },
value: { type: String, default: '' }, value: { type: String, default: '' },
df: { type: Object as PropType<Field> },
}, },
methods: { methods: {
async handleClick() { async handleClick() {

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div :title="df.label">
<div :class="labelClasses" v-if="showLabel"> <div :class="labelClasses" v-if="showLabel">
{{ df.label }} {{ df.label }}
</div> </div>

View File

@ -2,7 +2,12 @@
<div class="flex bg-gray-25 overflow-x-auto"> <div class="flex bg-gray-25 overflow-x-auto">
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<!-- Page Header (Title, Buttons, etc) --> <!-- Page Header (Title, Buttons, etc) -->
<PageHeader :title="title" :border="false" :searchborder="searchborder"> <PageHeader
v-if="showHeader"
:title="title"
:border="false"
:searchborder="searchborder"
>
<template #left> <template #left>
<slot name="header-left" /> <slot name="header-left" />
</template> </template>
@ -32,13 +37,16 @@
<slot name="quickedit" /> <slot name="quickedit" />
</div> </div>
</template> </template>
<script> <script lang="ts">
import { defineComponent } from 'vue';
import PageHeader from './PageHeader.vue'; import PageHeader from './PageHeader.vue';
export default {
export default defineComponent({
components: { PageHeader }, components: { PageHeader },
props: { props: {
title: { type: String, default: '' }, title: { type: String, default: '' },
showHeader: { type: Boolean, default: true },
searchborder: { type: Boolean, default: true }, searchborder: { type: Boolean, default: true },
}, },
}; });
</script> </script>

View File

@ -151,7 +151,7 @@ export default defineComponent({
return { return {
errors: {}, errors: {},
docOrNull: null, docOrNull: null,
activeTab: 'Default', activeTab: this.t`Default`,
groupedFields: null, groupedFields: null,
quickEditDoc: null, quickEditDoc: null,
isPrintable: false, isPrintable: false,
@ -173,6 +173,9 @@ export default defineComponent({
await this.setDoc(); await this.setDoc();
focusedDocsRef.add(this.docOrNull); focusedDocsRef.add(this.docOrNull);
this.updateGroupedFields(); this.updateGroupedFields();
if (this.groupedFields) {
this.activeTab = [...this.groupedFields.keys()][0];
}
this.isPrintable = await isPrintable(this.schemaName); this.isPrintable = await isPrintable(this.schemaName);
}, },
activated(): void { activated(): void {

View File

@ -2,14 +2,15 @@
<div v-if="(fields ?? []).length > 0"> <div v-if="(fields ?? []).length > 0">
<div <div
v-if="showTitle && title" v-if="showTitle && title"
class="flex justify-between items-center cursor-pointer select-none" class="flex justify-between items-center select-none"
:class="collapsed ? '' : 'mb-4'" :class="[collapsed ? '' : 'mb-4', collapsible ? 'cursor-pointer' : '']"
@click="collapsed = !collapsed" @click="toggleCollapsed"
> >
<h2 class="text-base text-gray-900 font-semibold"> <h2 class="text-base text-gray-900 font-semibold">
{{ title }} {{ title }}
</h2> </h2>
<feather-icon <feather-icon
v-if="collapsible"
:name="collapsed ? 'chevron-up' : 'chevron-down'" :name="collapsed ? 'chevron-up' : 'chevron-down'"
class="w-4 h-4 text-gray-600" class="w-4 h-4 text-gray-600"
/> />
@ -54,6 +55,7 @@ export default defineComponent({
errors: Object as PropType<Record<string, string>>, errors: Object as PropType<Record<string, string>>,
showTitle: Boolean, showTitle: Boolean,
doc: { type: Object as PropType<Doc>, required: true }, doc: { type: Object as PropType<Doc>, required: true },
collapsible: { type: Boolean, default: true },
fields: Array as PropType<Field[]>, fields: Array as PropType<Field[]>,
}, },
data() { data() {
@ -64,6 +66,15 @@ export default defineComponent({
mounted() { mounted() {
focusOrSelectFormControl(this.doc, this.$refs.nameField); focusOrSelectFormControl(this.doc, this.$refs.nameField);
}, },
methods: {
toggleCollapsed() {
if (!this.collapsible) {
return;
}
this.collapsed = !this.collapsed;
},
},
components: { FormControl }, components: { FormControl },
}); });
</script> </script>

View File

@ -1,146 +1,161 @@
<template> <template>
<div <FormContainer
class="flex-1 bg-gray-25 flex justify-center items-center" :show-header="false"
class="justify-content items-center h-full"
:class="{ 'window-drag': platform !== 'Windows' }" :class="{ 'window-drag': platform !== 'Windows' }"
> >
<!-- Setup Wizard Slide --> <template #body>
<Slide <FormHeader
:primary-disabled="!valuesFilled || loading" :form-title="t`Set up your organization`"
:secondary-disabled="loading" class="sticky top-0 bg-white border-b"
@primary-clicked="submit()" >
@secondary-clicked="$emit('setup-canceled')" </FormHeader>
:class="{ 'window-no-drag': platform !== 'Windows' }"
>
<template #title>
{{ t`Set up your organization` }}
</template>
<template #content> <!-- Section Container -->
<div v-if="doc"> <div class="overflow-auto custom-scroll" v-if="hasDoc">
<!-- Image Section --> <CommonFormSection
<div class="flex items-center p-4 gap-4"> v-for="([name, fields], idx) in activeGroup.entries()"
<FormControl :key="name + idx"
:df="getField('logo')" ref="section"
:value="doc.logo" class="p-4"
:read-only="loading" :class="idx !== 0 && activeGroup.size > 1 ? 'border-t' : ''"
@change="(value) => setValue('logo', value)" :show-title="activeGroup.size > 1 && name !== t`Default`"
/> :title="name"
<div> :fields="fields"
<FormControl :doc="doc"
ref="companyField" :errors="errors"
:df="getField('companyName')" :collapsible="false"
:value="doc.companyName" @value-change="onValueChange"
:read-only="loading" />
@change="(value) => setValue('companyName', value)" </div>
input-class="
font-semibold
text-xl
"
:autofocus="true"
/>
<FormControl
:df="getField('email')"
:value="doc.email"
:read-only="loading"
@change="(value) => setValue('email', value)"
/>
</div>
</div>
<p <!-- Buttons Bar -->
class="-mt-6 text-sm absolute text-red-400 w-full" <div
style="left: 7.75rem" class="
v-if="emailError" mt-auto
> p-4
{{ emailError }} flex
</p> items-center
justify-between
<TwoColumnForm :doc="doc" :read-only="loading" /> border-t
<Button flex-shrink-0
v-if="fyo.store.isDevelopment" sticky
class="m-4 text-sm min-w-28" bottom-0
@click="fill" bg-white
>Fill</Button "
> >
</div> <p v-if="loading" class="text-base text-gray-600">
</template> {{ t`Loading instance...` }}
<template #secondaryButton>{{ t`Cancel` }}</template> </p>
<template #primaryButton>{{ <Button v-if="!loading" class="w-24" @click="$emit('setup-canceled')">{{
loading ? t`Setting up...` : t`Submit` t`Cancel`
}}</template> }}</Button>
</Slide> <Button
</div> v-if="fyo.store.isDevelopment && !loading"
class="w-24 ml-auto mr-4"
:disabled="loading"
@click="fill"
>Fill</Button
>
<Button
type="primary"
class="w-24"
:disabled="!areAllValuesFilled || loading"
@click="submit"
>{{ t`Submit` }}</Button
>
</div>
</template>
</FormContainer>
</template> </template>
<script lang="ts">
<script> import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import { ValidationError } from 'fyo/utils/errors';
import { Field } from 'schemas/types';
import Button from 'src/components/Button.vue'; import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl.vue'; import FormContainer from 'src/components/FormContainer.vue';
import TwoColumnForm from 'src/components/TwoColumnForm.vue'; import FormHeader from 'src/components/FormHeader.vue';
import { getErrorMessage } from 'src/utils'; import { getErrorMessage } from 'src/utils';
import { getSetupWizardDoc } from 'src/utils/misc'; import { getSetupWizardDoc } from 'src/utils/misc';
import { showMessageDialog } from 'src/utils/ui'; import {
import Slide from './Slide.vue'; getFieldsGroupedByTabAndSection,
showMessageDialog,
} from 'src/utils/ui';
import { computed, defineComponent } from 'vue';
import CommonFormSection from '../CommonForm/CommonFormSection.vue';
export default { export default defineComponent({
name: 'SetupWizard', name: 'SetupWizard',
emits: ['setup-complete', 'setup-canceled'], emits: ['setup-complete', 'setup-canceled'],
data() { data() {
return { return {
doc: null, docOrNull: null,
errors: {},
loading: false, loading: false,
valuesFilled: false, } as {
emailError: null, errors: Record<string, string>;
docOrNull: null | Doc;
loading: boolean;
}; };
}, },
provide() { provide() {
return { return {
schemaName: 'SetupWizard', schemaName: computed(() => this.docOrNull?.schemaName),
name: 'SetupWizard', name: computed(() => this.docOrNull?.name),
doc: computed(() => this.docOrNull),
}; };
}, },
components: { components: {
TwoColumnForm,
FormControl,
Slide,
Button, Button,
FormContainer,
FormHeader,
CommonFormSection,
}, },
async mounted() { async mounted() {
this.doc = await getSetupWizardDoc(); this.docOrNull = getSetupWizardDoc();
this.doc.on('change', () => {
this.valuesFilled = this.allValuesFilled();
});
if (this.fyo.store.isDevelopment) { if (this.fyo.store.isDevelopment) {
// @ts-ignore
window.sw = this; window.sw = this;
} }
}, },
methods: { methods: {
async fill() { async fill() {
if (!this.hasDoc) {
return;
}
await this.doc.set('companyName', "Lin's Things"); await this.doc.set('companyName', "Lin's Things");
await this.doc.set('email', 'lin@lthings.com'); await this.doc.set('email', 'lin@lthings.com');
await this.doc.set('fullname', 'Lin Slovenly'); await this.doc.set('fullname', 'Lin Slovenly');
await this.doc.set('bankName', 'Max Finance'); await this.doc.set('bankName', 'Max Finance');
await this.doc.set('country', 'India'); await this.doc.set('country', 'India');
}, },
getField(fieldname) { async onValueChange(field: Field, value: DocValue) {
return this.doc.schema?.fields.find((f) => f.fieldname === fieldname); if (!this.hasDoc) {
}, return;
setValue(fieldname, value) { }
this.emailError = null;
this.doc.set(fieldname, value).catch((e) => { const { fieldname } = field;
if (fieldname === 'email') { delete this.errors[fieldname];
this.emailError = getErrorMessage(e, this.doc);
try {
await this.doc.set(fieldname, value);
} catch (err) {
if (!(err instanceof Error)) {
return;
} }
});
}, this.errors[fieldname] = getErrorMessage(err, this.doc as Doc);
allValuesFilled() { }
const values = this.doc.schema.fields
.filter((f) => f.required)
.map((f) => this.doc[f.fieldname]);
return values.every(Boolean);
}, },
async submit() { async submit() {
if (!this.allValuesFilled()) { if (!this.hasDoc) {
return;
}
if (!this.areAllValuesFilled) {
return await showMessageDialog({ return await showMessageDialog({
message: this.t`Please fill all values`, message: this.t`Please fill all values`,
}); });
@ -150,5 +165,40 @@ export default {
this.$emit('setup-complete', this.doc.getValidDict()); this.$emit('setup-complete', this.doc.getValidDict());
}, },
}, },
}; computed: {
hasDoc(): boolean {
return this.docOrNull instanceof Doc;
},
doc(): Doc {
if (this.docOrNull instanceof Doc) {
return this.docOrNull;
}
throw new Error(`Doc is null`);
},
areAllValuesFilled(): boolean {
if (!this.hasDoc) {
return false;
}
const values = this.doc.schema.fields
.filter((f) => f.required)
.map((f) => this.doc[f.fieldname]);
return values.every(Boolean);
},
activeGroup(): Map<string, Field[]> {
if (!this.hasDoc) {
return new Map();
}
const groupedFields = getFieldsGroupedByTabAndSection(
this.doc.schema,
this.doc
);
return [...groupedFields.values()][0];
},
},
});
</script> </script>

View File

@ -1,55 +0,0 @@
<template>
<div
class="w-form shadow-lg rounded-lg border relative bg-white"
style="height: 700px"
>
<!-- Slide Title -->
<div class="p-4">
<h1 class="text-2xl font-semibold select-none">
<slot name="title"></slot>
</h1>
</div>
<hr />
<!-- Slide Content -->
<div>
<slot name="content"></slot>
</div>
<!-- Slide Buttons -->
<div
class="flex justify-between px-4 pb-4 absolute w-form"
style="top: 100%; transform: translateY(-100%)"
>
<Button
class="text-sm text-grey-900 min-w-28"
@click="$emit('secondary-clicked')"
:disabled="secondaryDisabled"
>
<slot name="secondaryButton"></slot>
</Button>
<Button
@click="$emit('primary-clicked')"
type="primary"
class="text-sm text-white min-w-28"
:disabled="primaryDisabled"
>
<slot name="primaryButton"></slot>
</Button>
</div>
</div>
</template>
<script>
import Button from 'src/components/Button.vue';
export default {
emits: ['primary-clicked', 'secondary-clicked'],
components: { Button },
props: {
usePrimary: { type: Boolean, default: true },
primaryDisabled: { type: Boolean, default: false },
secondaryDisabled: { type: Boolean, default: false },
},
};
</script>

View File

@ -49,12 +49,12 @@ export function getDatesAndPeriodList(period: PeriodKey): {
}; };
} }
export async function getSetupWizardDoc() { export function getSetupWizardDoc() {
/** /**
* This is used cause when setup wizard is running * This is used cause when setup wizard is running
* the database isn't yet initialized. * the database isn't yet initialized.
*/ */
return await fyo.doc.getNewDoc( return fyo.doc.getNewDoc(
'SetupWizard', 'SetupWizard',
{}, {},
false, false,