Skip to content

[Due for payment 2025-06-27] [Multi-tags] Expense: Selecting Dependent Tags #61815

@yuwenmemon

Description

@yuwenmemon

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:

Expense: Selecting Dependent Tags

There is no change for Independent Multi-level Tags ; we display them as they are imported on the Confirmation page. In this section, we need to configure the display of dependent tags, For adding dependent tags to expenses, child tag will reveal themselves as parent tags get selected:

  1. Update logic to reveal child tag when parent tag gets selected

In MoneyRequestConfirmationListFooter while mapping the tags available we need to check if the current tags are dependent tags and if so then we need to update the logic of displaying child tags, they will reveal as parent tags get selected (Note that if we only have 1 child tag then it will get selected by default similar to how do currently do on OldDot.): \

const hasDependentTags = useMemo(
  () => PolicyUtils.hasDependentTags(policy, policyTags),
  [policy, policyTags]
);

// Use useAnimatedHighlightStyle from [here](https://github.com/Expensify/App/blob/9586baffc8c90230f9affa3d98fa4e4899f58359/src/hooks/useAnimatedHighlightStyle/index.ts#L49), to highlight the newly row added
const animatedHighlightStyle = useAnimatedHighlightStyle({
  borderRadius: variables.componentBorderRadius,
  shouldHighlight: item?.shouldAnimateInHighlight ?? false,
  highlightColor: theme.messageHighlightBG, // For yellow highlight 
  backgroundColor: theme.highlightBG,
});



...policyTagLists.map(({ name, required, tags }, index) => {
  const isTagRequired = required ?? false;
 // We should always show the Parent (index 0) tag regardless of whether it is 
// selected or not.

// For other cases, we should show the current tag only when the parent tag is 
// selected
  const shouldShowDependentTag =
    index === 0
      ? true
      : !!TransactionUtils.getTagForDisplay(transaction, index - 1);

// then if we are using dependent tag the use the logic of dependent while showing children tags
  const shouldShow = hasDependentTags
    ? shouldShowDependentTag
    : shouldShowTags &&
      (!isMultilevelTags || OptionsListUtils.hasEnabledOptions(tags));
  return {
    item: (
      <MenuItemWithTopDescription
	style={animatedHighlightStyle}
        ...props
      />
    ),
    shouldShow,
    isSupplementary: !isTagRequired,
  };
});,
  • Note: We will temporarily highlight the MenuItem when a new dependent tag row is added.
  1. Clear all children tag values when parent tags are updated

Now for cases where we change the lower-order index tags (Parent tags), we need to clear all the children tags (Similar to OldDot). For that, we will make use of the existing insertTagIntoTransactionTagsString util, but we would have to introduce two new props: hasDependentTags and policyTagListsLength, this is because we want to clear all the children tag values up to the total tagIndex in the given policyTagList. So first, introduce the new props:

function insertTagIntoTransactionTagsString(
  transactionTags: string,
  tag: string,
  tagIndex: number,
  hasDependentTag?: boolean,
  policyTagListsLength?: number
): string {
  const tagArray = TransactionUtils.getTagArrayFromName(transactionTags);
  tagArray[tagIndex] = tag;

  // If hasDependentTag is true, clear tags greater than tagIndex up to policyTagListsLength
  if (hasDependentTag) {
    for (let i = tagIndex + 1; i < policyTagListsLength; i++) {
      tagArray[i] = ""; // Clear the dependent tags
    }
  }

  while (tagArray.length > 0 && !tagArray.at(-1)) {
    tagArray.pop();
  }

  return tagArray.map((tagItem) => tagItem.trim()).join(CONST.COLON);
}

In IOURequestStepTag, we will pass these two new props:

const updateTag = (selectedTag: Partial<ReportUtils.OptionData>) => {
  const isSelectedTag = selectedTag.searchText === tag;
  const searchText = selectedTag.searchText ?? "";
  // here we pass the new props
  const updatedTag = IOUUtils.insertTagIntoTransactionTagsString(
    transactionTag,
    isSelectedTag ? "" : searchText,
    tagListIndex,
    hasDependentTags,
    policyTagLists.length
  );
 ..........
 
};

These changes will ensure that child tag push inputs reveal themselves as parent tags are selected and that child tags get cleared when the parent tag is changed.

  1. Filter tag list based on the parentFilter for dependent multi-level tags

The final part of this would be to filter the children tags according to the selection of the parent tags, for that we need to make a change to the base TagPicker component and we also need to introduce a new TransactionUtils named as getTagUptoIndex, we need to match the parentTagsFilter with the currently selected parent-child tag value: \

function getTagUptoIndex(
  transaction: OnyxInputOrEntry<Transaction>,
  tagIndex?: number
): string {
  if (tagIndex !== undefined && transaction?.tag) {
    const tagsArray = getTagArrayFromName(transaction?.tag ?? "");
    // Get tags from start up to (but not including) tagIndex of the current child tag and join them back into a string
    return tagsArray.slice(0, tagIndex).join(",");
  }

  return transaction?.tag ?? "";
}

This will give us a string array which we will then use in TagPicker to match the regex of parentTagsFilter, this allows us to only show the dependent tags corresponding to currently selected parent tag, So now, when we calculate the enabledTags here, \

const currentlySelectedTag = TransactionUtils.getTagUptoIndex(
  currentTransaction,
  tagListIndex
);


const enabledTags: PolicyTags | Array<PolicyTag | SelectedTagOption> =
  useMemo(() => {
    if (!shouldShowDisabledAndSelectedOption) {
      // we should only filter according to the parentTagsFilter when we have dependent tag and we are not on the first parent tag
      return hasDependentTags && !!currentlySelectedTag
        ? Object.values(policyTagList.tags).filter((tag) => {
            // Make sure to return the comparison result
            return (
              tag.rules?.parentTagsFilter ===
              `^${currentlySelectedTag.replace(",", "\\:")}$`
            );
          })
        : policyTagList.tags;
    }

    ........
  }, [selectedOptions, policyTagList, shouldShowDisabledAndSelectedOption]);


The regex ^${currentlySelectedTag.replace(",", "\\:")}$ regex will take the array currentlySelectedTag as input, and then it will try to match theparentTagsFilterValue, this way we will only show the children tags for the currently selected parent tag:
\

Image

POC video can be found here. \

Issue OwnerCurrent Issue Owner: @CortneyOfstad

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionDailyKSv2EngineeringNewFeatureSomething to build that is a new item.

Type

No type
No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions