diff --git a/src/libs/API/parameters/SetPolicyTagsRequired.ts b/src/libs/API/parameters/SetPolicyTagsRequired.ts new file mode 100644 index 000000000000..8defd4a80840 --- /dev/null +++ b/src/libs/API/parameters/SetPolicyTagsRequired.ts @@ -0,0 +1,7 @@ +type SetPolicyTagsRequired = { + policyID: string; + tagListIndex: number; + requireTagList: boolean; +}; + +export default SetPolicyTagsRequired; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 8baf8bb63485..52e76b842f38 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -172,6 +172,7 @@ export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceAppr export type {default as SetWorkspacePayerParams} from './SetWorkspacePayerParams'; export type {default as SetWorkspaceReimbursementParams} from './SetWorkspaceReimbursementParams'; export type {default as SetPolicyRequiresTag} from './SetPolicyRequiresTag'; +export type {default as SetPolicyTagsRequired} from './SetPolicyTagsRequired'; export type {default as RenamePolicyTaglistParams} from './RenamePolicyTaglistParams'; export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams'; export type {default as TrackExpenseParams} from './TrackExpenseParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 07f89e045c11..b6ae0ab23f7c 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -133,6 +133,7 @@ const WRITE_COMMANDS = { RENAME_POLICY_TAG: 'RenamePolicyTag', SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory', DELETE_WORKSPACE_CATEGORIES: 'DeleteWorkspaceCategories', + SET_POLICY_TAGS_REQUIRED: 'SetPolicyTagsRequired', SET_POLICY_REQUIRES_TAG: 'SetPolicyRequiresTag', RENAME_POLICY_TAG_LIST: 'RenamePolicyTaglist', DELETE_POLICY_TAGS: 'DeletePolicyTags', @@ -345,6 +346,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams; [WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams; [WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag; + [WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED]: Parameters.SetPolicyTagsRequired; [WRITE_COMMANDS.RENAME_POLICY_TAG_LIST]: Parameters.RenamePolicyTaglistParams; [WRITE_COMMANDS.CREATE_POLICY_TAG]: Parameters.CreatePolicyTagsParams; [WRITE_COMMANDS.RENAME_POLICY_TAG]: Parameters.RenamePolicyTagsParams; diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 85080d741011..a8607ca8f5f9 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -1,7 +1,7 @@ import type {NullishDeep, OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {EnablePolicyTagsParams, OpenPolicyTagsPageParams, RenamePolicyTaglistParams, RenamePolicyTagsParams, SetPolicyTagsEnabled} from '@libs/API/parameters'; +import type {EnablePolicyTagsParams, OpenPolicyTagsPageParams, RenamePolicyTaglistParams, RenamePolicyTagsParams, SetPolicyTagsEnabled, SetPolicyTagsRequired} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; @@ -161,6 +161,7 @@ function createPolicyTag(policyID: string, tagName: string) { tags: { [newTagName]: { errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'), + pendingAction: null, }, }, }, @@ -329,8 +330,8 @@ function deletePolicyTags(policyID: string, tagsToDelete: string[]) { API.write(WRITE_COMMANDS.DELETE_POLICY_TAGS, parameters, onyxData); } -function clearPolicyTagErrors(policyID: string, tagName: string) { - const tagListName = Object.keys(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})[0]; +function clearPolicyTagErrors(policyID: string, tagName: string, tagListIndex: number) { + const tagListName = Object.keys(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})[tagListIndex]; const tag = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`]?.[tagListName].tags?.[tagName]; if (!tag) { return; @@ -359,10 +360,25 @@ function clearPolicyTagErrors(policyID: string, tagName: string) { }); } +function clearPolicyTagListError(policyID: string, tagListIndex: number, errorField: string) { + const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[tagListIndex] ?? {}; + + if (!policyTag.name) { + return; + } + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, { + [policyTag.name]: { + errorFields: { + [errorField]: null, + }, + }, + }); +} + function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: string}, tagListIndex: number) { const tagList = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[tagListIndex] ?? {}; const tag = tagList.tags?.[policyTag.oldName]; - const oldTagName = policyTag.oldName; const newTagName = PolicyUtils.escapeTagName(policyTag.newName); const onyxData: OnyxData = { @@ -611,15 +627,75 @@ function setPolicyRequiresTag(policyID: string, requiresTag: boolean) { API.write(WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG, parameters, onyxData); } +function setPolicyTagsRequired(policyID: string, requiresTag: boolean, tagListIndex: number) { + const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[tagListIndex] ?? {}; + + if (!policyTag.name) { + return; + } + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + value: { + [policyTag.name]: { + required: requiresTag, + pendingFields: {required: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + errorFields: {required: null}, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + value: { + [policyTag.name]: { + pendingFields: {required: null}, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + value: { + [policyTag.name]: { + required: policyTag.required, + pendingFields: {required: null}, + errorFields: { + required: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'), + }, + }, + }, + }, + ], + }; + + const parameters: SetPolicyTagsRequired = { + policyID, + tagListIndex, + requireTagList: requiresTag, + }; + + API.write(WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED, parameters, onyxData); +} + export { openPolicyTagsPage, buildOptimisticPolicyRecentlyUsedTags, setPolicyRequiresTag, + setPolicyTagsRequired, renamePolicyTaglist, enablePolicyTags, createPolicyTag, renamePolicyTag, clearPolicyTagErrors, + clearPolicyTagListError, deletePolicyTags, setWorkspaceTagEnabled, }; diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index 66494e176e45..1e73267fec97 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -106,7 +106,7 @@ function TagSettingsPage({route, policyTags, navigation}: TagSettingsPageProps) errors={ErrorUtils.getLatestErrorMessageField(currentPolicyTag)} pendingAction={currentPolicyTag.pendingFields?.enabled} errorRowStyles={styles.mh5} - onClose={() => Tag.clearPolicyTagErrors(route.params.policyID, route.params.tagName)} + onClose={() => Tag.clearPolicyTagErrors(route.params.policyID, route.params.tagName, route.params.orderWeight)} > diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index f7afacce93ad..35c4dfa19e28 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -350,7 +350,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { customListHeader={getCustomListHeader()} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - onDismissError={(item) => Tag.clearPolicyTagErrors(policyID, item.value)} + onDismissError={(item) => !isMultiLevelTags && Tag.clearPolicyTagErrors(policyID, item.value, 0)} listHeaderContent={isSmallScreenWidth ? getHeaderText() : null} showScrollIndicator={false} /> diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 1b91777a22ad..0678b3344711 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -27,6 +27,7 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import * as Tag from '@userActions/Policy/Tag'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -65,10 +66,9 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { setSelectedTags({}); }, [isFocused]); - const policyTagList = useMemo(() => PolicyUtils.getTagLists(policyTags).find((policyTag) => policyTag.name === currentTagListName), [currentTagListName, policyTags]); const tagList = useMemo( () => - Object.values(policyTagList?.tags ?? {}) + Object.values(currentPolicyTag?.tags ?? {}) .sort((tagA, tagB) => localeCompare(tagA.name, tagB.name)) .map((tag) => ({ value: tag.name, @@ -81,7 +81,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { isDisabled: tag.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, rightElement: , })), - [policyTagList, selectedTags, translate], + [currentPolicyTag, selectedTags, translate], ); const tagListKeyedByName = useMemo( @@ -234,6 +234,18 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { cancelText={translate('common.cancel')} danger /> + + Tag.setPolicyTagsRequired(policyID, on, route.params.orderWeight)} + pendingAction={currentPolicyTag.pendingFields?.required} + errors={currentPolicyTag?.errorFields?.required ?? undefined} + onCloseError={() => Tag.clearPolicyTagListError(policyID, route.params.orderWeight, 'required')} + disabled={!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled)} + /> + Tag.clearPolicyTagErrors(policyID, item.value)} + onDismissError={(item) => { + Tag.clearPolicyTagErrors(policyID, item.value, route.params.orderWeight); + }} /> )} diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 467ba3271981..ec552515ec32 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -55,6 +55,9 @@ type PolicyTagList = Record< /** A list of errors keyed by microtime */ errors?: OnyxCommon.Errors; + + /** Error objects keyed by field name containing errors keyed by microtime */ + errorFields?: OnyxCommon.ErrorFields; }> >;