Skip to content
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4069,6 +4069,10 @@ const ROUTES = {
route: 'domain/:domainAccountID/members/move',
getRoute: (domainAccountID: number) => `domain/${domainAccountID}/members/move` as const,
},
DOMAIN_MEMBER_MOVE_TO_GROUP: {
route: 'domain/:domainAccountID/members/:accountID/move',
getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/members/${accountID}/move` as const,
},

MULTIFACTOR_AUTHENTICATION_MAGIC_CODE: `multifactor-authentication/magic-code`,
MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST: 'multifactor-authentication/scenario/biometrics-test',
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,7 @@ const SCREENS = {
MEMBER_FORCE_TWO_FACTOR_AUTH: 'Member_Force_Two_Factor_Auth',
MEMBER_LOCK_ACCOUNT: 'Member_Lock_Account',
MEMBERS_MOVE_TO_GROUP: 'Members_Move_To_Group',
MEMBER_MOVE_TO_GROUP: 'Member_Move_To_Group',
GROUP_DETAILS: 'Domain_Group_Details',
GROUP_EDIT_NAME: 'Domain_Group_Edit_Name',
},
Expand Down
2 changes: 2 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9136,6 +9136,8 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`,
emptyMembers: {title: 'Keine Mitglieder in dieser Gruppe', subtitle: 'Fügen Sie ein Mitglied hinzu oder versuchen Sie, den Filter oben zu ändern.'},
moveToGroup: 'In Gruppe verschieben',
chooseWhereToMove: ({count}: {count: number}) => `Wählen Sie aus, wohin Sie ${count} ${count === 1 ? 'Mitglied' : 'Mitglieder'} verschieben möchten.`,
domainGroup: 'Domain-Gruppe',
chooseWhereToMoveName: ({name}: {name: string}) => `Wähle aus, wohin ${name} verschoben werden soll.`,
},
common: {
settings: 'Einstellungen',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9110,7 +9110,9 @@ const translations = {
other: 'Close accounts',
}),
moveToGroup: 'Move to group',
domainGroup: 'Domain group',
chooseWhereToMove: ({count}: {count: number}) => `Choose where to move ${count} ${count === 1 ? 'member' : 'members'}.`,
chooseWhereToMoveName: ({name}: {name: string}) => `Choose where to move ${name}.`,
error: {
addMember: 'Unable to add this member. Please try again.',
removeMember: 'Unable to remove this user. Please try again.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9352,7 +9352,9 @@ ${amount} para ${merchant} - ${date}`,
reportSuspiciousActivityConfirmationPrompt:
'Revisaremos la cuenta para verificar que sea seguro desbloquearla y nos comunicaremos a través de Concierge si tenemos alguna pregunta.',
moveToGroup: 'Mover al grupo',
domainGroup: 'Grupo de dominio',
chooseWhereToMove: ({count}: {count: number}) => `Elige a dónde mover ${count} ${count === 1 ? 'miembro' : 'miembros'}.`,
chooseWhereToMoveName: ({name}: {name: string}) => `Elige a dónde mover a ${name}.`,
},
common: {
settings: 'Configuración',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9159,6 +9159,8 @@ Voici un *reçu test* pour vous montrer comment ça fonctionne :`,
emptyMembers: {title: 'Aucun membre dans ce groupe', subtitle: 'Ajoutez un membre ou essayez de modifier le filtre ci-dessus.'},
moveToGroup: 'Déplacer vers le groupe',
chooseWhereToMove: ({count}: {count: number}) => `Choisissez où déplacer ${count} ${count === 1 ? 'membre' : 'membres'}.`,
domainGroup: 'Groupe de domaines',
chooseWhereToMoveName: ({name}: {name: string}) => `Choisissez où déplacer ${name}.`,
},
common: {
settings: 'Paramètres',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9126,6 +9126,8 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`,
emptyMembers: {title: 'Nessun membro in questo gruppo', subtitle: 'Aggiungi un membro o prova a cambiare il filtro qui sopra.'},
moveToGroup: 'Sposta nel gruppo',
chooseWhereToMove: ({count}: {count: number}) => `Scegli dove spostare ${count} ${count === 1 ? 'membro' : 'membri'}.`,
domainGroup: 'Gruppo di domini',
chooseWhereToMoveName: ({name}: {name: string}) => `Scegli dove spostare ${name}.`,
},
common: {
settings: 'Impostazioni',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9012,6 +9012,8 @@ ${reportName}
emptyMembers: {title: 'このグループにはメンバーがいません', subtitle: 'メンバーを追加するか、上のフィルターを変更してみてください。'},
moveToGroup: 'グループへ移動',
chooseWhereToMove: ({count}: {count: number}) => `${count} ${count === 1 ? 'メンバー' : 'メンバー'} を移動する先を選択してください。`,
domainGroup: 'ドメイングループ',
chooseWhereToMoveName: ({name}: {name: string}) => `${name} をどこに移動するか選択してください。`,
},
common: {
settings: '設定',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9098,6 +9098,8 @@ Hier is een *proefbon* om je te laten zien hoe het werkt:`,
emptyMembers: {title: 'Geen leden in deze groep', subtitle: 'Voeg een lid toe of probeer het filter hierboven te wijzigen.'},
moveToGroup: 'Verplaatsen naar groep',
chooseWhereToMove: ({count}: {count: number}) => `Kies waar je ${count} ${count === 1 ? 'lid' : 'leden'} naartoe wilt verplaatsen.`,
domainGroup: 'Domeingroep',
chooseWhereToMoveName: ({name}: {name: string}) => `Kies waar je ${name} naartoe wilt verplaatsen.`,
},
common: {
settings: 'Instellingen',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9080,6 +9080,8 @@ Oto *paragon testowy*, żeby pokazać Ci, jak to działa:`,
emptyMembers: {title: 'Brak członków w tej grupie', subtitle: 'Dodaj członka lub spróbuj zmienić filtr powyżej.'},
moveToGroup: 'Przenieś do grupy',
chooseWhereToMove: ({count}: {count: number}) => `Wybierz, dokąd przenieść ${count} ${count === 1 ? 'członka' : 'członków'}.`,
domainGroup: 'Grupa domen',
chooseWhereToMoveName: ({name}: {name: string}) => `Wybierz, dokąd przenieść ${name}.`,
},
common: {
settings: 'Ustawienia',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9088,6 +9088,8 @@ Aqui está um *comprovante de teste* para mostrar como funciona:`,
emptyMembers: {title: 'Nenhum membro neste grupo', subtitle: 'Adicione um membro ou tente mudar o filtro acima.'},
moveToGroup: 'Mover para grupo',
chooseWhereToMove: ({count}: {count: number}) => `Escolha para onde mover ${count} ${count === 1 ? 'membro' : 'membros'}.`,
domainGroup: 'Grupo de domínio',
chooseWhereToMoveName: ({name}: {name: string}) => `Escolha para onde mover ${name}.`,
},
common: {
settings: 'Configurações',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8855,6 +8855,8 @@ ${reportName}
emptyMembers: {title: '此群组中没有成员', subtitle: '添加成员或尝试更改上方的筛选条件。'},
moveToGroup: '移至群组',
chooseWhereToMove: ({count}: {count: number}) => `选择将 ${count} 个 ${count === 1 ? '成员' : '成员'} 移动到哪里。`,
domainGroup: '域名组',
chooseWhereToMoveName: ({name}: {name: string}) => `选择将 ${name} 移动到哪里。`,
},
common: {
settings: '设置',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.DOMAIN.MEMBER_RESET_TWO_FACTOR_AUTH]: () => require<ReactComponentModule>('../../../../pages/domain/Members/TwoFactorAuth/DomainMemberResetTwoFactorAuthPage').default,
[SCREENS.DOMAIN.MEMBER_LOCK_ACCOUNT]: () => require<ReactComponentModule>('../../../../pages/domain/Members/DomainReportSuspiciousActivityPage').default,
[SCREENS.DOMAIN.MEMBERS_MOVE_TO_GROUP]: () => require<ReactComponentModule>('../../../../pages/domain/Members/MoveUsersBetweenGroupsPage').default,
[SCREENS.DOMAIN.MEMBER_MOVE_TO_GROUP]: () => require<ReactComponentModule>('../../../../pages/domain/Members/MoveUserBetweenGroupsPage').default,
[SCREENS.DOMAIN.GROUP_DETAILS]: () => require<ReactComponentModule>('../../../../pages/domain/Groups/DomainGroupDetailsPage').default,
[SCREENS.DOMAIN.GROUP_EDIT_NAME]: () => require<ReactComponentModule>('../../../../pages/domain/Groups/DomainGroupEditNamePage').default,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const DOMAIN_TO_RHP: Partial<Record<keyof DomainSplitNavigatorParamList, string[
SCREENS.DOMAIN.MEMBER_FORCE_TWO_FACTOR_AUTH,
SCREENS.DOMAIN.MEMBER_RESET_TWO_FACTOR_AUTH,
SCREENS.DOMAIN.MEMBER_LOCK_ACCOUNT,
SCREENS.DOMAIN.MEMBER_MOVE_TO_GROUP,
],
[SCREENS.DOMAIN.GROUPS]: [SCREENS.DOMAIN.GROUP_DETAILS, SCREENS.DOMAIN.GROUP_EDIT_NAME],
};
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,9 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
[SCREENS.DOMAIN.MEMBERS_MOVE_TO_GROUP]: {
path: ROUTES.DOMAIN_MEMBERS_MOVE_TO_GROUP.route,
},
[SCREENS.DOMAIN.MEMBER_MOVE_TO_GROUP]: {
path: ROUTES.DOMAIN_MEMBER_MOVE_TO_GROUP.route,
},
[SCREENS.DOMAIN.GROUP_DETAILS]: {
path: ROUTES.DOMAIN_GROUP_DETAILS.route,
},
Expand Down
4 changes: 4 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1607,6 +1607,10 @@ type SettingsNavigatorParamList = {
[SCREENS.DOMAIN.MEMBERS_MOVE_TO_GROUP]: {
domainAccountID: number;
};
[SCREENS.DOMAIN.MEMBER_MOVE_TO_GROUP]: {
domainAccountID: number;
accountID: number;
};
[SCREENS.DOMAIN.GROUP_DETAILS]: {
domainAccountID: number;
groupID: string;
Expand Down
19 changes: 19 additions & 0 deletions src/libs/actions/Domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1813,6 +1813,24 @@ function changeDomainSecurityGroup(
API.write(WRITE_COMMANDS.CHANGE_DOMAIN_SECURITY_GROUP, parameters, {optimisticData, successData, failureData});
}

function clearChangeDomainSecurityGroupError(domainAccountID: number, memberEmail: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`, {
memberErrors: {
[memberEmail]: {
changeDomainSecurityGroupErrors: null,
},
},
});

