Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8717,7 +8717,7 @@ const CONST = {
CATEGORY: 'category',
DATE: 'date',
MERCHANT: 'merchant',
TRANSACTION_FIELDS: ['date', 'merchant', 'amount', 'category'] as const,
TRANSACTION_FIELDS: ['date', 'merchant', 'amount', 'category', 'tag'] as const,
CARD_NUMBER: 'cardNumber',
CARD_NAME: 'cardName',
POSTED_DATE: 'postedDate',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,8 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Ups! Ein erforderliches Feld („${fieldName}“) wurde nicht zugeordnet. Bitte überprüfe es und versuche es erneut.`,
singleFieldMultipleColumns: (fieldName: string) => `Ups! Du hast ein einzelnes Feld („${fieldName}“) mehreren Spalten zugeordnet. Bitte überprüfe dies und versuche es erneut.`,
emptyMappedField: (fieldName: string) => `Ups! Das Feld („${fieldName}“) enthält einen oder mehrere leere Werte. Bitte überprüfe es und versuche es erneut.`,
fieldValueTooLong: (fieldName: string, limit: number) =>
`Ups! Das Feld („${fieldName}“) enthält einen oder mehrere Werte mit mehr als ${limit} Zeichen. Bitte überprüfe es und versuche es erneut.`,
importSuccessfulTitle: 'Import erfolgreich',
importCategoriesNoneAddedOrUpdated: 'Es wurden keine Kategorien hinzugefügt oder aktualisiert.',
importCategoriesAdded: ({count}: {count: number}) => ({
Expand Down
2 changes: 2 additions & 0 deletions src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,8 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Ουπς! Ένα υποχρεωτικό πεδίο («${fieldName}») δεν έχει αντιστοιχιστεί. Παρακαλούμε ελέγξτε και δοκιμάστε ξανά.`,
singleFieldMultipleColumns: (fieldName: string) => `Ωχ! Έχετε αντιστοιχίσει ένα μόνο πεδίο («${fieldName}») σε πολλές στήλες. Παρακαλούμε ελέγξτε και δοκιμάστε ξανά.`,
emptyMappedField: (fieldName: string) => `Ωχ! Το πεδίο («${fieldName}») περιέχει μία ή περισσότερες κενές τιμές. Παρακαλούμε ελέγξτε και δοκιμάστε ξανά.`,
fieldValueTooLong: (fieldName: string, limit: number) =>
`Ωχ! Το πεδίο («${fieldName}») περιέχει μία ή περισσότερες τιμές μεγαλύτερες από ${limit} χαρακτήρες. Παρακαλούμε ελέγξτε και δοκιμάστε ξανά.`,
importSuccessfulTitle: 'Η εισαγωγή ήταν επιτυχής',
importCategoriesNoneAddedOrUpdated: 'Δεν έχουν προστεθεί ή ενημερωθεί κατηγορίες.',
importCategoriesAdded: ({count}: {count: number}) => ({
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1290,6 +1290,7 @@ const translations = {
fieldNotMapped: (fieldName: string) => `Oops! A required field ("${fieldName}") hasn't been mapped. Please review and try again.`,
singleFieldMultipleColumns: (fieldName: string) => `Oops! You've mapped a single field ("${fieldName}") to multiple columns. Please review and try again.`,
emptyMappedField: (fieldName: string) => `Oops! The field ("${fieldName}") contains one or more empty values. Please review and try again.`,
fieldValueTooLong: (fieldName: string, limit: number) => `Oops! The field ("${fieldName}") contains one or more values longer than ${limit} characters. Please review and try again.`,
importSuccessfulTitle: 'Import successful',
importCategoriesNoneAddedOrUpdated: 'No categories have been added or updated.',
importCategoriesAdded: ({count}: {count: number}) => ({
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName) => `¡Vaya! Un campo obligatorio ("${fieldName}") no ha sido mapeado. Por favor, revisa e inténtalo de nuevo.`,
singleFieldMultipleColumns: (fieldName) => `¡Vaya! Has mapeado un solo campo ("${fieldName}") a varias columnas. Por favor, revisa e inténtalo de nuevo.`,
emptyMappedField: (fieldName) => `¡Vaya! El campo ("${fieldName}") contiene uno o más valores vacíos. Por favor, revísalo e inténtalo de nuevo.`,
fieldValueTooLong: (fieldName, limit) => `¡Vaya! El campo ("${fieldName}") contiene uno o más valores de más de ${limit} caracteres. Por favor, revísalo e inténtalo de nuevo.`,
importFailedTitle: 'Fallo en la importación',
importFailedDescription: 'Por favor, asegúrate de que todos los campos estén llenos correctamente e inténtalo de nuevo. Si el problema persiste, por favor contacta a Concierge.',
importCategoriesNoneAddedOrUpdated: 'No se han añadido ni actualizado categorías.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,8 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Oups ! Un champ obligatoire (« ${fieldName} ») n’a pas été associé. Veuillez examiner et réessayer.`,
singleFieldMultipleColumns: (fieldName: string) => `Oups ! Vous avez associé un seul champ (« ${fieldName} ») à plusieurs colonnes. Veuillez vérifier et réessayer.`,
emptyMappedField: (fieldName: string) => `Oups ! Le champ (« ${fieldName} ») contient une ou plusieurs valeurs vides. Veuillez vérifier et réessayer.`,
fieldValueTooLong: (fieldName: string, limit: number) =>
`Oups ! Le champ (« ${fieldName} ») contient une ou plusieurs valeurs de plus de ${limit} caractères. Veuillez vérifier et réessayer.`,
importSuccessfulTitle: 'Importation réussie',
importCategoriesNoneAddedOrUpdated: 'Aucune catégorie n’a été ajoutée ou mise à jour.',
importCategoriesAdded: ({count}: {count: number}) => ({
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Ops! Un campo obbligatorio ("${fieldName}") non è stato mappato. Controlla e riprova.`,
singleFieldMultipleColumns: (fieldName: string) => `Ops! Hai associato un singolo campo ("${fieldName}") a più colonne. Controlla e riprova.`,
emptyMappedField: (fieldName: string) => `Ops! Il campo ("${fieldName}") contiene uno o più valori vuoti. Controlla e riprova.`,
fieldValueTooLong: (fieldName: string, limit: number) => `Ops! Il campo ("${fieldName}") contiene uno o più valori più lunghi di ${limit} caratteri. Controlla e riprova.`,
importSuccessfulTitle: 'Importazione riuscita',
importCategoriesNoneAddedOrUpdated: 'Nessuna categoria è stata aggiunta o aggiornata.',
importCategoriesAdded: ({count}: {count: number}) => ({
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1180,6 +1180,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `おっと!必須フィールド(「${fieldName}」)がマッピングされていません。確認してもう一度お試しください。`,
singleFieldMultipleColumns: (fieldName: string) => `おっと!1 つのフィールド(「${fieldName}」)を複数の列に割り当てています。確認してもう一度お試しください。`,
emptyMappedField: (fieldName: string) => `おっと!フィールド(「${fieldName}」)に1つ以上の空の値が含まれています。確認してもう一度お試しください。`,
fieldValueTooLong: (fieldName: string, limit: number) => `おっと!フィールド(「${fieldName}」)に${limit}文字を超える値が1つ以上含まれています。確認してもう一度お試しください。`,
importSuccessfulTitle: 'インポートに成功しました',
importCategoriesNoneAddedOrUpdated: 'カテゴリーは追加も更新もされていません。',
importCategoriesAdded: ({count}: {count: number}) => ({
Expand Down
2 changes: 2 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,8 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Oeps! Een vereist veld ("${fieldName}") is niet toegewezen. Controleer het en probeer het opnieuw.`,
singleFieldMultipleColumns: (fieldName: string) => `Oeps! Je hebt één veld (‘${fieldName}’) aan meerdere kolommen gekoppeld. Controleer dit en probeer het opnieuw.`,
emptyMappedField: (fieldName: string) => `Oeps! Het veld („${fieldName}”) bevat een of meer lege waarden. Controleer het en probeer het opnieuw.`,
fieldValueTooLong: (fieldName: string, limit: number) =>
`Oeps! Het veld („${fieldName}”) bevat een of meer waarden die langer zijn dan ${limit} tekens. Controleer het en probeer het opnieuw.`,
importSuccessfulTitle: 'Import geslaagd',
importCategoriesNoneAddedOrUpdated: 'Er zijn geen categorieën toegevoegd of bijgewerkt.',
importCategoriesAdded: ({count}: {count: number}) => ({
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Ups! Wymagane pole („${fieldName}”) nie zostało zmapowane. Sprawdź i spróbuj ponownie.`,
singleFieldMultipleColumns: (fieldName: string) => `Ups! Przypisałeś jedno pole („${fieldName}”) do wielu kolumn. Sprawdź ustawienia i spróbuj ponownie.`,
emptyMappedField: (fieldName: string) => `Ups! Pole („${fieldName}”) zawiera jedną lub więcej pustych wartości. Sprawdź je i spróbuj ponownie.`,
fieldValueTooLong: (fieldName: string, limit: number) => `Ups! Pole („${fieldName}”) zawiera jedną lub więcej wartości dłuższych niż ${limit} znaków. Sprawdź je i spróbuj ponownie.`,
importSuccessfulTitle: 'Import zakończony powodzeniem',
importCategoriesNoneAddedOrUpdated: 'Nie dodano ani nie zaktualizowano żadnych kategorii.',
importCategoriesAdded: ({count}: {count: number}) => ({
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Ops! Um campo obrigatório ("${fieldName}") não foi mapeado. Revise e tente novamente.`,
singleFieldMultipleColumns: (fieldName: string) => `Ops! Você mapeou um único campo ("${fieldName}") para várias colunas. Revise e tente novamente.`,
emptyMappedField: (fieldName: string) => `Ops! O campo ("${fieldName}") contém um ou mais valores vazios. Revise e tente novamente.`,
fieldValueTooLong: (fieldName: string, limit: number) => `Ops! O campo ("${fieldName}") contém um ou mais valores com mais de ${limit} caracteres. Revise e tente novamente.`,
importSuccessfulTitle: 'Importação concluída',
importCategoriesNoneAddedOrUpdated: 'Nenhuma categoria foi adicionada ou atualizada.',
importCategoriesAdded: ({count}: {count: number}) => ({
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `哎呀!有一个必填字段(“${fieldName}”)尚未映射。请检查后重试。`,
singleFieldMultipleColumns: (fieldName: string) => `哎呀!你已将单个字段(“${fieldName}”)映射到多个列。请检查后重试。`,
emptyMappedField: (fieldName: string) => `哎呀!字段(“${fieldName}”)包含一个或多个空值。请检查后重试。`,
fieldValueTooLong: (fieldName: string, limit: number) => `哎呀!字段(“${fieldName}”)包含一个或多个超过 ${limit} 个字符的值。请检查后重试。`,
importSuccessfulTitle: '导入成功',
importCategoriesNoneAddedOrUpdated: '尚未添加或更新任何类别。',
importCategoriesAdded: ({count}: {count: number}) => ({
Expand Down
29 changes: 26 additions & 3 deletions src/libs/actions/ImportTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,18 @@ type TransactionFromCSV = {
merchant: string;
amount: number;
category?: string;
tag?: string;
};

type ColumnIndexes = {
date: number;
merchant: number;
amount: number;
category: number;
tag: number;
};

type TransactionField = 'date' | 'merchant' | 'amount' | 'category';
type TransactionField = 'date' | 'merchant' | 'amount' | 'category' | 'tag';

/**
* Type guard to check if a string is a valid transaction field
Expand All @@ -55,6 +57,7 @@ function getColumnIndexes(columns: Record<number, string> | undefined): ColumnIn
merchant: -1,
amount: -1,
category: -1,
tag: -1,
};

if (columns) {
Expand All @@ -80,13 +83,15 @@ function buildColumnLayout(spreadsheet: ImportedSpreadsheet, cardName: string, c
amount: false,
merchant: false,
category: false,
tag: false,
type: false,
};
const names: SavedCSVColumnLayoutData['columnMapping']['names'] = {
date: false,
amount: false,
merchant: false,
category: false,
tag: false,
type: false,
};

Expand Down Expand Up @@ -133,7 +138,7 @@ function buildTransactionListFromSpreadsheet(spreadsheet: ImportedSpreadsheet, s
const {flipAmountSign = false} = settings;

// Find the column indexes for each field
const {date: dateColumnIndex, merchant: merchantColumnIndex, amount: amountColumnIndex, category: categoryColumnIndex} = getColumnIndexes(columns);
const {date: dateColumnIndex, merchant: merchantColumnIndex, amount: amountColumnIndex, category: categoryColumnIndex, tag: tagColumnIndex} = getColumnIndexes(columns);

const transactions: TransactionFromCSV[] = [];
const startIndex = containsHeader ? 1 : 0;
Expand All @@ -151,6 +156,7 @@ function buildTransactionListFromSpreadsheet(spreadsheet: ImportedSpreadsheet, s
const merchantValue = merchantColumnIndex >= 0 ? data.at(merchantColumnIndex)?.at(rowIndex) : undefined;
const amountValue = amountColumnIndex >= 0 ? data.at(amountColumnIndex)?.at(rowIndex) : undefined;
const categoryValue = categoryColumnIndex >= 0 ? data.at(categoryColumnIndex)?.at(rowIndex) : undefined;
const tagValue = tagColumnIndex >= 0 ? data.at(tagColumnIndex)?.at(rowIndex) : undefined;

// Skip rows with missing required fields
if (!dateValue || !amountValue) {
Expand Down Expand Up @@ -184,12 +190,28 @@ function buildTransactionListFromSpreadsheet(spreadsheet: ImportedSpreadsheet, s
transaction.category = categoryValue;
}

if (tagValue) {
transaction.tag = tagValue;
}

transactions.push(transaction);
}

return transactions;
}

/**
* Checks whether any row that will be imported has a tag longer than the API accepts, so it can be flagged before import instead of failing on the server.
* Rows that buildTransactionListFromSpreadsheet skips (for example a footer or a row with an invalid date) are never sent, so their tags are not checked.
*/
function hasTagExceedingMaxLength(spreadsheet: ImportedSpreadsheet | undefined): boolean {
if (!spreadsheet || getColumnIndexes(spreadsheet.columns).tag < 0) {
return false;
}

return buildTransactionListFromSpreadsheet(spreadsheet, {}).some((transaction) => [...(transaction.tag ?? '')].length > CONST.API_TRANSACTION_TAG_MAX_LENGTH);
}

/**
* Creates an optimistic card object for the imported transactions
*/
Expand Down Expand Up @@ -235,6 +257,7 @@ function buildOptimisticTransactions(transactionList: TransactionFromCSV[], card
amount: csvTransaction.amount,
currency,
category: csvTransaction.category ?? '',
tag: csvTransaction.tag ?? '',
reimbursable: isReimbursable,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
comment: {
Expand Down Expand Up @@ -476,5 +499,5 @@ async function uploadOFXStatement(file: FileObject, settings: ImportTransactionS
}
}

export {getColumnIndexes, buildColumnLayout, buildTransactionListFromSpreadsheet, getExistingCardImportSettings, uploadOFXStatement};
export {getColumnIndexes, hasTagExceedingMaxLength, buildColumnLayout, buildTransactionListFromSpreadsheet, getExistingCardImportSettings, uploadOFXStatement};
export default importTransactionsFromCSV;
2 changes: 2 additions & 0 deletions src/libs/importSpreadsheetUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ function findColumnName(header: string, columnRoles?: ColumnRole[]): string {

case 'updatedtag':
case 'newtag':
case 'label':
case 'labels':
attribute = CONST.CSV_IMPORT_COLUMNS.TAG;
break;

Expand Down
7 changes: 5 additions & 2 deletions src/pages/settings/Wallet/TransactionsImportedPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';

import {applySavedColumnMappings} from '@libs/actions/ImportSpreadsheet';
import importTransactionsFromCSV, {getExistingCardImportSettings} from '@libs/actions/ImportTransactions';
import importTransactionsFromCSV, {getExistingCardImportSettings, hasTagExceedingMaxLength} from '@libs/actions/ImportTransactions';
import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
Expand Down Expand Up @@ -82,6 +82,7 @@ function TransactionsImportedPage({route}: TransactionsImportedPageProps) {
{text: translate('common.date'), value: CONST.CSV_IMPORT_COLUMNS.DATE, isRequired: true},
{text: translate('common.merchant'), value: CONST.CSV_IMPORT_COLUMNS.MERCHANT, isRequired: true},
{text: translate('common.category'), value: CONST.CSV_IMPORT_COLUMNS.CATEGORY},
{text: translate('common.tag'), value: CONST.CSV_IMPORT_COLUMNS.TAG},
{text: translate('iou.amount'), value: CONST.CSV_IMPORT_COLUMNS.AMOUNT, isRequired: true},
],
[translate],
Expand All @@ -102,12 +103,14 @@ function TransactionsImportedPage({route}: TransactionsImportedPageProps) {

if (duplicateColumn) {
errors.duplicates = translate('spreadsheet.singleFieldMultipleColumns', duplicateColumn.text);
} else if (hasTagExceedingMaxLength(spreadsheet)) {
errors.tagTooLong = translate('spreadsheet.fieldValueTooLong', translate('common.tag'), CONST.API_TRANSACTION_TAG_MAX_LENGTH);
} else {
errors = {};
}
}
return errors;
}, [spreadsheet?.columns, requiredColumns, translate, columnRoles]);
}, [spreadsheet, requiredColumns, translate, columnRoles]);

const closeImportPageAndModal = () => {
setIsClosing(true);
Expand Down
4 changes: 4 additions & 0 deletions src/types/onyx/SavedCSVColumnLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type ColumnMappingIndexes = {
amount?: number | boolean;
/** Category column index or false if not mapped */
category?: number | boolean;
/** Tag column index or false if not mapped */
tag?: number | boolean;
/** Ignore column index or false if not mapped */
ignore?: number | boolean;
/** Type (debit/credit) column index or false if not mapped */
Expand All @@ -24,6 +26,8 @@ type ColumnMappingNames = {
amount?: string | boolean;
/** Category column header name or false if not mapped */
category?: string | boolean;
/** Tag column header name or false if not mapped */
tag?: string | boolean;
/** Ignore column header name or false if not mapped */
ignore?: string | boolean;
/** Type (debit/credit) column header name or false if not mapped */
Expand Down
Loading
Loading