2
0
mirror of https://github.com/frappe/books.git synced 2024-11-09 23:30:56 +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 frappe from 'frappe';
import { t } from 'frappe';
import frappe, { t } from 'frappe';
import { h } from 'vue';
import PartyWidget from './PartyWidget.vue';
export default {
@ -44,9 +44,9 @@ export default {
},
],
quickEditWidget: (doc) => ({
render(h) {
render() {
return h(PartyWidget, {
props: { doc },
doc,
});
},
}),

View File

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

View File

@ -9,7 +9,7 @@ import electron, {
protocol,
shell,
} from 'electron';
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer';
import Store from 'electron-store';
import { autoUpdater } from 'electron-updater';
import fs from 'fs/promises';
@ -279,14 +279,8 @@ app.on('activate', () => {
app.on('ready', async () => {
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 {
await installExtension(VUEJS_DEVTOOLS);
await installExtension(VUEJS3_DEVTOOLS);
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString());
}

View File

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

View File

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

View File

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

View File

@ -25,29 +25,31 @@
</div>
</div>
</template>
<div class="text-sm py-3 px-2 text-center" slot="content">
<div>
<Row class="border-none" :column-count="5" gap="0.5rem">
<div
v-for="color in colors"
:key="color.value"
class="w-4 h-4 rounded cursor-pointer"
:style="{ backgroundColor: color.value }"
@click="setColorValue(color.value)"
></div>
</Row>
<template v-slot:content>
<div class="text-sm py-3 px-2 text-center">
<div>
<Row class="border-none" :column-count="5" gap="0.5rem">
<div
v-for="color in colors"
:key="color.value"
class="w-4 h-4 rounded cursor-pointer"
:style="{ backgroundColor: color.value }"
@click="setColorValue(color.value)"
></div>
</Row>
</div>
<div class="mt-3 w-28">
<input
type="text"
:placeholder="t('Custom Hex')"
:class="inputClasses"
:value="value"
@change="(e) => setColorValue(e.target.value)"
class="bg-gray-100"
/>
</div>
</div>
<div class="mt-3 w-28">
<input
type="text"
:placeholder="t('Custom Hex')"
:class="inputClasses"
:value="value"
@change="e => setColorValue(e.target.value)"
class="bg-gray-100"
/>
</div>
</div>
</template>
</Popover>
</div>
</template>
@ -62,7 +64,7 @@ export default {
extends: Base,
components: {
Popover,
Row
Row,
},
methods: {
setColorValue(value) {
@ -72,16 +74,16 @@ export default {
if (/^#[0-9A-F]{6}$/i.test(value)) {
this.triggerChange(value);
}
}
},
},
computed: {
colors() {
return this.df.colors;
},
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;
}
}
},
},
};
</script>

View File

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

View File

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

View File

@ -4,71 +4,71 @@
:hide-arrow="true"
:placement="right ? 'bottom-end' : 'bottom-start'"
>
<div
slot="target"
class="h-full"
v-on-outside-click="() => (isShown = false)"
>
<slot
:toggleDropdown="toggleDropdown"
:highlightItemUp="highlightItemUp"
:highlightItemDown="highlightItemDown"
:selectHighlightedItem="selectHighlightedItem"
></slot>
</div>
<div slot="content" class="bg-white rounded w-full min-w-40">
<div class="p-1 max-h-64 overflow-auto text-sm">
<div v-if="isLoading" class="p-2 text-gray-600 italic">
{{ t('Loading...') }}
</div>
<div
v-if="!isLoading && dropdownItems.length === 0"
class="p-2 text-gray-600 italic"
>
{{ getEmptyMessage() }}
</div>
<template v-else>
<div v-for="d in dropdownItems" :key="d.label">
<div
v-if="d.isGroup"
class="
px-2
pt-3
pb-1
text-xs
uppercase
text-gray-700
font-semibold
tracking-wider
"
>
{{ d.label }}
</div>
<a
v-else
ref="items"
class="
block
p-2
rounded-md
mt-1
first:mt-0
cursor-pointer
truncate
"
:class="d.index === highlightedIndex ? 'bg-gray-100' : ''"
@mouseenter="highlightedIndex = d.index"
@mouseleave="highlightedIndex = -1"
@mousedown.prevent
@click="selectItem(d)"
>
<component :is="d.component" v-if="d.component" />
<template v-else>{{ d.label }}</template>
</a>
</div>
</template>
<template v-slot:target>
<div class="h-full" v-on-outside-click="() => (isShown = false)">
<slot
:toggleDropdown="toggleDropdown"
:highlightItemUp="highlightItemUp"
:highlightItemDown="highlightItemDown"
:selectHighlightedItem="selectHighlightedItem"
></slot>
</div>
</div>
</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 v-if="isLoading" class="p-2 text-gray-600 italic">
{{ t('Loading...') }}
</div>
<div
v-if="!isLoading && dropdownItems.length === 0"
class="p-2 text-gray-600 italic"
>
{{ getEmptyMessage() }}
</div>
<template v-else>
<div v-for="d in dropdownItems" :key="d.label">
<div
v-if="d.isGroup"
class="
px-2
pt-3
pb-1
text-xs
uppercase
text-gray-700
font-semibold
tracking-wider
"
>
{{ d.label }}
</div>
<a
v-else
ref="items"
class="
block
p-2
rounded-md
mt-1
first:mt-0
cursor-pointer
truncate
"
:class="d.index === highlightedIndex ? 'bg-gray-100' : ''"
@mouseenter="highlightedIndex = d.index"
@mouseleave="highlightedIndex = -1"
@mousedown.prevent
@click="selectItem(d)"
>
<component :is="d.component" v-if="d.component" />
<template v-else>{{ d.label }}</template>
</a>
</div>
</template>
</div>
</div>
</template>
</Popover>
</template>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<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>
</div>
</template>
@ -9,21 +9,21 @@ export default {
props: {
columnWidth: {
type: String,
default: '1fr'
default: '1fr',
},
columnCount: {
type: Number,
default: 0
default: 0,
},
ratio: {
type: Array,
default: () => []
default: () => [],
},
gridTemplateColumns: {
type: String,
default: null
default: null,
},
gap: String
gap: String,
},
computed: {
style() {
@ -33,7 +33,9 @@ export default {
obj['grid-template-columns'] = `repeat(${this.columnCount}, ${this.columnWidth})`;
}
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) {
obj['grid-template-columns'] = this.gridTemplateColumns;
@ -43,7 +45,7 @@ export default {
}
return obj;
}
}
},
},
};
</script>

