Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1096,7 +1096,7 @@ const DYNAMIC_ROUTES = {
},
WORKSPACE_EDIT_TAGS: {
path: 'workspace-edit-tags/:orderWeight',
entryScreens: [SCREENS.WORKSPACE.DYNAMIC_TAGS_SETTINGS, SCREENS.WORKSPACE.DYNAMIC_TAG_LIST_VIEW],
entryScreens: [SCREENS.WORKSPACE.TAGS, SCREENS.WORKSPACE.DYNAMIC_TAGS_SETTINGS, SCREENS.WORKSPACE.DYNAMIC_TAG_LIST_VIEW],
getRoute: (orderWeight: number) => `workspace-edit-tags/${orderWeight}`,
},
WORKSPACE_CATEGORY_CREATE: {
Expand Down
12 changes: 12 additions & 0 deletions src/components/ButtonWithDropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Button from '@components/ButtonComposed';
import Icon from '@components/Icon';
import InlineIcon from '@components/Icon/InlineIcon';
import PopoverMenu from '@components/PopoverMenu';
import Switch from '@components/Switch';
import Text from '@components/Text';

import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
Expand Down Expand Up @@ -353,6 +354,17 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
containerStyles={containerStyles}
menuItems={options.map((item, index) => ({
...item,
...(item.switchProps
? {
shouldShowRightComponent: true,
// The Switch is display-only; the interactive row handles the toggle (mouse and keyboard) via onSelected, so there's no double-fire.
rightComponent: (
<View pointerEvents="none">
<Switch {...item.switchProps} />
</View>
),
}
: {}),
onSelected: item.onSelected
? () => {
item.onSelected?.();
Expand Down
18 changes: 18 additions & 0 deletions src/components/ButtonWithDropdownMenu/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type {PopoverMenuItem} from '@components/PopoverMenu';
import type {SwitchProps} from '@components/Switch';

import type {ButtonVariant} from '@styles/utils/types';

import type CONST from '@src/CONST';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import type IconAsset from '@src/types/utils/IconAsset';
Expand Down Expand Up @@ -55,6 +57,22 @@ type DropdownOption<TValueType> = WithSentryLabel & {
shouldShowLoadingSpinnerIcon?: boolean;
/** Whether to render a divider before this option */
addSeparatorBefore?: boolean;

/** When set, renders a Switch on the right of the option as an inline toggle row. The menu builds the Switch from this data so no JSX is threaded through the option config. */
switchProps?: SwitchProps;

/** Errors to display under the option (e.g. when an inline toggle's save fails) */
errors?: Errors | null;

/** Callback to dismiss the option's errors */
onCloseError?: () => void;

/** Whether to show the default right chevron icon (e.g. for a row that opens another page) */
shouldShowRightIcon?: boolean;

/** Style for the row that holds the title and the Switch (e.g. to vertically center a toggle against wrapped text) */
innerContainerStyle?: StyleProp<ViewStyle>;
/** The type of brick road indicator to show */
brickRoadIndicator?: ValueOf<typeof CONST.BRICK_ROAD_INDICATOR_STATUS>;
/** Whether selecting this option should bypass the delete confirmation modal */
shouldSkipDeleteModal?: boolean;
Expand Down
20 changes: 17 additions & 3 deletions src/components/PopoverMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {close} from '@userActions/Modal';

import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
import type IconAsset from '@src/types/utils/IconAsset';

Expand Down Expand Up @@ -73,6 +73,12 @@ type PopoverMenuItem = MenuItemProps & {
shouldCloseAllModals?: boolean;
pendingAction?: PendingAction;

/** Errors to display under the menu item (e.g. when an inline toggle's save fails) */
errors?: Errors | null;

/** Callback to dismiss the item's errors */
onCloseError?: () => void;

rightIcon?: IconAsset;

key?: string;
Expand Down Expand Up @@ -535,7 +541,12 @@ function BasePopoverMenu({
<React.Fragment key={reactKey}>
{/* Compact popovers need tighter divider spacing than full-page sections. */}
{addSeparatorBefore === true && menuIndex > 0 && <View style={[styles.sectionDividerLine, styles.mh4, styles.mv2]} />}
<OfflineWithFeedback pendingAction={item.pendingAction}>
<OfflineWithFeedback
pendingAction={item.pendingAction}
errors={item.errors}
onClose={item.onCloseError}
errorRowStyles={styles.ph5}
>
<FocusableMenuItem
key={reactKey}
pressableTestID={menuItemTestID ?? `PopoverMenuItem-${item.text}`}
Expand Down Expand Up @@ -599,8 +610,11 @@ function BasePopoverMenu({
if (focusedIndex === -1) {
return;
}
const staysOpenOnSelect = currentMenuItems.at(focusedIndex)?.shouldCloseModalOnSelect === false;

selectItem(focusedIndex);
setFocusedIndex(-1); // Reset the focusedIndex on selecting any menu
// Keep focus on a stay-open item (e.g. an inline toggle) so the next arrow key continues from it; otherwise reset.
setFocusedIndex(staysOpenOnSelect ? focusedIndex : -1);
},
{isActive: isVisible},
);
Expand Down
1 change: 1 addition & 0 deletions src/components/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,4 @@ function Switch({isOn, onToggle, accessibilityLabel, disabled, pending = false,
}

export default Switch;
export type {SwitchProps};
46 changes: 43 additions & 3 deletions src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,15 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import {getCurrentAccountingIntegrationName} from '@pages/workspace/accounting/utils';

import {close} from '@userActions/Modal';
import {clearCategoryErrors, deleteWorkspaceCategories, downloadCategoriesCSV, openPolicyCategoriesPage, setWorkspaceCategoryEnabled} from '@userActions/Policy/Category';
import {
clearCategoryErrors,
deleteWorkspaceCategories,
downloadCategoriesCSV,
openPolicyCategoriesPage,
setPolicyShowCategoryGLCodes,
setWorkspaceCategoryEnabled,
} from '@userActions/Policy/Category';
import {clearPolicyErrorField} from '@userActions/Policy/Policy';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -365,8 +373,31 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {

const secondaryActions = useMemo(() => {
const menuItems = [];
// Under the revamp the other settings moved to Rules, so this is only worth showing for the GL codes toggle.
if (canWriteCategories && (!isRulesRevampEnabled || !!policy?.glCodes)) {
// Under the revamp the Settings page is gone, so its remaining GL codes toggle is surfaced directly in this
// menu instead of behind a dedicated Settings page.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@marufsharifi Do we need a comment here? I think the code is clear enough to understand without it, so we can remove it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@huult This comment already existed on main — I only updated its wording for the new placement. I'd keep it for context, but can remove if you prefer. thanks

if (isRulesRevampEnabled) {
if (canWriteCategories && !!policy?.glCodes) {
menuItems.push({
text: translate('workspace.categories.showCategoryGLCodes'),
value: CONST.POLICY.SECONDARY_ACTIONS.SETTINGS,
// Selecting the row (click or Enter) toggles it; the Switch is a display-only indicator. Keep the menu open on select.
shouldCloseModalOnSelect: false,
onSelected: () => setPolicyShowCategoryGLCodes(policyId, !(policy?.showCategoryGLCodes ?? false)),
numberOfLinesTitle: 0,
innerContainerStyle: styles.alignItemsCenter,
titleStyle: [styles.textLabel, styles.fontWeightNormal],
pendingAction: policy?.pendingFields?.showCategoryGLCodes,
errors: policy?.errorFields?.showCategoryGLCodes,
onCloseError: () => clearPolicyErrorField(policyId, 'showCategoryGLCodes'),
switchProps: {
isOn: policy?.showCategoryGLCodes ?? false,
accessibilityLabel: translate('workspace.categories.showCategoryGLCodes'),
onToggle: (value: boolean) => setPolicyShowCategoryGLCodes(policyId, value),
disabled: !policy?.areCategoriesEnabled,
},
});
}
} else if (canWriteCategories) {
menuItems.push({
icon: icons.Gear,
text: translate('common.settings'),
Expand All @@ -380,6 +411,8 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
text: translate('spreadsheet.importSpreadsheet'),
onSelected: navigateToImportSpreadsheet,
value: CONST.POLICY.SECONDARY_ACTIONS.IMPORT_SPREADSHEET,
// Group the GL codes toggle apart from the spreadsheet actions under the revamp.
addSeparatorBefore: isRulesRevampEnabled,
});
}
if (hasVisibleCategories) {
Expand Down Expand Up @@ -416,11 +449,18 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
canWriteCategories,
isRulesRevampEnabled,
policy?.glCodes,
policy?.showCategoryGLCodes,
policy?.areCategoriesEnabled,
policy?.pendingFields?.showCategoryGLCodes,
policy?.errorFields?.showCategoryGLCodes,
policyHasAccountingConnections,
hasVisibleCategories,
navigateToImportSpreadsheet,
isOffline,
policyId,
styles.alignItemsCenter,
styles.textLabel,
styles.fontWeightNormal,
]);

const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine();
Expand Down
56 changes: 55 additions & 1 deletion src/pages/workspace/tags/WorkspaceTagsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
downloadMultiLevelTagsCSV,
downloadTagsCSV,
openPolicyTagsPage,
setPolicyShowTagGLCodes,
setPolicyTagsRequired,
setWorkspaceTagEnabled,
setWorkspaceTagRequired,
Expand Down Expand Up @@ -67,6 +68,7 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import {getCurrentAccountingIntegrationName} from '@pages/workspace/accounting/utils';

import {close} from '@userActions/Modal';
import {clearPolicyErrorField} from '@userActions/Policy/Policy';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -460,6 +462,11 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
Navigation.navigate(createDynamicRoute(isQuickSettingsFlow ? DYNAMIC_ROUTES.SETTINGS_TAGS_SETTINGS.path : DYNAMIC_ROUTES.WORKSPACE_TAGS_SETTINGS.path));
}, [isQuickSettingsFlow]);

const navigateToCustomTagName = useCallback(() => {
const orderWeight = policyTagLists.at(0)?.orderWeight ?? 0;
Navigation.navigate(createDynamicRoute(isQuickSettingsFlow ? DYNAMIC_ROUTES.SETTINGS_TAGS_EDIT.getRoute(orderWeight) : DYNAMIC_ROUTES.WORKSPACE_EDIT_TAGS.getRoute(orderWeight)));
}, [isQuickSettingsFlow, policyTagLists]);

const navigateToCreateTagPage = () => {
Navigation.navigate(isQuickSettingsFlow ? createDynamicRoute(DYNAMIC_ROUTES.SETTINGS_TAG_CREATE.path) : createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_TAG_CREATE.path));
};
Expand Down Expand Up @@ -497,7 +504,40 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy);
const secondaryActions = useMemo(() => {
const menuItems = [];
if (shouldShowTagsSettings) {
if (isRulesRevampEnabled) {
if (canWriteTags && !isMultiLevelTags) {
menuItems.push({
text: translate('workspace.tags.customTagName'),
description: policyTagLists.at(0)?.name ?? '',
onSelected: navigateToCustomTagName,
shouldShowRightIcon: true,
value: CONST.POLICY.SECONDARY_ACTIONS.SETTINGS,
pendingAction: policyTags?.[policyTagLists.at(0)?.name ?? '']?.pendingAction,
});
}
if (canWriteTags && !!policy?.glCodes) {
menuItems.push({
text: translate('workspace.tags.showTagGLCodes'),
value: CONST.POLICY.SECONDARY_ACTIONS.SETTINGS,
// Selecting the row (click or Enter) toggles it; the Switch is a display-only indicator. Keep the menu open on select.
shouldCloseModalOnSelect: false,
onSelected: () => setPolicyShowTagGLCodes(policyID, !(policy?.showTagGLCodes ?? false), policy?.showTagGLCodes),
// Let the label wrap fully and keep the Switch centered against it on narrow screens.
numberOfLinesTitle: 0,
innerContainerStyle: styles.alignItemsCenter,
titleStyle: [styles.textLabel, styles.fontWeightNormal],
pendingAction: policy?.pendingFields?.showTagGLCodes,
errors: policy?.errorFields?.showTagGLCodes,
onCloseError: () => clearPolicyErrorField(policyID, 'showTagGLCodes'),
switchProps: {
isOn: policy?.showTagGLCodes ?? false,
accessibilityLabel: translate('workspace.tags.showTagGLCodes'),
onToggle: (value: boolean) => setPolicyShowTagGLCodes(policyID, value, policy?.showTagGLCodes),
disabled: !policy?.areTagsEnabled,
},
});
}
} else if (shouldShowTagsSettings) {
menuItems.push({
icon: expensifyIcons.Gear,
text: translate('common.settings'),
Expand All @@ -512,6 +552,8 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
text: translate('spreadsheet.importSpreadsheet'),
onSelected: navigateToImportSpreadsheet,
value: CONST.POLICY.SECONDARY_ACTIONS.IMPORT_SPREADSHEET,
// Group the settings rows apart from the spreadsheet actions under the revamp.
addSeparatorBefore: isRulesRevampEnabled,
});
}

Expand Down Expand Up @@ -570,6 +612,18 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
expensifyIcons,
showConfirmModal,
canWriteTags,
isRulesRevampEnabled,
policyTagLists,
policyTags,
navigateToCustomTagName,
policy?.glCodes,
policy?.showTagGLCodes,
policy?.areTagsEnabled,
policy?.pendingFields?.showTagGLCodes,
policy?.errorFields?.showTagGLCodes,
styles.alignItemsCenter,
styles.textLabel,
styles.fontWeightNormal,
]);

const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine();
Expand Down
Loading