2
0
mirror of https://github.com/frappe/books.git synced 2025-01-22 14:48:25 +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",
"label": "Company Logo",
"fieldtype": "AttachImage"
"fieldtype": "AttachImage",
"section": "Default"
},
{
"fieldname": "country",
"label": "Country",
"fieldtype": "AutoComplete",
"placeholder": "Select Country",
"required": true
"fieldname": "companyName",
"label": "Company Name",
"placeholder": "Company Name",
"fieldtype": "Data",
"required": true,
"section": "Default"
},
{
"fieldname": "fullname",
"label": "Your Name",
"label": "Full Name",
"fieldtype": "Data",
"placeholder": "John Doe",
"required": true
@ -32,31 +34,11 @@
"required": true
},
{
"fieldname": "companyName",
"label": "Company Name",
"placeholder": "Company Name",
"fieldtype": "Data",
"required": true
},
{
"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",
"fieldname": "country",
"label": "Country",
"fieldtype": "AutoComplete",
"placeholder": "Select Country",
"section": "Locale",
"required": true
},
{
@ -64,6 +46,15 @@
"label": "Currency",
"fieldtype": "Data",
"placeholder": "Currency",
"section": "Locale",
"required": true
},
{
"fieldname": "bankName",
"label": "Bank Name",
"fieldtype": "Data",
"placeholder": "Prime Bank",
"section": "Accounting",
"required": true
},
{
@ -71,12 +62,30 @@
"label": "Chart of Accounts",
"fieldtype": "AutoComplete",
"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
},
{
"fieldname": "completed",
"label": "Completed",
"fieldtype": "Check",
"hidden": true,
"readOnly": true
}
],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
* the database isn't yet initialized.
*/
return await fyo.doc.getNewDoc(
return fyo.doc.getNewDoc(
'SetupWizard',
{},
false,