Tracking GH: https://github.com/Expensify/Expensify/issues/418655
Design Doc: https://docs.google.com/document/d/1_p8qZXVKG-s32i9GKqVjCLEcEREBUEgYcxk3KYHXMVg/edit?usp=sharing
From the Design Doc:
Admin - Choosing to import Multi-Level Tags
- Settings panel in empty state
- Update empty state modal for multi-level tags
We need to show a new empty state in the WorkspaceTagsPage page, There is an existing EmptyStateComponent component for the empty state, so we can re-use the same component with new copy, here to with the following conditions:
{
!hasVisibleTags && !isLoading && (
<EmptyStateComponent
...props
/>
);
}
SPANISH TRANSLATION
| English
|
Spanish
|
| You haven't created any tags
|
No has creado ninguna etiqueta.
|
| Import a spreadsheet to add tags for tracking projects, locations, departments, and more. Learn more about formatting tag files.
|
Importa una hoja de cálculo para añadir etiquetas y organizar proyectos, ubicaciones, departamentos y más. Obtén más información sobre cómo dar formato a los archivos de etiquetas.
|
Note: We will update the import button routes to point to the new page asking the user about the level of tag they are importing via spreadsheet.
- Hide Add tag button in case of multi-level tags
We also need to hide the Add Tag button if we are using multi-level tags (we only support adding multi-level tags via spreadsheet), So we need to update the the condition here: \
{
!hasAccountingConnections && !isUsingMultiLevelTags && (
<Button
success
onPress={navigateToCreateTagPage}
icon={Expensicons.Plus}
text={translate("workspace.tags.addTag")}
style={[shouldUseNarrowLayout && styles.flex1]}
/>
);
}
- Choosing between single or multi-level tags
Selecting the Import Spreadsheet option will open up the RHP for Selecting whether the user is importing Single level of tags or `Multiple levels of tags.
function ImportTagTypepage() {
....
....
....
return(
<AccessOrNotFoundWrapper
.....
>
<ScreenWrapper
.....
>
<HeaderWithBackButton
.......
/>
....
<View style={[styles.mt2, styles.mh5]}>
<View
style={[
styles.flexRow,
styles.mb5,
styles.mr2,
styles.alignItemsCenter,
styles.justifyContentBetween,
]}
>
<Text>{`Code your expenses with one type of tag or many.`}</Text>
</View>
</View>
<MenuItem
title={`Single level of tags`}
.....
shouldShowRightIcon
/>;
<MenuItem
title={`Multiple levels of tags`}
.....
shouldShowRightIcon
/>;
....
.....
</ScreenWrapper>
</AccessOrNotFoundWrapper>
)
}
SPANISH TRANSLATION
| English
|
Spanish
|
| Code your expenses with one type of tag or many.
|
Clasifica tus gastos con un tipo de etiqueta o con varios.
|
| Single level of tags
|
Un solo nivel de etiquetas
|
| Multiple levels of tags
|
Varios niveles de etiquetas
|
- Warning for when a user selects "Use multiple levels of tags"
When the user clicks on Multiple levels of tags then we need to show them a warning modal about the deletion of the currently present tags
const [showWarningSwitchTagLevelsModal, setShowWarningSwitchTagLevelsModal] =
useState(false);
<Switch ...props onToggle={() => setShowWarningSwitchTagLevelsModal(true)} />;
<ConfirmModal
...props
isVisible={showWarningSwitchTagLevelsModal}
onConfirm={() => togglePolicyMultiLevelTags(policyID)}
/>;
SPANISH TRANSLATION
| English
|
Spanish
|
| Switch Tag levels
|
Cambiar niveles de etiquetas
|
| Importing multiple tag levels will erase all current tags. We suggest you first download a backup by exporting your tags. Learn more about tag levels.
|
Importar varios niveles de etiquetas borrará todas las etiquetas actuales. Te sugerimos que primero descargues una copia de seguridad exportando tus etiquetas. Obtén más información sobre los niveles de etiquetas.
|
Note:
- This modal will only be shown when the user has tags present in the policy, else we will skip this warning and directly proceed with the import step.
- The modal will only show when a user is switching from Single to Multi-Level Tags (and vice versa - with adjusted copy).
- Upgrade flow for when a collect user tries to turn on Multi-Level Tagging
After the user clicks on Switch Tag Levels on, we need to check if the user is on control policy or not, if the later then we need to ask them to update the policy type first: \
if (!isControlPolicy(policy)) {
Navigation.navigate(
ROUTES.WORKSPACE_UPGRADE.getRoute(
// With params matching that current screen
);
return;
}
// Else we will now navigate to the drag-and-drop screen of importing tags
Admin: Uploading a multi-level tag CSV
For Multi-Level tags, we need to first update the logic on ImportTagsPage, if we are importing multi-level tags, then we need to display a confirmation screen to verify if the multi-level tags are dependent/independent, Is GL code present,etc:
- Update navigation logic after uploading spreadsheet for multi-level tags
So first for ImportTagsPage, we will check if isUsingMultiLevelTags is true and then pass that value down to the ImportSpreedsheet, this will help us in navigating to the **Configure spreadsheet options **step:
function ImportTagsPage({ route }: ImportTagsPageProps) {
const policyID = route.params.policyID;
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
// While navigating, we will add a new prop to the URL to let us know that we are
// Importing multi-level tags
const isUsingMultiLevelTags = route.params.isUsingMultiLevelTags ?? false;
const backTo = route.params.backTo;
const isQuickSettingsFlow = !!backTo;
return (
<ImportSpreedsheet
backTo={
isQuickSettingsFlow
? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID, backTo)
: ROUTES.WORKSPACE_TAGS.getRoute(policyID)
}
goTo={
isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_IMPORTED.getRoute(policyID, backTo)
: isUsingMultiLevelTags ?
ROUTES.WORKSPACE_MULTI_LEVEL_TAGS_IMPORT_CONFIRMATION.getRoute(policyID)
: ROUTES.WORKSPACE_TAGS_IMPORTED.getRoute(policyID)
}
isUsingMultiLevelTags={isUsingMultiLevelTags}
policyID={policyID}
/>
);
}
Then in ImportSpreedsheet we need to update the routing logic once the spreadsheet has been parsed (We still parse the spreadsheet as we want to display a preview for independent tags), We want to configure additional options for multi-level tags, so while navigating here, we will check hasMultipleTagLists is true, then we will navigate to the additional spreadsheet options page. We need to create a new route and page for that purpose: \
setSpreadsheetData(data as string[][]).then(() => {
// if we have multi-level tags enabled, we want to show
// configure spreadsheet options page
Navigation.navigate(goTo);
});
- Introduce new props in imported spreadsheet model
Now we need to update the onyx key IMPORTED_SPREADSHEET type ImportedSpreadsheet here, For containsHeader type we call Onyx.merge and not the API, So for additional options : Are these independent tags` `and Is there a GL Code in the adjacent column we will create onyx props, we also need to create a new File object to send the file to the Backend we will create new props in ImportedSpreadsheet type:
/** Model of imported spreadsheet data */
type ImportedSpreadsheet = {
.....existing props
/** Whether the first row of the spreadsheet contains headers */
containsHeader: boolean;
/** Are the multi-level tags independent*/
isIndependentMultiLevelTag?: boolean;
/**Is there GL code adjacent to the multi-level tag column */
isGLCodeInAdjacentColumn?: boolean;
//**File to send to the backend */
uploadedFile?: File
};
- Create util functions to save onyx values locally
Now we need to create Onyx.merge functions like we did for containsHeader to set the values in Onyx locally:
function setIsIndependentMultiLevelTag(
isMultiLevelTagIndependent: boolean
): Promise<void> {
return Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {
isIndependentMultiLevelTag,
});
}
function setIsGLCodeInAdjacentColumn(
isGLCodeInAdjacentColumn: boolean
): Promise<void> {
return Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {
isGLCodeInAdjacentColumn,
});
}
function setMultiTagsFile(file: File): Promise<void> {
return Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, { uploadedFile: file });
}
- Set onyx values on configure spreadsheet options page
We need to create a new page ImportTagsConfigureOptionsPage, for us to display the spreadsheet options, the page would contain the following 3 options:
<OfflineWithFeedback
...props
>
<View
>
<Text style={[styles.textNormal]}>
// these texts would be spanish translated too
Is the first line the title for each tag list?
</Text>
<Switch
onToggle={setContainsHeader}
...props
/>
</View>
</OfflineWithFeedback>
<OfflineWithFeedback
...props
>
<View
...props
>
// these texts would be spanish translated too
<Text style={[styles.textNormal]}>Are these independent tags?</Text>
<Switch
onToggle={setIsIndependentMultiLevelTag}
...props
/>
</View>
</OfflineWithFeedback>
<OfflineWithFeedback
...props
>
<View
...props
>
<Text style={[styles.textNormal]}>
// these texts would be spanish translated too
Is there a GL code in the adjacent column?
</Text>
<Switch
onToggle={setIsGLCodeInAdjacentColumn}
...props
/>
</View>
</OfflineWithFeedback>
Then if these are dependent tags, we will not show the Tags Preview. We will directly call the API: ImportMultiLevelTags (We will not show the preview for dependent tags).
SPANISH TRANSLATION
| English
|
Spanish
|
| Configure your list of tags for multi-level tagging.
|
Configura tu lista de etiquetas para el etiquetado multinivel.
|
| The first row is the title for each tag list
|
La primera fila es el título de cada lista de etiquetas.
|
| These are independent tags
|
Estas son etiquetas independientes.
|
| There is a GL code in the adjacent column
|
Hay un código GL en la columna adyacente.
|
- Save file to onyx in case of multi-level tags
Now, In case of Multi-level tags, we need to pass the whole file with the API, to do that we need to update ImportSpreedsheet component, so we need to call setMultiTagsFile when we have the file object here: \
const readFile = (file: File) => {
// we set file object in Onyx data to send it on confirmation page(Independent tags)
// set file object to call immediately in the API for dependent tags
if(isUsingMultiLevelTags){
setMultiTagsFile(file);
}
if (!validateFile(file)) {
return;
}
......
};
- Call importPolicyMultiLevelTags util for independent multi-level tags
If we are importing independent tags which we will check by isIndependentMultiLevelTag, then we will show the Tags Preview. For that we will update the existing ImportedTagsPage:
1. We need to update the field getColumnRoles where we will conditionally render the CONST.CSV_IMPORT_COLUMNS.ENABLED because for multi-level tags, we check the Required field for parent tags.
2. Then we need to update the API call we make based on whether we are importing normal tags or multi-level independent tags.
3. Update ImportSpreadsheetColumns to take `isUsingMultiLevelTags `as a prop, we need to conditionally render a few components on the tags preview page.
const [spreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET);
const isUsingMultiLevelTags = policy?.isUsingMultiLevelTags ?? false;
const isGLCodeInAdjacentColumn = spreadsheet?.isGLCodeInAdjacentColumn;
const getColumnRoles = (): ColumnRole[] => {
const roles = [];
roles.push(
{
text: translate("common.ignore"),
value: CONST.CSV_IMPORT_COLUMNS.IGNORE,
},
{
text: translate("common.name"),
value: CONST.CSV_IMPORT_COLUMNS.NAME,
isRequired: true,
}
);
if (!isUsingMultiLevelTags) {
roles.push({
text: translate("common.enabled"),
value: CONST.CSV_IMPORT_COLUMNS.ENABLED,
});
}
// GL codes are shown by default if we have control workspace
// We will check if the policy has multi-level tags and only show them if
// isGLCodeInAdjacentColumn is true
if (
isControlPolicy(policy) &&
(!isUsingMultiLevelTags || isGLCodeInAdjacentColumn)
) {
roles.push({
text: translate("workspace.tags.glCode"),
value: CONST.CSV_IMPORT_COLUMNS.GL_CODE,
});
}
return roles;
};
// Now we will call the API to import tags conditionally:
const importTags = useCallback(() => {
setIsValidationEnabled(true);
const errors = validate();
if (Object.keys(errors).length > 0) {
return;
}
// If we have multi-level tags then we will call importPolicyMultiLevelTags
// This command will be same for both independent/dependent tags
// For dependent tags we will call the command on the previous page itself
if (isUsingMultiLevelTags) {
setIsImportingTags(true);
importPolicyMultiLevelTags(policyID, spreadsheet);
return;
}
....
// We also need to pass additional isUsingMultiLevelTags prop to ImportSpreadsheetColumns
<ImportSpreadsheetColumns
...props
isUsingMultiLevelTags
/>;
- Create importPolicyMultiLevelTags util and call the API
We need to create the util importPolicyMultiLevelTags, util which will be as follows:
function importPolicyMultiLevelTags(
policyID: string,
spreadsheet: ImportedSpreadsheet
) {
const tagsNames = spreadsheet?.data[tagsNamesColumn].map((name) => name);
const tags = tagsNames?.slice(containsHeader ? 1 : 0).map((name, index) => {
// Right now we support only single-level tags, this check should be updated when we add multi-level support
const tagAlreadyExists = policyTagLists.at(0)?.tags?.[name];
const existingGLCodeOrDefault = tagAlreadyExists?.['GL Code'] ?? '';
return {
name,
enabled: tagsEnabledColumn !== -1 ? tagsEnabled?.[containsHeader ? index + 1 : index] === 'true' : true,
// eslint-disable-next-line @typescript-eslint/naming-convention
'GL Code': tagsGLCodeColumn !== -1 ? tagsGLCode?.[containsHeader ? index + 1 : index] ?? '' : existingGLCodeOrDefault,
};
});
const onyxData = updateImportSpreadsheetData(tags.length);
const isFirstLineHeader = spreadsheet.containsHeader;
const isIndependent = spreadsheet.isMultiLevelTagIndependent;
const isGLAdjacent = spreadsheet.isGLCodeInAdjacentColumn;
const file = spreadsheet.uploadedFile;
const parameters = {
policyID,
isFirstLineHeader,
isIndependent,
isGLAdjacent,
file,
};
// We will create a new API command IMPORT_MULT_LEVEL_TAGS_SPREADSHEET
API.write(
WRITE_COMMANDS.IMPORT_MULT_LEVEL_TAGS_SPREADSHEET,
parameters,
onyxData
);
}
- Update spreadsheet components to conditionally render items
We need to update ImportSpreadsheetColumns component to conditionally render text and toggle based on whether we are importing independent multi-level tags:
function ImportSpreadsheetColumns({
...props
isUsingMultiLevelTags = false,
}: ImportSpreadsheetColumnsProps) {
....
return (
<>
<ScrollView>
<View style={styles.mh5}>
// Render different header text if we are not importing Multi-Level tags
{!isUsingMultiLevelTags && (
<View>
<Text>
{translate("spreadsheet.importDescription")}
<TextLink href={learnMoreLink ?? ""}>{` ${translate(
"common.learnMore"
)}`}</TextLink>
</Text>
<View
style={[
styles.mt7,
styles.flexRow,
styles.justifyContentBetween,
styles.alignItemsCenter,
]}
>
<Text>{translate("spreadsheet.fileContainsHeader")}</Text>
<Switch
accessibilityLabel={translate(
"spreadsheet.fileContainsHeader"
)}
isOn={containsHeader}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onToggle={setContainsHeader}
/>
</View>
</View>
)}
// This will be the header for multi-level independent tags
{isUsingMultiLevelTags && (
<View>
// Pending spanish translations too
<Text>{`Here's a preview of your tags. If everything looks good, tap below to import them.`}</Text>
</View>
)}
</View>
</ScrollView>
</>
);
}
SPANISH TRANSLATION
| English
|
Spanish
|
| Here's a preview of your tags. If everything looks good, tap below to import them.
|
Este es un vistazo previo de tus etiquetas. Si todo se ve bien, toca abajo para importarlas.
|
Finally, we need to update the ImportColumn to conditionally render ButtonWithDropdownMenu: \
{
!spreadsheet?.isMultiLevelTagIndependent && (
<View style={styles.ml2}>
<ButtonWithDropdownMenu
onPress={() => {}}
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.SMALL}
shouldShowSelectedItemCheck
menuHeaderText={columnHeader}
isSplitButton={false}
onOptionSelected={(option) => {
setColumnName(columnIndex, option.value);
}}
defaultSelectedIndex={finalIndex}
options={options}
/>
</View>
);
}
This completes the importing multi-level tags section.
- Miscellaneous changes required on tags view page
We need to make a code change here, for multi-level tags currently we hide the *right caret *on tags list page: \

