summaryrefslogtreecommitdiffstats
path: root/web/react
diff options
context:
space:
mode:
Diffstat (limited to 'web/react')
-rw-r--r--web/react/components/admin_console/admin_controller.jsx4
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx6
-rw-r--r--web/react/components/center_panel.jsx42
-rw-r--r--web/react/components/channel_view.jsx8
-rw-r--r--web/react/components/create_post.jsx62
-rw-r--r--web/react/components/login.jsx2
-rw-r--r--web/react/components/navbar_dropdown.jsx2
-rw-r--r--web/react/components/posts_view.jsx6
-rw-r--r--web/react/components/posts_view_container.jsx9
-rw-r--r--web/react/components/search_autocomplete.jsx5
-rw-r--r--web/react/components/search_bar.jsx15
-rw-r--r--web/react/components/sidebar.jsx75
-rw-r--r--web/react/components/sidebar_header.jsx66
-rw-r--r--web/react/components/signup_team.jsx100
-rw-r--r--web/react/components/team_general_tab.jsx17
-rw-r--r--web/react/components/tutorial/tutorial_intro_screens.jsx152
-rw-r--r--web/react/components/tutorial/tutorial_tip.jsx131
-rw-r--r--web/react/utils/constants.jsx9
18 files changed, 610 insertions, 101 deletions
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index d309ced2e..8e0ab0555 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -6,6 +6,7 @@ var AdminStore = require('../../stores/admin_store.jsx');
var TeamStore = require('../../stores/team_store.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var LoadingScreen = require('../loading_screen.jsx');
+var Utils = require('../../utils/utils.jsx');
var EmailSettingsTab = require('./email_settings.jsx');
var LogSettingsTab = require('./log_settings.jsx');
@@ -46,7 +47,8 @@ export default class AdminController extends React.Component {
};
if (!props.tab) {
- history.replaceState(null, null, `/admin_console/${this.state.selected}`);
+ var tokenIndex = Utils.getUrlParameter('session_token_index');
+ history.replaceState(null, null, `/admin_console/${this.state.selected}?session_token_index=${tokenIndex}`);
}
}
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index f2fb1c96d..0d52ae347 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -3,6 +3,7 @@
var AdminSidebarHeader = require('./admin_sidebar_header.jsx');
var SelectTeamModal = require('./select_team_modal.jsx');
+var Utils = require('../../utils/utils.jsx');
export default class AdminSidebar extends React.Component {
constructor(props) {
@@ -24,12 +25,13 @@ export default class AdminSidebar extends React.Component {
handleClick(name, teamId, e) {
e.preventDefault();
this.props.selectTab(name, teamId);
- history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}`);
+ var tokenIndex = Utils.getUrlParameter('session_token_index');
+ history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}?session_token_index=${tokenIndex}`);
}
isSelected(name, teamId) {
if (this.props.selected === name) {
- if (name === 'team_users') {
+ if (name === 'team_users' || name === 'team_analytics') {
if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
return 'active';
}
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
index b871fe81a..242c2c637 100644
--- a/web/react/components/center_panel.jsx
+++ b/web/react/components/center_panel.jsx
@@ -1,17 +1,47 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var CreatePost = require('../components/create_post.jsx');
-var PostsViewContainer = require('../components/posts_view_container.jsx');
-var ChannelHeader = require('../components/channel_header.jsx');
-var Navbar = require('../components/navbar.jsx');
-var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
+const TutorialIntroScreens = require('./tutorial/tutorial_intro_screens.jsx');
+const CreatePost = require('./create_post.jsx');
+const PostsViewContainer = require('./posts_view_container.jsx');
+const ChannelHeader = require('./channel_header.jsx');
+const Navbar = require('./navbar.jsx');
+const FileUploadOverlay = require('./file_upload_overlay.jsx');
+
+const PreferenceStore = require('../stores/preference_store.jsx');
+const UserStore = require('../stores/user_store.jsx');
+
+const Constants = require('../utils/constants.jsx');
+const TutorialSteps = Constants.TutorialSteps;
+const Preferences = Constants.Preferences;
export default class CenterPanel extends React.Component {
constructor(props) {
super(props);
+
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
+
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ this.state = {showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS};
+ }
+ componentDidMount() {
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
+ }
+ componentWillUnmount() {
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
+ }
+ onPreferenceChange() {
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ this.setState({showTutorialScreens: parseInt(tutorialPref.value, 10) <= TutorialSteps.INTRO_SCREENS});
}
render() {
+ let postsContainer;
+ if (this.state.showTutorialScreens) {
+ postsContainer = <TutorialIntroScreens />;
+ } else {
+ postsContainer = <PostsViewContainer />;
+ }
+
return (
<div className='inner__wrap channel__wrap'>
<div className='row header'>
@@ -32,7 +62,7 @@ export default class CenterPanel extends React.Component {
<ChannelHeader />
</div>
<div id='post-list'>
- <PostsViewContainer />
+ {postsContainer}
</div>
<div
className='post-create__container'
diff --git a/web/react/components/channel_view.jsx b/web/react/components/channel_view.jsx
index beafa7d63..3f53a94c2 100644
--- a/web/react/components/channel_view.jsx
+++ b/web/react/components/channel_view.jsx
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var CenterPanel = require('../components/center_panel.jsx');
-var Sidebar = require('../components/sidebar.jsx');
-var SidebarRight = require('../components/sidebar_right.jsx');
-var SidebarRightMenu = require('../components/sidebar_right_menu.jsx');
+const CenterPanel = require('../components/center_panel.jsx');
+const Sidebar = require('../components/sidebar.jsx');
+const SidebarRight = require('../components/sidebar_right.jsx');
+const SidebarRightMenu = require('../components/sidebar_right_menu.jsx');
export default class ChannelView extends React.Component {
constructor(props) {
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 7c601af4b..1545cdfaa 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -1,21 +1,26 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+const MsgTyping = require('./msg_typing.jsx');
+const Textbox = require('./textbox.jsx');
+const FileUpload = require('./file_upload.jsx');
+const FilePreview = require('./file_preview.jsx');
+const TutorialTip = require('./tutorial/tutorial_tip.jsx');
+
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const Client = require('../utils/client.jsx');
const AsyncClient = require('../utils/async_client.jsx');
+const Utils = require('../utils/utils.jsx');
+
const ChannelStore = require('../stores/channel_store.jsx');
const PostStore = require('../stores/post_store.jsx');
const UserStore = require('../stores/user_store.jsx');
-const SocketStore = require('../stores/socket_store.jsx');
const PreferenceStore = require('../stores/preference_store.jsx');
-const MsgTyping = require('./msg_typing.jsx');
-const Textbox = require('./textbox.jsx');
-const FileUpload = require('./file_upload.jsx');
-const FilePreview = require('./file_preview.jsx');
-const Utils = require('../utils/utils.jsx');
+const SocketStore = require('../stores/socket_store.jsx');
const Constants = require('../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+const TutorialSteps = Constants.TutorialSteps;
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
@@ -36,15 +41,16 @@ export default class CreatePost extends React.Component {
this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleResize = this.handleResize.bind(this);
this.sendMessage = this.sendMessage.bind(this);
- this.onPreferenceChange = this.onPreferenceChange.bind(this);
PostStore.clearDraftUploads();
const draft = this.getCurrentDraft();
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
this.state = {
channelId: ChannelStore.getCurrentId(),
@@ -55,16 +61,12 @@ export default class CreatePost extends React.Component {
initialText: draft.messageText,
windowWidth: Utils.windowWidth(),
windowHeight: Utils.windowHeight(),
- ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value,
+ showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.POST_POPOVER
};
PreferenceStore.addChangeListener(this.onPreferenceChange);
}
- onPreferenceChange() {
- this.setState({
- ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
- });
- }
handleResize() {
this.setState({
windowWidth: Utils.windowWidth(),
@@ -318,11 +320,13 @@ export default class CreatePost extends React.Component {
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
this.resizePostHolder();
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
window.removeEventListener('resize', this.handleResize);
}
onChange() {
@@ -333,6 +337,13 @@ export default class CreatePost extends React.Component {
this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
}
}
+ onPreferenceChange() {
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ this.setState({
+ showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.POST_POPOVER,
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ });
+ }
getFileCount(channelId) {
if (channelId === this.state.channelId) {
return this.state.previews.length + this.state.uploadsInProgress.length;
@@ -367,6 +378,25 @@ export default class CreatePost extends React.Component {
});
}
}
+ createTutorialTip() {
+ const screens = [];
+
+ screens.push(
+ <div>
+ <h4>{'Sending Messages'}</h4>
+ <p>{'Type here to write a message.'}</p>
+ <p>{'Click the attachment button to upload an image or a file.'}</p>
+ </div>
+ );
+
+ return (
+ <TutorialTip
+ placement='top'
+ screens={screens}
+ overlayClass='tip-overlay--chat'
+ />
+ );
+ }
render() {
let serverError = null;
if (this.state.serverError) {
@@ -398,6 +428,11 @@ export default class CreatePost extends React.Component {
postFooterClassName += ' has-error';
}
+ let tutorialTip = null;
+ if (this.state.showTutorialTip) {
+ tutorialTip = this.createTutorialTip();
+ }
+
return (
<form
id='create_post'
@@ -436,6 +471,7 @@ export default class CreatePost extends React.Component {
>
<i className='fa fa-paper-plane' />
</a>
+ {tutorialTip}
</div>
<div className={postFooterClassName}>
{postError}
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index c519959af..2b9ce67ca 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -185,7 +185,7 @@ export default class Login extends React.Component {
if (this.props.inviteId) {
userSignUp = (
<div>
- <span>{'Do not have an account? '}
+ <span>{`Don't have an account? `}
<a
href={'/signup_user_complete/?id=' + this.props.inviteId}
className='signup-team-login'
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index dc21fad21..f43bdffdf 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -104,7 +104,7 @@ export default class NavbarDropdown extends React.Component {
</li>
);
- if (this.props.teamType === 'O') {
+ if (this.props.teamType === Constants.OPEN_TEAM) {
teamLink = (
<li>
<a
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index f5a492b85..2b81d1d79 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -4,6 +4,7 @@
const UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
const Post = require('./post.jsx');
+const Constants = require('../utils/constants.jsx');
export default class PostsView extends React.Component {
constructor(props) {
@@ -69,6 +70,11 @@ export default class PostsView extends React.Component {
const parentPost = posts[post.parent_id];
const prevPost = posts[order[i + 1]];
+ // If the post is a comment whose parent has been deleted, don't add it to the list.
+ if (parentPost && parentPost.state === Constants.POST_DELETED) {
+ continue;
+ }
+
let sameUser = false;
let sameRoot = false;
let hideProfilePic = false;
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
index 9eda2a158..7671ca01d 100644
--- a/web/react/components/posts_view_container.jsx
+++ b/web/react/components/posts_view_container.jsx
@@ -2,15 +2,18 @@
// See License.txt for license information.
const PostsView = require('./posts_view.jsx');
+const LoadingScreen = require('./loading_screen.jsx');
+
const ChannelStore = require('../stores/channel_store.jsx');
const PostStore = require('../stores/post_store.jsx');
-const Constants = require('../utils/constants.jsx');
-const ActionTypes = Constants.ActionTypes;
+
const Utils = require('../utils/utils.jsx');
const Client = require('../utils/client.jsx');
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const AsyncClient = require('../utils/async_client.jsx');
-const LoadingScreen = require('./loading_screen.jsx');
+
+const Constants = require('../utils/constants.jsx');
+const ActionTypes = Constants.ActionTypes;
import {createChannelIntroMessage} from '../utils/channel_intro_mssages.jsx';
diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx
index f7d772677..03e14ec49 100644
--- a/web/react/components/search_autocomplete.jsx
+++ b/web/react/components/search_autocomplete.jsx
@@ -142,7 +142,10 @@ export default class SearchAutocomplete extends React.Component {
let channels = ChannelStore.getAll();
if (filter) {
- channels = channels.filter((channel) => channel.name.startsWith(filter));
+ channels = channels.filter((channel) => channel.name.startsWith(filter) && channel.type !== 'D');
+ } else {
+ // don't show direct channels
+ channels = channels.filter((channel) => channel.type !== 'D');
}
channels.sort((a, b) => a.name.localeCompare(b.name));
diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx
index 1b81a5ee0..90865475b 100644
--- a/web/react/components/search_bar.jsx
+++ b/web/react/components/search_bar.jsx
@@ -90,14 +90,10 @@ export default class SearchBar extends React.Component {
this.refs.autocomplete.handleInputChange(e.target, term);
}
- handleMouseInput(e) {
- e.preventDefault();
- }
handleUserBlur() {
this.setState({focused: false});
}
- handleUserFocus(e) {
- e.target.select();
+ handleUserFocus() {
$('.search-bar__container').addClass('focused');
this.setState({focused: true});
@@ -106,14 +102,8 @@ export default class SearchBar extends React.Component {
if (terms.length) {
this.setState({isSearching: true});
- // append * if not present
- let searchTerms = terms;
- if (searchTerms.search(/\*\s*$/) === -1) {
- searchTerms = searchTerms + '*';
- }
-
client.search(
- searchTerms,
+ terms,
(data) => {
this.setState({isSearching: false});
if (utils.isMobile()) {
@@ -198,7 +188,6 @@ export default class SearchBar extends React.Component {
onBlur={this.handleUserBlur}
onChange={this.handleUserInput}
onKeyDown={this.handleKeyDown}
- onMouseUp={this.handleMouseInput}
/>
{isSearching}
<SearchAutocomplete
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 023955e97..c47919885 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -1,19 +1,26 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const AsyncClient = require('../utils/async_client.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-const Client = require('../utils/client.jsx');
-const Constants = require('../utils/constants.jsx');
-const PreferenceStore = require('../stores/preference_store.jsx');
const NewChannelFlow = require('./new_channel_flow.jsx');
const MoreDirectChannels = require('./more_direct_channels.jsx');
const SearchBox = require('./search_bar.jsx');
const SidebarHeader = require('./sidebar_header.jsx');
-const TeamStore = require('../stores/team_store.jsx');
const UnreadChannelIndicator = require('./unread_channel_indicator.jsx');
+const TutorialTip = require('./tutorial/tutorial_tip.jsx');
+
+const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
+const TeamStore = require('../stores/team_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
+
+const AsyncClient = require('../utils/async_client.jsx');
+const Client = require('../utils/client.jsx');
const Utils = require('../utils/utils.jsx');
+
+const Constants = require('../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+const TutorialSteps = Constants.TutorialSteps;
+
const Tooltip = ReactBootstrap.Tooltip;
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
@@ -155,12 +162,15 @@ export default class Sidebar extends React.Component {
visibleDirectChannels.sort(this.sortChannelsByDisplayName);
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+
return {
activeId: currentId,
channels: ChannelStore.getAll(),
members,
visibleDirectChannels,
- hiddenDirectChannelCount
+ hiddenDirectChannelCount,
+ showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.CHANNEL_POPOVER
};
}
@@ -308,6 +318,51 @@ export default class Sidebar extends React.Component {
this.setState({showDirectChannelsModal: false});
}
+ createTutorialTip() {
+ const screens = [];
+
+ screens.push(
+ <div>
+ <h4>{'Channels'}</h4>
+ <p><strong>{'Channels'}</strong>{' organize conversations across different topics. They’re open to everyone on your team. To send private communications use '}<strong>{'Direct Messages'}</strong>{' for a single person or '}<strong>{'Private Groups'}</strong>{' for multiple people.'}
+ </p>
+ </div>
+ );
+
+ screens.push(
+ <div>
+ <h4>{'"Town Square" and "Off-Topic" channels'}</h4>
+ <p>{'Here are two public channels to start:'}</p>
+ <p>
+ <strong>{'Town Square'}</strong>{' is a place for team-wide communication. Everyone in your team is a member of this channel.'}
+ </p>
+ <p>
+ <strong>{'Off-Topic'}</strong>{' is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.'}
+ </p>
+ </div>
+ );
+
+ screens.push(
+ <div>
+ <h4>{'Creating and Joining Channels'}</h4>
+ <p>
+ {'Click '}<strong>{'"More..."'}</strong>{' to create a new channel or join an existing one.'}
+ </p>
+ <p>
+ {'You can also create a new channel or private group by clicking the '}<strong>{'"+" symbol'}</strong>{' next to the channel or private group header.'}
+ </p>
+ </div>
+ );
+
+ return (
+ <TutorialTip
+ placement='right'
+ screens={screens}
+ overlayClass='tip-overlay--sidebar'
+ />
+ );
+ }
+
createChannelElement(channel, index, arr, handleClose) {
var members = this.state.members;
var activeId = this.state.activeId;
@@ -444,6 +499,11 @@ export default class Sidebar extends React.Component {
rowClass += ' has-close';
}
+ let tutorialTip = null;
+ if (this.state.showTutorialTip && channel.name === Constants.DEFAULT_CHANNEL) {
+ tutorialTip = this.createTutorialTip();
+ }
+
return (
<li
key={channel.name}
@@ -460,6 +520,7 @@ export default class Sidebar extends React.Component {
{badge}
{closeButton}
</a>
+ {tutorialTip}
</li>
);
}
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index 65e4c6d7e..3f777d93c 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -1,9 +1,16 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var NavbarDropdown = require('./navbar_dropdown.jsx');
-var UserStore = require('../stores/user_store.jsx');
+const NavbarDropdown = require('./navbar_dropdown.jsx');
+const TutorialTip = require('./tutorial/tutorial_tip.jsx');
+
+const UserStore = require('../stores/user_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
+
const Utils = require('../utils/utils.jsx');
+const Constants = require('../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+const TutorialSteps = Constants.TutorialSteps;
const Tooltip = ReactBootstrap.Tooltip;
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
@@ -13,8 +20,23 @@ export default class SidebarHeader extends React.Component {
super(props);
this.toggleDropdown = this.toggleDropdown.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
- this.state = {};
+ this.state = this.getStateFromStores();
+ }
+ componentDidMount() {
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
+ }
+ componentWillUnmount() {
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
+ }
+ getStateFromStores() {
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+
+ return {showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.MENU_POPOVER};
+ }
+ onPreferenceChange() {
+ this.setState(this.getStateFromStores());
}
toggleDropdown(e) {
e.preventDefault();
@@ -24,6 +46,38 @@ export default class SidebarHeader extends React.Component {
}
$('.team__header').find('.dropdown-toggle').dropdown('toggle');
}
+ createTutorialTip() {
+ const screens = [];
+
+ screens.push(
+ <div>
+ <h4>{'Main Menu'}</h4>
+ <p>
+ {'The '}<strong>{'Main Menu'}</strong>{' is where you can '}
+ <strong>{'Invite New Members'}</strong>
+ {', access your '}
+ <strong>{'Account Settings'}</strong>
+ {' and set your '}<strong>{'Theme Color'}</strong>{'.'}
+ </p>
+ <p>
+ {'Team administrators can also access their '}<strong>{'Team Settings'}</strong>{' from this menu.'}
+ </p>
+ </div>
+ );
+
+ return (
+ <div
+ onClick={this.toggleDropdown}
+ >
+ <TutorialTip
+ ref='tip'
+ placement='right'
+ screens={screens}
+ overlayClass='tip-overlay--header'
+ />
+ </div>
+ );
+ }
render() {
var me = UserStore.getCurrentUser();
var profilePicture = null;
@@ -41,8 +95,14 @@ export default class SidebarHeader extends React.Component {
);
}
+ let tutorialTip = null;
+ if (this.state.showTutorialTip) {
+ tutorialTip = this.createTutorialTip();
+ }
+
return (
<div className='team__header theme'>
+ {tutorialTip}
<a
href='#'
onClick={this.toggleDropdown}
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx
index f926f5cbb..37760a2a2 100644
--- a/web/react/components/signup_team.jsx
+++ b/web/react/components/signup_team.jsx
@@ -12,11 +12,6 @@ export default class TeamSignUp extends React.Component {
this.updatePage = this.updatePage.bind(this);
- if (global.window.mm_config.EnableTeamListing === 'true') {
- this.state = {page: 'team_listing'};
- return;
- }
-
var count = 0;
if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
@@ -41,50 +36,83 @@ export default class TeamSignUp extends React.Component {
}
render() {
- if (this.state.page === 'team_listing') {
- return (
- <div>
- <h3>{'Choose a Team'}</h3>
- <div className='signup-team-all'>
- {
- this.props.teams.map((team) => {
- return (
- <div
- key={'team_' + team.name}
- className='signup-team-dir'
- >
- <a
- href={'/' + team.name}
+ var teamListing = null;
+
+ if (global.window.mm_config.EnableTeamListing === 'true') {
+ if (this.props.teams.length === 0) {
+ if (global.window.mm_config.EnableTeamCreation !== 'true') {
+ teamListing = (<div>{'There are no teams include in the Team Directory and team creation has been disabled.'}</div>);
+ }
+ } else {
+ teamListing = (
+ <div>
+ <h3>{'Choose a Team'}</h3>
+ <div className='signup-team-all'>
+ {
+ this.props.teams.map((team) => {
+ return (
+ <div
+ key={'team_' + team.name}
+ className='signup-team-dir'
>
- <div className='signup-team-dir__group'>
- <span className='signup-team-dir__name'>{team.display_name}</span>
- <span
- className='glyphicon glyphicon-menu-right right signup-team-dir__arrow'
- aria-hidden='true'
- />
- </div>
- </a>
- </div>
- );
- })
- }
+ <a
+ href={'/' + team.name}
+ >
+ <div className='signup-team-dir__group'>
+ <span className='signup-team-dir__name'>{team.display_name}</span>
+ <span
+ className='glyphicon glyphicon-menu-right right signup-team-dir__arrow'
+ aria-hidden='true'
+ />
+ </div>
+ </a>
+ </div>
+ );
+ })
+ }
+ </div>
</div>
+ );
+ }
+ }
+
+ if (global.window.mm_config.EnableTeamCreation !== 'true') {
+ if (teamListing == null) {
+ return (<div>{'Team creation has been disabled. Please contact an administrator for access.'}</div>);
+ }
+
+ return (
+ <div>
+ {teamListing}
</div>
);
}
if (this.state.page === 'choose') {
return (
- <ChoosePage
- updatePage={this.updatePage}
- />
+ <div>
+ {teamListing}
+ <ChoosePage
+ updatePage={this.updatePage}
+ />
+ </div>
);
}
if (this.state.page === 'email') {
- return <EmailSignUpPage />;
+ return (
+ <div>
+ {teamListing}
+ <EmailSignUpPage />
+ </div>
+ );
} else if (this.state.page === 'gitlab') {
- return <SSOSignupPage service={Constants.GITLAB_SERVICE} />;
+ return (
+ <div>
+ {teamListing}
+ <SSOSignupPage service={Constants.GITLAB_SERVICE} />
+ </div>
+ );
}
}
}
diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx
index 69ba44664..c7c4fa2ea 100644
--- a/web/react/components/team_general_tab.jsx
+++ b/web/react/components/team_general_tab.jsx
@@ -54,7 +54,6 @@ export default class GeneralTab extends React.Component {
handleTeamListingRadio(listing) {
if (global.window.mm_config.EnableTeamListing !== 'true' && listing) {
- ReactDOM.findDOMNode(this.refs.teamListingRadioNo).checked = true;
this.setState({clientError: 'Team directory has been disabled. Please ask a system admin to enable it.'});
} else {
this.setState({allow_team_listing: listing});
@@ -278,13 +277,13 @@ export default class GeneralTab extends React.Component {
</label>
<br/>
</div>
- <div><br/>{'When allowed the team will appear on the main page as part of team directory.'}</div>
+ <div><br/>{'Including this team will display the team name from the Team Directory section of the Home Page, and provide a link to the sign-in page.'}</div>
</div>
];
teamListingSection = (
<SettingItemMax
- title='Allow in Team Directory'
+ title='Include this team in the Team Directory'
inputs={inputs}
submit={this.handleTeamListingSubmit}
server_error={serverError}
@@ -302,7 +301,7 @@ export default class GeneralTab extends React.Component {
teamListingSection = (
<SettingItemMin
- title='Allow in Team Directory'
+ title='Include this team in the Team Directory'
describe={describe}
updateSection={this.onUpdateTeamListingSection}
/>
@@ -337,13 +336,13 @@ export default class GeneralTab extends React.Component {
</label>
<br/>
</div>
- <div><br/>{'When allowed the team signup link will be included on the login page and anyone can signup to this team.'}</div>
+ <div><br/>{'When allowed, a link to account creation will be included on the sign-in page of this team and allow any visitor to sign-up.'}</div>
</div>
];
openInviteSection = (
<SettingItemMax
- title='Allow Open Invitations'
+ title='Allow anyone to sign-up from login page'
inputs={inputs}
submit={this.handleOpenInviteSubmit}
server_error={serverError}
@@ -360,7 +359,7 @@ export default class GeneralTab extends React.Component {
openInviteSection = (
<SettingItemMin
- title='Allow Open Invitations'
+ title='Allow anyone to sign-up from login page'
describe={describe}
updateSection={this.onUpdateOpenInviteSection}
/>
@@ -387,7 +386,7 @@ export default class GeneralTab extends React.Component {
maxLength='32'
/>
</div>
- <div><br/>{'When allowing open invites this code is used as part of the signup process. Changing this code will invalidate the previous open signup link.'}</div>
+ <div><br/>{'Your Invite Code is used in the URL sent to people to join your team. Regenerating your Invite Code will invalidate the URLs in previous invitations, unless "Allow anyone to sign-up from login page" is enabled.'}</div>
<div className='help-text'>
<button
className='btn btn-default'
@@ -413,7 +412,7 @@ export default class GeneralTab extends React.Component {
inviteSection = (
<SettingItemMin
title={`Invite Code`}
- describe={`Click 'Edit' to re-generate invite Code.`}
+ describe={`Click 'Edit' to regenerate Invite Code.`}
updateSection={this.onUpdateInviteIdSection}
/>
);
diff --git a/web/react/components/tutorial/tutorial_intro_screens.jsx b/web/react/components/tutorial/tutorial_intro_screens.jsx
new file mode 100644
index 000000000..c7abccae3
--- /dev/null
+++ b/web/react/components/tutorial/tutorial_intro_screens.jsx
@@ -0,0 +1,152 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const UserStore = require('../../stores/user_store.jsx');
+const ChannelStore = require('../../stores/channel_store.jsx');
+const TeamStore = require('../../stores/team_store.jsx');
+const PreferenceStore = require('../../stores/preference_store.jsx');
+const Utils = require('../../utils/utils.jsx');
+const AsyncClient = require('../../utils/async_client.jsx');
+
+const Constants = require('../../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+
+export default class TutorialIntroScreens extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleNext = this.handleNext.bind(this);
+ this.createScreen = this.createScreen.bind(this);
+
+ this.state = {currentScreen: 0};
+ }
+ handleNext() {
+ if (this.state.currentScreen < 2) {
+ this.setState({currentScreen: this.state.currentScreen + 1});
+ return;
+ }
+
+ Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL));
+
+ let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+
+ const newValue = (parseInt(preference.value, 10) + 1).toString();
+
+ preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue);
+ AsyncClient.savePreferences([preference]);
+ }
+ createScreen() {
+ switch (this.state.currentScreen) {
+ case 0:
+ return this.createScreenOne();
+ case 1:
+ return this.createScreenTwo();
+ case 2:
+ return this.createScreenThree();
+ }
+ }
+ createScreenOne() {
+ return (
+ <div>
+ <h3>{'Welcome to:'}</h3>
+ <h1>{'Mattermost'}</h1>
+ <p>{'Your team communications all in one place, instantly searchable and available anywhere.'}</p>
+ <p>{'Keep your team connected to help them achieve what matters most.'}</p>
+ <div className='tutorial__circles'>
+ <div className='circle active'/>
+ <div className='circle'/>
+ <div className='circle'/>
+ </div>
+ </div>
+ );
+ }
+ createScreenTwo() {
+ return (
+ <div>
+ <h3>{'How Mattermost works:'}</h3>
+ <p>{'Communication happens in public discussion channels, private groups and direct messages.'}</p>
+ <p>{'Everything is archived and searchable from any web-enabled laptop, tablet or phone.'}</p>
+ <div className='tutorial__circles'>
+ <div className='circle'/>
+ <div className='circle active'/>
+ <div className='circle'/>
+ </div>
+ </div>
+ );
+ }
+ createScreenThree() {
+ const team = TeamStore.getCurrent();
+ let inviteModalLink;
+ if (team.type === Constants.INVITE_TEAM) {
+ inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#invite_member'
+ >
+ {'Invite teammates'}
+ </a>
+ );
+ } else {
+ inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#get_link'
+ data-title='Team Invite'
+ data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + team.id}
+ >
+ {'Invite teammates'}
+ </a>
+ );
+ }
+
+ return (
+ <div>
+ <h3>{'You’re all set'}</h3>
+ <p>
+ {inviteModalLink}
+ {' when you’re ready.'}
+ </p>
+ <p>
+ {'Need anything, just email us at '}
+ <a
+ href='mailto:feedback@mattermost.com'
+ target='_blank'
+ >
+ {'feedback@mattermost.com'}
+ </a>
+ {'.'}
+ </p>
+ {'Click “Next” to enter Town Square. This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'}
+ <div className='tutorial__circles'>
+ <div className='circle'/>
+ <div className='circle'/>
+ <div className='circle active'/>
+ </div>
+ </div>
+ );
+ }
+ render() {
+ const screen = this.createScreen();
+
+ return (
+ <div className='tutorial-steps__container'>
+ <div className='tutorial__content'>
+ <div className='tutorial__steps'>
+ {screen}
+ <button
+ className='btn btn-primary'
+ tabIndex='1'
+ onClick={this.handleNext}
+ >
+ {'Next'}
+ </button>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/tutorial/tutorial_tip.jsx b/web/react/components/tutorial/tutorial_tip.jsx
new file mode 100644
index 000000000..c85acb346
--- /dev/null
+++ b/web/react/components/tutorial/tutorial_tip.jsx
@@ -0,0 +1,131 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const UserStore = require('../../stores/user_store.jsx');
+const PreferenceStore = require('../../stores/preference_store.jsx');
+const AsyncClient = require('../../utils/async_client.jsx');
+
+const Constants = require('../../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+
+const Overlay = ReactBootstrap.Overlay;
+
+export default class TutorialTip extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleNext = this.handleNext.bind(this);
+ this.toggle = this.toggle.bind(this);
+
+ this.state = {currentScreen: 0, show: false};
+ }
+ toggle() {
+ const show = !this.state.show;
+ this.setState({show});
+
+ if (!show && this.state.currentScreen >= this.props.screens.length - 1) {
+ let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+
+ const newValue = (parseInt(preference.value, 10) + 1).toString();
+
+ preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue);
+ AsyncClient.savePreferences([preference]);
+ }
+ }
+ handleNext() {
+ if (this.state.currentScreen < this.props.screens.length - 1) {
+ this.setState({currentScreen: this.state.currentScreen + 1});
+ return;
+ }
+
+ this.toggle();
+ }
+ skipTutorial(e) {
+ e.preventDefault();
+ const preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), '999');
+ AsyncClient.savePreferences([preference]);
+ }
+ render() {
+ const buttonText = this.state.currentScreen === this.props.screens.length - 1 ? 'Okay' : 'Next';
+
+ const dots = [];
+ if (this.props.screens.length > 1) {
+ for (let i = 0; i < this.props.screens.length; i++) {
+ if (i === this.state.currentScreen) {
+ dots.push(
+ <div
+ className='circle active'
+ key={'dotactive' + i}
+ />
+ );
+ } else {
+ dots.push(
+ <div
+ className='circle'
+ key={'dotinactive' + i}
+ />
+ );
+ }
+ }
+ }
+
+ return (
+ <div className={'tip-div ' + this.props.overlayClass}>
+ <img
+ className='tip-button'
+ src='/static/images/tutorialTip.gif'
+ width='35'
+ onClick={this.toggle}
+ ref='target'
+ />
+
+ <Overlay
+ show={this.state.show}
+ >
+ <div className='tip-backdrop'/>
+ </Overlay>
+
+ <Overlay
+ placement={this.props.placement}
+ show={this.state.show}
+ rootClose={true}
+ onHide={this.toggle}
+ target={() => this.refs.target}
+ >
+ <div className={'tip-overlay ' + this.props.overlayClass}>
+ <div className='arrow'></div>
+ {this.props.screens[this.state.currentScreen]}
+ <div className='tutorial__circles'>{dots}</div>
+ <div className='text-right'>
+ <button
+ className='btn btn-default'
+ onClick={this.handleNext}
+ >
+ {buttonText}
+ </button>
+ <div className='tip-opt'>
+ {'Seen this before? '}
+ <a
+ href='#'
+ onClick={this.skipTutorial}
+ >
+ {'Opt out of these tips.'}
+ </a>
+ </div>
+ </div>
+ </div>
+ </Overlay>
+ </div>
+ );
+ }
+}
+
+TutorialTip.defaultProps = {
+ overlayClass: ''
+};
+
+TutorialTip.propTypes = {
+ screens: React.PropTypes.array.isRequired,
+ placement: React.PropTypes.string.isRequired,
+ overlayClass: React.PropTypes.string
+};
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 8884d1d10..fd64b1554 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -314,7 +314,14 @@ module.exports = {
Preferences: {
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
CATEGORY_DISPLAY_SETTINGS: 'display_settings',
- CATEGORY_ADVANCED_SETTINGS: 'advanced_settings'
+ CATEGORY_ADVANCED_SETTINGS: 'advanced_settings',
+ TUTORIAL_STEP: 'tutorial_step'
+ },
+ TutorialSteps: {
+ INTRO_SCREENS: 0,
+ POST_POPOVER: 1,
+ CHANNEL_POPOVER: 2,
+ MENU_POPOVER: 3
},
KeyCodes: {
UP: 38,