Onyx.merge(`${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`, {
member: {
[memberEmail]: {
changeDomainSecurityGroup: null,
},
},
});
}

function setDomainMembersSelectedForMove(memberAccountIDs: string[]) {
Onyx.set(ONYXKEYS.DOMAIN_MEMBERS_SELECTED_FOR_MOVE, memberAccountIDs);
}
Expand Down Expand Up @@ -1982,6 +2000,7 @@ export {
resetDomainMemberTwoFactorAuth,
exportMembersToCSV,
changeDomainSecurityGroup,
clearChangeDomainSecurityGroupError,
setDomainMembersSelectedForMove,
clearDomainMembersSelectedForMove,
updateDomainSecurityGroup,
Expand Down
23 changes: 22 additions & 1 deletion src/pages/domain/Members/DomainMemberDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@ import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
import DecisionModal from '@components/DecisionModal';
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import {ModalActions} from '@components/Modal/Global/ModalContext';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import VacationDelegateMenuItem from '@components/VacationDelegateMenuItem';
import useConfirmModal from '@hooks/useConfirmModal';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {clearTwoFactorAuthExemptEmailsErrors, clearValidateDomainTwoFactorCodeError, closeUserAccount, setTwoFactorAuthExemptEmailForDomain} from '@libs/actions/Domain';
import {
clearChangeDomainSecurityGroupError,
clearTwoFactorAuthExemptEmailsErrors,
clearValidateDomainTwoFactorCodeError,
closeUserAccount,
setTwoFactorAuthExemptEmailForDomain,
} from '@libs/actions/Domain';
import {requestUnlockAccount} from '@libs/actions/User';
import {getLatestError} from '@libs/ErrorUtils';
import Navigation from '@navigation/Navigation';
Expand Down Expand Up @@ -135,6 +143,19 @@ function DomainMemberDetailsPage({route}: DomainMemberDetailsPageProps) {
accountID={accountID}
avatarButton={avatarButton}
>
<OfflineWithFeedback
errorRowStyles={styles.mh5}
pendingAction={domainPendingActions?.member?.[memberLogin]?.changeDomainSecurityGroup}
errors={getLatestError(domainErrors?.memberErrors?.[memberLogin]?.changeDomainSecurityGroupErrors)}
onClose={() => clearChangeDomainSecurityGroupError(domainAccountID, memberLogin)}
>
<MenuItemWithTopDescription
description={translate('domain.members.domainGroup')}
title={userSecurityGroup?.securityGroup?.name ?? ''}
onPress={() => Navigation.navigate(ROUTES.DOMAIN_MEMBER_MOVE_TO_GROUP.getRoute(domainAccountID, accountID))}
shouldShowRightIcon
/>
</OfflineWithFeedback>
<VacationDelegateMenuItem
vacationDelegate={vacationDelegate}
onPress={() => Navigation.navigate(ROUTES.DOMAIN_VACATION_DELEGATE.getRoute(domainAccountID, accountID))}
Expand Down
112 changes: 112 additions & 0 deletions src/pages/domain/Members/MoveUserBetweenGroupsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {domainNameSelector, groupsSelector, selectSecurityGroupForAccount} from '@selectors/Domain';
import React, {useState} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
import FixedFooter from '@components/FixedFooter';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
import type {ListItem} from '@components/SelectionList/ListItem/types';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import {changeDomainSecurityGroup} from '@libs/actions/Domain';
import Navigation from '@navigation/Navigation';
import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types';
import type {SettingsNavigatorParamList} from '@navigation/types';
import DomainNotFoundPageWrapper from '@pages/domain/DomainNotFoundPageWrapper';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import {personalDetailsLoginSelector} from '@src/selectors/PersonalDetails';
import type {Domain} from '@src/types/onyx';

type SecurityGroupItem = ListItem & {
value: 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.

docs

};

type MoveUserBetweenGroupsPageProps = PlatformStackScreenProps<SettingsNavigatorParamList, typeof SCREENS.DOMAIN.MEMBER_MOVE_TO_GROUP>;

function MoveUserBetweenGroupsPage({route}: MoveUserBetweenGroupsPageProps) {
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.

This component is pretty much overlapped with MoveUsersBetweenGroupsPage.
@MelvinBot is it possible to share common logic to avoid code duplication?

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.

Yes, there's meaningful overlap. The shared parts are: the SecurityGroupItem type, the securityGroups to list items mapping, handleSelectRow, and the entire JSX skeleton (wrapper, header, subtitle, selection list, footer button).

The differences are in data sourcing and save behavior:

Single-user page: gets accountID from route params, pre-selects current group, single changeDomainSecurityGroup call, navigates back to member details, checks selectedGroupId === currentGroupId and memberLogin for disabled state.

Bulk page: gets selectedMemberAccountIDs from Onyx, no pre-selection, loops changeDomainSecurityGroup for each member, navigates back to members list, only checks selectedGroupId for disabled state.

One approach: extract a shared SelectSecurityGroupPage component that accepts props for the subtitle, onSave callback, isDisabled, onBack, and initiallySelectedGroupId, and renders the common shell. Each page becomes a thin wrapper that supplies its specific data and save logic. This keeps the UI DRY without forcing a premature abstraction over the behavioral differences.

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.

Can we try this? Btw not blocker.

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.

@jakubstec Yeah maybe something for a follow up but I agree this is not a blocker

const {domainAccountID, accountID} = route.params;
const styles = useThemeStyles();
const {translate} = useLocalize();

const [selectedGroupId, setSelectedGroupId] = useState<string | undefined>();
const [domainName] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {selector: domainNameSelector});
const [securityGroups] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {selector: groupsSelector});

const securityGroupSelector = (domain: OnyxEntry<Domain>) => selectSecurityGroupForAccount(accountID)(domain);
const [userSecurityGroup] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {
selector: securityGroupSelector,
});
const [memberLogin] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: personalDetailsLoginSelector(accountID)});

