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

Create WorkspaceTagsSettingsPage #37755

Merged
merged 17 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 3 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ const ONYXKEYS = {
EXIT_SURVEY_REASON_FORM_DRAFT: 'exitSurveyReasonFormDraft',
EXIT_SURVEY_RESPONSE_FORM: 'exitSurveyResponseForm',
EXIT_SURVEY_RESPONSE_FORM_DRAFT: 'exitSurveyResponseFormDraft',
POLICY_TAG_NAME_FORM: 'policyTagNameForm',
POLICY_TAG_NAME_FORM_DRAFT: 'policyTagNameFormDraft',
},
} as const;

Expand Down Expand Up @@ -443,6 +445,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm;
[ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT]: FormTypes.PersonalBankAccountForm;
[ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm;
[ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM]: FormTypes.PolicyTagNameForm;
};

type OnyxFormDraftValuesMapping = {
Expand Down
9 changes: 9 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,15 @@ const ROUTES = {
route: 'workspace/:policyID/tags',
getRoute: (policyID: string) => `workspace/${policyID}/tags` as const,
},
WORKSPACE_TAGS_SETTINGS: {
route: 'workspace/:policyID/tags/settings',
getRoute: (policyID: string) => `workspace/${policyID}/tags/settings` as const,
},
WORKSPACE_EDIT_TAGS: {
route: 'workspace/:policyID/tags/edit',
getRoute: (policyID: string) => `workspace/${policyID}/tags/edit` as const,
},

// Referral program promotion
REFERRAL_DETAILS_MODAL: {
route: 'referral/:contentType',
Expand Down
2 changes: 2 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ const SCREENS = {
INVITE_MESSAGE: 'Workspace_Invite_Message',
CATEGORIES: 'Workspace_Categories',
TAGS: 'Workspace_Tags',
TAGS_SETTINGS: 'Tags_Settings',
TAGS_EDIT: 'Tags_Edit',
CURRENCY: 'Workspace_Profile_Currency',
WORKFLOWS: 'Workspace_Workflows',
WORKFLOWS_APPROVER: 'Workspace_Workflows_Approver',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1773,12 +1773,14 @@ export default {
},
tags: {
requiresTag: 'Members must tag all spend',
customTagName: 'Custom tag name',
enableTag: 'Enable tag',
subtitle: 'Tags add more detailed ways to classify costs.',
emptyTags: {
title: "You haven't created any tags",
subtitle: 'Add a tag to track projects, locations, departments, and more.',
},
genericFailureMessage: 'An error occurred while updating the tag, please try again.',
},
emptyWorkspace: {
title: 'Create a workspace',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1797,12 +1797,14 @@ export default {
},
tags: {
requiresTag: 'Los miembros deben etiquetar todos los gastos',
customTagName: 'Nombre de etiqueta personalizada',
enableTag: 'Habilitar etiqueta',
subtitle: 'Las etiquetas añaden formas más detalladas de clasificar los costos.',
emptyTags: {
title: 'No has creado ninguna etiqueta',
subtitle: 'Añade una etiqueta para realizar el seguimiento de proyectos, ubicaciones, departamentos y otros.',
},
genericFailureMessage: 'Se produjo un error al actualizar la etiqueta, inténtelo nuevamente.',
},
emptyWorkspace: {
title: 'Crea un espacio de trabajo',
Expand Down
7 changes: 7 additions & 0 deletions src/libs/API/parameters/RenamePolicyTaglist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type RenamePolicyTaglist = {
policyID: string;
oldName: string;
newName: string;
};

export default RenamePolicyTaglist;
6 changes: 6 additions & 0 deletions src/libs/API/parameters/SetPolicyRequiresTag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type SetPolicyRequiresTag = {
policyID: string;
requiresTag: boolean;
};

export default SetPolicyRequiresTag;
2 changes: 2 additions & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,6 @@ export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAut
export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams';
export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams';
export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams';
export type {default as SetPolicyRequiresTag} from './SetPolicyRequiresTag';
export type {default as RenamePolicyTaglist} from './RenamePolicyTaglist';
export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams';
4 changes: 4 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ const WRITE_COMMANDS = {
CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment',
SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled',
SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory',
SET_POLICY_REQUIRES_TAG: 'SetPolicyRequiresTag',
RENAME_POLICY_TAG_LIST: 'RenamePolicyTaglist',
CREATE_TASK: 'CreateTask',
CANCEL_TASK: 'CancelTask',
EDIT_TASK_ASSIGNEE: 'EditTaskAssignee',
Expand Down Expand Up @@ -265,6 +267,8 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams;
[WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams;
[WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
[WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag;
[WRITE_COMMANDS.RENAME_POLICY_TAG_LIST]: Parameters.RenamePolicyTaglist;
[WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams;
[WRITE_COMMANDS.CANCEL_TASK]: Parameters.CancelTaskParams;
[WRITE_COMMANDS.EDIT_TASK_ASSIGNEE]: Parameters.EditTaskAssigneeParams;
Expand Down
2 changes: 2 additions & 0 deletions src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceProfileCurrencyPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../pages/workspace/categories/CategorySettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require('../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAGS_EDIT]: () => require('../../../pages/workspace/tags/WorkspaceEditTagsPage').default as React.ComponentType,
[SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType,
[SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType,
[SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial<Record<CentralPaneName, string[]>> =
[SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE],
[SCREENS.WORKSPACE.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET],
[SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS],
[SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS],
};

export default CENTRAL_PANE_TO_RHP_MAPPING;
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const TAB_TO_CENTRAL_PANE_MAPPING: Record<BottomTabName, CentralPaneName[]> = {
SCREENS.WORKSPACE.TRAVEL,
SCREENS.WORKSPACE.MEMBERS,
SCREENS.WORKSPACE.CATEGORIES,
SCREENS.WORKSPACE.TAGS,
],
};

Expand Down
6 changes: 6 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route,
},
[SCREENS.WORKSPACE.TAGS_SETTINGS]: {
path: ROUTES.WORKSPACE_TAGS_SETTINGS.route,
},
[SCREENS.WORKSPACE.TAGS_EDIT]: {
path: ROUTES.WORKSPACE_EDIT_TAGS.route,
},
[SCREENS.REIMBURSEMENT_ACCOUNT]: {
path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route,
exact: true,
Expand Down
7 changes: 7 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,13 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
policyID: string;
};
[SCREENS.WORKSPACE.TAGS_SETTINGS]: {
policyID: string;
};
[SCREENS.WORKSPACE.TAGS_EDIT]: {
policyID: string;
tagName: string;
};
[SCREENS.GET_ASSISTANCE]: {
backTo: Routes;
};
Expand Down
95 changes: 95 additions & 0 deletions src/libs/actions/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2489,6 +2489,99 @@ function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolea
API.write('SetWorkspaceRequiresCategory', parameters, onyxData);
}

function setPolicyRequiresTag(policyID: string, requiresTag: boolean) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were only supporting single level tags here (#43754) so we updated this to suport multi-level tags in #44937

const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
requiresTag,
errors: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be errorFields. errors is just an object keyed by timestamp, so this wouldn't work as we have here. Same for the others below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errors are working, it shows an error, we have the same in categories
Maybe Im missing something

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm seeing the following issues when simulating an error from the API:

  1. For enable/disable the error shows up in the LHN instead of the RHN under the toggle. Clearing the error also clears the entire policy
Screen.Recording.2024-03-08.at.3.14.18.PM.mov
  1. App crashes when simulating an error on the rename API:
Screen.Recording.2024-03-08.at.3.15.17.PM.mov
Screenshot 2024-03-08 at 3 17 55 PM

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this thread resolved?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm retesting this now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is still happening. I think we can continue with this and I'll address it in a fast following PR.

requiresTag: null,
},
pendingFields: {
requiresTag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
},
},
],
successData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
errors: {
requiresTag: null,
},
pendingFields: {
requiresTag: null,
},
},
},
],
failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
requiresTag: !requiresTag,
errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'),
pendingFields: {
requiresTag: null,
},
},
},
],
};

const parameters = {
policyID,
requiresTag,
};

API.write(WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG, parameters, onyxData);
}

