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:
parent
f94ae5c2d2
commit
703b1d2138
@ -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
|
||||
}
|
||||
],
|
||||
|
@ -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() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :title="df.label">
|
||||
<div :class="labelClasses" v-if="showLabel">
|
||||
{{ df.label }}
|
||||
</div>
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user