diff --git a/src/CONST.ts b/src/CONST.ts index 789e9b588609..ea5923ddb706 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -866,6 +866,7 @@ const CONST = { EMPTY_ARRAY, EMPTY_OBJECT, DEFAULT_NUMBER_ID: 0, + EMPTY_STRING: '', USE_EXPENSIFY_URL, EXPENSIFY_URL, GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com', diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 7b85ddcb02c0..c16e642e4684 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -14,12 +14,12 @@ import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; import ScreenWrapper from '@components/ScreenWrapper'; -import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; +import Switch from '@components/Switch'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useAutoTurnSelectionModeOffWhenHasNoActiveOption from '@hooks/useAutoTurnSelectionModeOffWhenHasNoActiveOption'; @@ -42,8 +42,8 @@ import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Modal from '@userActions/Modal'; -import {deleteWorkspaceCategories, setWorkspaceCategoryEnabled} from '@userActions/Policy/Category'; import * as Category from '@userActions/Policy/Category'; +import {deleteWorkspaceCategories, setWorkspaceCategoryEnabled} from '@userActions/Policy/Category'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -105,6 +105,13 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { setSelectedCategories({}); }, [isFocused]); + const updateWorkspaceRequiresCategory = useCallback( + (value: boolean, categoryName: string) => { + Category.setWorkspaceCategoryEnabled(policyId, {[categoryName]: {name: categoryName, enabled: value}}); + }, + [policyId], + ); + const categoryList = useMemo(() => { const categories = lodashSortBy(Object.values(policyCategories ?? {}), 'name', localeCompare) as PolicyCategory[]; return categories.reduce((acc, value) => { @@ -121,12 +128,19 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { isDisabled, pendingAction: value.pendingAction, errors: value.errors ?? undefined, - rightElement: , + rightElement: ( + updateWorkspaceRequiresCategory(newValue, value.name)} + /> + ), }); return acc; }, []); - }, [policyCategories, isOffline, selectedCategories, canSelectMultiple, translate]); + }, [policyCategories, isOffline, selectedCategories, canSelectMultiple, translate, updateWorkspaceRequiresCategory]); useAutoTurnSelectionModeOffWhenHasNoActiveOption(categoryList); diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 19878036030b..5b21f4a5307e 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -8,11 +8,11 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; -import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; +import Switch from '@components/Switch'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -74,11 +74,11 @@ function PolicyDistanceRatesPage({ const dismissError = useCallback( (item: RateForList) => { if (customUnitRates[item.value].errors) { - DistanceRate.clearDeleteDistanceRateError(policyID, customUnit?.customUnitID ?? '', item.value); + DistanceRate.clearDeleteDistanceRateError(policyID, customUnit?.customUnitID ?? CONST.EMPTY_STRING, item.value); return; } - DistanceRate.clearCreateDistanceRateItemAndError(policyID, customUnit?.customUnitID ?? '', item.value); + DistanceRate.clearCreateDistanceRateItemAndError(policyID, customUnit?.customUnitID ?? CONST.EMPTY_STRING, item.value); }, [customUnit?.customUnitID, customUnitRates, policyID], ); @@ -98,16 +98,36 @@ function PolicyDistanceRatesPage({ setSelectedDistanceRates([]); }, [isFocused]); + const updateDistanceRateEnabled = useCallback( + (value: boolean, rateID: string) => { + if (!customUnit) { + return; + } + const rate = customUnit?.rates?.[rateID]; + // Rates can be disabled or deleted as long as in the remaining rates there is always at least one enabled rate and there are no pending delete action + const canDisableOrDeleteRate = Object.values(customUnit?.rates ?? {}).some( + (distanceRate: Rate) => distanceRate?.enabled && rateID !== distanceRate?.customUnitRateID && distanceRate?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + ); + + if (!rate?.enabled || canDisableOrDeleteRate) { + DistanceRate.setPolicyDistanceRatesEnabled(policyID, customUnit, [{...rate, enabled: value}]); + } else { + setIsWarningModalVisible(true); + } + }, + [customUnit, policyID], + ); + const distanceRatesList = useMemo( () => Object.values(customUnitRates) .sort((rateA, rateB) => (rateA?.rate ?? 0) - (rateB?.rate ?? 0)) .map((value) => ({ - value: value.customUnitRateID ?? '', + value: value.customUnitRateID ?? CONST.EMPTY_STRING, text: `${CurrencyUtils.convertAmountToDisplayString(value.rate, value.currency ?? CONST.CURRENCY.USD)} / ${translate( `common.${customUnit?.attributes?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}`, )}`, - keyForList: value.customUnitRateID ?? '', + keyForList: value.customUnitRateID ?? CONST.EMPTY_STRING, isSelected: selectedDistanceRates.find((rate) => rate.customUnitRateID === value.customUnitRateID) !== undefined && canSelectMultiple, isDisabled: value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, pendingAction: @@ -119,9 +139,16 @@ function PolicyDistanceRatesPage({ value.pendingFields?.taxClaimablePercentage ?? (policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD ? policy?.pendingAction : undefined), errors: value.errors ?? undefined, - rightElement: , + rightElement: ( + updateDistanceRateEnabled(newValue, value.customUnitRateID)} + disabled={value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE} + /> + ), })), - [customUnit?.attributes?.unit, customUnitRates, selectedDistanceRates, translate, policy?.pendingAction, canSelectMultiple], + [customUnitRates, translate, customUnit, selectedDistanceRates, canSelectMultiple, policy?.pendingAction, updateDistanceRateEnabled], ); const addRate = () => { @@ -170,7 +197,7 @@ function PolicyDistanceRatesPage({ DistanceRate.deletePolicyDistanceRates( policyID, customUnit, - selectedDistanceRates.map((rate) => rate.customUnitRateID ?? ''), + selectedDistanceRates.map((rate) => rate.customUnitRateID ?? CONST.EMPTY_STRING), ); setSelectedDistanceRates([]); setIsDeleteModalVisible(false); diff --git a/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx b/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx index 7203c37ca704..908888fc887e 100644 --- a/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx +++ b/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; @@ -10,12 +10,12 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; -import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; +import Switch from '@components/Switch'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -90,6 +90,18 @@ function ReportFieldsListValuesPage({ return [reportFieldValues, reportFieldDisabledValues]; }, [formDraft?.disabledListValues, formDraft?.listValues, policy?.fieldList, reportFieldID]); + const updateReportFieldListValueEnabled = useCallback( + (value: boolean, valueIndex: number) => { + if (reportFieldID) { + ReportField.updateReportFieldListValueEnabled(policyID, reportFieldID, [Number(valueIndex)], value); + return; + } + + ReportField.setReportFieldsListValueEnabled([valueIndex], value); + }, + [policyID, reportFieldID], + ); + const listValuesSections = useMemo(() => { const data = listValues .map((value, index) => ({ @@ -99,11 +111,17 @@ function ReportFieldsListValuesPage({ keyForList: value, isSelected: selectedValues[value] && canSelectMultiple, enabled: !disabledListValues.at(index) ?? true, - rightElement: , + rightElement: ( + updateReportFieldListValueEnabled(newValue, index)} + /> + ), })) .sort((a, b) => localeCompare(a.value, b.value)); return [{data, isDisabled: false}]; - }, [canSelectMultiple, disabledListValues, listValues, selectedValues, translate]); + }, [canSelectMultiple, disabledListValues, listValues, selectedValues, translate, updateReportFieldListValueEnabled]); const shouldShowEmptyState = Object.values(listValues ?? {}).length <= 0; const selectedValuesArray = Object.keys(selectedValues).filter((key) => selectedValues[key]); diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index cd3510cfffb5..fb94af11481c 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -15,11 +15,11 @@ import * as Illustrations from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import ScreenWrapper from '@components/ScreenWrapper'; -import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel'; import TableListItem from '@components/SelectionList/TableListItem'; import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; +import Switch from '@components/Switch'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useEnvironment from '@hooks/useEnvironment'; @@ -103,23 +103,49 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { : undefined; }; + const updateWorkspaceTagEnabled = useCallback( + (value: boolean, tagName: string) => { + Tag.setWorkspaceTagEnabled(policyID, {[tagName]: {name: tagName, enabled: value}}, 0); + }, + [policyID], + ); + + const updateWorkspaceRequiresTag = useCallback( + (value: boolean, orderWeight: number) => { + Tag.setPolicyTagsRequired(policyID, value, orderWeight); + }, + [policyID], + ); const tagList = useMemo(() => { if (isMultiLevelTags) { - return policyTagLists.map((policyTagList) => ({ - value: policyTagList.name, - orderWeight: policyTagList.orderWeight, - text: PolicyUtils.getCleanedTagName(policyTagList.name), - keyForList: String(policyTagList.orderWeight), - isSelected: selectedTags[policyTagList.name] && canSelectMultiple, - pendingAction: getPendingAction(policyTagList), - enabled: true, - required: policyTagList.required, - rightElement: ( - tag.enabled) ? translate('common.required') : undefined} - /> - ), - })); + return policyTagLists.map((policyTagList) => { + const areTagsEnabled = !!Object.values(policyTagList?.tags ?? {}).some((tag) => tag.enabled); + const isSwitchDisabled = !policyTagList.required && !areTagsEnabled; + const isSwitchEnabled = policyTagList.required && areTagsEnabled; + + if (policyTagList.required && !areTagsEnabled) { + updateWorkspaceRequiresTag(false, policyTagList.orderWeight); + } + + return { + value: policyTagList.name, + orderWeight: policyTagList.orderWeight, + text: PolicyUtils.getCleanedTagName(policyTagList.name), + keyForList: String(policyTagList.orderWeight), + isSelected: selectedTags[policyTagList.name] && canSelectMultiple, + pendingAction: getPendingAction(policyTagList), + enabled: true, + required: policyTagList.required, + rightElement: ( + updateWorkspaceRequiresTag(newValue, policyTagList.orderWeight)} + disabled={isSwitchDisabled} + /> + ), + }; + }); } const sortedTags = lodashSortBy(Object.values(policyTagLists.at(0)?.tags ?? {}), 'name', localeCompare) as PolicyTag[]; return sortedTags.map((tag) => ({ @@ -131,9 +157,16 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { errors: tag.errors ?? undefined, enabled: tag.enabled, isDisabled: tag.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - rightElement: , + rightElement: ( + updateWorkspaceTagEnabled(newValue, tag.name)} + /> + ), })); - }, [isMultiLevelTags, policyTagLists, selectedTags, canSelectMultiple, translate]); + }, [isMultiLevelTags, policyTagLists, selectedTags, canSelectMultiple, translate, updateWorkspaceRequiresTag, updateWorkspaceTagEnabled]); const tagListKeyedByName = useMemo( () => diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 173c89c41551..a69dccc2f606 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -10,10 +10,10 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; -import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel'; import TableListItem from '@components/SelectionList/TableListItem'; import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; +import Switch from '@components/Switch'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; @@ -52,7 +52,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const dropdownButtonRef = useRef(null); const [isDeleteTagsConfirmModalVisible, setIsDeleteTagsConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); - const policyID = route.params.policyID ?? '-1'; + const policyID = route.params.policyID; const backTo = route.params.backTo; const policy = usePolicy(policyID); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); @@ -80,6 +80,13 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { }; }, [isFocused]); + const updateWorkspaceTagEnabled = useCallback( + (value: boolean, tagName: string) => { + Tag.setWorkspaceTagEnabled(policyID, {[tagName]: {name: tagName, enabled: value}}, route.params.orderWeight); + }, + [policyID, route.params.orderWeight], + ); + const tagList = useMemo( () => Object.values(currentPolicyTag?.tags ?? {}) @@ -93,9 +100,16 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { errors: tag.errors ?? undefined, enabled: tag.enabled, isDisabled: tag.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - rightElement: , + rightElement: ( + updateWorkspaceTagEnabled(newValue, tag.name)} + /> + ), })), - [currentPolicyTag, selectedTags, canSelectMultiple, translate], + [currentPolicyTag?.tags, selectedTags, canSelectMultiple, translate, updateWorkspaceTagEnabled], ); const hasDependentTags = useMemo(() => PolicyUtils.hasDependentTags(policy, policyTags), [policy, policyTags]); diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index e064c04878a1..deb304808707 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -10,11 +10,11 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; -import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; +import Switch from '@components/Switch'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useEnvironment from '@hooks/useEnvironment'; @@ -111,24 +111,42 @@ function WorkspaceTaxesPage({ [defaultExternalID, foreignTaxDefault, translate], ); + const updateWorkspaceTaxEnabled = useCallback( + (value: boolean, taxID: string) => { + setPolicyTaxesEnabled(policyID, [taxID], value); + }, + [policyID], + ); + const taxesList = useMemo(() => { if (!policy) { return []; } return Object.entries(policy.taxRates?.taxes ?? {}) - .map(([key, value]) => ({ - text: value.name, - alternateText: textForDefault(key, value), - keyForList: key, - isSelected: !!selectedTaxesIDs.includes(key) && canSelectMultiple, - isDisabledCheckbox: !PolicyUtils.canEditTaxRate(policy, key), - isDisabled: value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - pendingAction: value.pendingAction ?? (Object.keys(value.pendingFields ?? {}).length > 0 ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : null), - errors: value.errors ?? ErrorUtils.getLatestErrorFieldForAnyField(value), - rightElement: , - })) + .map(([key, value]) => { + const canEditTaxRate = policy && PolicyUtils.canEditTaxRate(policy, key); + + return { + text: value.name, + alternateText: textForDefault(key, value), + keyForList: key, + isSelected: !!selectedTaxesIDs.includes(key) && canSelectMultiple, + isDisabledCheckbox: !PolicyUtils.canEditTaxRate(policy, key), + isDisabled: value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + pendingAction: value.pendingAction ?? (Object.keys(value.pendingFields ?? {}).length > 0 ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : null), + errors: value.errors ?? ErrorUtils.getLatestErrorFieldForAnyField(value), + rightElement: ( + updateWorkspaceTaxEnabled(newValue, key)} + /> + ), + }; + }) .sort((a, b) => (a.text ?? a.keyForList ?? '').localeCompare(b.text ?? b.keyForList ?? '')); - }, [policy, textForDefault, selectedTaxesIDs, canSelectMultiple, translate]); + }, [policy, textForDefault, selectedTaxesIDs, canSelectMultiple, translate, updateWorkspaceTaxEnabled]); const isLoading = !isOffline && taxesList === undefined;