const currentGroupId = userSecurityGroup?.key.replace(CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX, '');

const data: SecurityGroupItem[] = (securityGroups ?? []).map(({id, details}) => ({
text: details.name ?? '',
keyForList: id,
value: id,
isSelected: id === (selectedGroupId ?? currentGroupId),
}));

const handleSelectRow = (item: SecurityGroupItem) => {
setSelectedGroupId(item.value);
};

const handleSave = () => {
if (!selectedGroupId || !domainName || !userSecurityGroup || !memberLogin) {
return;
}

if (selectedGroupId === currentGroupId) {
Navigation.goBack(ROUTES.DOMAIN_MEMBER_DETAILS.getRoute(domainAccountID, accountID));
return;
}

const newSecurityGroupKey: `${typeof CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX}${string}` = `${CONST.DOMAIN.DOMAIN_SECURITY_GROUP_PREFIX}${selectedGroupId}`;
changeDomainSecurityGroup(domainAccountID, domainName, memberLogin, accountID, userSecurityGroup.key, userSecurityGroup.securityGroup, newSecurityGroupKey);
Navigation.goBack(ROUTES.DOMAIN_MEMBER_DETAILS.getRoute(domainAccountID, accountID));
};

return (
<DomainNotFoundPageWrapper domainAccountID={domainAccountID}>
<ScreenWrapper
shouldEnableMaxHeight
testID="MoveUserBetweenGroupsPage"
includeSafeAreaPaddingBottom
>
<HeaderWithBackButton
title={translate('domain.members.moveToGroup')}
onBackButtonPress={() => {
Navigation.goBack(ROUTES.DOMAIN_MEMBER_DETAILS.getRoute(domainAccountID, accountID));
}}
/>
<Text style={[styles.ph5, styles.pb3, styles.textSupporting]}>{translate('domain.members.chooseWhereToMoveName', {name: memberLogin ?? ''})}</Text>

<SelectionList<SecurityGroupItem>
data={data}
onSelectRow={handleSelectRow}
ListItem={SingleSelectListItem}
initiallyFocusedItemKey={currentGroupId}
/>
<FixedFooter>
<Button
success
large
pressOnEnter
text={translate('common.save')}
onPress={handleSave}
isDisabled={!selectedGroupId || selectedGroupId === currentGroupId || !memberLogin}
/>
</FixedFooter>
</ScreenWrapper>
</DomainNotFoundPageWrapper>
);
}

export default MoveUserBetweenGroupsPage;
2 changes: 1 addition & 1 deletion src/selectors/Domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ function selectSecurityGroupForAccount(accountID: number) {

const [key, group] = entry;

if (group.shared && accountIDStr in group.shared) {
if (group.shared?.[accountIDStr] != null) {
return {
key,
securityGroup: group,
Expand Down
Loading
Loading