Skip to content

Commit

Permalink
fix(feature): toolkit-for-ynab#3086 copy transactions to clipboard (t…
Browse files Browse the repository at this point in the history
…oolkit-for-ynab#3102)

Co-authored-by: Josh Madewell <joshmadewell@users.noreply.github.com>
  • Loading branch information
2 people authored and HelloThisIsFlo committed Jun 13, 2023
1 parent 74b9747 commit 13ac4a5
Show file tree
Hide file tree
Showing 14 changed files with 359 additions and 260 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { useState } from 'react';
import { YNABTransaction } from 'toolkit/types/ynab/data/transaction';
import copyTransactionsToClipboard from './copyTransactionsToClipboard';

interface Props {
transactions: YNABTransaction[];
}

export default function CopyTransactionsButton({ transactions }: Props) {
const [isCopied, setIsCopied] = useState(false);

const handleCopyTransactions = () => {
copyTransactionsToClipboard(transactions);

setIsCopied(true);

setTimeout(() => {
setIsCopied(false);
}, 2000);
};

if (!(Array.isArray(transactions) && transactions.length > 0)) return null;

return (
<button
id="tk-copy-transactions"
className="button button-primary"
onClick={handleCopyTransactions}
>
{isCopied ? 'Copied!' : 'Copy Transactions'}
</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getEntityManager } from 'toolkit/extension/utils/ynab';
import { YNABTransaction } from 'toolkit/types/ynab/data/transaction';

interface Activities {
Account: string;
Date: string;
Payee: any;
Category: string;
Memo: string;
Amount: string;
}

export default function copyTransactionsToClipboard(transactions: YNABTransaction[]) {
const entityManager = getEntityManager();
const activities = transactions.map<Activities>((transaction) => {
const parentEntityId = transaction.get('parentEntityId');
let payeeId = transaction.get('payeeId');

if (parentEntityId) {
payeeId = entityManager.transactionsCollection
.findItemByEntityId(parentEntityId)
.get('payeeId');
}

const payee = entityManager.payeesCollection.findItemByEntityId(payeeId);
return {
Account: transaction.get('accountName'),
Date: ynab.formatDateLong(transaction.get('date').toString()),
Payee: payee && payee.get('name') ? payee.get('name') : 'Unknown',
Category: transaction.get('subCategoryNameWrapped'),
Memo: transaction.get('memo'),
Amount: ynab.formatCurrency(transaction.get('amount')),
};
});

const replacer = (_key: string, value: null | string) => (value === null ? '' : value);
const header = Object.keys(activities[0]) as (keyof Activities)[];
let csv = activities.map((row) =>
header.map((fieldName) => JSON.stringify(row[fieldName], replacer)).join('\t')
);
csv.unshift(header.join('\t'));
navigator.clipboard.writeText(csv.join('\r\n'));
}
123 changes: 0 additions & 123 deletions src/extension/features/general/category-activity-copy/index.jsx

This file was deleted.

59 changes: 59 additions & 0 deletions src/extension/features/general/category-activity-copy/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { Feature } from 'toolkit/extension/features/feature';
import {
getModalService,
isCurrentRouteBudgetPage,
isCurrentRouteReportPage,
} from 'toolkit/extension/utils/ynab';
import { componentAppend } from 'toolkit/extension/utils/react';
import CopyTransactionsButton from './CopyTransactionsButtons';
import { YNABTransaction } from 'toolkit/types/ynab/data/transaction';

export class CategoryActivityCopy extends Feature {
shouldInvoke() {
return (
$('#tk-copy-transactions').length === 0 &&
!!getModalService()?.isModalOpen &&
!!document.querySelector('.modal-budget-activity')
);
}

invoke() {
const modal = document.querySelector('.modal-budget-activity');
const modalService = getModalService();

if (modal && modalService.isModalOpen) {
let transactions: YNABTransaction[] | undefined = undefined;
if (modalService.currentModal === 'modals/budget/activity' && isCurrentRouteBudgetPage()) {
transactions = modalService.modalValue?.selectedActivityTransactions;
} else if (
modalService.currentModal === 'modals/reports/transactions' &&
isCurrentRouteReportPage('any')
) {
transactions = modalService.modalValue?.modalTransactions;
}

if (Array.isArray(transactions) && transactions.length > 0) {
componentAppend(
<CopyTransactionsButton transactions={transactions} />,
modal.querySelector('.modal-actions')
);
}
}
}

observe(nodes: Set<string>) {
if (!this.shouldInvoke()) return;

if (
nodes.has('modal-overlay active ynab-u modal-popup modal-budget-activity') ||
nodes.has('modal-overlay active pure-u modal-popup modal-budget-activity')
) {
this.invoke();
}
}

destroy() {
$('#tk-copy-transactions').remove();
}
}
18 changes: 17 additions & 1 deletion src/extension/utils/ynab.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getRouter, controllerLookup, serviceLookup } from './ember';
import { YNABModalService } from 'toolkit/types/ynab/services/YNABModalService';
import { controllerLookup, serviceLookup } from './ember';

