Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement exporters config ui #107

Merged
merged 33 commits into from
Oct 7, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5b9e3ec
Add Json Exporter
Arielgordon123 Aug 19, 2020
40a903e
Merge remote-tracking branch 'origin/unifyRepos' into exportersBaruch
baruchiro Aug 26, 2020
1ceb9e7
fix merge lint
baruchiro Aug 26, 2020
e32f357
main.ts remove @ts-nocheck use composition
baruchiro Aug 31, 2020
9a48ff0
Merge branch 'unifyRepos' into exportersBaruch
baruchiro Aug 31, 2020
508914c
Wrap exporters with ExpansionPanel
baruchiro Sep 1, 2020
a121a48
Merge branch 'unifyRepos' into exportersBaruch
baruchiro Sep 8, 2020
1974ef0
Merge branch 'direct-vuex' into exportersBaruch
baruchiro Sep 8, 2020
ee613b6
Merge remote-tracking branch 'origin/unifyRepos' into exportersBaruch
baruchiro Sep 9, 2020
b50f477
Create JsonExporter (not saving)
baruchiro Sep 11, 2020
49cd140
JsonExporter
baruchiro Sep 15, 2020
c5c7d9f
Merge remote-tracking branch 'origin/unifyRepos' into exportersBaruch
baruchiro Sep 15, 2020
3fb7392
Initial implementation of google sheets exporter configuration component
brafdlog Sep 16, 2020
a11dce9
Initialize YnabExporter with almost nothing differ from JsonExporter
baruchiro Sep 16, 2020
91cb9f6
Merge branch 'exportersBaruch' of https://github.com/brafdlog/budget-…
baruchiro Sep 16, 2020
36d393d
Extract common exporter form logic to a common function
brafdlog Sep 16, 2020
7f265b0
Add budget id input to ynab config
brafdlog Sep 16, 2020
0a5ff4d
Allow saving when changing text in ynab exporter form
brafdlog Sep 20, 2020
ff03f32
Add ui for configuration of ynab account mapping
brafdlog Sep 20, 2020
8abef66
Align the plus button to the right
brafdlog Sep 20, 2020
e1b30b6
Add required validation to account mapping fields
brafdlog Sep 20, 2020
ee0d950
Extract ynab account mapping table to a separate component
brafdlog Sep 20, 2020
9a7dcb2
Don't set state to be the default config - fix issue with wrong accou…
brafdlog Sep 21, 2020
360be86
Unify the two definitions of OutputVendorName
brafdlog Sep 21, 2020
8eb9632
Make naming consistent
brafdlog Sep 21, 2020
a1bb31a
use emit from setup
baruchiro Oct 4, 2020
62d4026
Apply suggestions from code review
baruchiro Oct 4, 2020
eff92df
Custom event name 'mappingChanged' must be kebab-case.eslint-plugin-vue
baruchiro Oct 4, 2020
e958ec1
change instead of custom name
baruchiro Oct 4, 2020
8934166
Work with v-model
baruchiro Oct 5, 2020
91feae6
Cleanup
baruchiro Oct 5, 2020
3c4f936
change to input
baruchiro Oct 5, 2020
9bc42d1
meaningful names
baruchiro Oct 7, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/components/app/ConfigEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<script lang="ts">
import Vue from 'vue';
import { VForm } from '@/types/vuetify';
import { required, positive } from '@/components/shared/formValidations';
import store from '@/store';

