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
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8774,6 +8774,9 @@ const CONST = {
BACKDROP: 'MfaOverlay-Backdrop',
},
DOMAIN: {
ADMINS: {
ROW: 'DomainAdmins-Row',
},
GROUPS: {
CREATE_GROUP_BUTTON: 'DomainGroups-CreateGroupButton',
},
Expand Down
103 changes: 103 additions & 0 deletions src/components/Tables/DomainAdminsTable/DomainAdminsTableRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react';
import {View} from 'react-native';
import Badge from '@components/Badge';
import Icon from '@components/Icon';
import ReportActionAvatars from '@components/ReportActionAvatars';
import Table from '@components/Table';
import TextWithTooltip from '@components/TextWithTooltip';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {DomainAdminRowData} from '.';

type DomainAdminsTableRowProps = {
/** Data about the domain admin */
item: DomainAdminRowData;

/** The index of the row relative to all other rows */
rowIndex: number;

/** Whether to use narrow table row layout */
shouldUseNarrowTableLayout: boolean;
};

export default function DomainAdminsTableRow({item, rowIndex, shouldUseNarrowTableLayout}: DomainAdminsTableRowProps) {
const theme = useTheme();
const styles = useThemeStyles();
const styleUtils = useStyleUtils();
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']);

const avatarSize = shouldUseNarrowTableLayout ? CONST.AVATAR_SIZE.DEFAULT : CONST.AVATAR_SIZE.SMALL;
const primaryContactLabel = item.isPrimaryContact ? translate('domain.admins.primaryContact') : '';
const accessibilityLabel = [item.name, item.email, primaryContactLabel].filter(Boolean).join(', ');

const getSecondaryAvatarContainerStyle = (hovered: boolean) => [
styleUtils.getBackgroundAndBorderStyle(theme.sidebar),
hovered ? styleUtils.getBackgroundAndBorderStyle(styles.sidebarLinkHover?.backgroundColor ?? theme.sidebar) : undefined,
];

return (
<Table.Row
Comment thread
luacmartins marked this conversation as resolved.
interactive
rowIndex={rowIndex}
disabled={item.disabled}
accessibilityLabel={accessibilityLabel}
skeletonReasonAttributes={{context: 'domainAdminsTableRow'}}
sentryLabel={CONST.SENTRY_LABEL.DOMAIN.ADMINS.ROW}
offlineWithFeedback={{
errors: item.errors,
pendingAction: item.pendingAction,
onClose: item.dismissError,
}}
onPress={item.action}
>
{({hovered}) => (
<>
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}>
<ReportActionAvatars
size={avatarSize}
accountIDs={[item.accountID]}
fallbackDisplayName={item.name ?? item.email}

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.

item.name is non-optional, so item.email is never reached, right?

shouldShowTooltip
secondaryAvatarContainerStyle={getSecondaryAvatarContainerStyle(!!hovered)}
/>
<View style={[shouldUseNarrowTableLayout && styles.gap1, styles.flex1]}>
<TextWithTooltip
shouldShowTooltip
text={item.name}
style={[styles.optionDisplayName, styles.pre]}
/>
<TextWithTooltip
shouldShowTooltip
text={item.email}
style={[styles.textLabelSupporting, styles.lh16, styles.pre]}
/>
</View>
</View>

<View style={[styles.flexRow, styles.alignItemsCenter, styles.justifyContentEnd, styles.gap3]}>
{item.isPrimaryContact && (
<Badge
text={translate('domain.admins.primaryContact')}
badgeStyles={styles.ml0}
isCondensed={shouldUseNarrowTableLayout}
/>
)}
<Icon
src={icons.ArrowRight}
fill={theme.icon}
additionalStyles={[styles.alignSelfCenter, (!hovered || item.disabled) && styles.opacitySemiTransparent]}
width={variables.iconSizeNormal}
height={variables.iconSizeNormal}
/>
</View>
</>
)}
</Table.Row>
);
}
87 changes: 87 additions & 0 deletions src/components/Tables/DomainAdminsTable/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type {ListRenderItemInfo} from '@shopify/flash-list';
import React from 'react';
import type {CompareItemsCallback, IsItemInSearchCallback, TableColumn, TableData} from '@components/Table';
import Table from '@components/Table';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import tokenizedSearch from '@libs/tokenizedSearch';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import DomainAdminsTableRow from './DomainAdminsTableRow';

type DomainAdminsTableColumnKey = 'admin' | 'actions';

