MM-45981_Desktop: Add Server Screen: Improve Onboarding screens (#2243)
This commit is contained in:
361
src/renderer/components/ConfigureServer.tsx
Normal file
361
src/renderer/components/ConfigureServer.tsx
Normal file
@@ -0,0 +1,361 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useCallback, useEffect} from 'react';
|
||||
import {useIntl, FormattedMessage} from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {TeamWithIndex} from 'types/config';
|
||||
|
||||
import womanLaptop from 'renderer/assets/svg/womanLaptop.svg';
|
||||
|
||||
import Header from 'renderer/components/Header';
|
||||
import Input, {STATUS, SIZE} from 'renderer/components/Input';
|
||||
import LoadingBackground from 'renderer/components/LoadingScreen/LoadingBackground';
|
||||
import SaveButton from 'renderer/components/SaveButton/SaveButton';
|
||||
|
||||
import {PING_DOMAIN, PING_DOMAIN_RESPONSE} from 'common/communication';
|
||||
import {MODAL_TRANSITION_TIMEOUT} from 'common/utils/constants';
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import 'renderer/css/components/Button.scss';
|
||||
import 'renderer/css/components/ConfigureServer.scss';
|
||||
import 'renderer/css/components/LoadingScreen.css';
|
||||
|
||||
type ConfigureServerProps = {
|
||||
currentTeams: TeamWithIndex[];
|
||||
team?: TeamWithIndex;
|
||||
mobileView?: boolean;
|
||||
darkMode?: boolean;
|
||||
messageTitle?: string;
|
||||
messageSubtitle?: string;
|
||||
cardTitle?: string;
|
||||
alternateLinkMessage?: string;
|
||||
alternateLinkText?: string;
|
||||
alternateLinkURL?: string;
|
||||
onConnect: (data: TeamWithIndex) => void;
|
||||
};
|
||||
|
||||
function ConfigureServer({
|
||||
currentTeams,
|
||||
team,
|
||||
mobileView,
|
||||
darkMode,
|
||||
messageTitle,
|
||||
messageSubtitle,
|
||||
cardTitle,
|
||||
alternateLinkMessage,
|
||||
alternateLinkText,
|
||||
alternateLinkURL,
|
||||
onConnect,
|
||||
}: ConfigureServerProps) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const {
|
||||
name: prevName,
|
||||
url: prevURL,
|
||||
order = 0,
|
||||
index = NaN,
|
||||
} = team || {};
|
||||
|
||||
const [transition, setTransition] = useState<'inFromRight' | 'outToLeft'>();
|
||||
const [name, setName] = useState(prevName || '');
|
||||
const [url, setUrl] = useState(prevURL || '');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const [urlError, setURLError] = useState('');
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
const [waiting, setWaiting] = useState(false);
|
||||
|
||||
const canSave = name && url && !nameError && !urlError;
|
||||
|
||||
useEffect(() => {
|
||||
setTransition('inFromRight');
|
||||
setShowContent(true);
|
||||
}, []);
|
||||
|
||||
const checkProtocolInURL = (checkURL: string): Promise<string> => {
|
||||
if (urlUtils.startsWithProtocol(checkURL)) {
|
||||
return Promise.resolve(checkURL);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let eventCount = 0;
|
||||
|
||||
const handler = (event: {data: {type: string; data: string | Error}}) => {
|
||||
let newURL = checkURL;
|
||||
|
||||
if (event.data.type === PING_DOMAIN_RESPONSE) {
|
||||
if (event.data.data instanceof Error) {
|
||||
console.error(`Could not ping url: ${checkURL}`);
|
||||
} else {
|
||||
newURL = `${event.data.data}://${checkURL}`;
|
||||
setUrl(newURL);
|
||||
}
|
||||
|
||||
window.removeEventListener('message', handler);
|
||||
resolve(newURL);
|
||||
} else if (eventCount >= 3) {
|
||||
window.removeEventListener('message', handler);
|
||||
resolve(newURL);
|
||||
}
|
||||
|
||||
eventCount++;
|
||||
};
|
||||
|
||||
window.addEventListener('message', handler);
|
||||
window.postMessage({type: PING_DOMAIN, data: checkURL}, window.location.href);
|
||||
});
|
||||
};
|
||||
|
||||
const validateName = () => {
|
||||
const newName = name.trim();
|
||||
|
||||
if (!newName) {
|
||||
return formatMessage({
|
||||
id: 'renderer.components.newTeamModal.error.nameRequired',
|
||||
defaultMessage: 'Name is required.',
|
||||
});
|
||||
}
|
||||
|
||||
if (currentTeams.find(({name: existingName}) => existingName === newName)) {
|
||||
return formatMessage({
|
||||
id: 'renderer.components.newTeamModal.error.serverNameExists',
|
||||
defaultMessage: 'A server with the same name already exists.',
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const validateURL = async (fullURL: string) => {
|
||||
if (!fullURL) {
|
||||
return formatMessage({
|
||||
id: 'renderer.components.newTeamModal.error.urlRequired',
|
||||
defaultMessage: 'URL is required.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!urlUtils.startsWithProtocol(fullURL)) {
|
||||
return formatMessage({
|
||||
id: 'renderer.components.newTeamModal.error.urlNeedsHttp',
|
||||
defaultMessage: 'URL should start with http:// or https://.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!urlUtils.isValidURL(fullURL)) {
|
||||
return formatMessage({
|
||||
id: 'renderer.components.newTeamModal.error.urlIncorrectFormatting',
|
||||
defaultMessage: 'URL is not formatted correctly.',
|
||||
});
|
||||
}
|
||||
|
||||
if (currentTeams.find(({url: existingURL}) => existingURL === fullURL)) {
|
||||
return formatMessage({
|
||||
id: 'renderer.components.newTeamModal.error.serverUrlExists',
|
||||
defaultMessage: 'A server with the same URL already exists.',
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const handleNameOnChange = ({target: {value}}: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(value);
|
||||
|
||||
if (nameError) {
|
||||
setNameError('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleURLOnChange = ({target: {value}}: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUrl(value);
|
||||
|
||||
if (urlError) {
|
||||
setURLError('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnSaveButtonClick = (e: React.MouseEvent) => {
|
||||
submit(e);
|
||||
};
|
||||
|
||||
const handleOnCardEnterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
submit(e);
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async (e: React.MouseEvent | React.KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!canSave || waiting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWaiting(true);
|
||||
|
||||
const nameError = validateName();
|
||||
|
||||
if (nameError) {
|
||||
setTransition(undefined);
|
||||
setNameError(nameError);
|
||||
setWaiting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fullURL = await checkProtocolInURL(url.trim());
|
||||
const urlError = await validateURL(fullURL);
|
||||
|
||||
if (urlError) {
|
||||
setTransition(undefined);
|
||||
setURLError(urlError);
|
||||
setWaiting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setTransition('outToLeft');
|
||||
|
||||
setTimeout(() => {
|
||||
onConnect({
|
||||
url: fullURL,
|
||||
name,
|
||||
index,
|
||||
order,
|
||||
});
|
||||
}, MODAL_TRANSITION_TIMEOUT);
|
||||
};
|
||||
|
||||
const getAlternateLink = useCallback(() => {
|
||||
if (!alternateLinkURL || !alternateLinkMessage || !alternateLinkText) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('alternate-link', transition, {'alternate-link-inverted': darkMode})}>
|
||||
<span className='alternate-link__message'>
|
||||
{alternateLinkMessage}
|
||||
</span>
|
||||
<a
|
||||
className={classNames(
|
||||
'link-button link-small-button alternate-link__link',
|
||||
{'link-button-inverted': darkMode},
|
||||
)}
|
||||
href={alternateLinkURL}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{alternateLinkText}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}, [transition, darkMode, alternateLinkURL, alternateLinkMessage, alternateLinkText]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'LoadingScreen',
|
||||
{'LoadingScreen--darkMode': darkMode},
|
||||
'ConfigureServer',
|
||||
{'ConfigureServer-inverted': darkMode},
|
||||
)}
|
||||
>
|
||||
<LoadingBackground/>
|
||||
<Header
|
||||
darkMode={darkMode}
|
||||
alternateLink={mobileView ? getAlternateLink() : undefined}
|
||||
/>
|
||||
{showContent && (
|
||||
<div className='ConfigureServer__body'>
|
||||
{!mobileView && getAlternateLink()}
|
||||
<div className='ConfigureServer__content'>
|
||||
<div className={classNames('ConfigureServer__message', transition)}>
|
||||
<h1 className='ConfigureServer__message-title'>
|
||||
{messageTitle || formatMessage({id: 'renderer.components.configureServer.title', defaultMessage: 'Let’s connect to a server'})}
|
||||
</h1>
|
||||
<p className='ConfigureServer__message-subtitle'>
|
||||
{messageSubtitle || (
|
||||
<FormattedMessage
|
||||
id='renderer.components.configureServer.subtitle'
|
||||
defaultMessage='Set up your first server to connect to your<br></br>team’s communication hub'
|
||||
values={{
|
||||
br: (x: React.ReactNode) => (<><br/>{x}</>),
|
||||
}}
|
||||
/>)
|
||||
}
|
||||
</p>
|
||||
<div className='ConfigureServer__message-img'>
|
||||
<img
|
||||
src={womanLaptop}
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames('ConfigureServer__card', transition, {'with-error': nameError || urlError})}>
|
||||
<div
|
||||
className='ConfigureServer__card-content'
|
||||
onKeyDown={handleOnCardEnterKeyDown}
|
||||
tabIndex={0}
|
||||
>
|
||||
<p className='ConfigureServer__card-title'>
|
||||
{cardTitle || formatMessage({id: 'renderer.components.configureServer.cardtitle', defaultMessage: 'Enter your server details'})}
|
||||
</p>
|
||||
<div className='ConfigureServer__card-form'>
|
||||
<Input
|
||||
name='name'
|
||||
className='ConfigureServer__card-form-input'
|
||||
type='text'
|
||||
inputSize={SIZE.LARGE}
|
||||
value={name}
|
||||
onChange={handleNameOnChange}
|
||||
customMessage={nameError ? ({
|
||||
type: STATUS.ERROR,
|
||||
value: nameError,
|
||||
}) : ({
|
||||
type: STATUS.INFO,
|
||||
value: formatMessage({id: 'renderer.components.configureServer.name.info', defaultMessage: 'The name that will be displayed in your server list'}),
|
||||
})}
|
||||
placeholder={formatMessage({id: 'renderer.components.configureServer.name.placeholder', defaultMessage: 'Server display name'})}
|
||||
disabled={waiting}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
<Input
|
||||
name='url'
|
||||
className='ConfigureServer__card-form-input'
|
||||
containerClassName='ConfigureServer__card-form-input-container'
|
||||
type='text'
|
||||
inputSize={SIZE.LARGE}
|
||||
value={url}
|
||||
onChange={handleURLOnChange}
|
||||
customMessage={urlError ? ({
|
||||
type: STATUS.ERROR,
|
||||
value: urlError,
|
||||
}) : ({
|
||||
type: STATUS.INFO,
|
||||
value: formatMessage({id: 'renderer.components.configureServer.url.info', defaultMessage: 'The URL of your Mattermost server'}),
|
||||
})}
|
||||
placeholder={formatMessage({id: 'renderer.components.configureServer.url.placeholder', defaultMessage: 'Server URL'})}
|
||||
disabled={waiting}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
<SaveButton
|
||||
id='connectConfigureServer'
|
||||
extraClasses='ConfigureServer__card-form-button'
|
||||
saving={waiting}
|
||||
onClick={handleOnSaveButtonClick}
|
||||
defaultMessage={formatMessage({id: 'renderer.components.configureServer.connect.default', defaultMessage: 'Connect'})}
|
||||
savingMessage={formatMessage({id: 'renderer.components.configureServer.connect.saving', defaultMessage: 'Connecting…'})}
|
||||
disabled={!canSave}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='ConfigureServer__footer'/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfigureServer;
|
197
src/renderer/components/Input.tsx
Normal file
197
src/renderer/components/Input.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import 'renderer/css/components/Input.scss';
|
||||
|
||||
export enum STATUS {
|
||||
NONE = 'none',
|
||||
SUCCESS = 'success',
|
||||
INFO = 'info',
|
||||
WARNING = 'warning',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
export enum SIZE {
|
||||
MEDIUM = 'medium',
|
||||
LARGE = 'large',
|
||||
}
|
||||
|
||||
export type CustomMessageInputType = {
|
||||
type: 'info' | 'error' | 'warning' | 'success';
|
||||
value: string;
|
||||
} | null;
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
required?: boolean;
|
||||
hasError?: boolean;
|
||||
addon?: React.ReactElement;
|
||||
textPrefix?: string;
|
||||
inputPrefix?: JSX.Element;
|
||||
inputSuffix?: JSX.Element;
|
||||
label?: string;
|
||||
containerClassName?: string;
|
||||
wrapperClassName?: string;
|
||||
inputClassName?: string;
|
||||
limit?: number;
|
||||
useLegend?: boolean;
|
||||
customMessage?: CustomMessageInputType;
|
||||
inputSize?: SIZE;
|
||||
darkMode?: boolean;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef((
|
||||
{
|
||||
name,
|
||||
value,
|
||||
label,
|
||||
placeholder,
|
||||
useLegend = true,
|
||||
className,
|
||||
hasError,
|
||||
required,
|
||||
addon,
|
||||
textPrefix,
|
||||
inputPrefix,
|
||||
inputSuffix,
|
||||
containerClassName,
|
||||
wrapperClassName,
|
||||
inputClassName,
|
||||
limit,
|
||||
customMessage,
|
||||
maxLength,
|
||||
inputSize = SIZE.MEDIUM,
|
||||
disabled,
|
||||
darkMode,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onChange,
|
||||
...otherProps
|
||||
}: InputProps,
|
||||
ref?: React.Ref<HTMLInputElement>,
|
||||
) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [customInputLabel, setCustomInputLabel] = useState<CustomMessageInputType>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (customMessage !== undefined && customMessage !== null && customMessage.value !== '') {
|
||||
setCustomInputLabel(customMessage);
|
||||
}
|
||||
}, [customMessage]);
|
||||
|
||||
const handleOnFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
setFocused(true);
|
||||
|
||||
if (onFocus) {
|
||||
onFocus(event);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
setFocused(false);
|
||||
validateInput();
|
||||
|
||||
if (onBlur) {
|
||||
onBlur(event);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCustomInputLabel(null);
|
||||
|
||||
if (onChange) {
|
||||
onChange(event);
|
||||
}
|
||||
};
|
||||
|
||||
const validateInput = () => {
|
||||
if (!required || (value !== null && value !== '')) {
|
||||
return;
|
||||
}
|
||||
const validationErrorMsg = formatMessage({id: 'renderer.components.input.required', defaultMessage: 'This field is required'});
|
||||
setCustomInputLabel({type: STATUS.ERROR, value: validationErrorMsg});
|
||||
};
|
||||
|
||||
const showLegend = Boolean(focused || value);
|
||||
const error = customInputLabel?.type === 'error';
|
||||
const limitExceeded = limit && value && !Array.isArray(value) ? value.toString().length - limit : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'Input_container',
|
||||
containerClassName,
|
||||
{
|
||||
disabled,
|
||||
'Input_container-inverted': darkMode,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<fieldset
|
||||
className={classNames('Input_fieldset', className, {
|
||||
Input_fieldset___error: error || hasError || limitExceeded > 0,
|
||||
Input_fieldset___legend: showLegend,
|
||||
})}
|
||||
>
|
||||
{useLegend && (
|
||||
<legend className={classNames('Input_legend', {Input_legend___focus: showLegend})}>
|
||||
{showLegend ? label || placeholder : null}
|
||||
</legend>
|
||||
)}
|
||||
<div className={classNames('Input_wrapper', wrapperClassName)}>
|
||||
{inputPrefix}
|
||||
{textPrefix && <span>{textPrefix}</span>}
|
||||
<input
|
||||
ref={ref}
|
||||
id={`input_${name || ''}`}
|
||||
className={classNames('Input', inputSize, inputClassName, {Input__focus: showLegend})}
|
||||
value={value}
|
||||
placeholder={focused ? (label && placeholder) || label : label || placeholder}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
{...otherProps}
|
||||
maxLength={limit ? undefined : maxLength}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
{limitExceeded > 0 && (
|
||||
<span className='Input_limit-exceeded'>
|
||||
{'-'}{limitExceeded}
|
||||
</span>
|
||||
)}
|
||||
{inputSuffix}
|
||||
</div>
|
||||
{addon}
|
||||
</fieldset>
|
||||
{customInputLabel && (
|
||||
<div
|
||||
id={`customMessage_${name || ''}`}
|
||||
className={`Input___customMessage Input___${customInputLabel.type}`}
|
||||
>
|
||||
{customInputLabel.type !== STATUS.INFO && (
|
||||
<i
|
||||
className={classNames(`icon ${customInputLabel.type}`, {
|
||||
'icon-alert-outline': customInputLabel.type === STATUS.WARNING,
|
||||
'icon-alert-circle-outline': customInputLabel.type === STATUS.ERROR,
|
||||
'icon-check': customInputLabel.type === STATUS.SUCCESS,
|
||||
|
||||
// No icon wanted for info in desktop. Kept for further reference with Input component in webapp
|
||||
// 'icon-information-outline': customInputLabel.type === STATUS.INFO,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<span>{customInputLabel.value}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default Input;
|
28
src/renderer/components/SaveButton/LoadingSpinner.tsx
Normal file
28
src/renderer/components/SaveButton/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import 'renderer/css/components/LoadingSpinner.scss';
|
||||
|
||||
type Props = {
|
||||
text: React.ReactNode;
|
||||
}
|
||||
|
||||
export default class LoadingSpinner extends React.PureComponent<Props> {
|
||||
public static defaultProps: Props = {
|
||||
text: null,
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<span
|
||||
id='loadingSpinner'
|
||||
className={'LoadingSpinner' + (this.props.text ? ' with-text' : '')}
|
||||
>
|
||||
<span className='spinner'/>
|
||||
{this.props.text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
29
src/renderer/components/SaveButton/LoadingWrapper.tsx
Normal file
29
src/renderer/components/SaveButton/LoadingWrapper.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import LoadingSpinner from './LoadingSpinner';
|
||||
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
text: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default class LoadingWrapper extends React.PureComponent<Props> {
|
||||
public static defaultProps: Props = {
|
||||
loading: true,
|
||||
text: null,
|
||||
children: null,
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {text, loading, children} = this.props;
|
||||
if (!loading) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return <LoadingSpinner text={text}/>;
|
||||
}
|
||||
}
|
74
src/renderer/components/SaveButton/SaveButton.tsx
Normal file
74
src/renderer/components/SaveButton/SaveButton.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import LoadingWrapper from './LoadingWrapper';
|
||||
|
||||
import 'renderer/css/components/Button.scss';
|
||||
|
||||
type Props = {
|
||||
saving: boolean;
|
||||
disabled?: boolean;
|
||||
id?: string;
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
savingMessage?: React.ReactNode;
|
||||
defaultMessage?: React.ReactNode;
|
||||
extraClasses?: string;
|
||||
darkMode?: boolean;
|
||||
}
|
||||
|
||||
const SaveButton = ({
|
||||
id,
|
||||
defaultMessage = (
|
||||
<FormattedMessage
|
||||
id='renderer.components.saveButton.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
),
|
||||
disabled,
|
||||
extraClasses,
|
||||
saving,
|
||||
savingMessage = (
|
||||
<FormattedMessage
|
||||
id='renderer.components.saveButton.saving'
|
||||
defaultMessage='Saving'
|
||||
/>
|
||||
),
|
||||
darkMode,
|
||||
onClick,
|
||||
}: Props) => {
|
||||
const handleOnClick = (e: React.MouseEvent) => {
|
||||
if (saving) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
id={id}
|
||||
className={classNames(
|
||||
'primary-button primary-large-button',
|
||||
{
|
||||
'primary-button-inverted': darkMode,
|
||||
},
|
||||
extraClasses && extraClasses,
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
<LoadingWrapper
|
||||
loading={saving}
|
||||
text={savingMessage}
|
||||
>
|
||||
<span>{defaultMessage}</span>
|
||||
</LoadingWrapper>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveButton;
|
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import React, {useState, useEffect, useMemo} from 'react';
|
||||
import {useIntl, FormattedMessage} from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
@@ -14,6 +14,8 @@ import Carousel from 'renderer/components/Carousel';
|
||||
import Header from 'renderer/components/Header';
|
||||
import LoadingBackground from 'renderer/components/LoadingScreen/LoadingBackground';
|
||||
|
||||
import {MODAL_TRANSITION_TIMEOUT} from 'common/utils/constants';
|
||||
|
||||
import WelcomeScreenSlide from './WelcomeScreenSlide';
|
||||
|
||||
import 'renderer/css/components/Button.scss';
|
||||
@@ -31,6 +33,13 @@ function WelcomeScreen({
|
||||
}: WelcomeScreenProps) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const [transition, setTransition] = useState<'outToLeft'>();
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShowContent(true);
|
||||
}, []);
|
||||
|
||||
const slides = useMemo(() => [
|
||||
{
|
||||
key: 'welcome',
|
||||
@@ -97,7 +106,11 @@ function WelcomeScreen({
|
||||
], []);
|
||||
|
||||
const handleOnGetStartedClick = () => {
|
||||
onGetStarted();
|
||||
setTransition('outToLeft');
|
||||
|
||||
setTimeout(() => {
|
||||
onGetStarted();
|
||||
}, MODAL_TRANSITION_TIMEOUT);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -110,37 +123,39 @@ function WelcomeScreen({
|
||||
>
|
||||
<LoadingBackground/>
|
||||
<Header darkMode={darkMode}/>
|
||||
<div className='WelcomeScreen__body'>
|
||||
<div className='WelcomeScreen__content'>
|
||||
<Carousel
|
||||
slides={slides.map(({key, title, subtitle, image, main}) => ({
|
||||
key,
|
||||
content: (
|
||||
<WelcomeScreenSlide
|
||||
key={key}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
image={image}
|
||||
isMain={main}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
),
|
||||
}))}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
<button
|
||||
id='getStartedWelcomeScreen'
|
||||
className={classNames(
|
||||
'WelcomeScreen__button',
|
||||
'primary-button primary-medium-button',
|
||||
{'primary-button-inverted': darkMode},
|
||||
)}
|
||||
onClick={handleOnGetStartedClick}
|
||||
>
|
||||
{formatMessage({id: 'renderer.components.welcomeScreen.button.getStarted', defaultMessage: 'Get Started'})}
|
||||
</button>
|
||||
{showContent && (
|
||||
<div className={classNames('WelcomeScreen__body', transition)}>
|
||||
<div className='WelcomeScreen__content'>
|
||||
<Carousel
|
||||
slides={slides.map(({key, title, subtitle, image, main}) => ({
|
||||
key,
|
||||
content: (
|
||||
<WelcomeScreenSlide
|
||||
key={key}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
image={image}
|
||||
isMain={main}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
),
|
||||
}))}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
<button
|
||||
id='getStartedWelcomeScreen'
|
||||
className={classNames(
|
||||
'WelcomeScreen__button',
|
||||
'primary-button primary-medium-button',
|
||||
{'primary-button-inverted': darkMode},
|
||||
)}
|
||||
onClick={handleOnGetStartedClick}
|
||||
>
|
||||
{formatMessage({id: 'renderer.components.welcomeScreen.button.getStarted', defaultMessage: 'Get Started'})}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='WelcomeScreen__footer'/>
|
||||
</div>
|
||||
);
|
||||
|
Reference in New Issue
Block a user