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

#26126: Tag picker sections #27765

Merged
merged 27 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6c17f2b
feat: add tag support to getNewChatOptions
BeeMargarida Sep 8, 2023
a45d61a
fix: add missing jsdoc
BeeMargarida Sep 8, 2023
1f44b95
Merge branch 'feat/26126-tag_menu_item_and_picker' into feat/26126-ta…
BeeMargarida Sep 11, 2023
99a5d2e
Merge branch 'feat/26126-tag_menu_item_and_picker' into feat/26126-ta…
BeeMargarida Sep 13, 2023
a2b1759
Merge branch 'main' into feat/26126-tag_picker_sections
BeeMargarida Sep 14, 2023
4fdefef
feat: build sections and show search input
BeeMargarida Sep 14, 2023
e2f1dc9
fix: conditional check
BeeMargarida Sep 14, 2023
b94b226
fix: adapt to policyRecentlyUsedTags
BeeMargarida Sep 14, 2023
accee61
Merge branch 'main' into feat/26126-tag_picker_sections
BeeMargarida Sep 15, 2023
6a47b41
fix: use another approach for fetching report
BeeMargarida Sep 18, 2023
fb47dec
Merge branch 'main' into feat/26126-tag_picker_sections
BeeMargarida Sep 18, 2023
b4fff89
feat: save IOU tag and send it in API request
BeeMargarida Sep 18, 2023
eee0d51
fix: lint errors
BeeMargarida Sep 18, 2023
68a757f
feat: move update logic outside picker
BeeMargarida Sep 19, 2023
d601800
Merge branch 'main' into feat/26126-tag_picker_sections
BeeMargarida Sep 19, 2023
e98ca80
fix: filter out disabled tags
BeeMargarida Sep 19, 2023
df3ee73
refactor: reorder
BeeMargarida Sep 19, 2023
ccda254
Merge branch 'main' into feat/26126-tag_picker_sections
BeeMargarida Sep 19, 2023
8b4245f
revert: reorder
BeeMargarida Sep 19, 2023
1cd6b03
refactor: simplify
BeeMargarida Sep 19, 2023
9662822
fix: prettuer
BeeMargarida Sep 19, 2023
118a791
refactor: simplify
BeeMargarida Sep 19, 2023
43aa9e7
fix: change variable names
BeeMargarida Sep 20, 2023
56efc68
Merge branch 'main' into feat/26126-tag_picker_sections
BeeMargarida Sep 20, 2023
ec0ffa6
fix: check for recentlyUsedPolicyTags and default value
BeeMargarida Sep 20, 2023
2f4c295
refactor: rename getNewChatOptions to getFilteredOptions
BeeMargarida Sep 20, 2023
993b4c2
fix: adapt to null value
BeeMargarida Sep 20, 2023
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
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2632,6 +2632,7 @@ const CONST = {
INDENTS: ' ',
PARENT_CHILD_SEPARATOR: ': ',
CATEGORY_LIST_THRESHOLD: 8,
TAG_LIST_THRESHOLD: 8,
DEMO_PAGES: {
SAASTR: 'SaaStrDemoSetup',
SBE: 'SbeDemoSetup',
Expand Down
2 changes: 1 addition & 1 deletion src/components/MoneyRequestConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ function MoneyRequestConfirmationList(props) {
disabled={didConfirm || props.isReadOnly}
/>
)}
{canUseTags && !!tagList && (
{canUseTags && !_.isEmpty(tagList) && (
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
<MenuItemWithTopDescription
shouldShowRightIcon={!props.isReadOnly}
title={props.iouTag}
Expand Down
77 changes: 37 additions & 40 deletions src/components/TagPicker/index.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,58 @@
import React, {useMemo} from 'react';
import React, {useMemo, useState} from 'react';
import _ from 'underscore';
import lodashGet from 'lodash/get';
import {withOnyx} from 'react-native-onyx';
import CONST from '../../CONST';
import ONYXKEYS from '../../ONYXKEYS';
import styles from '../../styles/styles';
import Navigation from '../../libs/Navigation/Navigation';
import ROUTES from '../../ROUTES';
import useLocalize from '../../hooks/useLocalize';
import * as OptionsListUtils from '../../libs/OptionsListUtils';
import OptionsSelector from '../OptionsSelector';
import {propTypes, defaultProps} from './tagPickerPropTypes';

function TagPicker({policyTags, reportID, tag, iouType, iou}) {
function TagPicker({selectedTag, tag, policyTags, policyRecentlyUsedTags, onSubmit}) {
const {translate} = useLocalize();
const [searchValue, setSearchValue] = useState('');

const policyRecentlyUsedTagsList = lodashGet(policyRecentlyUsedTags, tag, []);
const policyTagList = lodashGet(policyTags, [tag, 'tags'], []);
const policyTagsCount = _.size(policyTagList);
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD;
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved

const shouldShowTextInput = !isTagsCountBelowThreshold;

const selectedOptions = useMemo(() => {
if (!iou.tag) {
if (!selectedTag) {
return [];
}

return [
{
name: iou.tag,
name: selectedTag,
enabled: true,
accountID: null,
},
];
}, [iou.tag]);

// Only shows one section, which will be the default behavior if there are
// less than 8 policy tags
// TODO: support sections with search
const sections = useMemo(() => {
const tagList = _.chain(lodashGet(policyTags, [tag, 'tags'], {}))
.values()
.map((t) => ({
text: t.name,
keyForList: t.name,
tooltipText: t.name,
}))
.value();
}, [selectedTag]);

return [
{
data: tagList,
},
];
}, [policyTags, tag]);
const initialFocusedIndex = useMemo(() => {
if (isTagsCountBelowThreshold && selectedOptions.length > 0) {
return _.chain(policyTagList)
.values()
.findIndex((policyTag) => policyTag.name === selectedOptions[0].name, true)
.value();
}

Comment on lines +38 to +44
Copy link
Contributor

Choose a reason for hiding this comment

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

Disabled tags should have been filtered out. It caused this issue: #30465

const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, '');
return 0;
}, [policyTagList, selectedOptions, isTagsCountBelowThreshold]);

const navigateBack = () => {
Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID));
};
const sections = useMemo(
() =>
OptionsListUtils.getNewChatOptions({}, {}, [], searchValue, selectedOptions, [], false, false, false, {}, [], true, policyTagList, policyRecentlyUsedTagsList, false).tagOptions,
[searchValue, selectedOptions, policyTagList, policyRecentlyUsedTagsList],
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
);

const updateTag = () => {
// TODO: add logic to save the selected tag
navigateBack();
};
const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, '');

return (
<OptionsSelector
Expand All @@ -66,9 +62,13 @@ function TagPicker({policyTags, reportID, tag, iouType, iou}) {
headerMessage={headerMessage}
textInputLabel={translate('common.search')}
boldStyle
value=""
onSelectRow={updateTag}
shouldShowTextInput={false}
highlightSelectedOptions
isRowMultilineSupported
shouldShowTextInput={shouldShowTextInput}
value={searchValue}
initialFocusedIndex={initialFocusedIndex}
onChangeText={setSearchValue}
onSelectRow={onSubmit}
/>
);
}
Expand All @@ -84,7 +84,4 @@ export default withOnyx({
policyRecentlyUsedTags: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`,
},
iou: {
key: ONYXKEYS.IOU,
},
})(TagPicker);
16 changes: 4 additions & 12 deletions src/components/TagPicker/tagPickerPropTypes.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import PropTypes from 'prop-types';
import tagPropTypes from '../tagPropTypes';
import {iouPropTypes, iouDefaultProps} from '../../pages/iou/propTypes';

const propTypes = {
/** The report ID of the IOU */
reportID: PropTypes.string.isRequired,

/** The policyID we are getting tags for */
policyID: PropTypes.string.isRequired,

/** The selected tag of the money request */
selectedTag: PropTypes.string.isRequired,

/** The name of tag list we are getting tags for */
tag: PropTypes.string.isRequired,

/** The type of IOU report, i.e. bill, request, send */
iouType: PropTypes.string.isRequired,

/** Callback to submit the selected tag */
onSubmit: PropTypes.func,
onSubmit: PropTypes.func.isRequired,

/* Onyx Props */
/** Collection of tags attached to a policy */
Expand All @@ -29,15 +25,11 @@ const propTypes = {

/** List of recently used tags */
policyRecentlyUsedTags: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)),

/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: iouPropTypes,
};

const defaultProps = {
policyTags: {},
policyRecentlyUsedTags: {},
iou: iouDefaultProps,
};

export {propTypes, defaultProps};
156 changes: 155 additions & 1 deletion src/libs/OptionsListUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ function isCurrentUser(userDetails) {
/**
* Build the options for the category tree hierarchy via indents
*
* @param {Object[]} options - an initial strings array
* @param {Object[]} options - an initial object array
* @param {Boolean} options[].enabled - a flag to enable/disable option in a list
* @param {String} options[].name - a name of an option
* @param {Boolean} [isOneLine] - a flag to determine if text should be one line
Expand Down Expand Up @@ -736,6 +736,132 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt
return categorySections;
}

/**
* Transforms the provided tags into objects with a specific structure.
*
* @param {Object[]} tags - an initial tag array
* @param {Boolean} tags[].enabled - a flag to enable/disable option in a list
* @param {String} tags[].name - a name of an option
* @returns {Array<Object>}
*/
function getTagsOptions(tags) {
const options = [];

_.each(tags, (tag) =>
options.push({
text: tag.name,
keyForList: tag.name,
searchText: tag.name,
tooltipText: tag.name,
isDisabled: !tag.enabled,
}),
);

return options;
}
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved

/**
* Build the section list for tags
*
* @param {Object[]} tags
* @param {String} tags[].name
* @param {Boolean} tags[].enabled
* @param {String[]} recentlyUsedTags
* @param {Object[]} selectedOptions
* @param {String} selectedOptions[].name
* @param {String} searchInputValue
* @param {Number} maxRecentReportsToShow
* @returns {Array<Object>}
*/
function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInputValue, maxRecentReportsToShow) {
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
const tagSections = [];
const numberOfTags = _.size(tags);
let indexOffset = 0;

if (!_.isEmpty(searchInputValue)) {
const searchTags = _.filter(tags, (tag) => tag.name.toLowerCase().includes(searchInputValue.toLowerCase()));

tagSections.push({
// "Search" section
title: '',
shouldShow: false,
indexOffset,
data: getTagsOptions(searchTags),
});

return tagSections;
}

if (numberOfTags < CONST.TAG_LIST_THRESHOLD) {
tagSections.push({
// "All" section when items amount less than the threshold
title: '',
shouldShow: false,
indexOffset,
data: getTagsOptions(tags),
});

return tagSections;
}

const selectedOptionNames = _.map(selectedOptions, (selectedOption) => selectedOption.name);
const filteredRecentlyUsedTags = _.map(
_.filter(recentlyUsedTags, (tag) => !_.includes(selectedOptionNames, tag)),
(tag) => {
const tagObject = _.find(tags, (t) => t.name === tag);
return {
name: tag,
enabled: tagObject && tagObject.enabled,
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
};
},
);
const filteredTags = _.filter(tags, (tag) => !_.includes(selectedOptionNames, tag.name));

if (!_.isEmpty(selectedOptions)) {
const selectedTagOptions = _.map(selectedOptions, (option) => {
const tagObject = _.find(tags, (t) => t.name === option.name);
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
return {
name: option.name,
enabled: tagObject && tagObject.enabled,
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
};
});

tagSections.push({
// "Selected" section
title: '',
shouldShow: false,
indexOffset,
data: getTagsOptions(selectedTagOptions),
});

indexOffset += selectedOptions.length;
}

if (!_.isEmpty(filteredRecentlyUsedTags)) {
const cutRecentlyUsedTags = filteredRecentlyUsedTags.slice(0, maxRecentReportsToShow);

tagSections.push({
// "Recent" section
title: Localize.translateLocal('common.recent'),
shouldShow: true,
indexOffset,
data: getTagsOptions(cutRecentlyUsedTags),
});

indexOffset += filteredRecentlyUsedTags.length;
}

tagSections.push({
// "All" section when items amount more than the threshold
title: Localize.translateLocal('common.all'),
shouldShow: true,
indexOffset,
data: getTagsOptions(filteredTags),
});

return tagSections;
}

/**
* Build the options
*
Expand Down Expand Up @@ -772,6 +898,9 @@ function getOptions(
includeCategories = false,
categories = {},
recentlyUsedCategories = [],
includeTags = false,
tags = {},
recentlyUsedTags = [],
canInviteUser = true,
},
) {
Expand All @@ -784,6 +913,20 @@ function getOptions(
userToInvite: null,
currentUserOption: null,
categoryOptions,
tagOptions: [],
};
}

if (includeTags) {
const tagOptions = getTagListSections(_.values(tags), recentlyUsedTags, selectedOptions, searchInputValue, maxRecentReportsToShow);

return {
recentReports: [],
personalDetails: [],
userToInvite: null,
currentUserOption: null,
categoryOptions: [],
tagOptions,
};
}

Expand All @@ -794,6 +937,7 @@ function getOptions(
userToInvite: null,
currentUserOption: null,
categoryOptions: [],
tagOptions: [],
};
}

Expand Down Expand Up @@ -1044,6 +1188,7 @@ function getOptions(
userToInvite: canInviteUser ? userToInvite : null,
currentUserOption,
categoryOptions: [],
tagOptions: [],
};
}

Expand Down Expand Up @@ -1128,6 +1273,9 @@ function getIOUConfirmationOptionsFromParticipants(participants, amountText) {
* @param {boolean} [includeCategories]
* @param {Object} [categories]
* @param {Array<String>} [recentlyUsedCategories]
* @param {boolean} [includeTags]
* @param {Object} [tags]
* @param {Array<String>} [recentlyUsedTags]
* @param {boolean} [canInviteUser]
* @returns {Object}
*/
Expand All @@ -1143,6 +1291,9 @@ function getNewChatOptions(
includeCategories = false,
categories = {},
recentlyUsedCategories = [],
includeTags = false,
tags = {},
recentlyUsedTags = [],
canInviteUser = true,
) {
return getOptions(reports, personalDetails, {
Expand All @@ -1158,6 +1309,9 @@ function getNewChatOptions(
includeCategories,
categories,
recentlyUsedCategories,
includeTags,
tags,
recentlyUsedTags,
canInviteUser,
});
}
Expand Down
Loading
Loading