type DomainAdminRowData = TableData & {
accountID: number;
login: string;

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.

Is this used? I see we use the name and email, but I can't see where it is being referenced.

name: string;
email: string;
isPrimaryContact: boolean;
errors?: OnyxCommon.Errors;
pendingAction?: OnyxCommon.PendingAction;
action: () => void;
dismissError: () => void;
};

type DomainAdminsTableProps = {
admins: DomainAdminRowData[];
};

export default function DomainAdminsTable({admins}: DomainAdminsTableProps) {
const {translate, localeCompare} = useLocalize();
const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();

const shouldUseNarrowTableLayout = shouldUseNarrowLayout || isMediumScreenWidth;

const domainAdminsTableColumns: Array<TableColumn<DomainAdminsTableColumnKey>> = [
{
key: 'admin',
label: translate('domain.admins.title'),
sortable: true,
},
{
key: 'actions',
label: '',
sortable: false,
width: variables.domainAdminsTableActionColumnWidth,
},
];

const compareTableItems: CompareItemsCallback<DomainAdminRowData> = (item1, item2, activeSorting) => {
const orderMultiplier = activeSorting.order === 'asc' ? 1 : -1;
return localeCompare(item1.name, item2.name) * orderMultiplier;
};

const isTableItemInSearch: IsItemInSearchCallback<DomainAdminRowData> = (item, searchValue) => {
const results = tokenizedSearch([item], searchValue, (option) => [option.name, option.email]);
return results.length > 0;
};

const renderTableItem = ({item, index}: ListRenderItemInfo<DomainAdminRowData>) => (
<DomainAdminsTableRow
item={item}
rowIndex={index}
shouldUseNarrowTableLayout={shouldUseNarrowTableLayout}
/>
);

return (
<Table
data={admins}
columns={domainAdminsTableColumns}
renderItem={renderTableItem}
compareItems={compareTableItems}
isItemInSearch={isTableItemInSearch}
initialSortColumn="admin"
title={translate('domain.admins.title')}
keyExtractor={(item) => item.keyForList}
>
{admins.length >= CONST.STANDARD_LIST_ITEM_LIMIT && <Table.SearchBar label={translate('domain.admins.findAdmin')} />}
<Table.Header />
<Table.Body />
</Table>
);
}