export function getApplicationController() {
return controllerLookup<YNABApplicationController>('application');
Expand Down Expand Up @@ -43,6 +44,17 @@ export function isCurrentRouteAccountsPage() {
);
}

export function isCurrentRouteReportPage(
report?: 'spending' | 'income-expense' | 'net-worth' | 'any'
) {
const currentRoute = getCurrentRouteName();
if (report === 'any' || !report) {
return currentRoute?.includes('reports.');
}

return currentRoute === `reports.${report}`;
}

export function getSelectedAccount() {
const selectedAccountId = getAccountsController()?.selectedAccountId;
if (selectedAccountId) {
Expand Down Expand Up @@ -76,6 +88,10 @@ export function getBudgetService() {
return serviceLookup<YNABBudgetService>('budget');
}

export function getModalService() {
return serviceLookup<YNABModalService>('modal') || {};
}

export function getRegisterGridService() {
return serviceLookup<YNABRegisterGridService>('registerGrid');
}
Expand Down
5 changes: 5 additions & 0 deletions src/test/utils/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function mockTransaction(overrides?: Partial<YNABTransaction>): YNABTrans
return {
accepted: true,
account: mockAccount(),
accountName: 'account-name',
accountId: 'account-id',
amount: 1000,
baseSubTransactions: [],
Expand Down Expand Up @@ -52,6 +53,7 @@ export function mockTransaction(overrides?: Partial<YNABTransaction>): YNABTrans
source: null,
subCategory: null,
subCategoryCreditAmountPreceding: 0,
subCategoryNameWrapped: 'subCategoryNameWrapped',
subCategoryId: null,
subTransactions: [],
transferAccountId: null,
Expand All @@ -62,6 +64,9 @@ export function mockTransaction(overrides?: Partial<YNABTransaction>): YNABTrans
transferTransactionId: null,
ynabId: null,
isUncleared: jest.fn(),
get(key) {
return this[key];
},
...overrides,
};
}
3 changes: 3 additions & 0 deletions src/types/ynab/data/payee-collection.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
interface YNABPayeeCollection extends YNABCollection<YNABPayee> {
findItemByEntityId(entityId: string | null): YNABPayee;
}
1 change: 1 addition & 0 deletions src/types/ynab/data/payee.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ interface YNABPayee {
entityId?: string;
isStartingBalancePayee(): boolean;
name: string;
get<T extends keyof YNABPayee>(key: T): YNABPayee[T];
}
4 changes: 3 additions & 1 deletion src/types/ynab/data/transaction-collection.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { YNABTransaction } from './transaction';

interface YNABTransactionCollection extends YNABCollection<YNABTransaction> {}
interface YNABTransactionCollection extends YNABCollection<YNABTransaction> {
findItemByEntityId(entityId: string | null): YNABTransaction;
}
4 changes: 4 additions & 0 deletions src/types/ynab/data/transaction.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { YNABAccount } from './account';
export interface YNABTransaction {
accepted: boolean;
account: YNABAccount;
accountName: string;
accountId: string;
amount: number;
baseSubTransactions: Array<YNABTransaction>;
Expand All @@ -28,6 +29,7 @@ export interface YNABTransaction {
month: DateWithoutTime;
originalImportedPayee: YNABPayee | null;
parentTransaction?: YNABTransaction;
parentEntityId?: string;
payee: YNABPayee | null;
payeeId: string | null;
scheduledTransactionId: string | null;
Expand All @@ -36,6 +38,7 @@ export interface YNABTransaction {
subCategory: YNABSubCategory | null;
subCategoryCreditAmountPreceding: number;
subCategoryId: string | null;
subCategoryNameWrapped: string;
subTransactions: YNABTransaction[];
transferAccountId: string | null;
transferAccounts: YNABAccount[] | null;
Expand All @@ -47,4 +50,5 @@ export interface YNABTransaction {

isUncleared?: () => boolean;
isReconciled?: () => boolean;
get: <T extends keyof YNABTransaction>(key: T) => YNABTransaction[T];
}
19 changes: 17 additions & 2 deletions src/types/ynab/services/YNABModalService.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
interface YNABModalService {
import { YNABTransaction } from '../data/transaction';

type YNABModalService = {
isModalOpen?: boolean;
closeModal?: (e?: unknown) => void;
}
} & (
| {
currentModal?: 'modals/budget/activity';
modalValue?: {
selectedActivityTransactions?: YNABTransaction[];
};
}
| {
currentModal?: 'modals/reports/transactions';
modalValue?: {
modalTransactions?: YNABTransaction[];
};
}
);

0 comments on commit 13ac4a5

Please sign in to comment.