View File

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

View File

@ -1,6 +1,6 @@
import { ipcRenderer } from 'electron';
import frappe from 'frappe';
import Vue from 'vue';
import { createApp } from 'vue';
import models from '../models';
import App from './App';
import FeatherIcon from './components/FeatherIcon';
@ -26,12 +26,29 @@ import { showToast, stringifyCircular } from './utils';
window.frappe = frappe;
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();
Vue.config.productionTip = false;
Vue.component('feather-icon', FeatherIcon);
Vue.directive('on-outside-click', outsideClickDirective);
Vue.mixin({
const app = createApp({
template: '<App/>',
});
app.use(router);
app.component('App', App);
app.component('feather-icon', FeatherIcon);
app.directive('on-outside-click', outsideClickDirective);
app.mixin({
computed: {
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 = {
info,
};
@ -67,28 +84,7 @@ import { showToast, stringifyCircular } from './utils';
console.error(err, vm, info);
};
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));
});
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: {
App,
},
template: '<App/>',
});
app.mount('#app');
})();
function registerIpcRendererListeners() {

View File

@ -1,128 +1,126 @@
<template>
<div class="flex flex-col overflow-y-hidden">
<PageHeader>
<h1 slot="title" class="text-2xl font-bold">
{{ t('Chart of Accounts') }}
</h1>
<template slot="actions">
<template v-slot:title>
<h1 class="text-2xl font-bold">
{{ t('Chart of Accounts') }}
</h1>
</template>
<template v-slot:actions>
<SearchBar class="ml-2" />
</template>
</PageHeader>
<div class="flex-1 flex px-8 overflow-y-auto">
<div class="flex-1" v-if="root">
<template v-for="account in allAccounts">
<div
class="
mt-2
px-4
py-2
cursor-pointer
hover:bg-gray-100
rounded-md
group
"
:class="[
account.level !== 0 ? 'text-base' : 'text-lg',
isQuickEditOpen(account) ? 'bg-gray-200' : '',
]"
:key="account.name"
@click="onClick(account)"
>
<div class="flex items-center" :class="`pl-${account.level * 8}`">
<component :is="getIconComponent(account)" />
<div class="flex items-baseline">
<div
class="ml-3"
:class="[!account.parentAccount && 'font-semibold']"
<div
v-for="account in allAccounts"
:key="account.name"
class="
mt-2
px-4
py-2
cursor-pointer
hover:bg-gray-100
rounded-md
group
"
:class="[
account.level !== 0 ? 'text-base' : 'text-lg',
isQuickEditOpen(account) ? 'bg-gray-200' : '',
]"
@click="onClick(account)"
>
<div class="flex items-center" :class="`pl-${account.level * 8}`">
<component :is="getIconComponent(account)" />
<div class="flex items-baseline">
<div
class="ml-3"
:class="[!account.parentAccount && 'font-semibold']"
>
{{ account.name }}
</div>
<div v-if="account.isGroup" class="ml-6 hidden group-hover:block">
<button
class="
text-xs text-gray-800
hover:text-gray-900
focus:outline-none
"
@click.stop="addAccount(account, 'addingAccount')"
>
{{ account.name }}
</div>
<div
v-if="account.isGroup"
class="ml-6 hidden group-hover:block"
{{ t('Add Account') }}
</button>
<button
class="
ml-3
text-xs text-gray-800
hover:text-gray-900
focus:outline-none
"
@click.stop="addAccount(account, 'addingGroupAccount')"
>
<button
class="
text-xs text-gray-800
hover:text-gray-900
focus:outline-none
"
@click.stop="addAccount(account, 'addingAccount')"
>
{{ t('Add Account') }}
</button>
<button
class="
ml-3
text-xs text-gray-800
hover:text-gray-900
focus:outline-none
"
@click.stop="addAccount(account, 'addingGroupAccount')"
>
{{ t('Add Group') }}
</button>
</div>
{{ t('Add Group') }}
</button>
</div>
</div>
</div>
</div>
<div
v-if="account.addingAccount || account.addingGroupAccount"
class="
mt-2
px-4
py-2
cursor-pointer
hover:bg-gray-100
rounded-md
group
"
:class="[account.level !== 0 ? 'text-base' : 'text-lg']"
:key="account.name + '-adding-account'"
>
<div
v-if="account.addingAccount || account.addingGroupAccount"
class="
mt-2
px-4
py-2
cursor-pointer
hover:bg-gray-100
rounded-md
group
"
:class="[account.level !== 0 ? 'text-base' : 'text-lg']"
:key="account.name + '-adding-account'"
class="flex items-center"
:class="`pl-${(account.level + 1) * 8}`"
>
<div
class="flex items-center"
:class="`pl-${(account.level + 1) * 8}`"
>
<component
:is="getIconComponent({ isGroup: account.addingGroupAccount })"
/>
<div class="flex items-baseline">
<div class="ml-3">
<input
class="focus:outline-none bg-transparent"
:class="{ 'text-gray-600': insertingAccount }"
:placeholder="t('New Account')"
:ref="account.name"
@keydown.esc="cancelAddingAccount(account)"
@keydown.enter="
(e) =>
createNewAccount(
e.target.value,
account,
account.addingGroupAccount
)
"
type="text"
:disabled="insertingAccount"
/>
<button
v-if="!insertingAccount"
class="
ml-4
text-xs text-gray-800
hover:text-gray-900
focus:outline-none
"
@click="cancelAddingAccount(account)"
>
{{ t('Cancel') }}
</button>
</div>
<component
:is="getIconComponent({ isGroup: account.addingGroupAccount })"
/>
<div class="flex items-baseline">
<div class="ml-3">
<input
class="focus:outline-none bg-transparent"
:class="{ 'text-gray-600': insertingAccount }"
:placeholder="t('New Account')"
:ref="account.name"
@keydown.esc="cancelAddingAccount(account)"
@keydown.enter="
(e) =>
createNewAccount(
e.target.value,
account,
account.addingGroupAccount
)
"
type="text"
:disabled="insertingAccount"
/>
<button
v-if="!insertingAccount"
class="
ml-4
text-xs text-gray-800
hover:text-gray-900
focus:outline-none
"
@click="cancelAddingAccount(account)"
>
{{ t('Cancel') }}
</button>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</div>

