diff --git a/web/components/stepper.react.js b/web/components/stepper.react.js index b824e44dd..7b52a21b4 100644 --- a/web/components/stepper.react.js +++ b/web/components/stepper.react.js @@ -1,108 +1,108 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import Button from './button.react.js'; import css from './stepper.css'; import LoadingIndicator from '../loading-indicator.react.js'; export type ButtonProps = { +content: React.Node, +disabled?: boolean, +loading?: boolean, - +onClick: () => void, + +onClick: () => mixed, }; type ButtonType = 'prev' | 'next'; type ActionButtonProps = { +buttonProps: ButtonProps, +type: ButtonType, }; function ActionButton(props: ActionButtonProps) { const { buttonProps, type } = props; const { content, loading, disabled, onClick } = buttonProps; const buttonContent = loading ? ( <>
{content}
) : ( content ); return ( ); } type ItemProps = { +content: React.Node, +name: string, +errorMessage?: string, +prevProps?: ButtonProps, +nextProps?: ButtonProps, }; function StepperItem(props: ItemProps): React.Node { const { content, errorMessage, prevProps, nextProps } = props; const prevButton = React.useMemo( () => prevProps ? : null, [prevProps], ); const nextButton = React.useMemo( () => nextProps ? : null, [nextProps], ); return ( <>
{content}
{errorMessage}
{prevButton} {nextButton}
); } type ContainerProps = { +activeStep: string, +className?: string, +children: React.ChildrenArray>, }; function StepperContainer(props: ContainerProps): React.Node { const { children, activeStep, className = '' } = props; const index = new Map( React.Children.toArray(children).map(child => [child.props.name, child]), ); const activeComponent = index.get(activeStep); const styles = classnames(css.stepperContainer, className); return
{activeComponent}
; } const Stepper = { Container: StepperContainer, Item: StepperItem, }; export default Stepper; diff --git a/web/modals/threads/create/compose-subchannel-modal.react.js b/web/modals/threads/create/compose-subchannel-modal.react.js index 635024369..67156ac35 100644 --- a/web/modals/threads/create/compose-subchannel-modal.react.js +++ b/web/modals/threads/create/compose-subchannel-modal.react.js @@ -1,278 +1,276 @@ // @flow import * as React from 'react'; import { useNewThread, newThreadActionTypes, } from 'lib/actions/thread-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { trimText } from 'lib/utils/text-utils.js'; import css from './compose-subchannel-modal.css'; import SubchannelMembers from './steps/subchannel-members.react.js'; import SubchannelSettings from './steps/subchannel-settings.react.js'; import type { VisibilityType } from './steps/subchannel-settings.react.js'; import Stepper from '../../../components/stepper.react.js'; import { updateNavInfoActionType } from '../../../redux/action-types.js'; import { useSelector } from '../../../redux/redux-utils.js'; import { nonThreadCalendarQuery } from '../../../selectors/nav-selectors.js'; import Modal from '../../modal.react.js'; type Props = { +onClose: () => void, +parentThreadInfo: ThreadInfo, }; const getThreadType = (visibility: VisibilityType, announcement: boolean) => { if (visibility === 'open') { return announcement ? threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD : threadTypes.COMMUNITY_OPEN_SUBTHREAD; } else { return announcement ? threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD : threadTypes.COMMUNITY_SECRET_SUBTHREAD; } }; type Steps = 'settings' | 'members'; type HeaderProps = { +parentThreadName: string, }; function ComposeSubchannelHeader(props: HeaderProps): React.Node { const { parentThreadName } = props; return (
{'within '}
{parentThreadName}
); } const createSubchannelLoadingStatusSelector = createLoadingStatusSelector(newThreadActionTypes); function ComposeSubchannelModal(props: Props): React.Node { const { parentThreadInfo, onClose } = props; const { uiName: parentThreadName } = useResolvedThreadInfo(parentThreadInfo); const [activeStep, setActiveStep] = React.useState('settings'); const [channelName, setChannelName] = React.useState(''); const [visibilityType, setVisibilityType] = React.useState('open'); const [announcement, setAnnouncement] = React.useState(false); const [selectedUsers, setSelectedUsers] = React.useState< $ReadOnlySet, >(new Set()); const [searchUserText, setSearchUserText] = React.useState(''); const loadingState = useSelector(createSubchannelLoadingStatusSelector); const [errorMessage, setErrorMessage] = React.useState(''); const calendarQuery = useSelector(nonThreadCalendarQuery); const callNewThread = useNewThread(); const dispatchActionPromise = useDispatchActionPromise(); const dispatch = useDispatch(); const createSubchannel = React.useCallback(async () => { try { const threadType = getThreadType(visibilityType, announcement); const query = calendarQuery(); const result = await callNewThread({ name: channelName, type: threadType, parentThreadID: parentThreadInfo.id, initialMemberIDs: Array.from(selectedUsers), calendarQuery: query, color: parentThreadInfo.color, }); return result; } catch (e) { await setErrorMessage('unknown error'); return null; } }, [ parentThreadInfo, selectedUsers, visibilityType, announcement, callNewThread, calendarQuery, channelName, ]); const dispatchCreateSubchannel = React.useCallback(async () => { await setErrorMessage(''); const response = createSubchannel(); await dispatchActionPromise(newThreadActionTypes, response); const result = await response; if (result) { const { newThreadID } = result; await dispatch({ type: updateNavInfoActionType, payload: { activeChatThreadID: newThreadID, }, }); props.onClose(); } }, [dispatchActionPromise, createSubchannel, props, dispatch]); const onChangeChannelName = React.useCallback( (event: SyntheticEvent) => { const target = event.currentTarget; setChannelName(target.value); }, [], ); const onOpenVisibilityTypeSelected = React.useCallback( () => setVisibilityType('open'), [], ); const onSecretVisibilityTypeSelected = React.useCallback( () => setVisibilityType('secret'), [], ); const onAnnouncementSelected = React.useCallback( () => setAnnouncement(!announcement), [announcement], ); const toggleUserSelection = React.useCallback((userID: string) => { setSelectedUsers((users: $ReadOnlySet) => { const newUsers = new Set(users); if (newUsers.has(userID)) { newUsers.delete(userID); } else { newUsers.add(userID); } return newUsers; }); }, []); const subchannelSettings = React.useMemo( () => ( ), [ channelName, visibilityType, announcement, onChangeChannelName, onOpenVisibilityTypeSelected, onSecretVisibilityTypeSelected, onAnnouncementSelected, ], ); const stepperButtons = React.useMemo( () => ({ settings: { nextProps: { content: 'Next', disabled: !channelName.trim(), onClick: () => { setErrorMessage(''); setChannelName(channelName.trim()); setActiveStep('members'); }, }, }, members: { prevProps: { content: 'Back', onClick: () => setActiveStep('settings'), }, nextProps: { content: 'Create', loading: loadingState === 'loading', disabled: selectedUsers.size === 0, - onClick: () => { - dispatchCreateSubchannel(); - }, + onClick: dispatchCreateSubchannel, }, }, }), [channelName, dispatchCreateSubchannel, loadingState, selectedUsers], ); const subchannelMembers = React.useMemo( () => ( ), [ selectedUsers, toggleUserSelection, parentThreadInfo, searchUserText, setSearchUserText, ], ); const modalName = activeStep === 'members' ? `Create channel - ${trimText(channelName, 11)}` : 'Create channel'; return (
); } export default ComposeSubchannelModal;