2
0
mirror of https://github.com/frappe/books.git synced 2025-02-08 23:18:31 +00:00

refactor: vue 2 → 3 migration

- render functions
- slots
- creation
This commit is contained in:
18alantom 2022-02-10 12:11:51 +05:30
parent ddf7cd2c62
commit 2f6a2f4cc2
34 changed files with 505 additions and 490 deletions

View File

@ -1,6 +1,6 @@
import router from '@/router'; import router from '@/router';
import frappe from 'frappe'; import frappe, { t } from 'frappe';
import { t } from 'frappe'; import { h } from 'vue';
import PartyWidget from './PartyWidget.vue'; import PartyWidget from './PartyWidget.vue';
export default { export default {
@ -44,9 +44,9 @@ export default {
}, },
], ],
quickEditWidget: (doc) => ({ quickEditWidget: (doc) => ({
render(h) { render() {
return h(PartyWidget, { return h(PartyWidget, {
props: { doc }, doc,
}); });
}, },
}), }),

View File

@ -1,6 +1,6 @@
import router from '@/router'; import router from '@/router';
import frappe from 'frappe'; import frappe, { t } from 'frappe';
import { t } from 'frappe'; import { h } from 'vue';
import PartyWidget from './PartyWidget.vue'; import PartyWidget from './PartyWidget.vue';
export default { export default {
@ -44,9 +44,9 @@ export default {
}, },
], ],
quickEditWidget: (doc) => ({ quickEditWidget: (doc) => ({
render(h) { render() {
return h(PartyWidget, { return h(PartyWidget, {
props: { doc }, doc,
}); });
}, },
}), }),

View File

