diff options
-rw-r--r-- | api/file.go | 2 | ||||
-rw-r--r-- | web/react/components/admin_console/team_analytics.jsx | 184 | ||||
-rw-r--r-- | web/react/components/edit_post_modal.jsx | 21 | ||||
-rw-r--r-- | web/react/components/popover_list_members.jsx | 32 | ||||
-rw-r--r-- | web/react/components/post_list.jsx | 47 | ||||
-rw-r--r-- | web/react/utils/constants.jsx | 2 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_admin-console.scss | 59 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_headers.scss | 2 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_popover.scss | 40 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_post.scss | 8 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_statistics.scss | 84 | ||||
-rw-r--r-- | web/sass-files/sass/styles.scss | 1 |
12 files changed, 305 insertions, 177 deletions
diff --git a/api/file.go b/api/file.go index f65be145d..8afc70692 100644 --- a/api/file.go +++ b/api/file.go @@ -137,7 +137,7 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = model.NewAppError("uploadFile", "Unable to upload image file.", err.Error()) return } else if config.Width*config.Height > MaxImageSize { - c.Err = model.NewAppError("uploadFile", "Unable to upload image file. File is too large.", err.Error()) + c.Err = model.NewAppError("uploadFile", "Unable to upload image file. File is too large.", "File exceeds max image size.") return } } diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx index a945a551c..0c9d1f61b 100644 --- a/web/react/components/admin_console/team_analytics.jsx +++ b/web/react/components/admin_console/team_analytics.jsx @@ -210,69 +210,89 @@ export default class TeamAnalytics extends React.Component { } var totalCount = ( - <div className='total-count text-center'> - <div>{'Total Users'}</div> - <div>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div> + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'>{'Total Users'}<i className='fa fa-users'/></div> + <div className='content'>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div> + </div> </div> ); var openChannelCount = ( - <div className='total-count text-center'> - <div>{'Public Groups'}</div> - <div>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div> + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'>{'Public Groups'}<i className='fa fa-unlock-alt'/></div> + <div className='content'>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div> + </div> </div> ); var openPrivateCount = ( - <div className='total-count text-center'> - <div>{'Private Groups'}</div> - <div>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div> + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div> + <div className='content'>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div> + </div> </div> ); var postCount = ( - <div className='total-count text-center'> - <div>{'Total Posts'}</div> - <div>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div> + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div> + <div className='content'>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div> + </div> </div> ); var postCountsByDay = ( - <div className='total-count-by-day'> - <div>{'Total Posts'}</div> - <div>{'Loading...'}</div> + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'>{'Total Posts'}</div> + <div className='content'>{'Loading...'}</div> + </div> </div> ); if (this.state.post_counts_day != null) { postCountsByDay = ( - <div className='total-count-by-day'> - <div>{'Total Posts'}</div> - <LineChart - data={this.state.post_counts_day} - width='740' - height='225' - /> + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'>{'Total Posts'}</div> + <div className='content'> + <LineChart + data={this.state.post_counts_day} + width='740' + height='225' + /> + </div> + </div> </div> ); } var usersWithPostsByDay = ( - <div className='total-count-by-day'> - <div>{'Total Posts'}</div> - <div>{'Loading...'}</div> + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'>{'Total Posts'}</div> + <div>{'Loading...'}</div> + </div> </div> ); if (this.state.user_counts_with_posts_day != null) { usersWithPostsByDay = ( - <div className='total-count-by-day'> - <div>{'Active Users With Posts'}</div> - <LineChart - data={this.state.user_counts_with_posts_day} - width='740' - height='225' - /> + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'>{'Active Users With Posts'}</div> + <div className='content'> + <LineChart + data={this.state.user_counts_with_posts_day} + width='740' + height='225' + /> + </div> + </div> </div> ); } @@ -286,22 +306,26 @@ export default class TeamAnalytics extends React.Component { if (this.state.recent_active_users != null) { recentActiveUser = ( - <div className='recent-active-users'> - <div>{'Recent Active Users'}</div> - <table width='90%'> - <tbody> - { - this.state.recent_active_users.map((user) => { - return ( - <tr key={user.id}> - <td className='recent-active-users-td'>{user.email}</td> - <td className='recent-active-users-td'>{Utils.displayDateTime(user.last_activity_at)}</td> - </tr> - ); - }) - } - </tbody> - </table> + <div className='col-sm-6'> + <div className='total-count recent-active-users'> + <div className='title'>{'Recent Active Users'}</div> + <div className='content'> + <table> + <tbody> + { + this.state.recent_active_users.map((user) => { + return ( + <tr key={user.id}> + <td>{user.email}</td> + <td>{Utils.displayDateTime(user.last_activity_at)}</td> + </tr> + ); + }) + } + </tbody> + </table> + </div> + </div> </div> ); } @@ -315,38 +339,50 @@ export default class TeamAnalytics extends React.Component { if (this.state.newly_created_users != null) { newUsers = ( - <div className='recent-active-users'> - <div>{'Newly Created Users'}</div> - <table width='90%'> - <tbody> - { - this.state.newly_created_users.map((user) => { - return ( - <tr key={user.id}> - <td className='recent-active-users-td'>{user.email}</td> - <td className='recent-active-users-td'>{Utils.displayDateTime(user.create_at)}</td> - </tr> - ); - }) - } - </tbody> - </table> + <div className='col-sm-6'> + <div className='total-count recent-active-users'> + <div className='title'>{'Newly Created Users'}</div> + <div className='content'> + <table> + <tbody> + { + this.state.newly_created_users.map((user) => { + return ( + <tr key={user.id}> + <td>{user.email}</td> + <td>{Utils.displayDateTime(user.create_at)}</td> + </tr> + ); + }) + } + </tbody> + </table> + </div> + </div> </div> ); } return ( - <div className='wrapper--fixed'> - <h2>{'Statistics for ' + this.props.team.name}</h2> + <div className='wrapper--fixed team_statistics'> + <h3>{'Statistics for ' + this.props.team.name}</h3> {serverError} - {totalCount} - {postCount} - {openChannelCount} - {openPrivateCount} - {postCountsByDay} - {usersWithPostsByDay} - {recentActiveUser} - {newUsers} + <div className='row'> + {totalCount} + {postCount} + {openChannelCount} + {openPrivateCount} + </div> + <div className='row'> + {postCountsByDay} + </div> + <div className='row'> + {usersWithPostsByDay} + </div> + <div className='row'> + {recentActiveUser} + {newUsers} + </div> </div> ); } diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index e5bede026..2abb3f151 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -6,6 +6,10 @@ var AsyncClient = require('../utils/async_client.jsx'); var Textbox = require('./textbox.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); var PostStore = require('../stores/post_store.jsx'); +var PreferenceStore = require('../stores/preference_store.jsx'); + +var Constants = require('../utils/constants.jsx'); +var KeyCodes = Constants.KeyCodes; export default class EditPostModal extends React.Component { constructor() { @@ -16,6 +20,8 @@ export default class EditPostModal extends React.Component { this.handleEditKeyPress = this.handleEditKeyPress.bind(this); this.handleUserInput = this.handleUserInput.bind(this); this.handleEditPostEvent = this.handleEditPostEvent.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.onPreferenceChange = this.onPreferenceChange.bind(this); this.state = {editText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''}; } @@ -51,7 +57,7 @@ export default class EditPostModal extends React.Component { this.setState({editText: editMessage}); } handleEditKeyPress(e) { - if (e.which === 13 && !e.shiftKey && !e.altKey) { + if (this.state.ctrlSend === 'false' && e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) { e.preventDefault(); ReactDOM.findDOMNode(this.refs.editbox).blur(); this.handleEdit(e); @@ -72,6 +78,16 @@ export default class EditPostModal extends React.Component { $(ReactDOM.findDOMNode(this.refs.modal)).modal('show'); } + handleKeyDown(e) { + if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) { + this.handleEdit(e); + } + } + onPreferenceChange() { + this.setState({ + ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value + }); + } componentDidMount() { var self = this; @@ -101,9 +117,11 @@ export default class EditPostModal extends React.Component { }); PostStore.addEditPostListener(this.handleEditPostEvent); + PreferenceStore.addChangeListener(this.onPreferenceChange); } componentWillUnmount() { PostStore.removeEditPostListener(this.handleEditPostEvent); + PreferenceStore.removeChangeListener(this.onPreferenceChange); } render() { var error = (<div className='form-group'><br /></div>); @@ -138,6 +156,7 @@ export default class EditPostModal extends React.Component { <Textbox onUserInput={this.handleEditInput} onKeyPress={this.handleEditKeyPress} + onKeyDown={this.handleKeyDown} messageText={this.state.editText} createMessage='Edit the post...' id='edit_textbox' diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index 9cffa2400..f3c0fa0b4 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -42,6 +42,10 @@ export default class PopoverListMembers extends React.Component { }; } + componentDidUpdate() { + $(ReactDOM.findDOMNode(this.refs.memebersPopover)).find('.popover-content').perfectScrollbar(); + } + handleShowDirectChannel(teammate, e) { e.preventDefault(); @@ -106,27 +110,27 @@ export default class PopoverListMembers extends React.Component { let button = ''; if (currentUserId !== m.id && ch.type !== 'D') { button = ( - <button - type='button' - className='btn btn-primary btn-message' + <a + href='#' + className='btn-message' onClick={(e) => this.handleShowDirectChannel(m, e)} > {'Message'} - </button> + </a> ); } if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) { popoverHtml.push( <div - className='text--nowrap' + className='text-nowrap' key={'popover-member-' + i} > <img - className='profile-img pull-left' - width='38' - height='38' + className='profile-img rounded pull-left' + width='26px' + height='26px' src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`} /> <div className='pull-left'> @@ -135,14 +139,9 @@ export default class PopoverListMembers extends React.Component { > {m.username} </div> - <div - className='more-description' - > - {details} - </div> </div> <div - className='pull-right profile-action' + className='pull-right' > {button} </div> @@ -182,12 +181,11 @@ export default class PopoverListMembers extends React.Component { placement='bottom' > <Popover + ref='memebersPopover' title='Members' id='member-list-popover' > - <div> - {popoverHtml} - </div> + {popoverHtml} </Popover> </Overlay> </div> diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 0d69e56bf..b9741bac4 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -9,6 +9,7 @@ const LoadingScreen = require('./loading_screen.jsx'); const PostStore = require('../stores/post_store.jsx'); const ChannelStore = require('../stores/channel_store.jsx'); const UserStore = require('../stores/user_store.jsx'); +const TeamStore = require('../stores/team_store.jsx'); const SocketStore = require('../stores/socket_store.jsx'); const PreferenceStore = require('../stores/preference_store.jsx'); @@ -386,17 +387,55 @@ export default class PostList extends React.Component { } } createDefaultIntroMessage(channel) { + 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' + > + <i className='fa fa-user-plus'></i>{'Invite others to this team'} + </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} + > + <i className='fa fa-user-plus'></i>{'Invite others to this team'} + </a> + ); + } + return ( <div className='channel-intro'> <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4> <p className='channel-intro__content'> - {'Welcome to ' + channel.display_name + '!'} + <strong>{'Welcome to ' + channel.display_name + '!'}</strong> <br/><br/> {'This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'} - <br/><br/> - {'To create a new channel or join an existing one, go to the Left Sidebar under “Channels” and click “More…”.'} - <br/> </p> + {inviteModalLink} + <a + className='intro-links' + href='#' + data-toggle='modal' + data-target='#edit_channel' + data-header={channel.header} + data-title={channel.display_name} + data-channelid={channel.id} + > + <i className='fa fa-pencil'></i>{'Set a header'} + </a> + <br/> </div> ); } diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 69e3b007d..43d81d322 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -127,6 +127,8 @@ module.exports = { MAX_DMS: 20, DM_CHANNEL: 'D', OPEN_CHANNEL: 'O', + INVITE_TEAM: 'I', + OPEN_TEAM: 'O', MAX_POST_LEN: 4000, EMOJI_SIZE: 16, ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>", diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss index 9c65e2d1e..206d5bfca 100644 --- a/web/sass-files/sass/partials/_admin-console.scss +++ b/web/sass-files/sass/partials/_admin-console.scss @@ -2,63 +2,16 @@ > div { height: 100%; } - .table { - background: #fff; - } - - .total-count { - width: 175px; - height: 100px; - border: 1px solid #ddd; - padding: 22px 10px 10px 10px; - margin: 10px 10px 10px 10px; - background: #fff; - float: left; - - > div { - font-size: 18px; - font-weight: 300; - } - } - - .total-count-by-day { - width: 760px; - height: 275px; - border: 1px solid #ddd; - padding: 5px 10px 10px 10px; - margin: 10px 10px 10px 10px; - background: #fff; - clear: both; - > div { - font-size: 18px; - font-weight: 300; - } + h3 { + font-weight: 600; + border-bottom: 1px solid #ddd; + padding-bottom: 0.5em; + margin: 1em 0; } - .recent-active-users { - width: 365px; - border: 1px solid #ddd; - padding: 5px 10px 10px 10px; - margin: 10px 10px 10px 10px; + .table { background: #fff; - float: left; - - > div { - font-size: 18px; - font-weight: 300; - } - - > table { - margin: 10px 10px 10px 10px; - } - - .recent-active-users-td { - font-size: 14px; - font-weight: 300; - border: 1px solid #ddd; - padding: 3px 3px 3px 5px; - } } .sidebar--left { diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index 8bf163f90..7e776bf76 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -215,7 +215,7 @@ color: #999; cursor: pointer; .fa { - margin-left: 3px; + margin-left: 4px; font-size: 16px; } } diff --git a/web/sass-files/sass/partials/_popover.scss b/web/sass-files/sass/partials/_popover.scss index 4f5f1d215..430813e63 100644 --- a/web/sass-files/sass/partials/_popover.scss +++ b/web/sass-files/sass/partials/_popover.scss @@ -41,6 +41,9 @@ padding: 6px 8px; margin: 3px 0; @include border-radius(2px); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; &:hover { background: rgba(black, 0.1); @@ -94,33 +97,20 @@ #member-list-popover { max-width: initial; - .popover-content > div { + .popover-content { + position: relative; + padding: 0; + width: 260px; max-height: 350px; - overflow-y: auto; - overflow-x: hidden; - > div { - border-bottom: 1px solid rgba(51,51,51,0.1); - padding: 8px 8px 8px 15px; + .text-nowrap { + padding: 6px 10px; width: 100%; - box-sizing: content-box; - @include clearfix; - .profile-img { - border-radius: 50px; - margin-right: 8px; - } - .more-name { - font-weight: 600; - font-size: 0.95em; - overflow: hidden; - text-overflow: ellipsis; - } - .more-description { - @include opacity(0.7); - } - .profile-action { - margin-left: 8px; - margin-right: 18px; - } + overflow: hidden; + line-height: 26px; + font-size: 13px; + } + .more-name { + margin-left: 6px; } } } diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 414ab0554..11816efd9 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -398,11 +398,17 @@ body.ios { } p { margin: 0 0 1em; - line-height: 1.6em; + line-height: 1.6em; font-size: 0.97em; white-space: pre-wrap; } + span { + p:last-child { + margin-bottom: 0.5em; + } + } + .post-loading-gif { height:10px; width:10px; diff --git a/web/sass-files/sass/partials/_statistics.scss b/web/sass-files/sass/partials/_statistics.scss new file mode 100644 index 000000000..a2401a70f --- /dev/null +++ b/web/sass-files/sass/partials/_statistics.scss @@ -0,0 +1,84 @@ +.team_statistics { + .total-count { + margin: 1em 0; + text-align: center; + background: #fff; + border: 1px solid #ddd; + width: 100%; + @include border-radius(3px); + + .title { + font-weight: 400; + padding: 7px 10px; + border-bottom: 1px solid #ddd; + text-align: left; + + .fa { + float: right; + margin: 3px 0 0; + color: #555; + font-size: 16px; + } + + } + + .content { + font-size: 30px; + font-weight: 600; + color: #555; + padding: 0.3em 0 0.35em; + overflow: auto; + } + + } + + .total-count--day { + width: 760px; + height: 275px; + border: 1px solid #ddd; + padding: 5px 10px 10px 10px; + margin: 10px 10px 10px 10px; + background: #fff; + clear: both; + + > div { + font-size: 18px; + font-weight: 300; + } + } + + .recent-active-users { + + table { + width: 100%; + table-layout: fixed; + } + + .content { + max-height: 400px; + overflow: auto; + padding: 0; + } + + tr { + &:first-child { + td { + border-top: none; + } + } + td { + font-weight: 400; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 13px; + border-left: 1px solid #ddd; + border-top: 1px solid #ddd; + padding: 5px 5px 6px; + @include clearfix; + &:first-child { + border-left: none; + } + } + } + } +}
\ No newline at end of file diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss index c614052da..ad2cae194 100644 --- a/web/sass-files/sass/styles.scss +++ b/web/sass-files/sass/styles.scss @@ -38,6 +38,7 @@ @import "partials/loading"; @import "partials/get-link"; @import "partials/markdown"; +@import "partials/statistics"; // Responsive Css @import "partials/responsive"; |