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

Add "Create Tag" page #38158

Merged
merged 13 commits into from
Mar 12, 2024
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,8 @@ const CONST = {
LOGIN_CHARACTER_LIMIT: 254,
CATEGORY_NAME_LIMIT: 256,

TAG_NAME_LIMIT: 256,

TITLE_CHARACTER_LIMIT: 100,
DESCRIPTION_LIMIT: 500,

Expand Down
3 changes: 3 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ const ONYXKEYS = {
WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm',
WORKSPACE_CATEGORY_CREATE_FORM: 'workspaceCategoryCreate',
WORKSPACE_CATEGORY_CREATE_FORM_DRAFT: 'workspaceCategoryCreateDraft',
WORKSPACE_TAG_CREATE_FORM: 'workspaceTagCreate',
WORKSPACE_TAG_CREATE_FORM_DRAFT: 'workspaceTagCreateDraft',
WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft',
WORKSPACE_DESCRIPTION_FORM: 'workspaceDescriptionForm',
WORKSPACE_DESCRIPTION_FORM_DRAFT: 'workspaceDescriptionFormDraft',
Expand Down Expand Up @@ -416,6 +418,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: FormTypes.AddDebitCardForm;
[ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm;
[ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM]: FormTypes.WorkspaceCategoryCreateForm;
[ONYXKEYS.FORMS.WORKSPACE_TAG_CREATE_FORM]: FormTypes.WorkspaceTagCreateForm;
[ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm;
[ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm;
[ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm;
Expand Down
12 changes: 8 additions & 4 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,13 +573,17 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/tags',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags` as const,
},
WORKSPACE_TAG_CREATE: {
route: 'settings/workspaces/:policyID/tags/new',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/new` as const,
},
WORKSPACE_TAGS_SETTINGS: {
route: 'workspace/:policyID/tags/settings',
getRoute: (policyID: string) => `workspace/${policyID}/tags/settings` as const,
route: 'settings/workspaces/:policyID/tags/settings',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/settings` as const,
},
WORKSPACE_EDIT_TAGS: {
route: 'workspace/:policyID/tags/edit',
getRoute: (policyID: string) => `workspace/${policyID}/tags/edit` as const,
route: 'settings/workspaces/:policyID/tags/edit',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/edit` as const,
},
WORKSPACE_MEMBER_DETAILS: {
route: 'settings/workspaces/:policyID/members/:accountID',
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ const SCREENS = {
TAGS: 'Workspace_Tags',
TAGS_SETTINGS: 'Tags_Settings',
TAGS_EDIT: 'Tags_Edit',
TAG_CREATE: 'Tag_Create',
CURRENCY: 'Workspace_Profile_Currency',
WORKFLOWS: 'Workspace_Workflows',
WORKFLOWS_PAYER: 'Workspace_Workflows_Payer',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1834,11 +1834,14 @@ export default {
requiresTag: 'Members must tag all spend',
customTagName: 'Custom tag name',
enableTag: 'Enable tag',
addTag: 'Add 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.',
},
tagRequiredError: 'Tag name is required.',
existingTagError: 'A tag with this name already exists.',
genericFailureMessage: 'An error occurred while updating the tag, please try again.',
},
emptyWorkspace: {
Expand Down
3 changes: 3 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1858,11 +1858,14 @@ export default {
requiresTag: 'Los miembros deben etiquetar todos los gastos',
customTagName: 'Nombre de etiqueta personalizada',
enableTag: 'Habilitar etiqueta',
addTag: 'Añadir 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.',
},
tagRequiredError: 'Lo nombre de la etiqueta es obligatorio.',
existingTagError: 'Ya existe una etiqueta con este nombre.',
genericFailureMessage: 'Se produjo un error al actualizar la etiqueta, inténtelo nuevamente.',
},
emptyWorkspace: {
Expand Down
10 changes: 10 additions & 0 deletions src/libs/API/parameters/CreatePolicyTagsParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
type CreatePolicyTagsParams = {
policyID: string;
/**
* Stringified JSON object with type of following structure:
* Array<{name: string;}>
*/
tags: string;
};

export default CreatePolicyTagsParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,4 @@ export type {default as DeclineJoinRequestParams} from './DeclineJoinRequest';
export type {default as JoinPolicyInviteLinkParams} from './JoinPolicyInviteLink';
export type {default as OpenPolicyWorkflowsPageParams} from './OpenPolicyWorkflowsPageParams';
export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams';
export type {default as CreatePolicyTagsParams} from './CreatePolicyTagsParams';
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ const WRITE_COMMANDS = {
CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment',
SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled',
CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories',
CREATE_POLICY_TAG: 'CreatePolicyTag',
SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory',
SET_POLICY_REQUIRES_TAG: 'SetPolicyRequiresTag',
RENAME_POLICY_TAG_LIST: 'RenamePolicyTaglist',
Expand Down Expand Up @@ -283,6 +284,7 @@ type WriteCommandParameters = {
[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_POLICY_TAG]: Parameters.CreatePolicyTagsParams;
[WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams;
[WRITE_COMMANDS.CANCEL_TASK]: Parameters.CancelTaskParams;
[WRITE_COMMANDS.EDIT_TASK_ASSIGNEE]: Parameters.EditTaskAssigneeParams;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../pages/workspace/categories/CreateCategoryPage').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.WORKSPACE.TAG_CREATE]: () => require('../../../pages/workspace/tags/WorkspaceCreateTagPage').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 @@ -11,7 +11,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial<Record<FullScreenName, string[]>> = {
SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET,
SCREENS.WORKSPACE.WORKFLOWS_PAYER,
],
[SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT],
[SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE],
[SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS],
};

Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,9 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.WORKSPACE.TAGS_EDIT]: {
path: ROUTES.WORKSPACE_EDIT_TAGS.route,
},
[SCREENS.WORKSPACE.TAG_CREATE]: {
path: ROUTES.WORKSPACE_TAG_CREATE.route,
},
[SCREENS.REIMBURSEMENT_ACCOUNT]: {
path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route,
exact: true,
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
policyID: string;
};
[SCREENS.WORKSPACE.TAG_CREATE]: {
policyID: string;
};
[SCREENS.WORKSPACE.TAGS_SETTINGS]: {
policyID: string;
};
Expand Down
65 changes: 65 additions & 0 deletions src/libs/actions/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2746,6 +2746,70 @@ function createPolicyCategory(policyID: string, categoryName: string) {
API.write(WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES, parameters, onyxData);
}

function createPolicyTag(policyID: string, tagName: string) {
const tagListName = Object.keys(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})[0];

const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
value: {
[tagListName]: {
tags: {
[tagName]: {
name: tagName,
enabled: false,
errors: null,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
},
},
},
},
],
successData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
value: {
[tagListName]: {
tags: {
[tagName]: {
errors: null,
pendingAction: null,
},
},
},
},
},
],
failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
value: {
[tagListName]: {
tags: {
[tagName]: {
errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'),
pendingAction: null,
},
},
},
},
},
],
};

const parameters = {
policyID,
tags: JSON.stringify([{name: tagName}]),
};

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

function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolean) {
const onyxData: OnyxData = {
optimisticData: [
Expand Down Expand Up @@ -3453,5 +3517,6 @@ export {
enablePolicyTaxes,
enablePolicyWorkflows,
openPolicyDistanceRatesPage,
createPolicyTag,
clearWorkspaceReimbursementErrors,
};
5 changes: 4 additions & 1 deletion src/pages/workspace/categories/CreateCategoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
Expand All @@ -34,6 +35,7 @@ type CreateCategoryPageProps = WorkspaceCreateCategoryPageOnyxProps & StackScree
function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();

const validate = useCallback(
(values: FormOnyxValues<typeof ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM>) => {
Expand Down Expand Up @@ -72,6 +74,7 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps)
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={CreateCategoryPage.displayName}
shouldEnableMaxHeight
>
<HeaderWithBackButton
title={translate('workspace.categories.addCategory')}
Expand All @@ -92,7 +95,7 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps)
accessibilityLabel={translate('common.name')}
inputID={INPUT_IDS.CATEGORY_NAME}
role={CONST.ROLE.PRESENTATION}
autoFocus
ref={inputCallbackRef}
/>
</FormProvider>
</ScreenWrapper>
Expand Down
113 changes: 113 additions & 0 deletions src/pages/workspace/tags/WorkspaceCreateTagPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback} from 'react';
import {Keyboard} 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 ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ValidationUtils from '@libs/ValidationUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/WorkspaceTagCreateForm';
import type {PolicyTagList} from '@src/types/onyx';

type WorkspaceCreateTagPageOnyxProps = {
/** All policy tags */
policyTags: OnyxEntry<PolicyTagList>;
};

type CreateTagPageProps = WorkspaceCreateTagPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.TAG_CREATE>;

function CreateTagPage({route, policyTags}: CreateTagPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();

const validate = useCallback(
(values: FormOnyxValues<typeof ONYXKEYS.FORMS.WORKSPACE_TAG_CREATE_FORM>) => {
const errors: FormInputErrors<typeof ONYXKEYS.FORMS.WORKSPACE_TAG_CREATE_FORM> = {};
const tagName = values.tagName.trim();
const {tags} = PolicyUtils.getTagList(policyTags, 0);

if (!ValidationUtils.isRequiredFulfilled(tagName)) {
errors.tagName = 'workspace.tags.tagRequiredError';
} else if (tags?.[tagName]) {
errors.tagName = 'workspace.tags.existingTagError';
} else if ([...tagName].length > CONST.TAG_NAME_LIMIT) {
// Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 code units.
ErrorUtils.addErrorMessage(errors, 'tagName', ['common.error.characterLimitExceedCounter', {length: [...tagName].length, limit: CONST.TAG_NAME_LIMIT}]);
}

return errors;
},
[policyTags],
);

const createTag = useCallback(
(values: FormOnyxValues<typeof ONYXKEYS.FORMS.WORKSPACE_TAG_CREATE_FORM>) => {
Policy.createPolicyTag(route.params.policyID, values.tagName.trim());
Keyboard.dismiss();
Navigation.goBack();
},
[route.params.policyID],
);

return (
<AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<ScreenWrapper
mountiny marked this conversation as resolved.
Show resolved Hide resolved
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={CreateTagPage.displayName}
shouldEnableMaxHeight
>
allroundexperts marked this conversation as resolved.
Show resolved Hide resolved
<HeaderWithBackButton
title={translate('workspace.tags.addTag')}
onBackButtonPress={Navigation.goBack}
/>
<FormProvider
formID={ONYXKEYS.FORMS.WORKSPACE_TAG_CREATE_FORM}
onSubmit={createTag}
submitButtonText={translate('common.save')}
validate={validate}
style={[styles.mh5, styles.flex1]}
enabledWhenOffline
>
<InputWrapper
InputComponent={TextInput}
maxLength={CONST.TAG_NAME_LIMIT}
label={translate('common.name')}
accessibilityLabel={translate('common.name')}
inputID={INPUT_IDS.TAG_NAME}
role={CONST.ROLE.PRESENTATION}
ref={inputCallbackRef}
/>
</FormProvider>
</ScreenWrapper>
</PaidPolicyAccessOrNotFoundWrapper>
</AdminPolicyAccessOrNotFoundWrapper>
);
}

CreateTagPage.displayName = 'CreateTagPage';

export default withOnyx<CreateTagPageProps, WorkspaceCreateTagPageOnyxProps>({
policyTags: {
key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${route?.params?.policyID}`,
},
})(CreateTagPage);
Loading
Loading