Replace vuex by pinia #7

Merged
jacob1123 merged 14 commits from pinia into master 2022-02-11 23:20:06 +01:00
25 changed files with 616 additions and 521 deletions

View File

@ -9,12 +9,13 @@
},
"dependencies": {
"@mdi/font": "5.9.55",
"@vueuse/core": "^7.6.1",
"autoprefixer": "^10.4.2",
"pinia": "^2.0.11",
"postcss": "^8.4.6",
"tailwindcss": "^3.0.18",
"vue": "^3.2.25",
"vue-router": "^4.0.12",
"vuex": "^4.0.2"
"vue-router": "^4.0.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.0.0",

View File

@ -1,9 +0,0 @@
import { ComponentCustomProperties } from 'vue'
import { State } from '../store'
import { Store } from 'vuex'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store<State>
}
}

View File

@ -1,62 +1,65 @@
<script lang="ts">
import { mapState } from "pinia";
import { defineComponent } from "vue";
import { LOGOUT } from "./store/mutation-types";
import { useBudgetsStore } from "./stores/budget";
import { useSessionStore } from "./stores/session";
import { useSettingsStore } from "./stores/settings";
export default defineComponent({
computed: {
loggedIn() {
return this.$store.state.Session.Token;
}
},
methods: {
logout () {
this.$store.commit(LOGOUT);
this.$router.push("/login")
computed: {
...mapState(useBudgetsStore, ["CurrentBudgetName"]),
...mapState(useSettingsStore, ["Menu"]),
...mapState(useSessionStore, ["LoggedIn"]),
},
toggleMenu () {
this.$store.commit("toggleMenu");
methods: {
logout() {
useSessionStore().logout();
this.$router.push("/login");
},
toggleMenu() {
useSettingsStore().toggleMenu();
},
toggleMenuSize() {
useSettingsStore().toggleMenuSize();
}
},
toggleMenuSize () {
this.$store.commit("toggleMenuSize");
}
},
beforeCreate () {
this.$store.commit("initializeStore");
}
})
</script>
<template>
<div class="box-border w-full">
<div class="flex bg-gray-400 p-4 m-2 rounded-lg">
<span class="flex-1 font-bold text-5xl -my-3 hidden md:inline" @click="toggleMenuSize"></span>
<span class="flex-1 font-bold text-5xl -my-3 md:hidden" @click="toggleMenu"></span>
<div class="box-border w-full">
<div class="flex bg-gray-400 p-4 m-2 rounded-lg">
<span class="flex-1 font-bold text-5xl -my-3 hidden md:inline" @click="toggleMenuSize"></span>
<span class="flex-1 font-bold text-5xl -my-3 md:hidden" @click="toggleMenu"></span>
<span class="flex-1">{{$store.getters.CurrentBudgetName}}</span>
<span class="flex-1">{{ CurrentBudgetName }}</span>
<div class="flex flex-1 flex-row justify-end -mx-4">
<router-link class="mx-4" v-if="loggedIn" to="/dashboard">Dashboard</router-link>
<router-link class="mx-4" v-if="!loggedIn" to="/login">Login</router-link>
<a class="mx-4" v-if="loggedIn" @click="logout">Logout</a>
</div>
<div class="flex flex-1 flex-row justify-end -mx-4">
<router-link class="mx-4" v-if="LoggedIn" to="/dashboard">Dashboard</router-link>
<router-link class="mx-4" v-if="!LoggedIn" to="/login">Login</router-link>
<a class="mx-4" v-if="LoggedIn" @click="logout">Logout</a>
</div>
</div>
<div class="flex flex-col md:flex-row flex-1">
<div
:class="[Menu.Expand ? 'md:w-72' : 'md:w-36', Menu.Show ? '' : 'hidden']"
class="md:block flex-shrink-0 w-full"
>
<router-view name="sidebar"></router-view>
</div>
<div class="flex-1 p-6">
<router-view></router-view>
</div>
</div>
</div>
<div class="flex flex-col md:flex-row flex-1">
<div :class="[$store.state.ExpandMenu ? 'md:w-72' : 'md:w-36', $store.state.ShowMenu ? '' : 'hidden']" class="md:block flex-shrink-0 w-full">
<router-view name="sidebar"></router-view>
</div>
<div class="flex-1 p-6">
<router-view></router-view>
</div>
</div>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>

View File

@ -1,5 +1,7 @@
<script lang="ts">
import { defineComponent, PropType } from "vue"
import { useAPI } from "../stores/api";
import { useBudgetsStore } from "../stores/budget";
export interface Suggestion {
ID : string
@ -40,16 +42,17 @@ export default defineComponent({
return;
}
fetch("/api/v1/budget/" + this.$store.getters.CurrentBudget.ID + "/autocomplete/" + this.type + "?s=" + text, {
headers: this.$store.getters.AuthHeaders
}) .then(x=>x.json())
.then(x => {
let suggestions = x || [];
if(suggestions.length > 10){
suggestions = suggestions.slice(0, 10);
}
this.$data.Suggestions = suggestions;
});
const api = useAPI();
const budgetStore = useBudgetsStore();
api.GET("/budget/" + budgetStore.CurrentBudgetID + "/autocomplete/" + this.type + "?s=" + text)
.then(x=>x.json())
.then(x => {
let suggestions = x || [];
if(suggestions.length > 10){
suggestions = suggestions.slice(0, 10);
}
this.$data.Suggestions = suggestions;
});
},
keypress(e : KeyboardEvent) {
console.log(e.key);

View File

@ -1,10 +1,15 @@
<script lang="ts">
import { mapState } from "pinia";
import { defineComponent } from "vue";
import { useBudgetsStore } from "../stores/budget";
import Currency from "./Currency.vue";
export default defineComponent({
props: [ "transaction", "index" ],
components: { Currency }
components: { Currency },
computed: {
...mapState(useBudgetsStore, ["CurrentBudgetID"])
}
})
</script>
@ -18,7 +23,7 @@ export default defineComponent({
{{ transaction.CategoryGroup ? transaction.CategoryGroup + " : " + transaction.Category : "" }}
</td>
<td>
<a :href="'/budget/' + $store.getters.CurrentBudgetID + '/transaction/' + transaction.ID">
<a :href="'/budget/' + CurrentBudgetID + '/transaction/' + transaction.ID">
{{ transaction.Memo }}
</a>
</td>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import Card from '../components/Card.vue';
import { defineComponent } from "vue";
import { NEW_BUDGET } from "../store/action-types";
import { useBudgetsStore } from '../stores/budget';
export default defineComponent({
data() {
@ -13,7 +13,7 @@ export default defineComponent({
components: { Card },
methods: {
saveBudget() {
this.$store.dispatch(NEW_BUDGET, this.$data.budgetName);
useBudgetsStore().NewBudget(this.$data.budgetName);
this.$data.dialog = false;
},
newBudget() {

View File

@ -2,19 +2,24 @@ import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import router from './router'
import { store, key } from './store'
import { SET_CURRENT_ACCOUNT, SET_CURRENT_BUDGET } from './store/action-types'
import { createPinia } from 'pinia'
import { useBudgetsStore } from './stores/budget';
import { useAccountStore } from './stores/budget-account'
import PiniaLogger from './pinia-logger'
const app = createApp(App)
app.use(router)
app.use(store, key)
const pinia = createPinia()
pinia.use(PiniaLogger())
app.use(pinia)
app.mount('#app')
router.beforeEach(async (to, from, next) => {
await store.dispatch(SET_CURRENT_BUDGET, to.params.budgetid);
await store.dispatch(SET_CURRENT_ACCOUNT, {
accountid: to.params.accountid,
budgetid: to.params.budgetid
});
const budgetStore = useBudgetsStore();
await budgetStore.SetCurrentBudget((<string>to.params.budgetid));
const accountStore = useAccountStore();
await accountStore.SetCurrentAccount((<string>to.params.budgetid), (<string>to.params.accountid));
next();
})

View File

@ -1,8 +1,12 @@
<script lang="ts">
import { mapState } from "pinia";
import { defineComponent } from "vue"
import Autocomplete, { Suggestion } from '../components/Autocomplete.vue'
import Currency from "../components/Currency.vue";
import TransactionRow from "../components/TransactionRow.vue";
import { useAPI } from "../stores/api";
import { useAccountStore } from "../stores/budget-account";
import { useSessionStore } from "../stores/session";
export default defineComponent({
data() {
@ -16,12 +20,14 @@ export default defineComponent({
},
components: { Autocomplete, Currency, TransactionRow },
props: ["budgetid", "accountid"],
computed: {
...mapState(useAccountStore, ["CurrentAccount", "TransactionsList"]),
},
methods: {
saveTransaction(e : MouseEvent) {
e.preventDefault();
fetch("/api/v1/transaction/new", {
method: "POST",
body: JSON.stringify({
const api = useAPI();
api.POST("/transaction/new", JSON.stringify({
budget_id: this.budgetid,
account_id: this.accountid,
date: this.$data.TransactionDate,
@ -30,9 +36,7 @@ export default defineComponent({
memo: this.$data.Memo,
amount: this.$data.Amount,
state: "Uncleared"
}),
headers: this.$store.getters.AuthHeaders,
})
}))
.then(x => x.json());
},
}
@ -40,10 +44,10 @@ export default defineComponent({
</script>
<template>
<h1>{{ $store.getters.CurrentAccount.Name }}</h1>
<h1>{{ CurrentAccount?.Name }}</h1>
<p>
Current Balance:
<Currency :value="$store.getters.CurrentAccount.Balance" />
<Currency :value="CurrentAccount?.Balance" />
</p>
<table>
<tr class="font-bold">
@ -76,7 +80,7 @@ export default defineComponent({
</td>
<td style="width: 20px;"></td>
</tr>
<TransactionRow v-for="(transaction, index) in $store.getters.Transactions"
<TransactionRow v-for="(transaction, index) in TransactionsList"
:transaction="transaction"
:index="index" />
</table>

View File

@ -1,10 +1,19 @@
<script lang="ts">
import { mapState } from "pinia"
import { defineComponent } from "vue"
import Currency from "../components/Currency.vue"
import { useBudgetsStore } from "../stores/budget"
import { useAccountStore } from "../stores/budget-account"
import { useSettingsStore } from "../stores/settings"
export default defineComponent({
props: ["budgetid", "accountid"],
components: { Currency }
components: { Currency },
computed: {
...mapState(useSettingsStore, ["ExpandMenu"]),
...mapState(useBudgetsStore, ["CurrentBudgetName", "CurrentBudgetID"]),
...mapState(useAccountStore, ["OnBudgetAccounts", "OnBudgetAccountsBalance", "OffBudgetAccounts", "OffBudgetAccountsBalance"])
}
})
</script>
@ -12,44 +21,44 @@ export default defineComponent({
<div class="flex flex-col">
<span class="m-1 p-1 px-3 text-xl">
<router-link to="/dashboard"></router-link>
{{$store.getters.CurrentBudgetName}}
{{CurrentBudgetName}}
</span>
<span class="bg-orange-200 rounded-lg m-1 p-1 px-3 flex flex-col">
<router-link :to="'/budget/'+budgetid+'/budgeting'">Budget</router-link><br />
<!--<router-link :to="'/budget/'+$store.getters.CurrentBudgetID+'/reports'">Reports</router-link>-->
<!--<router-link :to="'/budget/'+$store.getters.CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
</span>
<li class="bg-orange-200 rounded-lg m-1 p-1 px-3">
<div class="flex flex-row justify-between font-bold">
<span>On-Budget Accounts</span>
<Currency :class="$store.state.ExpandMenu?'md:inline':'md:hidden'" :value="$store.getters.OnBudgetAccountsBalance" />
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OnBudgetAccountsBalance" />
</div>
<div v-for="account in $store.getters.OnBudgetAccounts" class="flex flex-row justify-between">
<div v-for="account in OnBudgetAccounts" class="flex flex-row justify-between">
<router-link :to="'/budget/'+budgetid+'/account/'+account.ID">{{account.Name}}</router-link>
<Currency :class="$store.state.ExpandMenu?'md:inline':'md:hidden'" :value="account.Balance" />
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.Balance" />
</div>
</li>
<li class="bg-red-200 rounded-lg m-1 p-1 px-3">
<div class="flex flex-row justify-between font-bold">
<span>Off-Budget Accounts</span>
<Currency :class="$store.state.ExpandMenu?'md:inline':'md:hidden'" :value="$store.getters.OffBudgetAccountsBalance" />
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="OffBudgetAccountsBalance" />
</div>
<div v-for="account in $store.getters.OffBudgetAccounts" class="flex flex-row justify-between">
<div v-for="account in OffBudgetAccounts" class="flex flex-row justify-between">
<router-link :to="'/budget/'+budgetid+'/account/'+account.ID">{{account.Name}}</router-link>
<Currency :class="$store.state.ExpandMenu?'md:inline':'md:hidden'" :value="account.Balance" />
<Currency :class="ExpandMenu?'md:inline':'md:hidden'" :value="account.Balance" />
</div>
</li>
<li class="bg-red-200 rounded-lg m-1 p-1 px-3">
Closed Accounts
</li>
<!--<li>
<router-link :to="'/budget/'+$store.getters.CurrentBudgetID+'/accounts'">Edit accounts</router-link>
<router-link :to="'/budget/'+CurrentBudgetID+'/accounts'">Edit accounts</router-link>
</li>-->
<li class="bg-red-200 rounded-lg m-1 p-1 px-3">
+ Add Account
</li>
<li class="bg-red-200 rounded-lg m-1 p-1 px-3">
<router-link :to="'/budget/'+$store.getters.CurrentBudgetID+'/settings'">Budget-Settings</router-link>
<router-link :to="'/budget/'+CurrentBudgetID+'/settings'">Budget-Settings</router-link>
</li>
<!--<li><router-link to="/admin">Admin</router-link></li>-->
</div>

View File

@ -1,36 +1,31 @@
<script lang="ts">
import { defineComponent } from "vue";
import { FETCH_MONTH_BUDGET } from "../store/action-types";
import { TITLE } from "../store/mutation-types";
import { mapState } from "pinia";
import { defineComponent, PropType } from "vue";
import Currency from "../components/Currency.vue";
import { useBudgetsStore } from "../stores/budget";
import { Category, useAccountStore } from "../stores/budget-account";
interface Date {
Year:Number,
Month:Number,
Year: number,
Month: number,
}
export default defineComponent({
mounted() {
this.$store.commit(TITLE, "Budget for " + this.month + " " + this.year);
return this.$store.dispatch(FETCH_MONTH_BUDGET, { budgetid: this.budgetid, year: this.year, month: this.month });
props: {
budgetid: {} as PropType<string>,
year: {} as PropType<number>,
month: {} as PropType<number>,
},
watch: {
year() {
return this.$store.dispatch(FETCH_MONTH_BUDGET, { budgetid: this.budgetid, year: this.year, month: this.month });
},
month() {
return this.$store.dispatch(FETCH_MONTH_BUDGET, { budgetid: this.budgetid, year: this.year, month: this.month });
},
},
props: ["budgetid", "year", "month"],
computed: {
Categories() {
return this.$store.getters.Categories(this.year, this.month);
...mapState(useBudgetsStore, ["CurrentBudgetID"]),
Categories() : Category[] {
const accountStore = useAccountStore();
return [...accountStore.CategoriesForMonth(this.selected.Year, this.selected.Month)];
},
previous() : Date {
return {
Year: new Date(this.year, this.month - 1, 1).getFullYear(),
Month: new Date(this.year, this.month - 1, 1).getMonth(),
Year: new Date(this.selected.Year, this.selected.Month - 1, 1).getFullYear(),
Month: new Date(this.selected.Year, this.selected.Month - 1, 1).getMonth(),
};
},
current() : Date {
@ -41,17 +36,31 @@ export default defineComponent({
},
selected() : Date {
return {
Year: this.year,
Month: Number(this.month) + 1
Year: this.year ?? this.current.Year,
Month: Number(this.month ?? this.current.Month) + 1
}
},
next() : Date {
return {
Year: new Date(this.year, Number(this.month) + 1, 1).getFullYear(),
Month: new Date(this.year, Number(this.month) + 1, 1).getMonth(),
Year: new Date(this.selected.Year, Number(this.month) + 1, 1).getFullYear(),
Month: new Date(this.selected.Year, Number(this.month) + 1, 1).getMonth(),
};
}
},
mounted() : Promise<void> {
document.title = "Budgeteer - Budget for " + this.selected.Month + "/" + this.selected.Year;
return useAccountStore().FetchMonthBudget(this.budgetid ?? "", this.selected.Year, this.selected.Month);
},
watch: {
year() {
if (this.year != undefined && this.month != undefined)
return useAccountStore().FetchMonthBudget(this.budgetid ?? "", this.year, this.month);
},
month() {
if (this.year != undefined && this.month != undefined)
return useAccountStore().FetchMonthBudget(this.budgetid ?? "", this.year, this.month);
},
},
components: { Currency }
})
@ -61,13 +70,17 @@ export default defineComponent({
</script>
<template>
<h1>
Budget for {{selected.Month}}/{{selected.Year}}
</h1>
<h1>Budget for {{ selected.Month }}/{{ selected.Year }}</h1>
<div>
<router-link :to="'/budget/'+$store.getters.CurrentBudget.ID +'/budgeting/' + previous.Year + '/' + previous.Month">Previous Month</router-link> -
<router-link :to="'/budget/'+$store.getters.CurrentBudget.ID +'/budgeting/' + current.Year + '/' + current.Month">Current Month</router-link> -
<router-link :to="'/budget/'+$store.getters.CurrentBudget.ID +'/budgeting/' + next.Year + '/' + next.Month">Next Month</router-link>
<router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
>Previous Month</router-link>-
<router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
>Current Month</router-link>-
<router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
>Next Month</router-link>
</div>
<table class="container col-lg-12" id="content">
<tr>
@ -81,14 +94,22 @@ export default defineComponent({
<th>Available</th>
</tr>
<tr v-for="category in Categories">
<td>{{category.Group}}</td>
<td>{{category.Name}}</td>
<td>{{ category.Group }}</td>
<td>{{ category.Name }}</td>
<td></td>
<td></td>
<td class="text-right"><Currency :value="category.AvailableLastMonth" /></td>
<td class="text-right"><Currency :value="category.Assigned" /></td>
<td class="text-right"><Currency :value="category.Activity" /></td>
<td class="text-right"><Currency :value="category.Available" /></td>
<td class="text-right">
<Currency :value="category.AvailableLastMonth" />
</td>
<td class="text-right">
<Currency :value="category.Assigned" />
</td>
<td class="text-right">
<Currency :value="category.Activity" />
</td>
<td class="text-right">
<Currency :value="category.Available" />
</td>
</tr>
</table>
</template>

View File

@ -2,17 +2,22 @@
import NewBudget from '../dialogs/NewBudget.vue';
import Card from '../components/Card.vue';
import { defineComponent } from 'vue';
import { mapState } from 'pinia';
import { useSessionStore } from '../stores/session';
export default defineComponent({
props: ["budgetid"],
components: { NewBudget, Card }
components: { NewBudget, Card },
computed: {
...mapState(useSessionStore, ["BudgetsList"]),
}
})
</script>
<template>
<h1>Budgets</h1>
<div class="grid md:grid-cols-2 gap-6">
<Card v-for="budget in $store.getters.Budgets">
<Card v-for="budget in BudgetsList">
<router-link v-bind:to="'/budget/'+budget.ID+'/budgeting'" class="contents">
<!--<svg class="w-24"></svg>-->
<p class="w-24 text-center text-6xl"></p>

View File

@ -1,7 +1,6 @@
<script lang="ts">
import { TITLE } from "../store/mutation-types";
import { LOGIN } from '../store/action-types'
import { defineComponent } from "vue";
import { useSessionStore } from "../stores/session";
export default defineComponent({
data() {
@ -15,12 +14,12 @@ export default defineComponent({
}
},
mounted() {
this.$store.commit(TITLE, "Login");
document.title = "Budgeteer - Login";
},
methods: {
formSubmit(e : MouseEvent) {
e.preventDefault();
this.$store.dispatch(LOGIN, this.$data.login)
useSessionStore().login(this.$data.login)
.then(x => {
this.$data.error = "";
this.$router.replace("/dashboard");

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { REGISTER } from "../store/action-types";
import { useSessionStore } from '../stores/session';
export default defineComponent({
data() {
@ -15,11 +15,11 @@ export default defineComponent({
}
},
methods: {
formSubmit (e) {
formSubmit (e : FormDataEvent) {
e.preventDefault();
this.$store.dispatch(REGISTER, this.$data.login)
useSessionStore().register(this.$data.login)
.then(() => this.$data.error = "")
.catch(() => this.$data.error = ["Something went wrong!"]);
.catch(() => this.$data.error = "Something went wrong!");
// TODO display invalidCredentials
// TODO redirect to dashboard on success

View File

@ -1,7 +1,8 @@
<script lang="ts">
import { defineComponent } from "vue"
import { IMPORT_YNAB } from "../store/action-types";
import { TITLE } from "../store/mutation-types"
import { useAPI } from "../stores/api";
import { useBudgetsStore } from "../stores/budget";
import { useSessionStore } from "../stores/session";
export default defineComponent({
data() {
@ -11,12 +12,12 @@ export default defineComponent({
}
},
computed: {
filesIncomplete() {
filesIncomplete() : boolean {
return this.$data.transactionsFile == undefined || this.$data.assignmentsFile == undefined;
}
},
mounted() {
this.$store.commit(TITLE, "Settings")
document.title = "Budgeteer - Settings";
},
methods: {
gotAssignments(e : Event) {
@ -30,22 +31,21 @@ export default defineComponent({
this.$data.transactionsFile = input.files[0];
},
deleteBudget() {
fetch("/api/v1/budget/" + this.$store.getters.CurrentBudget.ID, {
method: "DELETE",
headers: {
'Authorization': 'Bearer ' + this.$store.state.Session.Token
},
});
this.$store.commit("deleteBudget", this.$store.getters.CurrentBudget.ID)
const currentBudgetID = useBudgetsStore().CurrentBudgetID;
if (currentBudgetID == null)
return;
const api = useAPI();
api.DELETE("/budget/" + currentBudgetID);
const budgetStore = useSessionStore();
budgetStore.Budgets.delete(currentBudgetID);
this.$router.push("/")
},
clearBudget() {
fetch("/api/v1/budget/" + this.$store.getters.CurrentBudget.ID + "/settings/clear", {
method: "POST",
headers: {
'Authorization': 'Bearer ' + this.$store.state.Session.Token
},
})
const currentBudgetID = useBudgetsStore().CurrentBudgetID;
const api = useAPI();
api.POST("/budget/" + currentBudgetID + "/settings/clear", null)
},
cleanNegative() {
// <a href="/budget/{{.Budget.ID}}/settings/clean-negative">Fix all historic negative category-balances</a>
@ -57,7 +57,8 @@ export default defineComponent({
let formData = new FormData();
formData.append("transactions", this.$data.transactionsFile);
formData.append("assignments", this.$data.assignmentsFile);
this.$store.dispatch(IMPORT_YNAB, formData);
const budgetStore = useBudgetsStore();
budgetStore.ImportYNAB(formData);
}
}
})

82
web/src/pinia-logger.ts Normal file
View File

@ -0,0 +1,82 @@
import { PiniaPluginContext, StoreGeneric, _ActionsTree, _StoreOnActionListenerContext } from 'pinia';
const cloneDeep = <T>(obj: T): T => {
try {
return JSON.parse(JSON.stringify(obj));
} catch {
return { ...obj };
}
};
const formatTime = (date = new Date()) => {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
const milliseconds = date.getMilliseconds().toString();
return `${hours}:${minutes}:${seconds}:${milliseconds}`;
};
export interface PiniaLoggerOptions {
disabled?: boolean;
expanded?: boolean;
showDuration?: boolean
showStoreName?: boolean;
logErrors?: boolean;
}
export type PiniaActionListenerContext = _StoreOnActionListenerContext<StoreGeneric, string, _ActionsTree>;
const defaultOptions: PiniaLoggerOptions = {
logErrors: true,
disabled: false,
expanded: true,
showStoreName: true,
showDuration: false,
};
export const PiniaLogger = (config = defaultOptions) => (ctx: PiniaPluginContext) => {
const options = {
...defaultOptions,
...config,
};
if (options.disabled) return;
ctx.store.$onAction((action: PiniaActionListenerContext) => {
const startTime = Date.now();
const prevState = cloneDeep(ctx.store.$state);
const log = (isError?: boolean, error?: any) => {
const endTime = Date.now();
const duration = endTime - startTime + 'ms';
const nextState = cloneDeep(ctx.store.$state);
const storeName = action.store.$id;
const title = `action 🍍 ${options.showStoreName ? `[${storeName}] ` : ''}${action.name} ${isError ? `failed after ${duration} ` : ''}@ ${formatTime()}`;
console[options.expanded ? 'group' : 'groupCollapsed'](`%c${title}`, `font-weight: bold; ${isError ? 'color: #ed4981;' : ''}`);
console.log('%cprev state', 'font-weight: bold; color: grey;', prevState);
console.log('%caction', 'font-weight: bold; color: #69B7FF;', {
type: action.name,
args: action.args.length > 0 ? { ...action.args } : undefined,
...(options.showStoreName && { store: action.store.$id }),
...(options.showDuration && { duration }),
...(isError && { error }),
});
console.log('%cnext state', 'font-weight: bold; color: #4caf50;', nextState);
console.groupEnd();
};
action.after(() => {
log();
});
if (options.logErrors) {
action.onError((error) => {
log(true, error);
});
}
});
};
export default PiniaLogger;

View File

@ -1,11 +0,0 @@
export const IMPORT_YNAB = "YNAB import";
export const GET = "GET";
export const POST = "POST";
export const NEW_BUDGET = "New budget";
export const SET_CURRENT_BUDGET = "Set current budget";
export const SET_CURRENT_ACCOUNT = "Set current account";
export const FETCH_BUDGET = "Fetch budget";
export const FETCH_MONTH_BUDGET = "Fetch budget for month";
export const LOGIN = 'Log in';
export const REGISTER = 'Register';
export const FETCH_ACCOUNT = "Fetch account";

View File

@ -1,152 +0,0 @@
import { Module } from "vuex";
import { FETCH_ACCOUNT, FETCH_BUDGET, FETCH_MONTH_BUDGET, SET_CURRENT_ACCOUNT } from "../action-types";
import { LOGOUT, TITLE } from "../mutation-types";
export interface BudgetState {
Accounts: Map<string, Account>,
CurrentAccountID?: string,
Categories: Map<string, Category>,
Months: Map<number, Map<number, Map<string, Category>>>,
Transactions: [],
Assignments: []
}
export interface Account {
ID: string
OnBudget: boolean
}
export interface Category {
Group: string
Name: string
AvailableLastMonth: number
Assigned: number
Activity: number
Available: number
}
export const budgetStore : Module<BudgetState, any> = {
state: {
Accounts: new Map<string, Account>(),
CurrentAccountID: undefined,
Months: new Map<number, Map<number, Map<string, Category>>>(),
Categories: new Map<string, Category>(),
Transactions: [],
Assignments: []
},
mutations: {
initializeStore(state) {
const store = localStorage.getItem("store");
if (!store)
return;
const restoredState = JSON.parse(store);
if (!restoredState)
return;
state.CurrentAccountID = restoredState.CurrentAccountID;
for (const account of restoredState.Accounts || []) {
state.Accounts.set(account[0], account[1]);
}
},
[LOGOUT](state) {
state.Accounts.clear();
state.Categories.clear();
state.Transactions = [];
state.Assignments = [];
},
addAccount(state, account) {
state.Accounts.set(account.ID, account);
},
addCategory(state, category) {
state.Categories.set(category.ID, category);
},
addCategoriesForMonth(state, {year, month, categories}) {
const yearMap = state.Months.get(year) || new Map<number, Map<string, Category>>();
state.Months.set(year, yearMap);
const monthMap = yearMap.get(month) || new Map<string, Category>();
yearMap.set(month, monthMap);
for (const category of categories){
monthMap.set(category.ID, category);
}
},
setCurrentAccountID(state, accountid) {
state.CurrentAccountID = accountid;
},
setTransactions(state, transactions) {
state.Transactions = transactions;
}
},
getters: {
Accounts(state) {
return state.Accounts.values();
},
Categories: (state) => (year : number, month : number) => {
const yearMap = state.Months.get(year);
return yearMap?.get(month)?.values();
},
CurrentAccount(state) : Account | undefined {
if (state.CurrentAccountID == null)
return undefined;
return state.Accounts.get(state.CurrentAccountID);
},
OnBudgetAccounts(state) {
return Array.from(state.Accounts.values()).filter(x => x.OnBudget);
},
OnBudgetAccountsBalance(state, getters){
return getters.OnBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
},
OffBudgetAccounts(state) {
return Array.from(state.Accounts.values()).filter(x => !x.OnBudget);
},
OffBudgetAccountsBalance(state, getters){
return getters.OffBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
},
Transactions(state) {
return (state.Transactions || []);
}
},
actions: {
async [SET_CURRENT_ACCOUNT]({ state, commit, dispatch, getters }, { budgetid, accountid }) {
if (budgetid == null)
return
commit("setCurrentAccountID", accountid);
if (accountid == null)
return
commit(TITLE, getters.CurrentAccount.Name);
await dispatch(FETCH_ACCOUNT, accountid)
},
async [FETCH_ACCOUNT]({ state, commit, rootState }, accountid) {
const result = await fetch("/api/v1/account/" + accountid + "/transactions", {
headers: {
'Authorization': 'Bearer ' + rootState.Session.Token
}
});
const response = await result.json();
commit("setTransactions", response.Transactions);
},
async [FETCH_BUDGET]({ state, commit, dispatch, rootState }, budgetid) {
const result = await dispatch("GET", { path: "/budget/" + budgetid });
const response = await result.json();
for (const account of response.Accounts || []) {
commit("addAccount", account);
}
for (const category of response.Categories || []) {
commit("addCategory", category);
}
},
async [FETCH_MONTH_BUDGET]({state, commit, dispatch, rootState }, {budgetid, month, year}) {
const result = await dispatch("GET", { path: "/budget/" + budgetid + "/" + year + "/" + month});
const response = await result.json();
commit("addCategoriesForMonth", {year, month, categories: response.Categories})
}
}
}

View File

@ -1,184 +0,0 @@
import { InjectionKey } from 'vue'
import { createStore, Store, createLogger } from 'vuex'
import { LOGIN_SUCCESS, LOGOUT, TITLE } from './mutation-types'
import { FETCH_ACCOUNT, FETCH_BUDGET, GET, REGISTER, IMPORT_YNAB, LOGIN, NEW_BUDGET, POST, SET_CURRENT_ACCOUNT, SET_CURRENT_BUDGET } from './action-types'
import { budgetStore } from './budget'
export interface State {
Session: {
Token?: string
User?: string
},
ShowMenu?: boolean,
ExpandMenu?: boolean,
Budgets: Map<string, Budget>,
CurrentBudgetID?: string,
}
export interface Budget {
ID: string
Name: string
AvailableBalance: number
}
export const key: InjectionKey<Store<State>> = Symbol()
export const store = createStore<State>({
state: {
Session: {
Token: undefined,
User: undefined
},
ShowMenu: undefined,
Budgets: new Map<string, Budget>(),
CurrentBudgetID: undefined,
},
mutations: {
deleteBudget(state: State, budgetid: string) {
state.Budgets.delete(budgetid)
},
toggleMenu(state) {
state.ShowMenu = !state.ShowMenu;
},
toggleMenuSize(state) {
state.ExpandMenu = !state.ExpandMenu;
},
initializeStore(state) {
const store = localStorage.getItem("store");
if (!store)
return;
const restoredState = JSON.parse(store);
if (!restoredState)
return;
state.Session = restoredState.Session;
state.CurrentBudgetID = restoredState.CurrentBudgetID;
state.ShowMenu = restoredState.ShowMenu;
state.ExpandMenu = restoredState.ExpandMenu;
for (const budget of restoredState.Budgets || []) {
state.Budgets.set(budget[0], budget[1]);
}
},
[TITLE](state, title) {
document.title = "Budgeteer - " + title;
},
[LOGIN_SUCCESS](state, result) {
state.Session = {
User: result.User,
Token: result.Token
};
for (const budget of result.Budgets) {
state.Budgets.set(budget.ID, budget)
}
},
addBudget(state, budget) {
state.Budgets.set(budget.ID, budget);
},
[LOGOUT](state) {
state.Session = { Token: undefined, User: undefined };
state.Budgets.clear();
},
setCurrentBudgetID(state, budgetid) {
state.CurrentBudgetID = budgetid;
},
},
actions: {
[LOGIN]({ state, commit }, login) {
return fetch("/api/v1/user/login", { method: "POST", body: JSON.stringify(login) })
.then(x => x.json())
.then(x => commit(LOGIN_SUCCESS, x))
},
[REGISTER]({ state, commit }, login) {
return fetch("/api/v1/user/register", { method: "POST", body: JSON.stringify(login) })
.then(x => x.json())
.then(x => commit(LOGIN_SUCCESS, x))
},
[IMPORT_YNAB]({ getters, dispatch }, formData) {
return dispatch("POST", { path: "/budget/" + getters.CurrentBudget.ID + "/import/ynab", body: formData });
},
[GET]({ getters }, { path }) {
return fetch("/api/v1" + path, {
headers: getters.AuthHeaders,
})
},
[POST]({ getters }, { path, body }) {
return fetch("/api/v1" + path, {
method: "POST",
headers: getters.AuthHeaders,
body: body,
})
},
/*async fetchDashboard ({state, commit, rootState}) {
const response = await fetch("/api/v1/dashboard", {
headers: {
'Authorization': 'Bearer ' + rootState.Session.Token
}
})
const data = await response.json();
commit("setBudgets", data.Budgets);
},*/
async [NEW_BUDGET]({ state, commit, dispatch, rootState }, budgetName) {
const result = await dispatch("POST", {
path: "/budget/new",
body: JSON.stringify({ name: budgetName })
});
const response = await result.json();
commit("addBudget", response)
},
async [SET_CURRENT_BUDGET]({ state, commit, dispatch, rootState }, budgetid) {
commit("setCurrentBudgetID", budgetid);
if (budgetid == null)
return
await dispatch(FETCH_BUDGET, budgetid)
},
},
getters: {
Budgets(state) {
return state.Budgets.values();
},
AuthHeaders(state) {
return {
'Authorization': 'Bearer ' + state.Session.Token
}
},
CurrentBudget(state) : Budget | undefined {
if (state.CurrentBudgetID == null)
return undefined;
return state.Budgets.get(state.CurrentBudgetID);
},
CurrentBudgetID(state) : string | undefined {
return state.CurrentBudgetID;
},
CurrentBudgetName(state) : string {
if (state.CurrentBudgetID == null)
return "";
const currentBudget = state.Budgets.get(state.CurrentBudgetID);
if(currentBudget != undefined)
return currentBudget.Name;
return "";
},
},
plugins: [createLogger()],
modules: {
budget: budgetStore
}
})
store.subscribe((mutation, state) => {
let persistedState = {
Session: state.Session,
Budgets: [...state.Budgets],
// Accounts: [...state.Accounts],
CurrentBudgetID: state.CurrentBudgetID,
//CurrentAccountID: state.CurrentAccountID,
ExpandMenu: state.ExpandMenu,
ShowMenu: state.ShowMenu
}
localStorage.setItem("store", JSON.stringify(persistedState));
})

View File

@ -1,3 +0,0 @@
export const LOGIN_SUCCESS = '✔ Logged in';
export const LOGOUT = 'Log out';
export const TITLE = 'Update title';

28
web/src/stores/api.ts Normal file
View File

@ -0,0 +1,28 @@
import { defineStore } from "pinia";
import { useSessionStore } from "./session";
export const useAPI = defineStore("api", {
actions: {
GET(path : string) {
const sessionStore = useSessionStore();
return fetch("/api/v1" + path, {
headers: sessionStore.AuthHeaders,
})
},
POST(path : string, body : FormData | string | null) {
const sessionStore = useSessionStore();
return fetch("/api/v1" + path, {
method: "POST",
headers: sessionStore.AuthHeaders,
body: body,
})
},
DELETE(path : string) {
const sessionStore = useSessionStore();
return fetch("/api/v1" + path, {
method: "DELETE",
headers: sessionStore.AuthHeaders,
})
},
}
});

View File

@ -0,0 +1,111 @@
import { defineStore } from "pinia"
import { useAPI } from "./api";
import { useSessionStore } from "./session";
interface State {
Accounts: Map<string, Account>,
CurrentAccountID: string | null,
Categories: Map<string, Category>,
Months: Map<number, Map<number, Map<string, Category>>>,
Transactions: [],
Assignments: []
}
export interface Account {
ID: string
Name: string
OnBudget: boolean
Balance: Number
}
export interface Category {
ID: string
Group: string
Name: string
AvailableLastMonth: number
Assigned: number
Activity: number
Available: number
}
export const useAccountStore = defineStore("budget/account", {
state: (): State => ({
Accounts: new Map<string, Account>(),
CurrentAccountID: null,
Months: new Map<number, Map<number, Map<string, Category>>>(),
Categories: new Map<string, Category>(),
Transactions: [],
Assignments: []
}),
getters: {
AccountsList(state) {
return [ ...state.Accounts.values() ];
},
CategoriesForMonth: (state) => (year : number, month : number) => {
console.log("MTH", state.Months)
const yearMap = state.Months.get(year);
return [ ...yearMap?.get(month)?.values() || [] ];
},
CurrentAccount(state) : Account | undefined {
if (state.CurrentAccountID == null)
return undefined;
return state.Accounts.get(state.CurrentAccountID);
},
OnBudgetAccounts(state) {
return [ ...state.Accounts.values() ].filter(x => x.OnBudget);
},
OnBudgetAccountsBalance(state) : Number {
return this.OnBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
},
OffBudgetAccounts(state) {
return [ ...state.Accounts.values() ].filter(x => !x.OnBudget);
},
OffBudgetAccountsBalance(state) : Number {
return this.OffBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
},
TransactionsList(state) {
return (state.Transactions || []);
}
},
actions: {
async SetCurrentAccount(budgetid : string, accountid : string) {
if (budgetid == null)
return
this.CurrentAccountID = accountid;
if (this.CurrentAccount == undefined)
return
useSessionStore().setTitle(this.CurrentAccount.Name);
await this.FetchAccount(accountid);
},
async FetchAccount(accountid : string) {
const api = useAPI();
const result = await api.GET("/account/" + accountid + "/transactions");
const response = await result.json();
this.Transactions = response.Transactions;
},
async FetchMonthBudget(budgetid : string, year : number, month : number) {
const api = useAPI();
const result = await api.GET("/budget/" + budgetid + "/" + year + "/" + month);
const response = await result.json();
this.addCategoriesForMonth(year, month, response.Categories);
},
addCategoriesForMonth(year : number, month : number, categories : Category[]) : void {
const yearMap = this.Months.get(year) || new Map<number, Map<string, Category>>();
this.Months.set(year, yearMap);
const monthMap = yearMap.get(month) || new Map<string, Category>();
yearMap.set(month, monthMap);
for (const category of categories){
monthMap.set(category.ID, category);
}
},
logout() {
this.$reset()
},
}
})

65
web/src/stores/budget.ts Normal file
View File

@ -0,0 +1,65 @@
import { defineStore } from "pinia";
import { useAPI } from "./api";
import { useAccountStore } from "./budget-account";
import { Budget, useSessionStore } from "./session";
interface State {
CurrentBudgetID: string | null,
}
export const useBudgetsStore = defineStore('budget', {
state: (): State => ({
CurrentBudgetID: null,
}),
getters: {
CurrentBudget(): Budget | undefined {
if (this.CurrentBudgetID == null)
return undefined;
const sessionStore = useSessionStore();
return sessionStore.Budgets.get(this.CurrentBudgetID);
},
CurrentBudgetName(state): string {
return this.CurrentBudget?.Name ?? "";
},
},
actions: {
ImportYNAB(formData: FormData) {
const api = useAPI();
return api.POST(
"/budget/" + this.CurrentBudgetID + "/import/ynab",
formData,
);
},
async NewBudget(budgetName: string): Promise<void> {
const api = useAPI();
const result = await api.POST(
"/budget/new",
JSON.stringify({ name: budgetName })
);
const response = await result.json();
const sessionStore = useSessionStore();
sessionStore.Budgets.set(response.ID, response);
},
async SetCurrentBudget(budgetid: string): Promise<void> {
this.CurrentBudgetID = budgetid;
if (budgetid == null)
return
await this.FetchBudget(budgetid);
},
async FetchBudget(budgetid: string) {
const api = useAPI();
const result = await api.GET("/budget/" + budgetid);
const response = await result.json();
for (const account of response.Accounts || []) {
useAccountStore().Accounts.set(account.ID, account);
}
for (const category of response.Categories || []) {
useAccountStore().Categories.set(category.ID, category);
}
},
}
})

58
web/src/stores/session.ts Normal file
View File

@ -0,0 +1,58 @@
import { StorageSerializers, useStorage } from '@vueuse/core';
import { defineStore } from 'pinia'
import { useAPI } from './api';
interface State {
Session: Session | null
Budgets: Map<string, Budget>,
}
interface Session {
Token: string
User: string
}
export interface Budget {
ID: string
Name: string
AvailableBalance: number
}
export const useSessionStore = defineStore('session', {
state: () => ({
Session: useStorage<Session>('session', null, undefined, { serializer: StorageSerializers.object }),
Budgets: useStorage<Map<string, Budget>>('budgets', new Map<string, Budget>()),
}),
getters: {
BudgetsList: (state) => [ ...state.Budgets.values() ],
AuthHeaders: (state) => ({'Authorization': 'Bearer ' + state.Session.Token}),
LoggedIn: (state) => state.Session != null,
},
actions: {
setTitle(title : string) {
document.title = "Budgeteer - " + title;
},
loginSuccess(x : any) {
this.Session = {
User: x.User,
Token: x.Token,
},
this.Budgets = x.Budgets;
},
async login(login: any) {
const api = useAPI();
const response = await api.POST("/user/login", JSON.stringify(login));
const result = await response.json();
return this.loginSuccess(result);
},
async register(login : any) {
const api = useAPI();
const response = await api.POST("/user/register", JSON.stringify(login));
const result = await response.json();
return this.loginSuccess(result);
},
logout() {
this.$reset()
},
}
})

View File

@ -0,0 +1,28 @@
import { useStorage } from "@vueuse/core";
import { defineStore } from "pinia";
interface State {
Menu: MenuSettings
}
interface MenuSettings {
Show: boolean | null,
Expand: boolean | null,
}
export const useSettingsStore = defineStore('settings', {
state: () => ({
Menu: useStorage<MenuSettings>('settings', {
Show: null,
Expand: false,
}),
}),
actions: {
toggleMenu() {
this.Menu.Show = !this.Menu.Show;
},
toggleMenuSize() {
this.Menu.Expand = !this.Menu.Expand;
},
}
});

View File

@ -1583,11 +1583,16 @@
optionalDependencies:
prettier "^1.18.2 || ^2.0.0"
"@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.0.0-beta.18":
"@vue/devtools-api@^6.0.0-beta.18":
version "6.0.0-beta.21.1"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.0-beta.21.1.tgz#f1410f53c42aa67fa3b01ca7bdba891f69d7bc97"
integrity sha512-FqC4s3pm35qGVeXRGOjTsRzlkJjrBLriDS9YXbflHLsfA9FrcKzIyWnLXoNm+/7930E8rRakXuAc2QkC50swAw==
"@vue/devtools-api@^6.0.0-beta.21":
version "6.0.5"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.5.tgz#7e35cfee4f44ada65cde0d19341fbaeb0ae353f4"
integrity sha512-2nM84dzo3B63pKgxwoArlT1d/yqSL0y2lG2GiyyGhwpyPTwkfIuJHlCNbputCoSCNnT6MMfenK1g7nv7Mea19A==
"@vue/reactivity-transform@3.2.29":
version "3.2.29"
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.29.tgz#a08d606e10016b7cf588d1a43dae4db2953f9354"
@ -1652,6 +1657,21 @@
resolved "https://registry.yarnpkg.com/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz#b6b40a7625429d2bd7c2281ddba601ed05dc7f1a"
integrity sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==
"@vueuse/core@^7.6.1":
version "7.6.1"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-7.6.1.tgz#6919dc0c9289a77f00bfa3403f861f7e4c7adc89"
integrity sha512-492y7R9HRu6TXzcGBMVG5qg5o9CHjrWLfOHh+TEknJeLe3LIYHsIBi1IlUN5s/yP3OHlBynjrzMMUm4gEyBmQg==
dependencies:
"@vueuse/shared" "7.6.1"
vue-demi "*"
"@vueuse/shared@7.6.1":
version "7.6.1"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-7.6.1.tgz#48db62a4ad160838353ae78d0dcbfc7c9c94c89c"
integrity sha512-VhURBjuyELYLW94TLqwyM+tUZ0uyWAOjp8zDnJts5wwyHZlGt/yabLbuEl70cKmt0zR9psVyAyHC+LTgRrA1Zw==
dependencies:
vue-demi "*"
"@webassemblyjs/ast@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
@ -6260,6 +6280,14 @@ pify@^4.0.1:
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
pinia@^2.0.11:
version "2.0.11"
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.0.11.tgz#ff03c714f5e5f16207280a4fc2eab01f3701ee2b"
integrity sha512-JzcmnMqu28PNWOjDgEDK6fTrIzX8eQZKPPKvu/fpHdpXARUj1xeVdFi3YFIMOWswqaBd589cpmAMdSSTryI9iw==
dependencies:
"@vue/devtools-api" "^6.0.0-beta.21"
vue-demi "*"
pkg-dir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
@ -8167,6 +8195,11 @@ vue-cli-plugin-vuetify@~2.4.5:
semver "^7.1.2"
shelljs "^0.8.3"
vue-demi@*:
version "0.12.1"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.12.1.tgz#f7e18efbecffd11ab069d1472d7a06e319b4174c"
integrity sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw==
vue-hot-reload-api@^2.3.0:
version "2.3.4"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
@ -8212,13 +8245,6 @@ vue@^3.2.25:
"@vue/server-renderer" "3.2.29"
"@vue/shared" "3.2.29"
vuex@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.2.tgz#f896dbd5bf2a0e963f00c67e9b610de749ccacc9"
integrity sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==
dependencies:
"@vue/devtools-api" "^6.0.0-beta.11"
watchpack-chokidar2@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"