If we are using dependent tags, we need to update the header text to: "You are using [dependent tags](link to new help doc about dependent tags).". So we need to update WorkspaceTagsPage, we need to put conditional rendering in getHeaderText:
const getHeaderText = () => (
<View
...props
>
{!hasSyncError && isConnectedToAccounting ? (
<Text>
... text
</Text>
) : policy?.isUsingMultiLevelTags ? (
// we will use TextLink here to link to the HelpDot article
<Text style={[styles.textNormal, styles.colorMuted]}>
You are using dependent tags
</Text>
) : (
<Text style={[styles.textNormal, styles.colorMuted]}>
..text
</Text>
)}
</View>
);
SPANISH TRANSLATION
| English
|
Spanish
|
| You are using dependent tags
|
Estás utilizando etiquetas dependientes.
|
Issue Owner
Current Issue Owner: @kadiealexander
Tracking GH: https://github.com/Expensify/Expensify/issues/418655
Design Doc: https://docs.google.com/document/d/1_p8qZXVKG-s32i9GKqVjCLEcEREBUEgYcxk3KYHXMVg/edit?usp=sharing
From the Design Doc:
Admin - Choosing to import Multi-Level Tags
We need to show a new empty state in the WorkspaceTagsPage page, There is an existing EmptyStateComponent component for the empty state, so we can re-use the same component with new copy, here to with the following conditions:
SPANISH TRANSLATION
Note: We will update the import button routes to point to the new page asking the user about the level of tag they are importing via spreadsheet.
We also need to hide the Add Tag button if we are using multi-level tags (we only support adding multi-level tags via spreadsheet), So we need to update the the condition here: \
Selecting the
Import Spreadsheetoption will open up theRHPfor Selecting whether the user is importingSingle level of tagsor `Multiple levels of tags.SPANISH TRANSLATION
When the user clicks on
Multiple levels of tagsthen we need to show them a warning modal about the deletion of the currently present tagsSPANISH TRANSLATION
Note:
After the user clicks on
Switch Tag Levelson, we need to check if the user is on control policy or not, if the later then we need to ask them to update the policy type first: \Admin: Uploading a multi-level tag CSV
For Multi-Level tags, we need to first update the logic on ImportTagsPage, if we are importing multi-level tags, then we need to display a confirmation screen to verify if the multi-level tags are dependent/independent, Is GL code present,etc:
So first for ImportTagsPage, we will check if
isUsingMultiLevelTagsis true and then pass that value down to theImportSpreedsheet, this will help us in navigating to the **Configure spreadsheet options **step:Then in
ImportSpreedsheetwe need to update the routing logic once the spreadsheet has been parsed (We still parse the spreadsheet as we want to display a preview for independent tags), We want to configure additional options for multi-level tags, so while navigating here, we will checkhasMultipleTagListsis true, then we will navigate to the additional spreadsheet options page. We need to create a new route and page for that purpose: \Now we need to update the onyx key
IMPORTED_SPREADSHEETtypeImportedSpreadsheethere, ForcontainsHeadertype we call Onyx.merge and not the API, So for additional options :Are these independent tags` `andIs there a GL Code in the adjacent columnwe will create onyx props,we also need to create a new File object to send the file to the Backend we will create new props in ImportedSpreadsheet type:Now we need to create Onyx.merge functions like we did for
containsHeaderto set the values in Onyx locally:We need to create a new page ImportTagsConfigureOptionsPage, for us to display the spreadsheet options, the page would contain the following 3 options:
Then if these are dependent tags, we will not show the Tags Preview. We will directly call the API:
ImportMultiLevelTags(We will not show the preview for dependent tags).SPANISH TRANSLATION
Now, In case of Multi-level tags, we need to pass the whole file with the API, to do that we need to update ImportSpreedsheet component, so we need to call
setMultiTagsFilewhen we have the file object here: \If we are importing independent tags which we will check by
isIndependentMultiLevelTag,then we will show the Tags Preview. For that we will update the existing ImportedTagsPage:We need to create the util
importPolicyMultiLevelTags,util which will be as follows:We need to update
ImportSpreadsheetColumnscomponent to conditionally render text and toggle based on whether we are importing independent multi-level tags:SPANISH TRANSLATION
Finally, we need to update the
ImportColumnto conditionally renderButtonWithDropdownMenu: \This completes the importing multi-level tags section.
We need to make a code change here, for multi-level tags currently we hide the *right caret *on tags list page: \
If we are using dependent tags, we need to update the header text to: "You are using [dependent tags](link to new help doc about dependent tags).". So we need to update WorkspaceTagsPage, we need to put conditional rendering in getHeaderText:
SPANISH TRANSLATION
Issue Owner
Current Issue Owner: @kadiealexander