export default Vue.extend({
Expand All @@ -64,8 +65,8 @@ export default Vue.extend({
this.reset();
},
methods: {
required: (value) => !!value || 'Required.',
positive: (value: number) => value > 0 || 'Must be grater than 0',
required,
positive,
updateGlobalConfig: store.dispatch.Config.updateGlobalConfig,
reset() {
this.globalConfig = JSON.parse(JSON.stringify(this.storeGlobalConfig));
Expand Down
4 changes: 3 additions & 1 deletion src/components/app/Exporters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import YnabExporter from './exporters/YnabExporter.vue';
import ExpansionPanel from './exporters/ExpansionPanel.vue';

export default Vue.extend({
components: { ExpansionPanel, JsonExporter, GoogleSheetsExporter, YnabExporter },
components: {
ExpansionPanel, JsonExporter, GoogleSheetsExporter, YnabExporter
},
});
</script>

Expand Down
24 changes: 4 additions & 20 deletions src/components/app/exporters/GoogleSheetsExporter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,35 +39,19 @@

<script lang="ts">
import Vue from 'vue';
import { computed, ref, reactive } from '@vue/composition-api';
import store from '@/store';
import { GoogleSheetsConfig } from '@/originalBudgetTrackingApp/configManager/configManager';
import { VForm } from '@/types/vuetify';
import { OutputVendorName } from '@/originalBudgetTrackingApp/commonTypes';
import { setupExporterConfigForm } from '@/components/app/exporters/exportersCommon';

export default Vue.extend({
name: 'GoogleSheetsExporter',

setup() {
const vForm = ref<VForm>();

const exporterName = 'googleSheets';
const exporter = reactive(JSON.parse(JSON.stringify(store.getters.Config.getExporter(exporterName))) as GoogleSheetsConfig);

const validated = ref(true);
const changed = ref(false);
const readyToSave = computed(() => validated && changed);
const submit = async () => {
if (vForm.value?.validate()) {
await store.dispatch.Config.updateExporter({ name: exporterName, exporter });
changed.value = false;
}
};

return {
validated, changed, readyToSave, submit, exporter, vForm
...setupExporterConfigForm(OutputVendorName.GOOGLE_SHEETS)
};
}
});

</script>

<style scoped>
Expand Down
22 changes: 3 additions & 19 deletions src/components/app/exporters/JsonExporter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,15 @@

<script lang="ts">
import Vue from 'vue';
import { computed, ref, reactive } from '@vue/composition-api';
import store from '@/store';
import { JsonConfig } from '@/originalBudgetTrackingApp/configManager/configManager';
import { VForm } from '@/types/vuetify';
import { setupExporterConfigForm } from '@/components/app/exporters/exportersCommon';
import { OutputVendorName } from '@/originalBudgetTrackingApp/commonTypes';

export default Vue.extend({
name: 'JsonExporter',

setup() {
const vForm = ref<VForm>();

const exporter = reactive(JSON.parse(JSON.stringify(store.getters.Config.getExporter('json'))) as JsonConfig);

const validated = ref(true);
const changed = ref(false);
const readyToSave = computed(() => validated && changed);
const submit = () => {
if (vForm.value?.validate()) {
store.dispatch.Config.updateExporter({ name: 'json', exporter })
.then(() => { changed.value = false; });
}
};

return {
validated, changed, readyToSave, submit, exporter, vForm
...setupExporterConfigForm(OutputVendorName.JSON)
};
}
});
Expand Down
81 changes: 81 additions & 0 deletions src/components/app/exporters/YnabAccountMappingTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<template>
<v-simple-table dense>
<template v-slot:default>
<thead>
<tr>
<th>Account number</th>
<th>Ynab account id</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<tr
v-for="accountNumberAndId in accountNumbersToYnabAccountIds"
:key="accountNumberAndId.accountNumber"
>
<td>
<v-text-field
v-model="accountNumberAndId.accountNumber"
full-width
dense
:rules="[rules.required]"
@keydown="$emit('mappingChanged')"
/>
</td>
<td>
<v-text-field
v-model="accountNumberAndId.ynabAccountId"
full-width
dense
:rules="[rules.required]"
@keydown="$emit('mappingChanged')"
/>
</td>
<td>
<v-btn
icon
color="red"
@click="deleteAccountMapping(accountNumberAndId.accountNumber)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</template>

<script lang="ts">
import Vue from 'vue';
import { required } from '@/components/shared/formValidations';

export default Vue.extend({
name: 'YnabAccountMappingTable',
props: {
accountNumbersToYnabAccountIds: {
type: Array,
required: true
}
},
setup(_props) {
return {
deleteAccountMapping(accountNumberToDelete) {
// @ts-ignore
this.$emit('deleteAccountMapping', accountNumberToDelete);
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a bug here:

  1. Add new Account to Ynab item.
  2. Set value for Account number.
  3. Try to delete this item.

Also, the + button is stuck after that (and maybe without these steps)

searchlist2

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't research this deeply. Just a question (I'm not sure how Two Way Binding works, I'm always googling it), why you're using these events and not just use v-model?

As I remember, v-model is shortcut for value prop and input event, so you can get an array of pairs, and add/remove/change it inside YnabAccountMappingTable, and emit it in any change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solved. I don't know if my solution is better in all the aspects, but I think we need to use v-model.

Then I had a lot of problems, I worked on this a day, so first I managed to get it to work, and now we have something to improve.

addAccountMapping() {
// @ts-ignore
this.$emit('addAccountMapping');
},
rules: {
required
}
};
}
baruchiro marked this conversation as resolved.
Show resolved Hide resolved
});
</script>

<style scoped>

</style>
100 changes: 74 additions & 26 deletions src/components/app/exporters/YnabExporter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,47 +14,95 @@
type="password"
label="Access Token"
outlined
@change="changed = true"
/>
<v-text-field
v-model="exporter.options.budgetId"
label="Budget id"
outlined
@change="changed = true"
/>
<ynab-account-mapping-table
:account-numbers-to-ynab-account-ids="accountNumbersToYnabAccountIdsArray"
@mappingChanged="markChanged()"
@addAccountMapping="addAccountMapping()"
@deleteAccountMapping="deleteAccountMapping($event)"
/>
<v-btn
color="primary"
:disabled="!readyToSave.value"
@click="submit()"
>
Save
</v-btn>
<v-container>
<v-row>
<v-col cols="10">
<v-btn
color="primary"
:disabled="!readyToSave.value"
@click="submit()"
>
Save
</v-btn>
</v-col>
<v-col cols="2">
<v-btn
color="blue"
fab
dark
small
@click="addAccountMapping()"
>
<v-icon dark>
mdi-plus
</v-icon>
</v-btn>
</v-col>
</v-row>
</v-container>
</v-form>
</template>

<script lang="ts">
import Vue from 'vue';
import { computed, ref, reactive } from '@vue/composition-api';
import store from '@/store';
import { OutputVendorsConfig } from '@/originalBudgetTrackingApp/configManager/configManager';
import { VForm } from '@/types/vuetify';

const exporterName = 'ynab';
import { setupExporterConfigForm } from '@/components/app/exporters/exportersCommon';
import { OutputVendorName } from '@/originalBudgetTrackingApp/commonTypes';
import { computed } from '@vue/composition-api';
import YnabAccountMappingTable from '@/components/app/exporters/YnabAccountMappingTable.vue';
import { YnabConfig } from '@/originalBudgetTrackingApp/configManager/configManager';

export default Vue.extend({
name: 'YnabExporter',

components: { YnabAccountMappingTable },

setup() {
const vForm = ref<VForm>();
const dataToReturn = setupExporterConfigForm(OutputVendorName.YNAB);
const ynabConfig = dataToReturn.exporter as YnabConfig;
const accountNumbersToYnabAccountIdsArray = computed(() => {
return Object.keys(ynabConfig.options.accountNumbersToYnabAccountIds).map((accountNumber) => ({
accountNumber,
ynabAccountId: ynabConfig.options.accountNumbersToYnabAccountIds[accountNumber]
}));
});

const exporter = reactive(JSON.parse(JSON.stringify(store.getters.Config.getExporter(exporterName))) as OutputVendorsConfig<typeof exporterName>);
return {
...dataToReturn,
accountNumbersToYnabAccountIdsArray,
submit: () => {
const updatedAccountNumbersToYnabAccountIds = {};
accountNumbersToYnabAccountIdsArray.value.forEach(({ accountNumber, ynabAccountId }) => {
updatedAccountNumbersToYnabAccountIds[accountNumber] = ynabAccountId;
});
ynabConfig.options.accountNumbersToYnabAccountIds = updatedAccountNumbersToYnabAccountIds;

const validated = ref(true);
const changed = ref(false);
const readyToSave = computed(() => validated && changed);
const submit = () => {
if (vForm.value?.validate()) {
store.dispatch.Config.updateExporter({ name: exporterName, exporter })
.then(() => { changed.value = false; });
dataToReturn.submit();
},
deleteAccountMapping: (accountNumber) => {
Vue.delete(ynabConfig.options.accountNumbersToYnabAccountIds, accountNumber);
baruchiro marked this conversation as resolved.
Show resolved Hide resolved
dataToReturn.changed.value = true;
},
addAccountMapping: () => {
Vue.set(ynabConfig.options.accountNumbersToYnabAccountIds, '###', '###');
},
baruchiro marked this conversation as resolved.
Show resolved Hide resolved
markChanged: () => {
dataToReturn.changed.value = true;
}
};

return {
validated, changed, readyToSave, submit, exporter, vForm
};
}
});
</script>
Expand Down
25 changes: 25 additions & 0 deletions src/components/app/exporters/exportersCommon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { cloneDeep } from 'lodash';
import { computed, reactive, ref } from '@vue/composition-api';
import { VForm } from '@/types/vuetify';
import store from '@/store';
import { OutputVendorName } from '@/originalBudgetTrackingApp/commonTypes';

export function setupExporterConfigForm<T extends OutputVendorName>(exporterName: T) {
const vForm = ref<VForm>();

const exporter = reactive(cloneDeep(store.getters.Config.getExporter(exporterName)));

const validated = ref(true);
const changed = ref(false);
const readyToSave = computed(() => validated && changed);
const submit = async () => {
if (vForm.value?.validate()) {
await store.dispatch.Config.updateExporter({ name: exporterName, exporter });
baruchiro marked this conversation as resolved.
Show resolved Hide resolved
changed.value = false;
}
};

return {
validated, changed, readyToSave, submit, exporter, vForm
};
}
7 changes: 7 additions & 0 deletions src/components/shared/formValidations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function required(value) {
return !!value || 'Required.';
}

export function positive(value: number) {
return value > 0 || 'Must be grater than 0';
}
baruchiro marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 2 additions & 1 deletion src/originalBudgetTrackingApp/commonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export interface EnrichedTransaction extends Transaction {

export enum OutputVendorName {
YNAB = 'ynab',
GOOGLE_SHEETS = 'googleSheets'
GOOGLE_SHEETS = 'googleSheets',
JSON = 'json'
baruchiro marked this conversation as resolved.
Show resolved Hide resolved
}

export type ExportTransactionsParams = {
Expand Down
4 changes: 1 addition & 3 deletions src/store/modules/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { randomHex } from '@/modules/encryption/crypto';
import {
AccountToScrapeConfig, Config, OutputVendorsConfig, OutputVendorsNames
} from '@/originalBudgetTrackingApp/configManager/configManager';
import defaultConfig from '@/originalBudgetTrackingApp/configManager/defaultConfig';
baruchiro marked this conversation as resolved.
Show resolved Hide resolved
import { defineModule } from 'direct-vuex';
import { moduleActionContext } from '..';

Expand All @@ -15,7 +14,6 @@ type ExporterPayload<T extends OutputVendorsNames> = { name: T, exporter: Output

const configModule = defineModule({
namespaced: true as true,
state: defaultConfig,
mutations: {
addImporter: (state: Config, importer: AccountToScrapeConfig) => state.scraping.accountsToScrape.push(importer),
removeImporter: (state: Config, importerId: string) => {
Expand All @@ -37,7 +35,7 @@ const configModule = defineModule({
});
return importers;
},
getExporter: (state) => (name: OutputVendorsNames) => state.outputVendors[name],
getExporter: <T extends OutputVendorsNames>(state) => (name: T): OutputVendorsConfig<T> => state.outputVendors[name],
globalConfig: ({ scraping }): GlobalConfig => {
const { numDaysBack, showBrowser } = scraping;
return { numDaysBack, showBrowser };
Expand Down