View File

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

View File

@ -1,12 +1,10 @@
<template>
<div class="flex flex-col h-full">
<SectionHeader>
<template slot="title">{{ t('Top Expenses') }}</template>
<PeriodSelector
slot="action"
:value="period"
@change="(value) => (period = value)"
/>
<template v-slot:title>{{ t('Top Expenses') }}</template>
<template v-slot:action>
<PeriodSelector :value="period" @change="(value) => (period = value)" />
</template>
</SectionHeader>
<div class="flex relative" v-show="hasData">
<div class="w-1/2">
@ -38,7 +36,10 @@
@change="(value) => (active = value)"
/>
</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">
{{ t('No expenses in this period') }}
</span>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,12 +10,7 @@ import PrintView from '@/pages/PrintView/PrintView';
import QuickEditForm from '@/pages/QuickEditForm';
import Report from '@/pages/Report';
import Settings from '@/pages/Settings/Settings';
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
import { createRouter, createWebHistory } from 'vue-router';
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') {
window.router = router;

View File

@ -1,5 +1,4 @@
import frappe, { t } from 'frappe';
import Icon from './components/Icon';
const config = {
getTitle: async () => {
@ -10,16 +9,18 @@ const config = {
{
title: t('Get Started'),
route: '/get-started',
icon: getIcon('general', '24', '5'),
icon: 'general',
iconSize: '24',
iconHeight: '5',
},
{
title: t('Dashboard'),
route: '/',
icon: getIcon('dashboard'),
icon: 'dashboard',
},
{
title: t('Sales'),
icon: getIcon('sales'),
icon: 'sales',
route: '/list/SalesInvoice',
items: [
{
@ -36,7 +37,7 @@ const config = {
},
{
title: t('Purchases'),
icon: getIcon('purchase'),
icon: 'purchase',
route: '/list/PurchaseInvoice',
items: [
{
@ -53,7 +54,7 @@ const config = {
},
{
title: t('Common'),
icon: getIcon('common-entries'),
icon: 'common-entries',
route: '/list/Item',
items: [
{
@ -75,7 +76,7 @@ const config = {
},
{
title: t('Reports'),
icon: getIcon('reports'),
icon: 'reports',
route: '/report/general-ledger',
items: [
{
@ -108,7 +109,7 @@ const config = {
},
{
title: t('Setup'),
icon: getIcon('settings'),
icon: 'settings',
route: '/chart-of-accounts',
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;

View File

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