export type {DomainAdminRowData, DomainAdminsTableColumnKey};
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default function WorkspaceCategoriesTableRow({rowIndex, shouldUseNarrowTa
errors: item.errors,
pendingAction: item.pendingAction,
shouldHideOnDelete: false,
dismissError: item.dismissError,
onClose: item.dismissError,
}}
>
{({hovered}) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function WorkspaceDistanceRatesTableRow({item, rowIndex, shouldUseNarrowTableLay
accessibilityLabel={accessibilityLabel}
skeletonReasonAttributes={reasonAttributes}
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.DISTANCE_RATES.ROW}
offlineWithFeedback={{errors, pendingAction, dismissError: item.dismissError}}
offlineWithFeedback={{errors, pendingAction, onClose: item.dismissError}}
onPress={item.action}
>
{({hovered}) => (
Expand Down
93 changes: 58 additions & 35 deletions src/pages/domain/Admins/DomainAdminsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import {adminAccountIDsSelector, adminPendingActionSelector, domainNameSelector, technicalContactSettingsSelector} from '@selectors/Domain';
import React from 'react';
import {View} from 'react-native';
import Badge from '@components/Badge';
import Button from '@components/Button';
import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import type {DomainAdminRowData} from '@components/Tables/DomainAdminsTable';
import DomainAdminsTable from '@components/Tables/DomainAdminsTable';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDomainDocumentTitle from '@hooks/useDomainDocumentTitle';
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useShouldDisplayButtonsInSeparateLine from '@hooks/useShouldDisplayButtonsInSeparateLine';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {hasDomainAdminsSettingsErrors} from '@libs/DomainUtils';
import {getLatestError} from '@libs/ErrorUtils';
import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
import Navigation from '@navigation/Navigation';
import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types';
import type {DomainSplitNavigatorParamList} from '@navigation/types';
import BaseDomainMembersPage from '@pages/domain/BaseDomainMembersPage';
import DomainNotFoundPageWrapper from '@pages/domain/DomainNotFoundPageWrapper';
import {clearAdminError} from '@userActions/Domain';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
Expand All @@ -28,9 +34,10 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) {
const {domainAccountID} = route.params;
const [domainName] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {selector: domainNameSelector});
useDomainDocumentTitle(domainName, 'domain.domainAdmins');
const {translate} = useLocalize();
const {translate, formatPhoneNumber} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const illustrations = useMemoizedLazyIllustrations(['UserShield']);
const icons = useMemoizedLazyExpensifyIcons(['Gear', 'Plus', 'DotIndicator']);

Expand All @@ -52,26 +59,34 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) {
const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const isAdmin = adminAccountIDs?.includes(currentUserAccountID);

const getCustomRightElement = (accountID: number) => {
const technicalContactEmail = technicalContactSettings?.technicalContactEmail;
const login = personalDetails?.[accountID]?.login;
if (!technicalContactEmail || !login || technicalContactEmail !== login) {
return null;
}
return <Badge text={translate('domain.admins.primaryContact')} />;
};
const technicalContactEmail = technicalContactSettings?.technicalContactEmail;

const getCustomRowProps = (accountID: number) => ({
errors: domainErrors?.adminErrors?.[accountID]?.errors,
pendingAction: domainPendingAction?.[accountID]?.pendingAction,
});
const admins: DomainAdminRowData[] = (adminAccountIDs ?? [])
.filter((accountID) => {
const details = personalDetails?.[accountID];
return !!details?.login || !!details?.displayName;
})
.map((accountID) => {
const details = personalDetails?.[accountID];
const login = details?.login ?? '';
const errors = domainErrors?.adminErrors?.[accountID]?.errors;
const pendingAction = domainPendingAction?.[accountID]?.pendingAction;
const isPendingActionDelete = pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;

const getCustomListHeader = () => (
<CustomListHeader
canSelectMultiple={false}
leftHeaderText={translate('domain.admins.title')}
/>
);
return {
keyForList: String(accountID),
accountID,
login,
name: formatPhoneNumber(getDisplayNameOrDefault(details)),
email: formatPhoneNumber(login),
isPrimaryContact: !!technicalContactEmail && !!login && technicalContactEmail === login,
errors: getLatestError(errors),
pendingAction,
disabled: isPendingActionDelete || !!details?.isOptimisticPersonalDetail,
action: () => Navigation.navigate(ROUTES.DOMAIN_ADMIN_DETAILS.getRoute(domainAccountID, accountID)),
dismissError: () => clearAdminError(domainAccountID, accountID),
};
});

const hasSettingsErrors = hasDomainAdminsSettingsErrors(domainErrors);
const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine();
Expand Down Expand Up @@ -99,19 +114,27 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) {
) : null;

return (
<BaseDomainMembersPage
domainAccountID={domainAccountID}
accountIDs={adminAccountIDs ?? []}
headerTitle={translate('domain.admins.title')}
searchPlaceholder={translate('domain.admins.findAdmin')}
headerIcon={illustrations.UserShield}
headerContent={headerContent}
getCustomListHeader={getCustomListHeader}
getCustomRightElement={getCustomRightElement}
getCustomRowProps={getCustomRowProps}
onDismissError={(item) => clearAdminError(domainAccountID, item.accountID)}
onSelectRow={(item) => Navigation.navigate(ROUTES.DOMAIN_ADMIN_DETAILS.getRoute(domainAccountID, item.accountID))}
/>
<DomainNotFoundPageWrapper domainAccountID={domainAccountID}>
<ScreenWrapper
enableEdgeToEdgeBottomSafeAreaPadding
shouldEnableMaxHeight
shouldShowOfflineIndicatorInWideScreen
testID="DomainAdminsPage"
>
<HeaderWithBackButton
title={translate('domain.admins.title')}
onBackButtonPress={Navigation.goBack}
icon={illustrations.UserShield}
shouldShowBackButton={shouldUseNarrowLayout}
shouldUseHeadlineHeader
shouldDisplayHelpButton
>
{!shouldDisplayButtonsInSeparateLine && headerContent}
</HeaderWithBackButton>
{shouldDisplayButtonsInSeparateLine && !!headerContent && <View style={[styles.ph5, styles.flexRow, styles.gap2]}>{headerContent}</View>}
<DomainAdminsTable admins={admins} />
</ScreenWrapper>
</DomainNotFoundPageWrapper>
);
}

Expand Down
1 change: 1 addition & 0 deletions src/styles/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export default {
tableSwitchColumnWidth: 58,
tableCaretColumnWidth: 20,
domainTableActionColumnWidth: 64,
domainAdminsTableActionColumnWidth: 140,
workspaceTableActionColumnWidth: 64,
sectionMenuItemHeight: 52,
sectionMenuItemHeightCompact: 44,
Expand Down
Loading