From eb396829273e1b00d6cf49cbfab3cb355b9eb6d2 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 23 Aug 2024 13:57:13 +0200 Subject: [PATCH 01/10] Add import flow to Tags page --- src/ROUTES.ts | 8 + src/SCREENS.ts | 2 + src/languages/en.ts | 4 + src/languages/es.ts | 4 + .../API/parameters/ImportTagsSpreadsheet.ts | 10 + src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../ModalStackNavigators/index.tsx | 2 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 2 + src/libs/Navigation/linkingConfig/config.ts | 6 + src/libs/Navigation/types.ts | 6 + src/libs/actions/Policy/Tag.ts | 42 +++++ src/pages/workspace/tags/ImportTagsPage.tsx | 21 +++ src/pages/workspace/tags/ImportedTagsPage.tsx | 171 ++++++++++++++++++ .../workspace/tags/WorkspaceTagsPage.tsx | 35 +++- 15 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 src/libs/API/parameters/ImportTagsSpreadsheet.ts create mode 100644 src/pages/workspace/tags/ImportTagsPage.tsx create mode 100644 src/pages/workspace/tags/ImportedTagsPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0e9ff6bfad9d..463624627b2e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -802,6 +802,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/gl-code', getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/gl-code` as const, }, + WORKSPACE_TAGS_IMPORT: { + route: 'settings/workspaces/:policyID/tags/import', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/import` as const, + }, + WORKSPACE_TAGS_IMPORTED: { + route: 'settings/workspaces/:policyID/tags/imported', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/imported` as const, + }, WORKSPACE_TAXES: { route: 'settings/workspaces/:policyID/taxes', getRoute: (policyID: string) => `settings/workspaces/${policyID}/taxes` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9aa2c27c4905..3db5fa8ef693 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -386,6 +386,8 @@ const SCREENS = { TAGS: 'Workspace_Tags', TAGS_SETTINGS: 'Tags_Settings', TAGS_EDIT: 'Tags_Edit', + TAGS_IMPORT: 'Tags_Import', + TAGS_IMPORTED: 'Tags_Imported', TAG_EDIT: 'Tag_Edit', TAXES: 'Workspace_Taxes', REPORT_FIELDS: 'Workspace_ReportFields', diff --git a/src/languages/en.ts b/src/languages/en.ts index 988aba2b6333..97f00e2a1261 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -678,6 +678,7 @@ export default { singleFieldMultipleColumns: (fieldName: string) => `Oops! You've mapped a single field ("${fieldName}") to multiple columns. Please review and try again.`, importSuccessfullTitle: 'Import successful', importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `${categories} categories have been added.` : '1 category has been added.'), + importTagsSuccessfullDescription: (tags: number) => (tags > 1 ? `${tags} tags have been added.` : '1 tag has been added.'), importFailedTitle: 'Oops! There was an issue with your upload.', importFailedDescription: 'Please ensure all fields are correctly filled and try again. If the problem persists, please reach out to Concierge.', }, @@ -3015,6 +3016,9 @@ export default { importedFromAccountingSoftware: 'The tags below are imported from your', glCode: 'GL code', updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.', + importTags: 'Import tags', + importedTagsMessage: (columnCounts: number) => + `We found *${columnCounts} columns* in your spreadsheet. Select *Name* next to the column that contains tags names. You can also select *Enabled* next to the column that sets tags status.`, }, taxes: { subtitle: 'Add tax names, rates, and set defaults.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5cc9fa39ee7e..99152ec09dbb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -672,6 +672,7 @@ export default { importFailedDescription: 'La importación ha fallado. Por favor, revise los errores e inténtelo de nuevo', importFailedTitle: 'Importación fallida', importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `Se han agregado ${categories} categorías.` : 'Se ha agregado 1 categoría.'), + importTagsSuccessfullDescription: (tags: number) => (tags > 1 ? `Se han agregado ${tags} etiquetas.` : 'Se ha agregado 1 etiqueta.'), importSuccessfullTitle: 'Importar categorías', }, receipt: { @@ -3063,6 +3064,9 @@ export default { importedFromAccountingSoftware: 'Etiquetas importadas desde', glCode: 'Código de Libro Mayor', updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código de Libro Mayor. Por favor, inténtelo nuevamente.', + importTags: 'Importar categorías', + importedTagsMessage: (columnCounts: number) => + `Hemos encontrado *${columnCounts} columnas* en su hoja de cálculo. Seleccione *Nombre* junto a la columna que contiene los nombres de las etiquetas. También puede seleccionar *Habilitado* junto a la columna que establece el estado de la etiqueta.`, }, taxes: { subtitle: 'Añade nombres, tasas y establezca valores por defecto para los impuestos.', diff --git a/src/libs/API/parameters/ImportTagsSpreadsheet.ts b/src/libs/API/parameters/ImportTagsSpreadsheet.ts new file mode 100644 index 000000000000..59134fc315ef --- /dev/null +++ b/src/libs/API/parameters/ImportTagsSpreadsheet.ts @@ -0,0 +1,10 @@ +type ImportTagsSpreadsheetParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array<{name: string, enabled: boolean, 'GL Code': string}> + */ + tags: string; +}; + +export default ImportTagsSpreadsheetParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 7f1167e6655f..42e87e8c5879 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -280,3 +280,4 @@ export type {default as OpenCardDetailsPageParams} from './OpenCardDetailsPagePa export type {default as ToggleCardContinuousReconciliationParams} from './ToggleCardContinuousReconciliationParams'; export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams'; export type {default as ImportCategoriesSpreadsheetParams} from './ImportCategoriesSpreadsheet'; +export type {default as ImportTagsSpreadsheetParams} from './ImportTagsSpreadsheet'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index e2b6c958ecab..dd45f13c5976 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -131,6 +131,7 @@ const WRITE_COMMANDS = { SET_POLICY_TAGS_ENABLED: 'SetPolicyTagsEnabled', CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories', IMPORT_CATEGORIES_SREADSHEET: 'ImportCategoriesSpreadsheet', + IMPORT_TAGS_SREADSHEET: 'ImportTagsSpreadsheet', RENAME_WORKSPACE_CATEGORY: 'RenameWorkspaceCategory', CREATE_POLICY_TAG: 'CreatePolicyTag', RENAME_POLICY_TAG: 'RenamePolicyTag', @@ -457,6 +458,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams; [WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES]: Parameters.CreateWorkspaceCategoriesParams; [WRITE_COMMANDS.IMPORT_CATEGORIES_SREADSHEET]: Parameters.ImportCategoriesSpreadsheetParams; + [WRITE_COMMANDS.IMPORT_TAGS_SREADSHEET]: Parameters.ImportTagsSpreadsheetParams; [WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams; [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams; [WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index ac74f8120fd9..8f7e29e1ba01 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -256,6 +256,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/distanceRates/PolicyDistanceRateTaxReclaimableEditPage').default, [SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RATE_EDIT]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage').default, + [SCREENS.WORKSPACE.TAGS_IMPORT]: () => require('../../../../pages/workspace/tags/ImportTagsPage').default, + [SCREENS.WORKSPACE.TAGS_IMPORTED]: () => require('../../../../pages/workspace/tags/ImportedTagsPage').default, [SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require('../../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default, [SCREENS.WORKSPACE.TAG_SETTINGS]: () => require('../../../../pages/workspace/tags/TagSettingsPage').default, [SCREENS.WORKSPACE.TAG_LIST_VIEW]: () => require('../../../../pages/workspace/tags/WorkspaceViewTagsPage').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 881ac49f6dba..d30c6e4b90fb 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -133,6 +133,8 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.TAG_EDIT, SCREENS.WORKSPACE.TAG_LIST_VIEW, SCREENS.WORKSPACE.TAG_GL_CODE, + SCREENS.WORKSPACE.TAGS_IMPORT, + SCREENS.WORKSPACE.TAGS_IMPORTED, ], [SCREENS.WORKSPACE.CATEGORIES]: [ SCREENS.WORKSPACE.CATEGORY_CREATE, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index b2d25497e0c5..8d66a33ddeb7 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -622,6 +622,12 @@ const config: LinkingOptions['config'] = { orderWeight: Number, }, }, + [SCREENS.WORKSPACE.TAGS_IMPORT]: { + path: ROUTES.WORKSPACE_TAGS_IMPORT.route, + }, + [SCREENS.WORKSPACE.TAGS_IMPORTED]: { + path: ROUTES.WORKSPACE_TAGS_IMPORTED.route, + }, [SCREENS.WORKSPACE.TAG_CREATE]: { path: ROUTES.WORKSPACE_TAG_CREATE.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 5c2fae4ac364..451856db66b0 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -262,6 +262,12 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.TAGS_SETTINGS]: { policyID: string; }; + [SCREENS.WORKSPACE.TAGS_IMPORT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.TAGS_IMPORTED]: { + policyID: string; + }; [SCREENS.WORKSPACE.TAG_SETTINGS]: { policyID: string; orderWeight: number; diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 49a285c12bbe..919b67acaa10 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -13,6 +13,7 @@ import type { import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; +import {translateLocal} from '@libs/Localize'; import Log from '@libs/Log'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -122,6 +123,34 @@ function buildOptimisticPolicyRecentlyUsedTags(policyID?: string, transactionTag return newOptimisticPolicyRecentlyUsedTags; } +function updateImportSpreadsheetData(tagsLength: number) { + const onyxData: OnyxData = { + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IMPORTED_SPREADSHEET, + value: { + shouldFinalModalBeOpened: true, + importFinalModal: {title: translateLocal('spreadsheet.importSuccessfullTitle'), prompt: translateLocal('spreadsheet.importTagsSuccessfullDescription', tagsLength)}, + }, + }, + ], + + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IMPORTED_SPREADSHEET, + value: { + shouldFinalModalBeOpened: true, + importFinalModal: {title: translateLocal('spreadsheet.importFailedTitle'), prompt: translateLocal('spreadsheet.importFailedDescription')}, + }, + }, + ], + }; + + return onyxData; +} + function createPolicyTag(policyID: string, tagName: string) { const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[0] ?? {}; const newTagName = PolicyUtils.escapeTagName(tagName); @@ -187,6 +216,18 @@ function createPolicyTag(policyID: string, tagName: string) { API.write(WRITE_COMMANDS.CREATE_POLICY_TAG, parameters, onyxData); } +function importPolicyTags(policyID: string, tags: PolicyTag[]) { + const onyxData = updateImportSpreadsheetData(tags.length); + + const parameters = { + policyID, + // eslint-disable-next-line @typescript-eslint/naming-convention + tags: JSON.stringify([...tags.map((tag) => ({name: tag.name, enabled: tag.enabled, 'GL Code': tag['GL Code']}))]), + }; + + API.write(WRITE_COMMANDS.IMPORT_TAGS_SREADSHEET, parameters, onyxData); +} + function setWorkspaceTagEnabled(policyID: string, tagsToUpdate: Record, tagListIndex: number) { const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[tagListIndex] ?? {}; @@ -861,6 +902,7 @@ export { renamePolicyTaglist, setWorkspaceTagEnabled, setPolicyTagGLCode, + importPolicyTags, }; export type {NewCustomUnit}; diff --git a/src/pages/workspace/tags/ImportTagsPage.tsx b/src/pages/workspace/tags/ImportTagsPage.tsx new file mode 100644 index 000000000000..0dd7c01f0af3 --- /dev/null +++ b/src/pages/workspace/tags/ImportTagsPage.tsx @@ -0,0 +1,21 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import ImportSpreedsheet from '@components/ImportSpreadsheet'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ImportTagsPageProps = StackScreenProps; + +function ImportTagsPage({route}: ImportTagsPageProps) { + const policyID = route.params.policyID; + + return ( + + ); +} + +export default ImportTagsPage; diff --git a/src/pages/workspace/tags/ImportedTagsPage.tsx b/src/pages/workspace/tags/ImportedTagsPage.tsx new file mode 100644 index 000000000000..181cc0dd733a --- /dev/null +++ b/src/pages/workspace/tags/ImportedTagsPage.tsx @@ -0,0 +1,171 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import ConfirmModal from '@components/ConfirmModal'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {ColumnRole} from '@components/ImportColumn'; +import ImportSpreadsheetColumns from '@components/ImportSpreadsheetColumns'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; +import {closeImportPage} from '@libs/actions/ImportSpreadsheet'; +import {importPolicyTags} from '@libs/actions/Policy/Tag'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {isControlPolicy} from '@libs/PolicyUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ImportedTagsPageProps = StackScreenProps; + +function numberToColumn(index: number) { + let column = ''; + let number = index; + while (number >= 0) { + column = String.fromCharCode((number % 26) + 65) + column; + number = Math.floor(number / 26) - 1; + } + return column; +} + +function generateColumnNames(length: number) { + return Array.from({length}, (_, i) => numberToColumn(i)); +} + +function findDuplicate(array: string[]): string | null { + const frequencyCounter: Record = {}; + + for (const item of array) { + if (item !== CONST.CSV_IMPORT_COLUMNS.IGNORE) { + if (frequencyCounter[item]) { + return item; + } + frequencyCounter[item] = (frequencyCounter[item] || 0) + 1; + } + } + + return null; +} + +function ImportedTagsPage({route}: ImportedTagsPageProps) { + const {translate} = useLocalize(); + const [spreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET); + const [isImporting, setIsImporting] = useState(false); + const [containsHeader, setContainsHeader] = useState(false); + const policyID = route.params.policyID; + const policy = usePolicy(policyID); + const columnNames = generateColumnNames(spreadsheet?.data?.length ?? 0); + + const isControl = isControlPolicy(policy); + + const getColumnRoles = (): ColumnRole[] => { + const roles = []; + roles.push( + {text: translate('common.ignore'), value: CONST.CSV_IMPORT_COLUMNS.IGNORE}, + {text: translate('common.name'), value: CONST.CSV_IMPORT_COLUMNS.NAME, isRequired: true}, + {text: translate('common.enabled'), value: CONST.CSV_IMPORT_COLUMNS.ENABLED, isRequired: true}, + ); + + if (isControl) { + roles.push({text: translate('workspace.tags.glCode'), value: CONST.CSV_IMPORT_COLUMNS.GL_CODE, isRequired: true}); + } + + return roles; + }; + + const columnRoles = getColumnRoles(); + + const requiredColumns = columnRoles.filter((role) => role.isRequired).map((role) => role); + + const validate = useCallback(() => { + const columns = Object.values(spreadsheet?.columns ?? {}); + let errors: Record = {}; + + if (!requiredColumns.every((requiredColumn) => columns.includes(requiredColumn.value))) { + // eslint-disable-next-line rulesdir/prefer-early-return + requiredColumns.forEach((requiredColumn) => { + if (!columns.includes(requiredColumn.value)) { + errors.required = translate('spreadsheet.fieldNotMapped', requiredColumn.text); + } + }); + } else { + const duplicate = findDuplicate(columns); + if (duplicate) { + errors.duplicates = translate('spreadsheet.singleFieldMultipleColumns', duplicate); + } else { + errors = {}; + } + } + + return errors; + }, [requiredColumns, spreadsheet?.columns, translate]); + + const importTags = useCallback(() => { + validate(); + const columns = Object.values(spreadsheet?.columns ?? {}); + const tagsNamesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.NAME); + const tagsGLCodeColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.GL_CODE); + const tagsEnabledColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ENABLED); + const tagsNames = spreadsheet?.data[tagsNamesColumn].map((name) => name); + const tagsEnabled = tagsEnabledColumn !== -1 ? spreadsheet?.data[tagsEnabledColumn].map((enabled) => enabled) : []; + const tagsGLCode = tagsGLCodeColumn !== -1 ? spreadsheet?.data[tagsGLCodeColumn].map((glCode) => glCode) : []; + const tags = tagsNames?.slice(containsHeader ? 1 : 0).map((name, index) => ({ + name, + enabled: tagsEnabledColumn !== -1 ? tagsEnabled?.[containsHeader ? index + 1 : index] === 'true' : true, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'GL Code': tagsGLCodeColumn !== -1 ? tagsGLCode?.[containsHeader ? index + 1 : index] : '', + })); + + if (tags) { + setIsImporting(true); + importPolicyTags(policyID, tags); + } + }, [validate, spreadsheet, containsHeader, policyID]); + + const spreadsheetColumns = spreadsheet?.data; + if (!spreadsheetColumns) { + return; + } + + return ( + + Navigation.goBack(ROUTES.WORKSPACE_TAGS_IMPORT.getRoute(policyID))} + /> + + + { + setIsImporting(false); + closeImportPage(); + Navigation.navigate(ROUTES.WORKSPACE_TAGS.getRoute(policyID)); + }} + confirmText={translate('common.buttonConfirm')} + shouldShowCancelButton={false} + /> + + ); +} + +ImportedTagsPage.displayName = 'ImportedTagsPage'; + +export default ImportedTagsPage; diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index adf200a99dd1..dc721ca26cba 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -26,6 +26,7 @@ import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import localeCompare from '@libs/LocaleCompare'; @@ -49,8 +50,10 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); + const {windowWidth} = useWindowDimensions(); const [selectedTags, setSelectedTags] = useState>({}); const [isDeleteTagsConfirmModalVisible, setIsDeleteTagsConfirmModalVisible] = useState(false); + const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const isFocused = useIsFocused(); const policyID = route.params.policyID ?? '-1'; const policy = usePolicy(policyID); @@ -193,7 +196,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { if (shouldUseNarrowLayout ? !selectionMode?.isEnabled : selectedTagsArray.length === 0) { return ( - + {!hasAccountingConnections && !isMultiLevelTags && (