function renamePolicyTaglist(policyID: string, policyTagListName: {oldName: string; newName: string}, policyTags: OnyxEntry<PolicyTagList>) {
const newName = policyTagListName.newName;
const oldName = policyTagListName.oldName;
const oldPolicyTags = policyTags?.[oldName] ?? {};
const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
value: {
[newName]: {...oldPolicyTags, name: newName},
[oldName]: null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think the correct pattern here is to have the old tagList pending as delete and the new one pending as add

Suggested change
[newName]: {...oldPolicyTags, name: newName},
[oldName]: null,
[newName]: {...oldPolicyTags, name: newName, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD},
[oldName]:
{pendingAction:CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE},

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition, we'd have successData to remove the pendingActions and delete the old taglist

},
},
],
failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
value: {
errors: {
[oldName]: oldName,
[newName]: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'),
},
[newName]: null,
[oldName]: oldPolicyTags,
},
},
],
};
const parameters = {
policyID,
oldName,
newName,
};

API.write(WRITE_COMMANDS.RENAME_POLICY_TAG_LIST, parameters, onyxData);
}

function clearCategoryErrors(policyID: string, categoryName: string) {
const category = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName];

Expand Down Expand Up @@ -2553,4 +2646,6 @@ export {
setWorkspaceCategoryEnabled,
setWorkspaceRequiresCategory,
clearCategoryErrors,
setPolicyRequiresTag,
renamePolicyTaglist,
};
91 changes: 91 additions & 0 deletions src/pages/workspace/tags/WorkspaceEditTagsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useMemo, useRef} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Policy from '@libs/actions/Policy';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/PolicyTagNameForm';
import type * as OnyxTypes from '@src/types/onyx';

