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

fix: #3086 copy transactions to clipboard from modal #3102

Merged
merged 4 commits into from
May 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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[];
};
}
);