diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 12608eac2274..1289060f3a8b 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -8776,6 +8776,7 @@ const CONST = { DOMAIN: { GROUPS: { CREATE_GROUP_BUTTON: 'DomainGroups-CreateGroupButton', + ROW: 'DomainGroups-Row', }, }, }, diff --git a/src/components/Tables/DomainGroupsTable/DomainGroupsTableRow.tsx b/src/components/Tables/DomainGroupsTable/DomainGroupsTableRow.tsx new file mode 100644 index 000000000000..120387e75a3e --- /dev/null +++ b/src/components/Tables/DomainGroupsTable/DomainGroupsTableRow.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import Badge from '@components/Badge'; +import Icon from '@components/Icon'; +import type {TableData} from '@components/Table'; +import Table from '@components/Table'; +import Text from '@components/Text'; +import TextWithTooltip from '@components/TextWithTooltip'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; + +type DomainGroupRowData = TableData & { + groupID: string; + name: string; + memberCount: number; + isDefault: boolean; + errors?: OnyxCommon.Errors; + pendingAction?: OnyxCommon.PendingAction; + brickRoadIndicator?: ValueOf; + action: () => void; + dismissError: () => void; +}; + +type DomainGroupsTableRowProps = { + /** Data about the domain group */ + item: DomainGroupRowData; + + /** The index of the row relative to all other rows */ + rowIndex: number; + + /** Whether to use narrow table row layout */ + shouldUseNarrowTableLayout: boolean; +}; + +export default function DomainGroupsTableRow({item, rowIndex, shouldUseNarrowTableLayout}: DomainGroupsTableRowProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'DotIndicator']); + + const memberCountSubtitle = translate('domain.groups.memberCount', {count: item.memberCount}); + const accessibilityLabel = [item.name, memberCountSubtitle, item.isDefault ? translate('common.default') : null].filter(Boolean).join(', '); + + const brickRoadIndicator = !!item.brickRoadIndicator && ( + + ); + + return ( + + {({hovered}) => ( + <> + {shouldUseNarrowTableLayout && ( + + + + + {item.isDefault && ( + + )} + + {memberCountSubtitle} + + + + + {brickRoadIndicator} + + + + )} + + {!shouldUseNarrowTableLayout && ( + <> + + + {item.isDefault && ( + + )} + + + + {memberCountSubtitle} + + + + {brickRoadIndicator} + + + + )} + + )} + + ); +} + +export type {DomainGroupRowData}; diff --git a/src/components/Tables/DomainGroupsTable/index.tsx b/src/components/Tables/DomainGroupsTable/index.tsx new file mode 100644 index 000000000000..0a5fce4a5ee3 --- /dev/null +++ b/src/components/Tables/DomainGroupsTable/index.tsx @@ -0,0 +1,86 @@ +import type {ListRenderItemInfo} from '@shopify/flash-list'; +import React from 'react'; +import type {CompareItemsCallback, IsItemInSearchCallback, TableColumn} 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 {DomainGroupRowData} from './DomainGroupsTableRow'; +import DomainGroupsTableRow from './DomainGroupsTableRow'; + +type DomainGroupsTableColumnKey = 'name' | 'members' | 'actions'; + +type DomainGroupsTableProps = { + groups: DomainGroupRowData[]; +}; + +export default function DomainGroupsTable({groups}: DomainGroupsTableProps) { + const {translate, localeCompare} = useLocalize(); + const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); + + const shouldUseNarrowTableLayout = shouldUseNarrowLayout || isMediumScreenWidth; + + const domainGroupsTableColumns: Array> = [ + { + key: 'name', + label: translate('common.name'), + sortable: true, + }, + { + key: 'members', + label: translate('common.members'), + sortable: true, + width: variables.domainGroupsTableMembersColumnWidth, + }, + { + key: 'actions', + label: '', + sortable: false, + width: variables.domainTableActionColumnWidth, + }, + ]; + + const compareTableItems: CompareItemsCallback = (item1, item2, activeSorting) => { + const orderMultiplier = activeSorting.order === 'asc' ? 1 : -1; + + if (activeSorting.columnKey === 'members') { + return (item1.memberCount - item2.memberCount) * orderMultiplier; + } + + return localeCompare(item1.name, item2.name) * orderMultiplier; + }; + + const isTableItemInSearch: IsItemInSearchCallback = (item, searchValue) => { + const results = tokenizedSearch([item], searchValue, (option) => [option.name]); + return results.length > 0; + }; + + const renderTableItem = ({item, index}: ListRenderItemInfo) => ( + + ); + + return ( + item.keyForList} + > + {groups.length >= CONST.STANDARD_LIST_ITEM_LIMIT && } + + +
+ ); +} + +export type {DomainGroupRowData}; diff --git a/src/pages/domain/Groups/DomainGroupsPage.tsx b/src/pages/domain/Groups/DomainGroupsPage.tsx index 62f9f9b951ad..18e254154189 100644 --- a/src/pages/domain/Groups/DomainGroupsPage.tsx +++ b/src/pages/domain/Groups/DomainGroupsPage.tsx @@ -2,27 +2,21 @@ import type {DomainSecurityGroupWithID} from '@selectors/Domain'; import {defaultSecurityGroupIDSelector, domainNameSelector, groupsSelector, isSecurityGroupPendingDeleteSelector} from '@selectors/Domain'; import React from 'react'; import {View} from 'react-native'; -import Badge from '@components/Badge'; import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import SearchBar from '@components/SearchBar'; -import SelectionList from '@components/SelectionList'; -import TableListItem from '@components/SelectionList/ListItem/TableListItem'; -import type {ListItem} from '@components/SelectionList/ListItem/types'; -import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; -import Text from '@components/Text'; +import type {DomainGroupRowData} from '@components/Tables/DomainGroupsTable'; +import DomainGroupsTable from '@components/Tables/DomainGroupsTable'; import useDomainDocumentTitle from '@hooks/useDomainDocumentTitle'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSearchResults from '@hooks/useSearchResults'; import useShouldDisplayButtonsInSeparateLine from '@hooks/useShouldDisplayButtonsInSeparateLine'; import useThemeStyles from '@hooks/useThemeStyles'; import {hasDomainGroupDetailsErrors} from '@libs/DomainUtils'; import {getLatestError} from '@libs/ErrorUtils'; -import tokenizedSearch from '@libs/tokenizedSearch'; import Navigation from '@navigation/Navigation'; import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; import type {DomainSplitNavigatorParamList} from '@navigation/types'; @@ -35,11 +29,6 @@ import type SCREENS from '@src/SCREENS'; import {isEmptyValueObject} from '@src/types/utils/EmptyObject'; import getEmptyArray from '@src/types/utils/getEmptyArray'; -type GroupOption = Omit & { - /** Group ID */ - groupID: string; -}; - type DomainGroupsPageProps = PlatformStackScreenProps; function DomainGroupsPage({route}: DomainGroupsPageProps) { @@ -51,6 +40,7 @@ function DomainGroupsPage({route}: DomainGroupsPageProps) { const {translate} = useLocalize(); const illustrations = useMemoizedLazyIllustrations(['Members']); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {isOffline} = useNetwork(); const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); const [groups = getEmptyArray()] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {selector: groupsSelector}); @@ -58,74 +48,38 @@ function DomainGroupsPage({route}: DomainGroupsPageProps) { const [pendingActions] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`); const [domainErrors] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`); - const data = groups.map((group) => { - const isDefault = group.id === defaultGroupID; - const groupPendingActions = pendingActions?.[`${CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX}${group.id}`]; - const groupErrorMessage = getLatestError(domainErrors?.[`${CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX}${group.id}`]?.errors); - const isFailedCreate = groupPendingActions?.createGroup === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyValueObject(groupErrorMessage); - return { - keyForList: group.id, - groupID: group.id, - text: group.details.name ?? '', - errors: groupErrorMessage, - brickRoadIndicator: - hasDomainGroupDetailsErrors(domainErrors?.[`${CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX}${group.id}`]) && !isSecurityGroupPendingDeleteSelector(group.id)(pendingActions) - ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR - : undefined, - rightElement: ( - - {translate('domain.groups.memberCount', {count: Object.keys(group.details.shared).length})} - {isDefault && } - - ), - pendingAction: groupPendingActions?.deleteGroup ?? groupPendingActions?.createGroup ?? Object.values(groupPendingActions ?? {}).find(Boolean), - isDisabled: isSecurityGroupPendingDeleteSelector(group.id)(pendingActions) || isFailedCreate, - }; - }); - - const filterGroup = (item: GroupOption, searchQuery: string) => tokenizedSearch([item], searchQuery, (o) => [o.text ?? '']).length > 0; - - const [inputValue, setInputValue, filteredData] = useSearchResults(data, filterGroup); - - const shouldShowSearchBar = data.length >= CONST.STANDARD_LIST_ITEM_LIMIT; - const shouldShowEmptySearchMessage = shouldShowSearchBar && inputValue.length > 0 && filteredData.length === 0; + const groupRows: DomainGroupRowData[] = groups + .filter((group) => isOffline || !isSecurityGroupPendingDeleteSelector(group.id)(pendingActions)) + .map((group) => { + const isDefault = group.id === defaultGroupID; + const groupKey: `${typeof CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX}${string}` = `${CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX}${group.id}`; + const groupErrors = domainErrors?.[groupKey]; + const groupPendingActions = pendingActions?.[groupKey]; + const groupErrorMessage = getLatestError(groupErrors?.errors); + const isFailedCreate = groupPendingActions?.createGroup === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyValueObject(groupErrorMessage); + const isPendingDelete = isSecurityGroupPendingDeleteSelector(group.id)(pendingActions); + const hasDetailsErrors = hasDomainGroupDetailsErrors(groupErrors) && !isPendingDelete; - const listHeaderContent = shouldShowSearchBar ? ( - - - - - - - {shouldShowEmptySearchMessage && ( - - {translate('common.noResultsFoundMatching', inputValue)} - - )} - - ) : null; - - const getCustomListHeader = () => { - if (filteredData.length === 0) { - return null; - } - - return ( - - ); - }; + return { + keyForList: group.id, + groupID: group.id, + name: group.details.name ?? '', + memberCount: Object.keys(group.details.shared).length, + isDefault, + errors: groupErrorMessage, + brickRoadIndicator: hasDetailsErrors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + pendingAction: groupPendingActions?.deleteGroup ?? groupPendingActions?.createGroup ?? Object.values(groupPendingActions ?? {}).find(Boolean), + disabled: isPendingDelete || isFailedCreate, + action: () => Navigation.navigate(ROUTES.DOMAIN_GROUP_DETAILS.getRoute(domainAccountID, group.id)), + dismissError: () => { + if (groupPendingActions?.createGroup) { + clearGroupCreateError(domainAccountID, group.id); + return; + } + clearGroupDeleteError(domainAccountID, group.id); + }, + }; + }); const createGroupHeaderButton = (