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
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,10 @@ const ROUTES = {
route: 'workspace/:policyID/tags',
getRoute: (policyID: string) => `workspace/${policyID}/tags` as const,
},
WORKSPACE_TAG_CREATE: {
route: 'workspace/:policyID/tags/new',
getRoute: (policyID: string) => `workspace/${policyID}/tags/new` as const,
},
WORKSPACE_TAGS_SETTINGS: {
route: 'workspace/:policyID/tags/settings',
getRoute: (policyID: string) => `workspace/${policyID}/tags/settings` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,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: 'Tag name is required.',
allroundexperts marked this conversation as resolved.
Show resolved Hide resolved
existingTagError: 'A tag with this name already exists.',
allroundexperts marked this conversation as resolved.
Show resolved Hide resolved
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/CreateWorkspaceTagsParams.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 JoinPolicyInviteLinkParams} from './JoinPolicyInviteLink';
export type {default as OpenPolicyWorkflowsPageParams} from './OpenPolicyWorkflowsPageParams';
export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams';
export type {default as CreatePolicyTagsParams} from './CreatePolicyTagsParams';

Check failure on line 176 in src/libs/API/parameters/index.ts

View workflow job for this annotation

GitHub Actions / typecheck

Cannot find module './CreatePolicyTagsParams' or its corresponding type declarations.
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 @@ -257,6 +257,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 @@ -5,13 +5,13 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial<Record<CentralPaneName, string[]>> =
[SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE],
[SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT, SCREENS.WORKSPACE.RATE_AND_UNIT_RATE, SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT],
[SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE, SCREENS.WORKSPACE.MEMBER_DETAILS, SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION],
[SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE],
[SCREENS.WORKSPACE.WORKFLOWS]: [
SCREENS.WORKSPACE.WORKFLOWS_APPROVER,
SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY,
SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET,
SCREENS.WORKSPACE.WORKFLOWS_PAYER,
],
[SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT],
[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 @@ -306,6 +306,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 @@ -217,6 +217,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
policyID: string;
};
[SCREENS.WORKSPACE.TAG_CREATE]: {
policyID: string;
};
[SCREENS.WORKSPACE.TAGS_SETTINGS]: {
policyID: string;
};
Expand Down
63 changes: 63 additions & 0 deletions src/libs/actions/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2746,6 +2746,68 @@ function createPolicyCategory(policyID: string, categoryName: string) {
API.write(WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES, parameters, onyxData);
}

function createPolicyTag(policyID: string, tagName: string) {
const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
value: {
Tag: {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is accurate. A user can rename the tagList name and this request would fail. We need to dynamically update this name. Same goes for the other cases 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.

Any idea how to get that name? We can have multiple items in a tag list. How would that be handled?

Copy link
Contributor Author

@allroundexperts allroundexperts Mar 12, 2024

Choose a reason for hiding this comment

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

Moving this discussion over to Slack.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Handled!

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: {
Tag: {
tags: {
[tagName]: {
errors: null,
pendingAction: null,
},
},
},
},
},
],
failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
value: {
Tag: {
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 +3515,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
111 changes: 111 additions & 0 deletions src/pages/workspace/tags/WorkspaceCreateTagPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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 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();

if (!ValidationUtils.isRequiredFulfilled(tagName)) {
errors.tagName = 'workspace.tags.tagRequiredError';
} else if (policyTags?.Tag?.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