diff --git a/web/components/button.react.js b/web/components/button.react.js index 2f0ce303d..5690e392b 100644 --- a/web/components/button.react.js +++ b/web/components/button.react.js @@ -1,40 +1,42 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import css from './button.css'; +export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'round'; + type Props = { +onClick: (event: SyntheticEvent) => mixed, +children: React.Node, - +variant?: 'primary' | 'secondary' | 'danger' | 'round', + +variant?: ButtonVariant, +type?: string, +disabled?: boolean, +className?: string, }; function Button(props: Props): React.Node { const { onClick, children, variant = 'primary', type, disabled = false, className = '', } = props; const btnCls = classnames(css.btn, css[variant]); return ( ); } export default Button; diff --git a/web/settings/relationship/add-users-list.css b/web/settings/relationship/add-users-list.css index b158a6e87..a9420e014 100644 --- a/web/settings/relationship/add-users-list.css +++ b/web/settings/relationship/add-users-list.css @@ -1,36 +1,54 @@ .container { - height: 580px; + height: 625px; display: flex; flex-direction: column; } .userTagsContainer { display: flex; flex-wrap: wrap; gap: 6px; + margin: 8px; } .userRowsContainer { overflow: auto; display: flex; flex-direction: column; flex: 1; + margin-bottom: 8px; } .addUserButton { display: flex; flex-direction: row; justify-content: space-between; padding: 16px; color: var(--relationship-modal-color); font-size: var(--l-font-18); line-height: var(--line-height-display); background: transparent; border: none; } .addUserButtonUsername { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.buttons { + display: flex; + justify-content: space-between; +} + +.confirmButtonContainer { + display: flex; + flex-direction: column; + align-items: center; +} + +.hidden { + visibility: hidden; + height: 0; +} diff --git a/web/settings/relationship/add-users-list.react.js b/web/settings/relationship/add-users-list.react.js index 01d6fac38..81a2a9571 100644 --- a/web/settings/relationship/add-users-list.react.js +++ b/web/settings/relationship/add-users-list.react.js @@ -1,166 +1,237 @@ // @flow import * as React from 'react'; +import { + updateRelationships, + updateRelationshipsActionTypes, +} from 'lib/actions/relationship-actions.js'; import { searchUsers } from 'lib/actions/user-actions.js'; +import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { userStoreSearchIndex as userStoreSearchIndexSelector } from 'lib/selectors/user-selectors.js'; -import type { UserRelationshipStatus } from 'lib/types/relationship-types.js'; +import type { + UserRelationshipStatus, + RelationshipAction, +} from 'lib/types/relationship-types.js'; import type { GlobalAccountUserInfo } from 'lib/types/user-types.js'; -import { useServerCall } from 'lib/utils/action-utils.js'; +import { + useDispatchActionPromise, + useServerCall, +} from 'lib/utils/action-utils.js'; +import Button from '../../components/button.react.js'; +import type { ButtonVariant } from '../../components/button.react.js'; import Label from '../../components/label.react.js'; +import LoadingIndicator from '../../loading-indicator.react.js'; import { useSelector } from '../../redux/redux-utils.js'; import AddUsersListItem from './add-users-list-item.react.js'; import css from './add-users-list.css'; +const loadingStatusSelector = createLoadingStatusSelector( + updateRelationshipsActionTypes, +); + type Props = { +searchText: string, +excludedStatuses?: $ReadOnlySet, + +closeModal: () => void, + +confirmButtonContent: React.Node, + +confirmButtonVariant: ButtonVariant, + +relationshipAction: RelationshipAction, }; function AddUsersList(props: Props): React.Node { - const { searchText, excludedStatuses = new Set() } = props; + const { + searchText, + excludedStatuses = new Set(), + closeModal, + confirmButtonContent, + confirmButtonVariant, + relationshipAction, + } = props; const userStoreSearchIndex = useSelector(userStoreSearchIndexSelector); const [userStoreSearchResults, setUserStoreSearchResults] = React.useState< $ReadOnlySet, >(new Set(userStoreSearchIndex.getSearchResults(searchText))); React.useEffect(() => { setUserStoreSearchResults( new Set(userStoreSearchIndex.getSearchResults(searchText)), ); }, [searchText, userStoreSearchIndex]); const [serverSearchResults, setServerSearchResults] = React.useState< $ReadOnlyArray, >([]); const callSearchUsers = useServerCall(searchUsers); React.useEffect(() => { (async () => { if (searchText.length === 0) { setServerSearchResults([]); } else { const { userInfos } = await callSearchUsers(searchText); setServerSearchResults(userInfos); } })(); }, [callSearchUsers, searchText]); const searchTextPresent = searchText.length > 0; const userInfos = useSelector(state => state.userStore.userInfos); const mergedUserInfos = React.useMemo(() => { const mergedInfos = {}; for (const userInfo of serverSearchResults) { mergedInfos[userInfo.id] = userInfo; } const userStoreUserIDs = searchTextPresent ? userStoreSearchResults : Object.keys(userInfos); for (const id of userStoreUserIDs) { const { username, relationshipStatus } = userInfos[id]; if (username) { mergedInfos[id] = { id, username, relationshipStatus }; } } return mergedInfos; }, [ searchTextPresent, serverSearchResults, userInfos, userStoreSearchResults, ]); const sortedUsers = React.useMemo( () => Object.keys(mergedUserInfos) .map(userID => mergedUserInfos[userID]) .filter(user => !excludedStatuses.has(user.relationshipStatus)) .sort((user1, user2) => user1.username.localeCompare(user2.username)), [excludedStatuses, mergedUserInfos], ); const [pendingUsersToAdd, setPendingUsersToAdd] = React.useState< $ReadOnlyArray, >([]); const selectUser = React.useCallback( (userID: string) => { setPendingUsersToAdd(pendingUsers => { const username = mergedUserInfos[userID]?.username; if (!username || pendingUsers.some(user => user.id === userID)) { return pendingUsers; } const newPendingUser = { id: userID, username, }; let targetIndex = 0; while ( targetIndex < pendingUsers.length && newPendingUser.username.localeCompare( pendingUsers[targetIndex].username, ) > 0 ) { targetIndex++; } return [ ...pendingUsers.slice(0, targetIndex), newPendingUser, ...pendingUsers.slice(targetIndex), ]; }); }, [mergedUserInfos], ); const deselectUser = React.useCallback( (userID: string) => setPendingUsersToAdd(pendingUsers => pendingUsers.filter(userInfo => userInfo.id !== userID), ), [], ); const pendingUserIDs = React.useMemo( () => new Set(pendingUsersToAdd.map(userInfo => userInfo.id)), [pendingUsersToAdd], ); const userTags = React.useMemo(() => { if (pendingUsersToAdd.length === 0) { return null; } const tags = pendingUsersToAdd.map(userInfo => ( )); return
{tags}
; }, [deselectUser, pendingUsersToAdd]); const filteredUsers = React.useMemo( () => sortedUsers.filter(userInfo => !pendingUserIDs.has(userInfo.id)), [pendingUserIDs, sortedUsers], ); const userRows = React.useMemo( () => filteredUsers.map(userInfo => ( )), [filteredUsers, selectUser], ); + + const callUpdateRelationships = useServerCall(updateRelationships); + const dispatchActionPromise = useDispatchActionPromise(); + const confirmSelection = React.useCallback(async () => { + await dispatchActionPromise( + updateRelationshipsActionTypes, + callUpdateRelationships({ + action: relationshipAction, + userIDs: Array.from(pendingUserIDs), + }), + ); + closeModal(); + }, [ + callUpdateRelationships, + dispatchActionPromise, + closeModal, + pendingUserIDs, + relationshipAction, + ]); + const loadingStatus = useSelector(loadingStatusSelector); + let buttonContent = confirmButtonContent; + if (loadingStatus === 'loading') { + buttonContent = ( + <> +
{confirmButtonContent}
+ + + ); + } + return (
{userTags}
{userRows}
+
+ + +
); } export default AddUsersList;