@ -9,7 +9,7 @@ import electron, {
protocol, protocol,
shell, shell,
} from 'electron'; } from 'electron';
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'; import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer';
import Store from 'electron-store'; import Store from 'electron-store';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import fs from 'fs/promises'; import fs from 'fs/promises';
@ -279,14 +279,8 @@ app.on('activate', () => {
app.on('ready', async () => { app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) { if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
// Devtools extensions are broken in Electron 6.0.0 and greater
// See https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/378 for more info
// Electron will not launch with Devtools extensions installed on Windows 10 with dark mode
// If you are not using Windows 10 dark mode, you may uncomment these lines
// In addition, if the linked issue is closed, you can upgrade electron and uncomment these lines
try { try {
await installExtension(VUEJS_DEVTOOLS); await installExtension(VUEJS3_DEVTOOLS);
} catch (e) { } catch (e) {
console.error('Vue Devtools failed to install:', e.toString()); console.error('Vue Devtools failed to install:', e.toString());
} }

View File

@ -1,7 +1,6 @@
<template> <template v-slot:title>
<a <a
class="cursor-pointer font-semibold flex items-center" class="cursor-pointer font-semibold flex items-center"
slot="title"
@click="$router.back()" @click="$router.back()"
> >
<feather-icon name="chevron-left" class="w-5 h-5" /> <feather-icon name="chevron-left" class="w-5 h-5" />

View File

@ -4,7 +4,6 @@
:style="style" :style="style"
:class="_class" :class="_class"
v-bind="$attrs" v-bind="$attrs"
v-on="$listeners"
> >
<slot></slot> <slot></slot>
</button> </button>

View File

@ -20,7 +20,6 @@
</template> </template>
<script> <script>
import { showMessageDialog } from '../../utils';
export default { export default {
name: 'Base', name: 'Base',
props: [ props: [

View File

@ -25,7 +25,8 @@
</div> </div>
</div> </div>
</template> </template>
<div class="text-sm py-3 px-2 text-center" slot="content"> <template v-slot:content>
<div class="text-sm py-3 px-2 text-center">
<div> <div>
<Row class="border-none" :column-count="5" gap="0.5rem"> <Row class="border-none" :column-count="5" gap="0.5rem">
<div <div
@ -43,11 +44,12 @@
:placeholder="t('Custom Hex')" :placeholder="t('Custom Hex')"
:class="inputClasses" :class="inputClasses"
:value="value" :value="value"
@change="e => setColorValue(e.target.value)" @change="(e) => setColorValue(e.target.value)"
class="bg-gray-100" class="bg-gray-100"
/> />
</div> </div>
</div> </div>
</template>
</Popover> </Popover>
</div> </div>
</template> </template>
@ -62,7 +64,7 @@ export default {
extends: Base, extends: Base,
components: { components: {
Popover, Popover,
Row Row,
}, },
methods: { methods: {
setColorValue(value) { setColorValue(value) {
@ -72,16 +74,16 @@ export default {
if (/^#[0-9A-F]{6}$/i.test(value)) { if (/^#[0-9A-F]{6}$/i.test(value)) {
this.triggerChange(value); this.triggerChange(value);
} }
} },
}, },
computed: { computed: {
colors() { colors() {
return this.df.colors; return this.df.colors;
}, },
selectedColorLabel() { selectedColorLabel() {
let color = this.colors.find(c => this.value === c.value); let color = this.colors.find((c) => this.value === c.value);
return color ? color.label : this.value; return color ? color.label : this.value;
} },
} },
}; };
</script> </script>

View File

@ -29,6 +29,7 @@
<script> <script>
import frappe from 'frappe'; import frappe from 'frappe';
import { markRaw } from 'vue';
import Float from './Float'; import Float from './Float';
export default { export default {

View File

@ -12,10 +12,11 @@ import Float from './Float';
import Currency from './Currency'; import Currency from './Currency';
import Text from './Text'; import Text from './Text';
import Color from './Color'; import Color from './Color';
import { h } from 'vue';
export default { export default {
name: 'FormControl', name: 'FormControl',
render(h) { render() {
let controls = { let controls = {
Data, Data,
Select, Select,
@ -30,13 +31,12 @@ export default {
Float, Float,
Currency, Currency,
Text, Text,
Color Color,
}; };
let { df } = this.$attrs; let { df } = this.$attrs;
return h(controls[df.fieldtype] || Data, { return h(controls[df.fieldtype] || Data, {
props: this.$attrs, ...this.$attrs,
on: this.$listeners, ref: 'control',
ref: 'control'
}); });
}, },
methods: { methods: {
@ -45,6 +45,6 @@ export default {
}, },
getInput() { getInput() {
return this.$refs.control.$refs.input; return this.$refs.control.$refs.input;
} },
} },
}; };

View File

@ -4,11 +4,8 @@
:hide-arrow="true" :hide-arrow="true"
:placement="right ? 'bottom-end' : 'bottom-start'" :placement="right ? 'bottom-end' : 'bottom-start'"
> >
<div <template v-slot:target>
slot="target" <div class="h-full" v-on-outside-click="() => (isShown = false)">
class="h-full"
v-on-outside-click="() => (isShown = false)"
>
<slot <slot
:toggleDropdown="toggleDropdown" :toggleDropdown="toggleDropdown"
:highlightItemUp="highlightItemUp" :highlightItemUp="highlightItemUp"
@ -16,7 +13,9 @@
:selectHighlightedItem="selectHighlightedItem" :selectHighlightedItem="selectHighlightedItem"
></slot> ></slot>
</div> </div>
<div slot="content" class="bg-white rounded w-full min-w-40"> </template>
<template v-slot:content>
<div class="bg-white rounded w-full min-w-40">
<div class="p-1 max-h-64 overflow-auto text-sm"> <div class="p-1 max-h-64 overflow-auto text-sm">
<div v-if="isLoading" class="p-2 text-gray-600 italic"> <div v-if="isLoading" class="p-2 text-gray-600 italic">
{{ t('Loading...') }} {{ t('Loading...') }}
@ -69,6 +68,7 @@
</template> </template>
</div> </div>
</div> </div>
</template>
</Popover> </Popover>
</template> </template>

View File

@ -1,5 +1,6 @@
<script> <script>
import feather from 'feather-icons'; import feather from 'feather-icons';
import { h } from 'vue';
const validIcons = Object.keys(feather.icons); const validIcons = Object.keys(feather.icons);
@ -7,38 +8,29 @@ export default {
props: { props: {
name: { name: {
type: String, type: String,
// default: 'middle-finger',
required: true, required: true,
validator(value) { validator: (value) => validIcons.includes(value),
const valid = validIcons.includes(value);
if (!valid) {
console.warn(
`name property for feather-icon must be one of `,
validIcons
);
}
return valid;
}
}
}, },
render(h) { },
let icon = feather.icons[this.name]; render() {
return h('svg', { const icon = feather.icons[this.name];
attrs: Object.assign({}, icon.attrs, { const svg = h('svg', {
...Object.assign({}, icon.attrs, {
fill: 'none', fill: 'none',
stroke: 'currentColor', stroke: 'currentColor',
'stroke-linecap': 'round', 'stroke-linecap': 'round',
'stroke-linejoin': 'round', 'stroke-linejoin': 'round',
'stroke-width': 1.5, 'stroke-width': 1.5,
width: null, width: null,
height: null height: null,
}), }),
on: this.$listeners,
class: [icon.attrs.class], class: [icon.attrs.class],
domProps: { innerHTML: icon.contents,
innerHTML: icon.contents
}
}); });
}
return svg;
},
}; };
// https://github.com/frappe/frappejs/commits/master/ui/components/FeatherIcon.vue // https://github.com/frappe/frappejs/commits/master/ui/components/FeatherIcon.vue

View File

@ -15,7 +15,8 @@
</span> </span>
</Button> </Button>
</template> </template>
<div slot="content"> <template v-slot:content>
<div>
<div class="p-3"> <div class="p-3">
<template v-if="filters.length"> <template v-if="filters.length">
<div <div
@ -33,10 +34,10 @@
placeholder: 'Field', placeholder: 'Field',
fieldname: 'fieldname', fieldname: 'fieldname',
fieldtype: 'Select', fieldtype: 'Select',
options: fieldOptions options: fieldOptions,
}" }"
:value="filter.fieldname" :value="filter.fieldname"
@change="value => (filter.fieldname = value)" @change="(value) => (filter.fieldname = value)"
/> />
</div> </div>
<div class="ml-2 w-24"> <div class="ml-2 w-24">
@ -47,10 +48,10 @@
placeholder: 'Condition', placeholder: 'Condition',
fieldname: 'condition', fieldname: 'condition',
fieldtype: 'Select', fieldtype: 'Select',
options: conditions options: conditions,
}" }"
:value="filter.condition" :value="filter.condition"
@change="value => (filter.condition = value)" @change="(value) => (filter.condition = value)"
/> />
</div> </div>
<div class="ml-2 w-24"> <div class="ml-2 w-24">
@ -60,15 +61,23 @@
:df="{ :df="{
placeholder: 'Value', placeholder: 'Value',
fieldname: 'value', fieldname: 'value',
fieldtype: 'Data' fieldtype: 'Data',
}" }"
:value="filter.value" :value="filter.value"
@change="value => (filter.value = value)" @change="(value) => (filter.value = value)"
/> />
</div> </div>
</div> </div>
<div <div
class="ml-2 cursor-pointer w-5 h-5 flex-center hover:bg-gray-100 rounded-md" class="
ml-2
cursor-pointer
w-5
h-5
flex-center
hover:bg-gray-100
rounded-md
"
> >
<feather-icon <feather-icon
name="x" name="x"
@ -85,13 +94,24 @@
</template> </template>
</div> </div>
<div <div
class="text-base border-t px-3 py-2 flex items-center text-gray-600 cursor-pointer hover:bg-gray-100" class="
text-base
border-t
px-3
py-2
flex
items-center
text-gray-600
cursor-pointer
hover:bg-gray-100
"
@click="addNewFilter" @click="addNewFilter"
> >
<feather-icon name="plus" class="w-4 h-4" /> <feather-icon name="plus" class="w-4 h-4" />
<span class="ml-2">{{ t('Add a filter') }}</span> <span class="ml-2">{{ t('Add a filter') }}</span>
</div> </div>
</div> </div>
</template>
</Popover> </Popover>
</template> </template>
@ -109,7 +129,7 @@ let conditions = [
{ label: 'Greater Than', value: '>' }, { label: 'Greater Than', value: '>' },
{ label: 'Less Than', value: '<' }, { label: 'Less Than', value: '<' },
{ label: 'Is Empty', value: 'is null' }, { label: 'Is Empty', value: 'is null' },
{ label: 'Is Not Empty', value: 'is not null' } { label: 'Is Not Empty', value: 'is not null' },
]; ];
export default { export default {
@ -118,12 +138,12 @@ export default {
Popover, Popover,
Button, Button,
Icon, Icon,
FormControl FormControl,
}, },
props: ['fields'], props: ['fields'],
data() { data() {
return { return {
filters: [] filters: [],
}; };
}, },
created() { created() {
@ -138,11 +158,11 @@ export default {
this.filters.push({ fieldname, condition, value }); this.filters.push({ fieldname, condition, value });
}, },
removeFilter(filter) { removeFilter(filter) {
this.filters = this.filters.filter(f => f !== filter); this.filters = this.filters.filter((f) => f !== filter);
}, },
setFilter(filters) { setFilter(filters) {
this.filters = []; this.filters = [];
Object.keys(filters).map(fieldname => { Object.keys(filters).map((fieldname) => {
let parts = filters[fieldname]; let parts = filters[fieldname];
let condition, value; let condition, value;
if (Array.isArray(parts)) { if (Array.isArray(parts)) {
@ -166,24 +186,27 @@ export default {
}, {}); }, {});
this.$emit('change', filters); this.$emit('change', filters);
} },
}, },
computed: { computed: {
fieldOptions() { fieldOptions() {
return this.fields.map(df => ({ label: df.label, value: df.fieldname })); return this.fields.map((df) => ({
label: df.label,
value: df.fieldname,
}));
}, },
conditions() { conditions() {
return conditions; return conditions;
}, },
activeFilterCount() { activeFilterCount() {
return this.filters.filter(filter => filter.value).length; return this.filters.filter((filter) => filter.value).length;
}, },
filterAppliedMessage() { filterAppliedMessage() {
if (this.activeFilterCount === 1) { if (this.activeFilterCount === 1) {
return this.t('1 filter applied'); return this.t('1 filter applied');
} }
return this.t('{0} filters applied', [this.activeFilterCount]); return this.t('{0} filters applied', [this.activeFilterCount]);
} },
} },
}; };
</script> </script>

View File

@ -1,7 +1,6 @@
<template> <template>
<component <component
v-bind="$attrs" v-bind="$attrs"
v-on="$listeners"
:is="iconComponent" :is="iconComponent"
:class="iconClasses" :class="iconClasses"
:active="active" :active="active"

View File

@ -55,7 +55,7 @@ export default {
}; };
}, },
mounted() { mounted() {
let listener = (e) => { this.listener = (e) => {
let $els = [this.$refs.reference, this.$refs.popover]; let $els = [this.$refs.reference, this.$refs.popover];
let insideClick = $els.some( let insideClick = $els.some(
($el) => $el && (e.target === $el || $el.contains(e.target)) ($el) => $el && (e.target === $el || $el.contains(e.target))
@ -65,15 +65,17 @@ export default {
} }
this.close(); this.close();
}; };
if (this.show == null) { if (this.show == null) {
document.addEventListener('click', listener); document.addEventListener('click', this.listener);
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('click', listener);
});
} }
}, },
beforeDestroy() { beforeDestroy() {
this.popper && this.popper.destroy(); this.popper && this.popper.destroy();
if (this.listener) {
document.removeEventListener('click', this.listener);
delete this.listener;
}
}, },
methods: { methods: {
setupPopper() { setupPopper() {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="inline-grid border-b" :style="style" v-on="$listeners"> <div class="inline-grid border-b" :style="style" v-bind="$attrs">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
@ -9,21 +9,21 @@ export default {
props: { props: {
columnWidth: { columnWidth: {
type: String, type: String,
default: '1fr' default: '1fr',
}, },
columnCount: { columnCount: {
type: Number, type: Number,
default: 0 default: 0,
}, },
ratio: { ratio: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
gridTemplateColumns: { gridTemplateColumns: {
type: String, type: String,
default: null default: null,
}, },
gap: String gap: String,
}, },
computed: { computed: {
style() { style() {
@ -33,7 +33,9 @@ export default {
obj['grid-template-columns'] = `repeat(${this.columnCount}, ${this.columnWidth})`; obj['grid-template-columns'] = `repeat(${this.columnCount}, ${this.columnWidth})`;
} }
if (this.ratio.length) { if (this.ratio.length) {
obj['grid-template-columns'] = this.ratio.map(r => `${r}fr`).join(' '); obj['grid-template-columns'] = this.ratio
.map((r) => `${r}fr`)
.join(' ');
} }
if (this.gridTemplateColumns) { if (this.gridTemplateColumns) {
obj['grid-template-columns'] = this.gridTemplateColumns; obj['grid-template-columns'] = this.gridTemplateColumns;
@ -43,7 +45,7 @@ export default {
} }
return obj; return obj;
} },
} },
}; };
</script> </script>

View File

@ -48,9 +48,10 @@
:class="isActiveGroup(group) && !group.items ? 'bg-white' : ''" :class="isActiveGroup(group) && !group.items ? 'bg-white' : ''"
@click="onGroupClick(group)" @click="onGroupClick(group)"
> >
<component <Icon
:is="group.icon" :name="group.icon"
class="w-5 h-5" :size="group.iconSize || '18'"
:height="group.iconHeight"
:active="isActiveGroup(group)" :active="isActiveGroup(group)"
/> />
<div <div
@ -102,6 +103,7 @@ import WindowControls from './WindowControls';
import { routeTo } from '@/utils'; import { routeTo } from '@/utils';
import path from 'path'; import path from 'path';
import router from '../router'; import router from '../router';
import Icon from './Icon.vue';
export default { export default {
components: [Button], components: [Button],
@ -123,6 +125,7 @@ export default {
}, },
components: { components: {
WindowControls, WindowControls,
Icon,
}, },
async mounted() { async mounted() {
this.companyName = await sidebarConfig.getTitle(); this.companyName = await sidebarConfig.getTitle();
@ -168,7 +171,7 @@ export default {
routeTo, routeTo,
reportIssue, reportIssue,
setActiveGroup() { setActiveGroup() {
const { fullPath } = this.$router.currentRoute; const { fullPath } = this.$router.currentRoute.value;
const fallBackGroup = this.activeGroup; const fallBackGroup = this.activeGroup;
this.activeGroup = this.groups.find((g) => { this.activeGroup = this.groups.find((g) => {
if (fullPath.startsWith(g.route) && g.route !== '/') { if (fullPath.startsWith(g.route) && g.route !== '/') {

View File

@ -1,6 +1,6 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import frappe from 'frappe'; import frappe from 'frappe';
import Vue from 'vue'; import { createApp } from 'vue';
import models from '../models'; import models from '../models';
import App from './App'; import App from './App';
import FeatherIcon from './components/FeatherIcon'; import FeatherIcon from './components/FeatherIcon';
@ -26,12 +26,29 @@ import { showToast, stringifyCircular } from './utils';
window.frappe = frappe; window.frappe = frappe;
window.frappe.store = {}; window.frappe.store = {};
window.onerror = (message, source, lineno, colno, error) => {
error = error ?? new Error('triggered in window.onerror');
handleError(true, error, { message, source, lineno, colno });
};
process.on('unhandledRejection', (error) => {
handleError(true, error);
});
process.on('uncaughtException', (error) => {
handleError(true, error, () => process.exit(1));
});
registerIpcRendererListeners(); registerIpcRendererListeners();
Vue.config.productionTip = false; const app = createApp({
Vue.component('feather-icon', FeatherIcon); template: '<App/>',
Vue.directive('on-outside-click', outsideClickDirective); });
Vue.mixin({ app.use(router);
app.component('App', App);
app.component('feather-icon', FeatherIcon);
app.directive('on-outside-click', outsideClickDirective);
app.mixin({
computed: { computed: {
frappe() { frappe() {
return frappe; return frappe;
@ -50,7 +67,7 @@ import { showToast, stringifyCircular } from './utils';
}, },
}); });
Vue.config.errorHandler = (err, vm, info) => { app.config.errorHandler = (err, vm, info) => {
const more = { const more = {
info, info,
}; };
@ -67,28 +84,7 @@ import { showToast, stringifyCircular } from './utils';
console.error(err, vm, info); console.error(err, vm, info);
}; };
window.onerror = (message, source, lineno, colno, error) => { app.mount('#app');
error = error ?? new Error('triggered in window.onerror');
handleError(true, error, { message, source, lineno, colno });
};
process.on('unhandledRejection', (error) => {
handleError(true, error);
});
process.on('uncaughtException', (error) => {
handleError(true, error, () => process.exit(1));
});
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: {
App,
},
template: '<App/>',
});
})(); })();
function registerIpcRendererListeners() { function registerIpcRendererListeners() {

View File

@ -1,17 +1,20 @@
<template> <template>
<div class="flex flex-col overflow-y-hidden"> <div class="flex flex-col overflow-y-hidden">
<PageHeader> <PageHeader>
<h1 slot="title" class="text-2xl font-bold"> <template v-slot:title>
<h1 class="text-2xl font-bold">
{{ t('Chart of Accounts') }} {{ t('Chart of Accounts') }}
</h1> </h1>
<template slot="actions"> </template>
<template v-slot:actions>
<SearchBar class="ml-2" /> <SearchBar class="ml-2" />
</template> </template>
</PageHeader> </PageHeader>
<div class="flex-1 flex px-8 overflow-y-auto"> <div class="flex-1 flex px-8 overflow-y-auto">
<div class="flex-1" v-if="root"> <div class="flex-1" v-if="root">
<template v-for="account in allAccounts">
<div <div
v-for="account in allAccounts"
:key="account.name"
class=" class="
mt-2 mt-2
px-4 px-4
@ -25,7 +28,6 @@
account.level !== 0 ? 'text-base' : 'text-lg', account.level !== 0 ? 'text-base' : 'text-lg',
isQuickEditOpen(account) ? 'bg-gray-200' : '', isQuickEditOpen(account) ? 'bg-gray-200' : '',
]" ]"
:key="account.name"
@click="onClick(account)" @click="onClick(account)"
> >
<div class="flex items-center" :class="`pl-${account.level * 8}`"> <div class="flex items-center" :class="`pl-${account.level * 8}`">
@ -37,10 +39,7 @@
> >
{{ account.name }} {{ account.name }}
</div> </div>
<div <div v-if="account.isGroup" class="ml-6 hidden group-hover:block">
v-if="account.isGroup"
class="ml-6 hidden group-hover:block"
>
<button <button
class=" class="
text-xs text-gray-800 text-xs text-gray-800
@ -122,7 +121,6 @@
</div> </div>
</div> </div>
</div> </div>
</template>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,8 +1,10 @@
<template> <template>
<div class="flex flex-col"> <div class="flex flex-col">
<PageHeader> <PageHeader>
<h1 slot="title" class="text-2xl font-bold">{{ t('Dashboard') }}</h1> <template v-slot:title>
<template slot="actions"> <h1 class="text-2xl font-bold">{{ t('Dashboard') }}</h1>
</template>
<template v-slot:actions>
<SearchBar class="ml-2" /> <SearchBar class="ml-2" />
</template> </template>
</PageHeader> </PageHeader>

View File

@ -1,12 +1,10 @@
<template> <template>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<SectionHeader> <SectionHeader>
<template slot="title">{{ t('Top Expenses') }}</template> <template v-slot:title>{{ t('Top Expenses') }}</template>
<PeriodSelector <template v-slot:action>
slot="action" <PeriodSelector :value="period" @change="(value) => (period = value)" />
:value="period" </template>
@change="(value) => (period = value)"
/>
</SectionHeader> </SectionHeader>
<div class="flex relative" v-show="hasData"> <div class="flex relative" v-show="hasData">
<div class="w-1/2"> <div class="w-1/2">
@ -38,7 +36,10 @@
@change="(value) => (active = value)" @change="(value) => (active = value)"
/> />
</div> </div>
<div v-if="expenses.length === 0" class="flex-1 w-full h-full flex-center my-20"> <div
v-if="expenses.length === 0"
class="flex-1 w-full h-full flex-center my-20"
>
<span class="text-base text-gray-600"> <span class="text-base text-gray-600">
{{ t('No expenses in this period') }} {{ t('No expenses in this period') }}
</span> </span>

View File

@ -1,13 +1,14 @@
<template> <template>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<SectionHeader> <SectionHeader>
<template slot="title">{{ t('Profit and Loss') }}</template> <template v-slot:title>{{ t('Profit and Loss') }}</template>
<template v-slot:action>
<PeriodSelector <PeriodSelector
slot="action"
:value="period" :value="period"
:options="['This Year', 'This Quarter']" :options="['This Year', 'This Quarter']"
@change="(value) => (period = value)" @change="(value) => (period = value)"
/> />
</template>
</SectionHeader> </SectionHeader>
<BarChart <BarChart
v-if="hasData" v-if="hasData"

View File

@ -6,22 +6,22 @@
:key="invoice.title" :key="invoice.title"
> >
<SectionHeader> <SectionHeader>
<template slot="title">{{ invoice.title }}</template> <template v-slot:title>{{ invoice.title }}</template>
<template v-slot:action>
<PeriodSelector <PeriodSelector
v-if="invoice.hasData" v-if="invoice.hasData"
slot="action"
:value="$data[invoice.periodKey]" :value="$data[invoice.periodKey]"
@change="(value) => ($data[invoice.periodKey] = value)" @change="(value) => ($data[invoice.periodKey] = value)"
/> />
<Button <Button
v-else v-else
slot="action"
:icon="true" :icon="true"
type="primary" type="primary"
@click="newInvoice(invoice)" @click="newInvoice(invoice)"
> >
<feather-icon name="plus" class="w-4 h-4 text-white" /> <feather-icon name="plus" class="w-4 h-4 text-white" />
</Button> </Button>
</template>
</SectionHeader> </SectionHeader>
<div> <div>
<div class="mt-6 flex justify-between"> <div class="mt-6 flex justify-between">

View File

@ -5,17 +5,23 @@
@change-db-file="$emit('change-db-file')" @change-db-file="$emit('change-db-file')"
/> />
<div class="flex flex-1 overflow-y-hidden bg-white"> <div class="flex flex-1 overflow-y-hidden bg-white">
<router-view class="flex-1" :key="$route.path" v-slot="{ Component }">
<keep-alive> <keep-alive>
<router-view class="flex-1" :key="$route.path" /> <component :is="Component" />
</keep-alive> </keep-alive>
</router-view>
<div class="flex" v-if="showQuickEdit"> <div class="flex" v-if="showQuickEdit">
<keep-alive>
<router-view <router-view
name="edit" name="edit"
class="w-80 flex-1" class="w-80 flex-1"
:key="$route.query.doctype + $route.query.name" :key="$route.query.doctype + $route.query.name"
/> v-slot="{ Component }"
>
<keep-alive>
<component :is="Component" />
</keep-alive> </keep-alive>
</router-view>
</div> </div>
<div <div
id="toast-container" id="toast-container"

View File

@ -1,9 +1,11 @@
<template> <template>
<div class="flex flex-col overflow-y-hidden"> <div class="flex flex-col overflow-y-hidden">
<PageHeader> <PageHeader>
<h1 slot="title" class="text-2xl font-bold"> <template v-slot:title>
<h1 class="text-2xl font-bold">
{{ t('Setup your workspace') }} {{ t('Setup your workspace') }}
</h1> </h1>
</template>
</PageHeader> </PageHeader>
<div class="px-8"> <div class="px-8">
<div class="border-t"></div> <div class="border-t"></div>
@ -91,6 +93,7 @@ import { openSettings } from '@/utils';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { IPC_MESSAGES } from '@/messages'; import { IPC_MESSAGES } from '@/messages';
import { routeTo } from '@/utils'; import { routeTo } from '@/utils';
import { h } from 'vue';
export default { export default {
name: 'GetStarted', name: 'GetStarted',
@ -391,9 +394,9 @@ export default {
let size = completed ? '24' : '18'; let size = completed ? '24' : '18';
return { return {
name, name,
render(h) { render() {
return h(Icon, { return h(Icon, {
props: Object.assign( ...Object.assign(
{ {
name, name,
size, size,

View File

@ -1,8 +1,10 @@
<template> <template>
<div class="flex flex-col" v-if="doc"> <div class="flex flex-col" v-if="doc">
<PageHeader> <PageHeader>
<BackLink slot="title" /> <template v-slot:title>
<template slot="actions"> <BackLink />
</template>
<template v-slot:actions>
<StatusBadge :status="status" /> <StatusBadge :status="status" />
<Button <Button
v-if="doc.submitted" v-if="doc.submitted"

View File

@ -1,8 +1,10 @@
<template> <template>
<div class="flex flex-col"> <div class="flex flex-col">
<PageHeader> <PageHeader>
<BackLink slot="title" /> <template v-slot:title>
<template slot="actions" v-if="doc"> <BackLink />
</template>
<template v-slot:actions v-if="doc">
<StatusBadge :status="status" /> <StatusBadge :status="status" />
<DropdownWithActions class="ml-2" :actions="actions" /> <DropdownWithActions class="ml-2" :actions="actions" />
<Button <Button
@ -123,11 +125,7 @@ import DropdownWithActions from '@/components/DropdownWithActions';
import FormControl from '@/components/Controls/FormControl'; import FormControl from '@/components/Controls/FormControl';
import BackLink from '@/components/BackLink'; import BackLink from '@/components/BackLink';
import StatusBadge from '@/components/StatusBadge'; import StatusBadge from '@/components/StatusBadge';
import { import { showMessageDialog, getActionsForDocument, routeTo } from '@/utils';
showMessageDialog,
getActionsForDocument,
routeTo,
} from '@/utils';
import { handleErrorWithDialog } from '../errorHandling'; import { handleErrorWithDialog } from '../errorHandling';
export default { export default {

View File

@ -1,8 +1,12 @@
<template> <template>
<div class="flex flex-col"> <div class="flex flex-col">
<PageHeader> <PageHeader>
<h1 slot="title" class="text-2xl font-bold" v-if="title">{{ title }}</h1> <template v-slot:title>
<template slot="actions"> <h1 class="text-2xl font-bold" v-if="title">
{{ title }}
</h1>
</template>
<template v-slot:actions>
<FilterDropdown <FilterDropdown
ref="filterDropdown" ref="filterDropdown"
@change="applyFilter" @change="applyFilter"
@ -35,7 +39,7 @@ import List from './List';
import listConfigs from './listConfig'; import listConfigs from './listConfig';
// import Icon from '@/components/Icon'; // import Icon from '@/components/Icon';
import FilterDropdown from '@/components/FilterDropdown'; import FilterDropdown from '@/components/FilterDropdown';
import { routeTo } from '@/utils' import { routeTo } from '@/utils';
export default { export default {
name: 'ListView', name: 'ListView',
@ -46,7 +50,7 @@ export default {
Button, Button,
SearchBar, SearchBar,
// Icon, // Icon,
FilterDropdown FilterDropdown,
}, },
activated() { activated() {
if (typeof this.filters === 'object') { if (typeof this.filters === 'object') {
@ -83,10 +87,10 @@ export default {
query: { query: {
edit: 1, edit: 1,
doctype: this.doctype, doctype: this.doctype,
name name,
} },
}; };
} },
}, },
computed: { computed: {
meta() { meta() {
@ -99,13 +103,13 @@ export default {
return { return {
title: this.doctype, title: this.doctype,
doctype: this.doctype, doctype: this.doctype,
columns: this.meta.getKeywordFields() columns: this.meta.getKeywordFields(),
}; };
} }
}, },
title() { title() {
return this.listConfig.title || this.doctype; return this.listConfig.title || this.doctype;
} },
} },
}; };
</script> </script>

View File

@ -2,8 +2,10 @@
<div class="flex"> <div class="flex">
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<PageHeader class="bg-white z-10"> <PageHeader class="bg-white z-10">
<BackLink slot="title" /> <template v-slot:title>
<template slot="actions"> <BackLink />
</template>
<template v-slot:actions>
<Button <Button
class="text-gray-900 text-xs ml-2" class="text-gray-900 text-xs ml-2"
@click="showCustomiser = !showCustomiser" @click="showCustomiser = !showCustomiser"

View File

@ -1,8 +1,10 @@
<template> <template>
<div class="flex flex-col max-w-full"> <div class="flex flex-col max-w-full">
<PageHeader> <PageHeader>
<h1 slot="title" class="text-2xl font-bold">{{ report.title }}</h1> <template v-slot:title>
<template slot="actions"> <h1 class="text-2xl font-bold">{{ report.title }}</h1>
</template>
<template v-slot:actions>
<DropdownWithActions <DropdownWithActions
v-for="group of actionGroups" v-for="group of actionGroups"
@click="group.action(reportData, filters)" @click="group.action(reportData, filters)"
@ -104,6 +106,7 @@ import WithScroll from '@/components/WithScroll';
import FormControl from '@/components/Controls/FormControl'; import FormControl from '@/components/Controls/FormControl';
import DropdownWithActions from '../components/DropdownWithActions.vue'; import DropdownWithActions from '../components/DropdownWithActions.vue';
import reportViewConfig from '@/../reports/view'; import reportViewConfig from '@/../reports/view';
import { h } from 'vue';
export default { export default {
name: 'Report', name: 'Report',
@ -161,7 +164,10 @@ export default {
rows = data; rows = data;
} }
this.reportData.columns = this.report.getColumns({ filters: this.filters, data }); this.reportData.columns = this.report.getColumns({
filters: this.filters,
data,
});
if (!rows) { if (!rows) {
rows = []; rows = [];
@ -250,7 +256,7 @@ export default {
? frappe.format(cellValue, column) ? frappe.format(cellValue, column)
: ''; : '';
return { return {
render(h) { render() {
return h('span', formattedValue); return h('span', formattedValue);
}, },
}; };

View File

@ -1,9 +1,11 @@
<template> <template>
<div class="flex flex-col overflow-hidden"> <div class="flex flex-col overflow-hidden">
<PageHeader> <PageHeader>
<h1 slot="title" class="text-2xl font-bold"> <template v-slot:title>
<h1 class="text-2xl font-bold">
{{ t('Settings') }} {{ t('Settings') }}
</h1> </h1>
</template>
</PageHeader> </PageHeader>
<div class="flex justify-center flex-1 mb-8 mt-2"> <div class="flex justify-center flex-1 mb-8 mt-2">
<div <div
@ -60,6 +62,7 @@ import PageHeader from '@/components/PageHeader';
import StatusBadge from '@/components/StatusBadge'; import StatusBadge from '@/components/StatusBadge';
import { callInitializeMoneyMaker } from '../../utils'; import { callInitializeMoneyMaker } from '../../utils';
import { showToast } from '../../utils'; import { showToast } from '../../utils';
import { h } from 'vue';
export default { export default {
name: 'Settings', name: 'Settings',
@ -138,10 +141,10 @@ export default {
}, },
getIconComponent(tab) { getIconComponent(tab) {
return { return {
render(h) { render() {
return h(Icon, { return h(Icon, {
class: 'w-6 h-6', class: 'w-6 h-6',
props: Object.assign( ...Object.assign(
{ {
name: tab.icon, name: tab.icon,
size: '24', size: '24',

View File

@ -29,7 +29,7 @@
:autofocus="true" :autofocus="true"
/> />
<Popover placement="auto" :show-popup="Boolean(emailError)"> <Popover placement="auto" :show-popup="Boolean(emailError)">
<template slot="target"> <template v-slot:target>
<FormControl <FormControl
:df="meta.getField('email')" :df="meta.getField('email')"
:value="doc.email" :value="doc.email"
@ -41,7 +41,7 @@
" "
/> />
</template> </template>
<template slot="content"> <template v-slot:content>
<div class="p-2 text-sm"> <div class="p-2 text-sm">
{{ emailError }} {{ emailError }}
</div> </div>

View File

@ -10,12 +10,7 @@ import PrintView from '@/pages/PrintView/PrintView';
import QuickEditForm from '@/pages/QuickEditForm'; import QuickEditForm from '@/pages/QuickEditForm';
import Report from '@/pages/Report'; import Report from '@/pages/Report';
import Settings from '@/pages/Settings/Settings'; import Settings from '@/pages/Settings/Settings';
import Vue from 'vue'; import { createRouter, createWebHistory } from 'vue-router';
import Router from 'vue-router';
Vue.use(Router);
const routes = [ const routes = [
{ {
@ -107,7 +102,7 @@ const routes = [
}, },
]; ];
let router = new Router({ routes }); let router = createRouter({ routes, history: createWebHistory() });
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
window.router = router; window.router = router;

View File

@ -1,5 +1,4 @@
import frappe, { t } from 'frappe'; import frappe, { t } from 'frappe';
import Icon from './components/Icon';
const config = { const config = {
getTitle: async () => { getTitle: async () => {
@ -10,16 +9,18 @@ const config = {
{ {
title: t('Get Started'), title: t('Get Started'),
route: '/get-started', route: '/get-started',
icon: getIcon('general', '24', '5'), icon: 'general',
iconSize: '24',
iconHeight: '5',
}, },
{ {
title: t('Dashboard'), title: t('Dashboard'),
route: '/', route: '/',
icon: getIcon('dashboard'), icon: 'dashboard',
}, },
{ {
title: t('Sales'), title: t('Sales'),
icon: getIcon('sales'), icon: 'sales',
route: '/list/SalesInvoice', route: '/list/SalesInvoice',
items: [ items: [
{ {
@ -36,7 +37,7 @@ const config = {
}, },
{ {
title: t('Purchases'), title: t('Purchases'),
icon: getIcon('purchase'), icon: 'purchase',
route: '/list/PurchaseInvoice', route: '/list/PurchaseInvoice',
items: [ items: [
{ {
@ -53,7 +54,7 @@ const config = {
}, },
{ {
title: t('Common'), title: t('Common'),
icon: getIcon('common-entries'), icon: 'common-entries',
route: '/list/Item', route: '/list/Item',
items: [ items: [
{ {
@ -75,7 +76,7 @@ const config = {
}, },
{ {
title: t('Reports'), title: t('Reports'),
icon: getIcon('reports'), icon: 'reports',
route: '/report/general-ledger', route: '/report/general-ledger',
items: [ items: [
{ {
@ -108,7 +109,7 @@ const config = {
}, },
{ {
title: t('Setup'), title: t('Setup'),
icon: getIcon('settings'), icon: 'settings',
route: '/chart-of-accounts', route: '/chart-of-accounts',
items: [ items: [
{ {
@ -129,22 +130,4 @@ const config = {
], ],
}; };
function getIcon(name, size = '18', height = null) {
return {
name,
render(h) {
return h(Icon, {
props: Object.assign(
{
name,
size,
height,
},
this.$attrs
),
});
},
};
}
export default config; export default config;

View File

@ -5,7 +5,7 @@ import { ipcRenderer } from 'electron';
import frappe, { t } from 'frappe'; import frappe, { t } from 'frappe';
import { isPesa } from 'frappe/utils'; import { isPesa } from 'frappe/utils';
import lodash from 'lodash'; import lodash from 'lodash';
import Vue from 'vue'; import { createApp } from 'vue';
import { handleErrorWithDialog } from './errorHandling'; import { handleErrorWithDialog } from './errorHandling';
import { IPC_ACTIONS, IPC_MESSAGES } from './messages'; import { IPC_ACTIONS, IPC_MESSAGES } from './messages';
@ -333,7 +333,7 @@ export async function getSavePath(name, extention) {
} }
export function showToast(props) { export function showToast(props) {
new Vue({ createApp({
el: '#toast-target', el: '#toast-target',
render(createElement) { render(createElement) {
return createElement(Toast, { props }); return createElement(Toast, { props });