type WorkspaceEditTagsPageOnyxProps = {
/** Collection of tags attached to a policy */
policyTags: OnyxEntry<OnyxTypes.PolicyTagList>;
};

type WorkspaceEditTagsPageProps = WorkspaceEditTagsPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.TAGS_EDIT>;

function WorkspaceEditTagsPage({route, policyTags}: WorkspaceEditTagsPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const taglistName = useMemo(() => PolicyUtils.getTagLists(policyTags)[0].name, [policyTags]);
const inputRef = useRef<AnimatedTextInputRef>(null);

const validateTagName = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM>) => {
waterim marked this conversation as resolved.
Show resolved Hide resolved
const errors: FormInputErrors<typeof ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM> = {};
if (!values[INPUT_IDS.POLICY_TAGS_NAME] && values[INPUT_IDS.POLICY_TAGS_NAME].trim() === '') {
errors[INPUT_IDS.POLICY_TAGS_NAME] = 'common.error.fieldRequired';
}
return errors;
};

const updateTaglistName = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM>) => {
waterim marked this conversation as resolved.
Show resolved Hide resolved
Policy.renamePolicyTaglist(route.params.policyID, {oldName: taglistName, newName: values[INPUT_IDS.POLICY_TAGS_NAME]}, policyTags);
Navigation.goBack();
};

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
shouldEnableMaxHeight
onEntryTransitionEnd={() => {
inputRef.current?.focus();
}}
waterim marked this conversation as resolved.
Show resolved Hide resolved
testID={WorkspaceEditTagsPage.displayName}
>
<HeaderWithBackButton title={translate(`workspace.tags.customTagName`)} />
<FormProvider
style={[styles.flexGrow1, styles.ph5]}
formID={ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM}
onSubmit={updateTaglistName}
validate={validateTagName}
submitButtonText={translate('common.save')}
enabledWhenOffline
>
<View style={styles.mb4}>
<InputWrapper
InputComponent={TextInput}
inputID={INPUT_IDS.POLICY_TAGS_NAME}
label={translate(`workspace.tags.customTagName`)}
accessibilityLabel={translate(`workspace.tags.customTagName`)}
defaultValue={taglistName}
role={CONST.ROLE.PRESENTATION}
ref={inputRef}
/>
</View>
</FormProvider>
</ScreenWrapper>
);
}

WorkspaceEditTagsPage.displayName = 'WorkspaceEditTagsPage';

export default withOnyx<WorkspaceEditTagsPageProps, WorkspaceEditTagsPageOnyxProps>({
policyTags: {
key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${route.params.policyID}`,
},
})(WorkspaceEditTagsPage);
Loading
Loading