diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f2af8e9330ba..43f9a4bc280a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -697,6 +697,9 @@ const ONYXKEYS = { /** Stores the information about the state of issuing a new card */ ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard_', + + /** Used for identifying user as admin of a domain */ + SHARED_NVP_PRIVATE_ADMIN_ACCESS: 'sharedNVP_private_admin_access_', }, /** List of Form ids */ @@ -1081,6 +1084,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED]: OnyxTypes.FundID; [ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST]: OnyxTypes.CardOnWaitlist; [ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard; + [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS]: boolean; }; type OnyxValuesMapping = { diff --git a/src/components/Domain/DomainsListRow.tsx b/src/components/Domain/DomainsListRow.tsx new file mode 100644 index 000000000000..79cecbc180f9 --- /dev/null +++ b/src/components/Domain/DomainsListRow.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import TextWithTooltip from '@components/TextWithTooltip'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type DomainsListRowProps = { + /** Name of the domain */ + title: string; + + /** Whether the row is hovered, so we can modify its style */ + isHovered: boolean; + + /** Whether the icon at the end of the row should be displayed */ + shouldShowRightIcon: boolean; +}; + +function DomainsListRow({title, isHovered, shouldShowRightIcon}: DomainsListRowProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + + return ( + + + + + + + {shouldShowRightIcon && ( + + + + )} + + ); +} + +DomainsListRow.displayName = 'DomainsListRow'; + +export default DomainsListRow; diff --git a/src/languages/de.ts b/src/languages/de.ts index b0c96cfabd9d..12949c72e071 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -673,6 +673,7 @@ const translations = { pinned: 'Angeheftet', read: 'Gelesen', copyToClipboard: 'In die Zwischenablage kopieren', + domains: 'Domänen', }, supportalNoAccess: { title: 'Nicht so schnell', diff --git a/src/languages/en.ts b/src/languages/en.ts index 01c577acb605..947eb5515c94 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -666,6 +666,7 @@ const translations = { pinned: 'Pinned', read: 'Read', copyToClipboard: 'Copy to clipboard', + domains: 'Domains', }, supportalNoAccess: { title: 'Not so fast', diff --git a/src/languages/es.ts b/src/languages/es.ts index 85ad0ee72c50..7df324308ab5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -652,6 +652,7 @@ const translations = { pinned: 'Fijado', read: 'Leído', copyToClipboard: 'Copiar al portapapeles', + domains: 'Dominios', }, supportalNoAccess: { title: 'No tan rápido', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 4d3f16b2544c..efc1aedd9611 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -671,6 +671,7 @@ const translations = { pinned: 'Épinglé', read: 'Lu', copyToClipboard: 'Copier dans le presse-papiers', + domains: 'Domaines', }, supportalNoAccess: { title: 'Pas si vite', diff --git a/src/languages/it.ts b/src/languages/it.ts index e975417f7f45..7ebd0283f29f 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -671,6 +671,7 @@ const translations = { pinned: 'Fissato', read: 'Letto', copyToClipboard: 'Copia negli appunti', + domains: 'Domini', }, supportalNoAccess: { title: 'Non così in fretta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 84b9cfe017a2..f2186339f478 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -671,6 +671,7 @@ const translations = { pinned: '固定済み', read: '既読', copyToClipboard: 'クリップボードにコピー', + domains: 'ドメイン', }, supportalNoAccess: { title: 'ちょっと待ってください', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index fa13aaaa535b..a46cf568be9c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -670,6 +670,7 @@ const translations = { pinned: 'Vastgezet', read: 'Gelezen', copyToClipboard: 'Kopiëren naar klembord', + domains: 'Domeinen', }, supportalNoAccess: { title: 'Niet zo snel', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 67e4b21a96ec..fa977cec4c74 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -671,6 +671,7 @@ const translations = { pinned: 'Przypięte', read: 'Przeczytane', copyToClipboard: 'Skopiuj do schowka', + domains: 'Domeny', }, supportalNoAccess: { title: 'Nie tak szybko', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index cdee8c9cbe63..5219b7edb6bf 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -673,6 +673,7 @@ const translations = { pinned: 'Fixado', read: 'Lido', copyToClipboard: 'Copiar para a área de transferência', + domains: 'Domínios', }, supportalNoAccess: { title: 'Não tão rápido', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 69fb7d33a47b..495e9fa108f5 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -672,6 +672,7 @@ const translations = { pinned: '已固定', read: '已读', copyToClipboard: '复制到剪贴板', + domains: '域名', }, supportalNoAccess: { title: '慢一点', diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index a2f45e492e21..e337409c4ee1 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -1,10 +1,12 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; +import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {FlatList, InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; +import DomainsListRow from '@components/Domain/DomainsListRow'; import EmptyStateComponent from '@components/EmptyStateComponent'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -38,6 +40,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolationOfWorkspace from '@hooks/useTransactionViolationOfWorkspace'; import {isConnectionInProgress} from '@libs/actions/connections'; +import {openOldDotLink} from '@libs/actions/Link'; import {clearWorkspaceOwnerChangeFlow, requestWorkspaceOwnerChange} from '@libs/actions/Policy/Member'; import {calculateBillNewDot, clearDeleteWorkspaceError, clearDuplicateWorkspace, clearErrors, deleteWorkspace, leaveWorkspace, removeWorkspace} from '@libs/actions/Policy/Policy'; import {callFunctionIfActionIsAllowed} from '@libs/actions/Session'; @@ -75,7 +78,7 @@ import type {PolicyDetailsForNonMembers} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import WorkspacesListRow from './WorkspacesListRow'; -type WorkspaceItem = ListItem & +type WorkspaceItem = {listItemType: 'workspace'} & ListItem & Required> & Pick & Pick & @@ -87,9 +90,11 @@ type WorkspaceItem = ListItem & policyID?: string; isJoinRequestPending?: boolean; }; +type DomainItem = {listItemType: 'domain'; title: string; action: () => void; disabled: boolean} & Pick; +type WorkspaceOrDomainListItem = WorkspaceItem | DomainItem | {listItemType: 'domains-header' | 'workspaces-empty-state'}; -// eslint-disable-next-line react/no-unused-prop-types -type GetMenuItem = {item: WorkspaceItem; index: number}; +type GetWorkspaceMenuItem = {item: WorkspaceItem; index: number}; +type GetDomainMenuItem = {item: DomainItem; index: number}; /** * Dismisses the errors on one item @@ -130,6 +135,9 @@ function WorkspacesListPage() { const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); const [reimbursementAccountError] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: true, selector: reimbursementAccountErrorSelector}); + const [allDomains] = useOnyx(ONYXKEYS.COLLECTION.DOMAIN, {canBeMissing: false}); + const [adminAccess] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS, {canBeMissing: false}); + // This hook preloads the screens of adjacent tabs to make changing tabs faster. usePreloadFullScreenNavigators(); @@ -248,8 +256,8 @@ function WorkspacesListPage() { /** * Gets the menu item for each workspace */ - const getMenuItem = useCallback( - ({item, index}: GetMenuItem) => { + const getWorkspaceMenuItem = useCallback( + ({item, index}: GetWorkspaceMenuItem) => { const isAdmin = isPolicyAdmin(item as unknown as PolicyType, session?.email); const isOwner = item.ownerAccountID === session?.accountID; const isDefault = activePolicyID === item.policyID; @@ -378,11 +386,7 @@ function WorkspacesListPage() { translate, policies, fundList, - styles.ph5, - styles.mb2, - styles.mh5, - styles.hoveredComponentBG, - styles.offlineFeedbackDeleted, + styles, loadingSpinnerIconIndex, shouldCalculateBillNewDot, setIsDeletingPaidWorkspace, @@ -395,6 +399,36 @@ function WorkspacesListPage() { ], ); + /** + * Gets the menu item for each domain + */ + const getDomainMenuItem = useCallback( + ({item, index}: GetDomainMenuItem) => ( + + + {({hovered}) => ( + + )} + + + ), + [styles], + ); + const navigateToWorkspace = useCallback( (policyID: string) => { // On the wide layout, we always want to open the Profile page when opening workspace settings from the list @@ -407,6 +441,8 @@ function WorkspacesListPage() { [shouldUseNarrowLayout], ); + const navigateToDomain = useCallback(() => openOldDotLink(CONST.OLDDOT_URLS.ADMIN_DOMAINS_URL), []); + /** * Add free policies (workspaces) to the list of menu items and returns the list of menu items */ @@ -425,6 +461,7 @@ function WorkspacesListPage() { const policyInfo = Object.values(policy.policyDetailsForNonMembers).at(0) as PolicyDetailsForNonMembers; const id = Object.keys(policy.policyDetailsForNonMembers).at(0); return { + listItemType: 'workspace', title: policyInfo.name, icon: policyInfo?.avatar ? policyInfo.avatar : getDefaultWorkspaceAvatar(policy.name), disabled: true, @@ -442,6 +479,7 @@ function WorkspacesListPage() { }; } return { + listItemType: 'workspace', title: policy.name, icon: policy.avatarURL ? policy.avatarURL : getDefaultWorkspaceAvatar(policy.name), action: () => navigateToWorkspace(policy.id), @@ -473,6 +511,28 @@ function WorkspacesListPage() { const sortWorkspace = useCallback((workspaceItems: WorkspaceItem[]) => workspaceItems.sort((a, b) => localeCompare(a.title, b.title)), [localeCompare]); const [inputValue, setInputValue, filteredWorkspaces] = useSearchResults(workspaces, filterWorkspace, sortWorkspace); + const domains = useMemo(() => { + if (!allDomains) { + return []; + } + + return Object.values(allDomains).reduce((domainItems, domain) => { + if (!domain) { + return domainItems; + } + + domainItems.push({ + listItemType: 'domain', + title: Str.extractEmailDomain(domain.email), + action: navigateToDomain, + disabled: !adminAccess?.[`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${domain.accountID}`], + pendingAction: domain.pendingAction, + }); + + return domainItems; + }, []); + }, [navigateToDomain, allDomains, adminAccess]); + useEffect(() => { if (isEmptyObject(duplicateWorkspace) || !filteredWorkspaces.length || !isFocused) { return; @@ -530,14 +590,39 @@ function WorkspacesListPage() { ); - const getHeaderButton = () => ( -