Merge branch 'master' into PLT-12-log
Этот коммит содержится в:
@@ -21,6 +21,8 @@ Documentation
|
||||
|
||||
- Updated software and hardware requirements documentation
|
||||
- Re-organized install instructions out of README and into separate files
|
||||
- Added Code Contribution Guidelines
|
||||
- Added new hardware sizing recommendations
|
||||
- Consolidated licensing information into LICENSE.txt and NOTICE.txt
|
||||
|
||||
Performance
|
||||
@@ -33,7 +35,8 @@ Code Quality
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed performance issue with slow typing on iOS
|
||||
- Fixed performance issue with slow typing on iOS
|
||||
- Fixed issue so that SSO option automatically set EmailVerified=true (it was false previously)
|
||||
|
||||
### Contributors
|
||||
|
||||
|
||||
10
Makefile
10
Makefile
@@ -78,9 +78,10 @@ travis:
|
||||
|
||||
mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js
|
||||
|
||||
@sed -i'.bak' 's|react-with-addons-0.13.1.js|react-with-addons-0.13.1.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|react-with-addons-0.13.3.js|react-with-addons-0.13.3.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|jquery-1.11.1.js|jquery-1.11.1.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|bootstrap-3.3.1.js|bootstrap-3.3.1.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|react-bootstrap-0.25.1.js|react-bootstrap-0.25.1.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|perfect-scrollbar.js|perfect-scrollbar.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
rm $(DIST_PATH)/web/templates/*.bak
|
||||
@@ -231,9 +232,10 @@ dist: install
|
||||
|
||||
mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js
|
||||
|
||||
@sed -i'.bak' 's|react-with-addons-0.13.1.js|react-with-addons-0.13.1.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|react-with-addons-0.13.3.js|react-with-addons-0.13.3.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|jquery-1.11.1.js|jquery-1.11.1.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|bootstrap-3.3.1.js|bootstrap-3.3.1.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|react-bootstrap-0.25.1.js|react-bootstrap-0.25.1.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|perfect-scrollbar.js|perfect-scrollbar.min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
@sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||
rm $(DIST_PATH)/web/templates/*.bak
|
||||
|
||||
47
README.md
47
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
Mattermost is an open source, on-prem Slack-alternative.
|
||||
|
||||
It modernizes team communication without locking in your data to a single provider. Offer your end users messaging and file sharing across PCs and phones with archiving and instant search--without losing control of your data.
|
||||
It offers modern communication from behind your firewall, including messaging and file sharing across PCs and phones with archiving and instant search.
|
||||
|
||||
## All team communication in one place, searchable and accessible anywhere
|
||||
|
||||
@@ -26,12 +26,6 @@ Please see the [features pages of the Mattermost website](http://www.mattermost.
|
||||
- Attach sound, video and image files from mobile devices
|
||||
- Define team-specific branding and color themes across your devices
|
||||
|
||||
#### Enterprise Compatible
|
||||
|
||||
- Self-host Mattermost entirely within your organization's insfrastructure
|
||||
- GitLab Mattermost omnibus supports install to over 100,000 organizations using GitLab
|
||||
- LDAP/Active Directory connectivity to Mattermost enabled via GitLab Single-Sign-On
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Product Vision and Target Audiences](http://www.mattermost.org/vision/) - What we're solving and for whom are we building
|
||||
@@ -48,9 +42,9 @@ There are multiple ways to install Mattermost depending on your needs.
|
||||
|
||||
#### Quick Start Install for Product Evaluation
|
||||
|
||||
- [Local Machine Install and Upgrade with Docker](doc/install/single-container-install.md) - Explore product functionality using a single-container Docker install on a local machine, including Mac OSX, Ubuntu, or Arch Linux). Optionally set up email and upgrade your instance using DockerHub.
|
||||
- [Local Machine Install with Docker](doc/install/single-container-install.md) - Explore product functionality using a single-container Docker install on a local machine, including Mac OSX, Ubuntu, or Arch Linux). Optionally set up email and upgrade your instance using DockerHub.
|
||||
|
||||
- [AWS EBS Install and Upgrade with Docker](doc/install/aws-ebs-setup.md) - Explore product functionality using a single-container Docker install for Amazon Web Services Elastic Beanstalk. Optionally set up email and upgrade your instance using DockerHub.
|
||||
- [AWS EBS Install with Docker](doc/install/aws-ebs-setup.md) - Explore product functionality using a single-container Docker install for Amazon Web Services Elastic Beanstalk. Optionally set up email and upgrade your instance using DockerHub.
|
||||
|
||||
#### Development Install
|
||||
|
||||
@@ -63,3 +57,38 @@ Prior to production installation, please review [Mattermost system requirements]
|
||||
- [GitLab Mattermost Production Installation](https://gitlab.com/gitlab-org/gitlab-mattermost) - Install Mattermost for production environments bundled with GitLab, a leading open source Git repository, using an omnibus package for Ubuntu 12.04, Ubuntu 14.04, Debian 7, Debian 8, and CentOS 6 (and RedHat/Oracle/Scientific Linux 6), CentOS 7 (and RedHat/Oracle/Scientific Linux 7).
|
||||
|
||||
For technical questions and answers, please visit the [Mattermost forum](http://forum.mattermost.org).
|
||||
|
||||
## Get Involved with Mattermost
|
||||
|
||||
Joining the Mattermost community is a great way to build relationships with other talented and like-minded professionals, increase awareness of the interesting work you are doing, and sharpen your skills. Here are some of the ways that you can make a difference in the Mattermost ecosystem:
|
||||
|
||||
#### Discuss
|
||||
|
||||
- Visit the [Mattermost Forum](http://forum.mattermost.org/) for technical questions and answers.
|
||||
- [Join](https://groups.google.com/a/mattermost.com/forum/#!forum/general/join) the [Mattermost User Mailing List](https://groups.google.com/a/mattermost.com/forum/#!forum/general) for general, open-ended questions and software announcements.
|
||||
- [Join](https://groups.google.com/a/mattermost.com/forum/#!forum/developer/join) the [Mattermost Developers Mailing List](https://groups.google.com/a/mattermost.com/forum/#!forum/developer) for discussing contributing code to Mattermost.
|
||||
|
||||
#### Review
|
||||
|
||||
- Share feedback on [Mattermost Design Feedback Requests](http://forum.mattermost.org/c/feature-ideas/specifications), which offer early previews of designs community comments and feedback.
|
||||
- Visit the [Feature Ideas Forum](http://mattermost.uservoice.com/forums/306457-general) and consider upvoting community feature ideas, which are reviewed for each monthly release.
|
||||
|
||||
#### Share
|
||||
|
||||
- Blog about your Mattermost experiences and use cases, either on your personal blog, the [Mattermost forum](http://forum.mattermost.org), or contribute a guest post to the [Mattermost blog](http://www.mattermost.org/category/blog/). If you write about Mattermost. please contact our community team at info@mattermost.com for help promoting your content.
|
||||
- Tweet to share with your community and friends why you use Mattermost. Follow [@MattermostHQ](https://twitter.com/mattermosthq) on Twitter and conversations on [#mattermost](https://twitter.com/search?q=%23mattermost&src=typd).
|
||||
|
||||
#### Write
|
||||
|
||||
- As with most open source projects, Mattermost documentation is maintained in a public repository. You can propose changes by [submitting pull requests (no programming skills required)](http://forum.mattermost.org/t/help-improve-mattermost-documentation/194). We highly welcome you to add improvements, write guides and tutorials, and expand on sections.
|
||||
- Prior to contributing, please review [Mattermost Documentation Guidelines](http://www.mattermost.org/documentation-guidelines/), which include standards on writing Mattermost documentation for a global audience, who might not use English as their first language.
|
||||
|
||||
#### Contribute
|
||||
|
||||
- Share [feature ideas](http://www.mattermost.org/feature-requests/) with the Mattermost community
|
||||
- Review the [Mattermost Code Contribution Guidelines](doc/developer/code-contribution.md) to submit patches for the core product
|
||||
- Consider building tools that help developers and IT professionals manage Mattermost more effectively (API documentation coming in Beta2)
|
||||
|
||||
#### Have other ideas or suggestions?
|
||||
|
||||
If there’s some other way you’d like to contribute, please contact us at info@mattermost.com. We’d love to meet you!
|
||||
|
||||
@@ -191,7 +191,7 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) {
|
||||
if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
|
||||
c.Err = model.NewAppError("updateChannel", "You do not have the appropriate permissions", "")
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
@@ -514,7 +514,7 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) {
|
||||
if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
|
||||
c.Err = model.NewAppError("deleteChannel", "You do not have the appropriate permissions", "")
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
@@ -756,7 +756,7 @@ func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) {
|
||||
if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
|
||||
c.Err = model.NewAppError("updateChannel", "You do not have the appropriate permissions ", "")
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
|
||||
@@ -106,6 +106,9 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isApi {
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Content-Security-Policy", "frame-ancestors none")
|
||||
} else {
|
||||
// All api response bodies will be JSON formatted
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
sessionId := ""
|
||||
@@ -308,7 +311,7 @@ func (c *Context) IsTeamAdmin(userId string) bool {
|
||||
return false
|
||||
} else {
|
||||
user := uresult.Data.(*model.User)
|
||||
return model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) && user.TeamId == c.Session.TeamId
|
||||
return model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) && user.TeamId == c.Session.TeamId
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -349,6 +349,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Cache-Control", "max-age=2592000, public")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(f)))
|
||||
w.Header().Set("Content-Type", "") // need to provide proper Content-Type in the future
|
||||
w.Write(f)
|
||||
}
|
||||
|
||||
|
||||
@@ -716,7 +716,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if post.UserId != c.Session.UserId && !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) {
|
||||
if post.UserId != c.Session.UserId && !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
|
||||
c.Err = model.NewAppError("deletePost", "You do not have the appropriate permissions", "")
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
|
||||
@@ -50,6 +50,15 @@ func SlackConvertTimeStamp(ts string) int64 {
|
||||
return timeStamp * 1000 // Convert to milliseconds
|
||||
}
|
||||
|
||||
func SlackConvertChannelName(channelName string) string {
|
||||
newName := strings.Trim(channelName, "_-")
|
||||
if len(newName) == 1 {
|
||||
return "slack-channel-" + newName
|
||||
}
|
||||
|
||||
return newName
|
||||
}
|
||||
|
||||
func SlackParseChannels(data io.Reader) []SlackChannel {
|
||||
decoder := json.NewDecoder(data)
|
||||
|
||||
@@ -172,7 +181,7 @@ func SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[str
|
||||
TeamId: teamId,
|
||||
Type: model.CHANNEL_OPEN,
|
||||
DisplayName: sChannel.Name,
|
||||
Name: sChannel.Name,
|
||||
Name: SlackConvertChannelName(sChannel.Name),
|
||||
Description: sChannel.Topic["value"],
|
||||
}
|
||||
mChannel := ImportChannel(&newChannel)
|
||||
|
||||
@@ -477,7 +477,7 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str
|
||||
sender := user.GetDisplayName()
|
||||
|
||||
senderRole := ""
|
||||
if model.IsInRole(user.Roles, model.ROLE_ADMIN) || model.IsInRole(user.Roles, model.ROLE_SYSTEM_ADMIN) {
|
||||
if model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) || model.IsInRole(user.Roles, model.ROLE_SYSTEM_ADMIN) {
|
||||
senderRole = "administrator"
|
||||
} else {
|
||||
senderRole = "member"
|
||||
@@ -536,7 +536,7 @@ func updateTeamDisplayName(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) {
|
||||
if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
|
||||
c.Err = model.NewAppError("updateTeamDisplayName", "You do not have the appropriate permissions", "userId="+c.Session.UserId)
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
@@ -576,7 +576,7 @@ func updateValetFeature(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) {
|
||||
if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) {
|
||||
c.Err = model.NewAppError("updateValetFeature", "You do not have the appropriate permissions", "userId="+c.Session.UserId)
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
|
||||
14
api/user.go
14
api/user.go
@@ -170,7 +170,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
|
||||
|
||||
channelRole := ""
|
||||
if team.Email == user.Email {
|
||||
user.Roles = model.ROLE_ADMIN
|
||||
user.Roles = model.ROLE_TEAM_ADMIN
|
||||
channelRole = model.CHANNEL_ROLE_ADMIN
|
||||
} else {
|
||||
user.Roles = ""
|
||||
@@ -945,7 +945,7 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) && !c.IsSystemAdmin() {
|
||||
if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) && !c.IsSystemAdmin() {
|
||||
c.Err = model.NewAppError("updateRoles", "You do not have the appropriate permissions", "userId="+user_id)
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
@@ -984,7 +984,7 @@ func UpdateRoles(c *Context, user *model.User, roles string) *model.User {
|
||||
// make sure there is at least 1 other active admin
|
||||
|
||||
if !model.IsInRole(roles, model.ROLE_SYSTEM_ADMIN) {
|
||||
if model.IsInRole(user.Roles, model.ROLE_ADMIN) && !model.IsInRole(roles, model.ROLE_ADMIN) {
|
||||
if model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) && !model.IsInRole(roles, model.ROLE_TEAM_ADMIN) {
|
||||
if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return nil
|
||||
@@ -992,7 +992,7 @@ func UpdateRoles(c *Context, user *model.User, roles string) *model.User {
|
||||
activeAdmins := -1
|
||||
profileUsers := result.Data.(map[string]*model.User)
|
||||
for _, profileUser := range profileUsers {
|
||||
if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_ADMIN) {
|
||||
if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_TEAM_ADMIN) {
|
||||
activeAdmins = activeAdmins + 1
|
||||
}
|
||||
}
|
||||
@@ -1042,14 +1042,14 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) && !c.IsSystemAdmin() {
|
||||
if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) && !c.IsSystemAdmin() {
|
||||
c.Err = model.NewAppError("updateActive", "You do not have the appropriate permissions", "userId="+user_id)
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
}
|
||||
|
||||
// make sure there is at least 1 other active admin
|
||||
if !active && model.IsInRole(user.Roles, model.ROLE_ADMIN) {
|
||||
if !active && model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) {
|
||||
if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
@@ -1057,7 +1057,7 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
activeAdmins := -1
|
||||
profileUsers := result.Data.(map[string]*model.User)
|
||||
for _, profileUser := range profileUsers {
|
||||
if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_ADMIN) {
|
||||
if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_TEAM_ADMIN) {
|
||||
activeAdmins = activeAdmins + 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,7 +509,7 @@ func TestUserUpdate(t *testing.T) {
|
||||
user.TeamId = "12345678901234567890123456"
|
||||
user.LastActivityAt = time2
|
||||
user.LastPingAt = time2
|
||||
user.Roles = model.ROLE_ADMIN
|
||||
user.Roles = model.ROLE_TEAM_ADMIN
|
||||
user.LastPasswordUpdate = 123
|
||||
|
||||
if result, err := Client.UpdateUser(user); err != nil {
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
### Configuration
|
||||
|
||||
- [GitLab SSO Configuration](integrations/sso/gitlab-sso.md)
|
||||
- [SMTP Email Setup](smtp-email-setup.md)
|
||||
- [SMTP Email Setup](config/smtp-email-setup.md)
|
||||
|
||||
## Developer Documentation
|
||||
|
||||
- [Developer Machine Setup](doc/install/dev-setup.md)
|
||||
- [Code Contribution Guidelines](developer/code-contribution.md)
|
||||
- [Developer Machine Setup](install/dev-setup.md)
|
||||
- [Mattermost Style Guide](developer/style-guide.md)
|
||||
|
||||
44
doc/developer/code-contribution.md
Обычный файл
44
doc/developer/code-contribution.md
Обычный файл
@@ -0,0 +1,44 @@
|
||||
# Code Contribution Guidelines
|
||||
|
||||
Thank you for your interest in contributing to Mattermost. This guide provides an overview of important information for contributors to know.
|
||||
|
||||
## Choose a Ticket
|
||||
|
||||
1. Review the list of [Good First Contribution](https://mattermost.atlassian.net/issues/?filter=10206) tickets listed in Jira.
|
||||
|
||||
2. These projects are intended to be a straight forward first pull requests from new contributors.
|
||||
If you don't find something appropriate for your interests, please see the full list of tickets [Accepting Pull Requests](https://mattermost.atlassian.net/issues/?filter=10101).
|
||||
|
||||
3. If you have any questions at all about a ticket, please post to the [Contributor Discussion section](http://forum.mattermost.org/) of the Mattermost forum, or email the [Mattermost Developer Mailing list](https://groups.google.com/a/mattermost.com/forum/#!forum/developer/join).
|
||||
|
||||
## Install Mattermost and set up a Fork
|
||||
|
||||
1. Follow [developer setup instructions](https://github.com/mattermost/platform/blob/master/doc/install/dev-setup.md) to install Mattermost.
|
||||
|
||||
2. Create a branch with <branch name> set to the ID of the ticket you're working on, for example ```PLT-394```, using command:
|
||||
|
||||
```
|
||||
git checkout -b <branch name>
|
||||
```
|
||||
|
||||
## Programming and Testing
|
||||
|
||||
1. Please review the [Mattermost Style Guide](developer/style-guide.md) prior to making changes.
|
||||
|
||||
2. Please make sure to thoroughly test your change before submitting a pull request.
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
1. Please add yourself to the contributor list prior to submitting by completing the [contributor license agreement](http://www.mattermost.org/mattermost-contributor-agreement/).
|
||||
|
||||
2. When you submit your pull request please include the Ticket ID at the beginning of your pull request comment, followed by a colon.
|
||||
|
||||
For example, for a ticket ID `PLT-394` start your comment with: `PLT-394:`. See [previously closed pull requests](https://github.com/mattermost/platform/pulls?q=is%3Apr+is%3Aclosed) for examples.
|
||||
|
||||
3. Once submitted, your pull request will be checked via an automated build process and will be reviewed by the Mattermost core team, who may either accept the PR or follow-up with feedback.
|
||||
|
||||
4. If you've included your mailing address in Step 1, you'll be receiving a [Limited Edition Mattermost Mug](http://forum.mattermost.org/t/limited-edition-mattermost-mugs/143) as a thank you gift after your first pull request has been accepted.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Developer Machine Setup
|
||||
2. Get your Docker IP address with `boot2docker ip`
|
||||
3. Add a line to your /etc/hosts that goes `<Docker IP> dockerhost`
|
||||
4. Run `boot2docker shellinit` and copy the export statements to your ~/.bash_profile
|
||||
2. Download Go from http://golang.org/dl/
|
||||
2. Download Go (version 1.4.2) from http://golang.org/dl/
|
||||
3. Set up your Go workspace
|
||||
1. `mkdir ~/go`
|
||||
2. Add the following to your ~/.bash_profile
|
||||
@@ -22,13 +22,13 @@ Developer Machine Setup
|
||||
1. Download Homebrew from http://brew.sh/
|
||||
2. `brew install node`
|
||||
5. Install Compass
|
||||
1. Make sure you have the latest verison of Ruby
|
||||
2. `gem install compass`
|
||||
1. Run `ruby -v` and check the ruby version is 1.8.7 or higher
|
||||
2. `sudo gem install compass`
|
||||
6. Download Mattermost
|
||||
`cd ~/go`
|
||||
`mkdir -p src/github.com/mattermost`
|
||||
`cd src/github.com/mattermost`
|
||||
`git clone github.com/mattermost/platform.git`
|
||||
`git clone https://github.com/mattermost/platform.git`
|
||||
`cd platform`
|
||||
7. Run unit tests on Mattermost using `make test` to make sure the installation was successful
|
||||
8. If tests passed, you can now run Mattermost using `make run`
|
||||
@@ -50,7 +50,7 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org
|
||||
`127.0.0.1 dockerhost`
|
||||
3. Install build essentials
|
||||
1. `apt-get install build-essential`
|
||||
4. Download Go from http://golang.org/dl/
|
||||
4. Download Go (version 1.4.2) from http://golang.org/dl/
|
||||
5. Set up your Go workspace and add Go to the PATH
|
||||
1. `mkdir ~/go`
|
||||
2. Add the following to your ~/.bashrc
|
||||
@@ -74,7 +74,7 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org
|
||||
`cd ~/go`
|
||||
`mkdir -p src/github.com/mattermost`
|
||||
`cd src/github.com/mattermost`
|
||||
`git clone github.com/mattermost/platform.git`
|
||||
`git clone https://github.com/mattermost/platform.git`
|
||||
`cd platform`
|
||||
9. Run unit tests on Mattermost using `make test` to make sure the installation was successful
|
||||
10. If tests passed, you can now run Mattermost using `make run`
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
**Mattermost Alpha**
|
||||
**Team Communication Service**
|
||||
**Development Build**
|
||||
Mattermost
|
||||
==========
|
||||
|
||||
|
||||
About Mattermost
|
||||
================
|
||||
http:/mattermost.org
|
||||
|
||||
Mattermost is an open-source team communication service. It brings team messaging and file sharing into one place, accessible across PCs and phones, with archiving and search.
|
||||
|
||||
|
||||
|
||||
Installing Mattermost
|
||||
=====================
|
||||
|
||||
You're installing "Mattermost Alpha", a pre-released version providing an early look at what we're building. While the core team runs this version internally, it's not recommended for production since we can't guarantee API stability or backwards compatibility.
|
||||
|
||||
That said, any issues at all, please let us know on the Mattermost forum.
|
||||
|
||||
Notes:
|
||||
- For Alpha, Docker is intentionally setup as a single container, since production deployment is not yet recommended.
|
||||
|
||||
Local Machine Setup (Docker)
|
||||
-----------------------------
|
||||
|
||||
To run an instance of the latest version of mattermost on your local machine you can run:
|
||||
|
||||
`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`
|
||||
@@ -35,9 +20,4 @@ To run an instance of the latest code from the master branch on GitHub you can r
|
||||
|
||||
`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:dev`
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Mattermost is licensed under an "Apache-wrapped AGPL" model inspired by MongoDB. Similar to MongoDB, you can run and link to the system using Configuration Files and Admin Tools licensed under Apache, version 2.0, as described in the LICENSE file, as an explicit exception to the terms of the GNU Affero General Public License (AGPL) that applies to most of the remaining source files. See individual files for details.
|
||||
|
||||
Any questions, please visit http://forum.mattermost.org
|
||||
|
||||
@@ -328,9 +328,9 @@ Usage:
|
||||
Example:
|
||||
platform -create_user -team_name="name" -email="user@example.com" -password="mypassword"
|
||||
|
||||
-assign_role Assigns role to a user. It requres the -team_name,
|
||||
-email and -role flag. If you're assigning the
|
||||
"system_admin" role it must be for a user on the
|
||||
-assign_role Assigns role to a user. It requres the -role,
|
||||
-email and -team_name flag. If you're assigning the
|
||||
role="system_admin" role it must be for a user on the
|
||||
team_name="admin"
|
||||
Example:
|
||||
platform -assign_role -team_name="name" -email="user@example.com" -role="admin"
|
||||
|
||||
@@ -158,7 +158,7 @@ func IsReservedTeamName(s string) bool {
|
||||
|
||||
func IsValidTeamName(s string) bool {
|
||||
|
||||
if !IsValidAlphaNum(s) {
|
||||
if !IsValidAlphaNum(s, false) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ROLE_ADMIN = "admin"
|
||||
ROLE_TEAM_ADMIN = "admin"
|
||||
ROLE_SYSTEM_ADMIN = "system_admin"
|
||||
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
|
||||
USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute
|
||||
@@ -289,7 +289,7 @@ func isValidRole(role string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if role == ROLE_ADMIN {
|
||||
if role == ROLE_TEAM_ADMIN {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ func GetSubDomain(s string) (string, string) {
|
||||
|
||||
func IsValidChannelIdentifier(s string) bool {
|
||||
|
||||
if !IsValidAlphaNum(s) {
|
||||
if !IsValidAlphaNum(s, true) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -213,10 +213,16 @@ func IsValidChannelIdentifier(s string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var validAlphaNumUnderscore = regexp.MustCompile(`^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]+$`)
|
||||
var validAlphaNum = regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`)
|
||||
|
||||
func IsValidAlphaNum(s string) bool {
|
||||
match := validAlphaNum.MatchString(s)
|
||||
func IsValidAlphaNum(s string, allowUnderscores bool) bool {
|
||||
var match bool
|
||||
if allowUnderscores {
|
||||
match = validAlphaNumUnderscore.MatchString(s)
|
||||
} else {
|
||||
match = validAlphaNum.MatchString(s)
|
||||
}
|
||||
|
||||
if !match {
|
||||
return false
|
||||
|
||||
@@ -74,7 +74,7 @@ func (us SqlUserStore) Save(user *model.User) StoreChannel {
|
||||
close(storeChannel)
|
||||
return
|
||||
} else if int(count) > utils.Cfg.TeamSettings.MaxUsersPerTeam {
|
||||
result.Err = model.NewAppError("SqlUserStore.Save", "You've reached the limit of the number of allowed accounts.", "teamId="+user.TeamId)
|
||||
result.Err = model.NewAppError("SqlUserStore.Save", "This team has reached the maxmium number of allowed accounts. Contact your systems administrator to set a higher limit.", "teamId="+user.TeamId)
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
return
|
||||
|
||||
@@ -141,17 +141,14 @@ var FUZZY_STRINGS_NAMES = []string{
|
||||
// Strings that should pass as acceptable emails
|
||||
var FUZZY_STRINGS_EMAILS = []string{
|
||||
"sue@thatmightbe",
|
||||
"sue@thatmightbe.",
|
||||
"sue@thatmightbe.c",
|
||||
"sue@thatmightbe.co",
|
||||
"su+san@thatmightbe.com",
|
||||
"a@b.中国",
|
||||
"1@2.am",
|
||||
"a@b.co.uk",
|
||||
"a@b.cancerresearch",
|
||||
"su+s+an@thatmightbe.com",
|
||||
"per.iod@thatmightbe.com",
|
||||
"per..iods@thatmightbe.com",
|
||||
}
|
||||
|
||||
// Lovely giberish for all to use
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"React": false
|
||||
"React": false,
|
||||
"ReactBootstrap": false
|
||||
},
|
||||
"rules": {
|
||||
"comma-dangle": [2, "never"],
|
||||
|
||||
177
web/react/components/change_url_modal.jsx
Обычный файл
177
web/react/components/change_url_modal.jsx
Обычный файл
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Modal = ReactBootstrap.Modal;
|
||||
var Utils = require('../utils/utils.jsx');
|
||||
|
||||
export default class ChangeUrlModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onURLChanged = this.onURLChanged.bind(this);
|
||||
this.doSubmit = this.doSubmit.bind(this);
|
||||
this.doCancel = this.doCancel.bind(this);
|
||||
|
||||
this.state = {
|
||||
currentURL: props.currentURL,
|
||||
urlError: '',
|
||||
userEdit: false
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// This check prevents the url being deleted when we re-render
|
||||
// because of user status check
|
||||
if (!this.state.userEdit) {
|
||||
this.setState({
|
||||
currentURL: nextProps.currentURL
|
||||
});
|
||||
}
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.show === true && prevProps.show === false) {
|
||||
React.findDOMNode(this.refs.urlinput).select();
|
||||
}
|
||||
}
|
||||
onURLChanged(e) {
|
||||
const url = e.target.value.trim();
|
||||
this.setState({currentURL: url.replace(/[^A-Za-z0-9-_]/g, '').toLowerCase(), userEdit: true});
|
||||
}
|
||||
getURLError(url) {
|
||||
let error = []; //eslint-disable-line prefer-const
|
||||
if (url.length < 2) {
|
||||
error.push(<span key='error1'>{'Must be longer than two characters'}<br/></span>);
|
||||
}
|
||||
if (url.charAt(0) === '-' || url.charAt(0) === '_') {
|
||||
error.push(<span key='error2'>{'Must start with a letter or number'}<br/></span>);
|
||||
}
|
||||
if (url.length > 1 && (url.charAt(url.length - 1) === '-' || url.charAt(url.length - 1) === '_')) {
|
||||
error.push(<span key='error3'>{'Must end with a letter or number'}<br/></span>);
|
||||
}
|
||||
if (url.indexOf('__') > -1) {
|
||||
error.push(<span key='error4'>{'Can not contain two underscores in a row.'}<br/></span>);
|
||||
}
|
||||
|
||||
// In case of error we don't detect
|
||||
if (error.length === 0) {
|
||||
error.push(<span key='errorlast'>{'Invalid URL'}<br/></span>);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
doSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const url = React.findDOMNode(this.refs.urlinput).value;
|
||||
const cleanedURL = Utils.cleanUpUrlable(url);
|
||||
if (cleanedURL !== url || url.length < 2 || url.indexOf('__') > -1) {
|
||||
this.setState({urlError: this.getURLError(url)});
|
||||
return;
|
||||
}
|
||||
this.setState({urlError: '', userEdit: false});
|
||||
this.props.onModalSubmit(url);
|
||||
}
|
||||
doCancel() {
|
||||
this.setState({urlError: '', userEdit: false});
|
||||
this.props.onModalDismissed();
|
||||
}
|
||||
render() {
|
||||
let urlClass = 'input-group input-group--limit';
|
||||
let urlError = null;
|
||||
let serverError = null;
|
||||
|
||||
if (this.state.urlError) {
|
||||
urlClass += ' has-error';
|
||||
urlError = (<p className='input__help error'>{this.state.urlError}</p>);
|
||||
}
|
||||
|
||||
if (this.props.serverError) {
|
||||
serverError = <div className='form-group has-error'><p className='input__help error'>{this.props.serverError}</p></div>;
|
||||
}
|
||||
|
||||
const fullTeamUrl = Utils.getTeamURLFromAddressBar();
|
||||
const teamURL = Utils.getShortenedTeamURL();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.doCancel}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>{this.props.title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<form
|
||||
role='form'
|
||||
className='form-horizontal'
|
||||
>
|
||||
<Modal.Body>
|
||||
<div className='modal-intro'>{this.props.description}</div>
|
||||
<div className='form-group'>
|
||||
<label className='col-sm-2 form__label control-label'>{this.props.urlLabel}</label>
|
||||
<div className='col-sm-10'>
|
||||
<div className={urlClass}>
|
||||
<span
|
||||
data-toggle='tooltip'
|
||||
title={fullTeamUrl}
|
||||
className='input-group-addon'
|
||||
>
|
||||
{teamURL}
|
||||
</span>
|
||||
<input
|
||||
type='text'
|
||||
ref='urlinput'
|
||||
className='form-control'
|
||||
maxLength='22'
|
||||
onChange={this.onURLChanged}
|
||||
value={this.state.currentURL}
|
||||
autoFocus={true}
|
||||
tabIndex='1'
|
||||
/>
|
||||
</div>
|
||||
{urlError}
|
||||
{serverError}
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.doCancel}
|
||||
>
|
||||
{'Close'}
|
||||
</button>
|
||||
<button
|
||||
onClick={this.doSubmit}
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
tabIndex='2'
|
||||
>
|
||||
{this.props.submitButtonText}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChangeUrlModal.defaultProps = {
|
||||
show: false,
|
||||
title: 'Change URL',
|
||||
desciption: '',
|
||||
urlLabel: 'URL',
|
||||
submitButtonText: 'Submit',
|
||||
currentURL: '',
|
||||
serverError: ''
|
||||
};
|
||||
|
||||
ChangeUrlModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.string,
|
||||
urlLabel: React.PropTypes.string,
|
||||
submitButtonText: React.PropTypes.string,
|
||||
currentURL: React.PropTypes.string,
|
||||
serverError: React.PropTypes.string,
|
||||
onModalSubmit: React.PropTypes.func.isRequired,
|
||||
onModalDismissed: React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -20,12 +20,11 @@ export default class FileUpload extends React.Component {
|
||||
}
|
||||
|
||||
fileUploadSuccess(channelId, data) {
|
||||
var parsedData = $.parseJSON(data);
|
||||
this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId);
|
||||
this.props.onFileUpload(data.filenames, data.client_ids, channelId);
|
||||
|
||||
var requests = this.state.requests;
|
||||
for (var j = 0; j < parsedData.client_ids.length; j++) {
|
||||
delete requests[parsedData.client_ids[j]];
|
||||
for (var j = 0; j < data.client_ids.length; j++) {
|
||||
delete requests[data.client_ids[j]];
|
||||
}
|
||||
this.setState({requests: requests});
|
||||
}
|
||||
@@ -53,7 +52,7 @@ export default class FileUpload extends React.Component {
|
||||
}
|
||||
|
||||
// generate a unique id that can be used by other components to refer back to this upload
|
||||
var clientId = utils.generateId();
|
||||
let clientId = utils.generateId();
|
||||
|
||||
// prepare data to be uploaded
|
||||
var formData = new FormData();
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var client = require('../utils/client.jsx');
|
||||
var asyncClient = require('../utils/async_client.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
export default class NewChannelModal extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.displayNameKeyUp = this.displayNameKeyUp.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
|
||||
this.state = {channelType: ''};
|
||||
}
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var channel = {};
|
||||
var state = {serverError: ''};
|
||||
|
||||
channel.display_name = React.findDOMNode(this.refs.display_name).value.trim();
|
||||
if (!channel.display_name) {
|
||||
state.displayNameError = 'This field is required';
|
||||
state.inValid = true;
|
||||
} else if (channel.display_name.length > 22) {
|
||||
state.displayNameError = 'This field must be less than 22 characters';
|
||||
state.inValid = true;
|
||||
} else {
|
||||
state.displayNameError = '';
|
||||
}
|
||||
|
||||
channel.name = React.findDOMNode(this.refs.channel_name).value.trim();
|
||||
if (!channel.name) {
|
||||
state.nameError = 'This field is required';
|
||||
state.inValid = true;
|
||||
} else if (channel.name.length > 22) {
|
||||
state.nameError = 'This field must be less than 22 characters';
|
||||
state.inValid = true;
|
||||
} else {
|
||||
var cleanedName = utils.cleanUpUrlable(channel.name);
|
||||
if (cleanedName !== channel.name) {
|
||||
state.nameError = "Must be lowercase alphanumeric characters, allowing '-' but not starting or ending with '-'";
|
||||
state.inValid = true;
|
||||
} else {
|
||||
state.nameError = '';
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(state);
|
||||
|
||||
if (state.inValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cu = UserStore.getCurrentUser();
|
||||
channel.team_id = cu.team_id;
|
||||
|
||||
channel.description = React.findDOMNode(this.refs.channel_desc).value.trim();
|
||||
channel.type = this.state.channelType;
|
||||
|
||||
client.createChannel(channel,
|
||||
function success(data) {
|
||||
$(React.findDOMNode(this.refs.modal)).modal('hide');
|
||||
|
||||
asyncClient.getChannel(data.id);
|
||||
utils.switchChannel(data);
|
||||
|
||||
React.findDOMNode(this.refs.display_name).value = '';
|
||||
React.findDOMNode(this.refs.channel_name).value = '';
|
||||
React.findDOMNode(this.refs.channel_desc).value = '';
|
||||
}.bind(this),
|
||||
function error(err) {
|
||||
state.serverError = err.message;
|
||||
state.inValid = true;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
displayNameKeyUp() {
|
||||
var displayName = React.findDOMNode(this.refs.display_name).value.trim();
|
||||
var channelName = utils.cleanUpUrlable(displayName);
|
||||
React.findDOMNode(this.refs.channel_name).value = channelName;
|
||||
}
|
||||
componentDidMount() {
|
||||
var self = this;
|
||||
$(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function onModalShow(e) {
|
||||
var button = e.relatedTarget;
|
||||
self.setState({channelType: $(button).attr('data-channeltype')});
|
||||
});
|
||||
$(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
$(React.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose);
|
||||
}
|
||||
handleClose() {
|
||||
$(React.findDOMNode(this)).find('.form-control').each(function clearForms() {
|
||||
this.value = '';
|
||||
});
|
||||
|
||||
this.setState({channelType: '', displayNameError: '', nameError: '', serverError: '', inValid: false});
|
||||
}
|
||||
render() {
|
||||
var displayNameError = null;
|
||||
var nameError = null;
|
||||
var serverError = null;
|
||||
var displayNameClass = 'form-group';
|
||||
var nameClass = 'form-group';
|
||||
|
||||
if (this.state.displayNameError) {
|
||||
displayNameError = <label className='control-label'>{this.state.displayNameError}</label>;
|
||||
displayNameClass += ' has-error';
|
||||
}
|
||||
if (this.state.nameError) {
|
||||
nameError = <label className='control-label'>{this.state.nameError}</label>;
|
||||
nameClass += ' has-error';
|
||||
}
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var channelTerm = 'Channel';
|
||||
if (this.state.channelType === 'P') {
|
||||
channelTerm = 'Group';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='modal fade'
|
||||
id='new_channel'
|
||||
ref='modal'
|
||||
tabIndex='-1'
|
||||
role='dialog'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<div className='modal-dialog'>
|
||||
<div className='modal-content'>
|
||||
<div className='modal-header'>
|
||||
<button
|
||||
type='button'
|
||||
className='close'
|
||||
data-dismiss='modal'
|
||||
>
|
||||
<span aria-hidden='true'>×</span>
|
||||
<span className='sr-only'>Cancel</span>
|
||||
</button>
|
||||
<h4 className='modal-title'>New {channelTerm}</h4>
|
||||
</div>
|
||||
<form role='form'>
|
||||
<div className='modal-body'>
|
||||
<div className={displayNameClass}>
|
||||
<label className='control-label'>Display Name</label>
|
||||
<input
|
||||
onKeyUp={this.displayNameKeyUp}
|
||||
type='text'
|
||||
ref='display_name'
|
||||
className='form-control'
|
||||
placeholder='Enter display name'
|
||||
maxLength='22'
|
||||
/>
|
||||
{displayNameError}
|
||||
</div>
|
||||
<div className={nameClass}>
|
||||
<label className='control-label'>Handle</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
ref='channel_name'
|
||||
placeholder="lowercase alphanumeric's only"
|
||||
maxLength='22'
|
||||
/>
|
||||
{nameError}
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label className='control-label'>Description</label>
|
||||
<textarea
|
||||
className='form-control no-resize'
|
||||
ref='channel_desc'
|
||||
rows='3'
|
||||
placeholder='Description'
|
||||
maxLength='1024'
|
||||
/>
|
||||
</div>
|
||||
{serverError}
|
||||
</div>
|
||||
<div className='modal-footer'>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
data-dismiss='modal'
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={this.handleSubmit}
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
>
|
||||
Create New {channelTerm}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
206
web/react/components/new_channel_flow.jsx
Обычный файл
206
web/react/components/new_channel_flow.jsx
Обычный файл
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Utils = require('../utils/utils.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var Client = require('../utils/client.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
|
||||
var NewChannelModal = require('./new_channel_modal.jsx');
|
||||
var ChangeURLModal = require('./change_url_modal.jsx');
|
||||
|
||||
const SHOW_NEW_CHANNEL = 1;
|
||||
const SHOW_EDIT_URL = 2;
|
||||
const SHOW_EDIT_URL_THEN_COMPLETE = 3;
|
||||
|
||||
export default class NewChannelFlow extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.doSubmit = this.doSubmit.bind(this);
|
||||
this.typeSwitched = this.typeSwitched.bind(this);
|
||||
this.urlChangeRequested = this.urlChangeRequested.bind(this);
|
||||
this.urlChangeSubmitted = this.urlChangeSubmitted.bind(this);
|
||||
this.urlChangeDismissed = this.urlChangeDismissed.bind(this);
|
||||
this.channelDataChanged = this.channelDataChanged.bind(this);
|
||||
|
||||
this.state = {
|
||||
serverError: '',
|
||||
channelType: 'O',
|
||||
flowState: SHOW_NEW_CHANNEL,
|
||||
channelDisplayName: '',
|
||||
channelName: '',
|
||||
channelDescription: '',
|
||||
nameModified: false
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// If we are being shown, grab channel type from props and clear
|
||||
if (nextProps.show === true && this.props.show === false) {
|
||||
this.setState({
|
||||
serverError: '',
|
||||
channelType: nextProps.channelType,
|
||||
flowState: SHOW_NEW_CHANNEL,
|
||||
channelDisplayName: '',
|
||||
channelName: '',
|
||||
channelDescription: '',
|
||||
nameModified: false
|
||||
});
|
||||
}
|
||||
}
|
||||
doSubmit() {
|
||||
var channel = {};
|
||||
|
||||
channel.display_name = this.state.channelDisplayName;
|
||||
if (!channel.display_name) {
|
||||
this.setState({serverError: 'Invalid Channel Name'});
|
||||
return;
|
||||
}
|
||||
|
||||
channel.name = this.state.channelName;
|
||||
if (channel.name.length < 2) {
|
||||
this.setState({flowState: SHOW_EDIT_URL_THEN_COMPLETE});
|
||||
return;
|
||||
}
|
||||
|
||||
const cu = UserStore.getCurrentUser();
|
||||
channel.team_id = cu.team_id;
|
||||
channel.description = this.state.channelDescription;
|
||||
channel.type = this.state.channelType;
|
||||
|
||||
Client.createChannel(channel,
|
||||
(data) => {
|
||||
this.props.onModalDismissed();
|
||||
AsyncClient.getChannel(data.id);
|
||||
Utils.switchChannel(data);
|
||||
},
|
||||
(err) => {
|
||||
if (err.message === 'Name must be 2 or more lowercase alphanumeric characters') {
|
||||
this.setState({flowState: SHOW_EDIT_URL_THEN_COMPLETE});
|
||||
}
|
||||
if (err.message === 'A channel with that handle already exists') {
|
||||
this.setState({serverError: 'A channel with that URL already exists'});
|
||||
return;
|
||||
}
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
typeSwitched() {
|
||||
if (this.state.channelType === 'P') {
|
||||
this.setState({channelType: 'O'});
|
||||
} else {
|
||||
this.setState({channelType: 'P'});
|
||||
}
|
||||
}
|
||||
urlChangeRequested() {
|
||||
this.setState({flowState: SHOW_EDIT_URL});
|
||||
}
|
||||
urlChangeSubmitted(newURL) {
|
||||
if (this.state.flowState === SHOW_EDIT_URL_THEN_COMPLETE) {
|
||||
this.setState({channelName: newURL, nameModified: true}, this.doSubmit);
|
||||
} else {
|
||||
this.setState({flowState: SHOW_NEW_CHANNEL, serverError: '', channelName: newURL, nameModified: true});
|
||||
}
|
||||
}
|
||||
urlChangeDismissed() {
|
||||
this.setState({flowState: SHOW_NEW_CHANNEL});
|
||||
}
|
||||
channelDataChanged(data) {
|
||||
this.setState({
|
||||
channelDisplayName: data.displayName,
|
||||
channelDescription: data.description
|
||||
});
|
||||
if (!this.state.nameModified) {
|
||||
this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())});
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const channelData = {
|
||||
name: this.state.channelName,
|
||||
displayName: this.state.channelDisplayName,
|
||||
description: this.state.channelDescription
|
||||
};
|
||||
|
||||
let showChannelModal = false;
|
||||
let showGroupModal = false;
|
||||
let showChangeURLModal = false;
|
||||
|
||||
let changeURLTitle = '';
|
||||
let changeURLSubmitButtonText = '';
|
||||
let channelTerm = '';
|
||||
|
||||
// Only listen to flow state if we are being shown
|
||||
if (this.props.show) {
|
||||
switch (this.state.flowState) {
|
||||
case SHOW_NEW_CHANNEL:
|
||||
if (this.state.channelType === 'O') {
|
||||
showChannelModal = true;
|
||||
channelTerm = 'Channel';
|
||||
} else {
|
||||
showGroupModal = true;
|
||||
channelTerm = 'Group';
|
||||
}
|
||||
break;
|
||||
case SHOW_EDIT_URL:
|
||||
showChangeURLModal = true;
|
||||
changeURLTitle = 'Change ' + channelTerm + ' URL';
|
||||
changeURLSubmitButtonText = 'Change ' + channelTerm + ' URL';
|
||||
break;
|
||||
case SHOW_EDIT_URL_THEN_COMPLETE:
|
||||
showChangeURLModal = true;
|
||||
changeURLTitle = 'Set ' + channelTerm + ' URL';
|
||||
changeURLSubmitButtonText = 'Create ' + channelTerm;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
<NewChannelModal
|
||||
show={showChannelModal}
|
||||
channelType={'O'}
|
||||
channelData={channelData}
|
||||
serverError={this.state.serverError}
|
||||
onSubmitChannel={this.doSubmit}
|
||||
onModalDismissed={this.props.onModalDismissed}
|
||||
onTypeSwitched={this.typeSwitched}
|
||||
onChangeURLPressed={this.urlChangeRequested}
|
||||
onDataChanged={this.channelDataChanged}
|
||||
/>
|
||||
<NewChannelModal
|
||||
show={showGroupModal}
|
||||
channelType={'P'}
|
||||
channelData={channelData}
|
||||
serverError={this.state.serverError}
|
||||
onSubmitChannel={this.doSubmit}
|
||||
onModalDismissed={this.props.onModalDismissed}
|
||||
onTypeSwitched={this.typeSwitched}
|
||||
onChangeURLPressed={this.urlChangeRequested}
|
||||
onDataChanged={this.channelDataChanged}
|
||||
/>
|
||||
<ChangeURLModal
|
||||
show={showChangeURLModal}
|
||||
title={changeURLTitle}
|
||||
description={'Some characters are not allowed in URLs and may be removed.'}
|
||||
urlLabel={channelTerm + ' URL'}
|
||||
submitButtonText={changeURLSubmitButtonText}
|
||||
currentURL={this.state.channelName}
|
||||
serverError={this.state.serverError}
|
||||
onModalSubmit={this.urlChangeSubmitted}
|
||||
onModalDismissed={this.urlChangeDismissed}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NewChannelFlow.defaultProps = {
|
||||
show: false,
|
||||
channelType: 'O'
|
||||
};
|
||||
|
||||
NewChannelFlow.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
channelType: React.PropTypes.string.isRequired,
|
||||
onModalDismissed: React.PropTypes.func.isRequired
|
||||
};
|
||||
198
web/react/components/new_channel_modal.jsx
Обычный файл
198
web/react/components/new_channel_modal.jsx
Обычный файл
@@ -0,0 +1,198 @@
|
||||
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
const Utils = require('../utils/utils.jsx');
|
||||
var Modal = ReactBootstrap.Modal;
|
||||
|
||||
export default class NewChannelModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
displayNameError: ''
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.show === true && this.props.show === false) {
|
||||
this.setState({
|
||||
displayNameError: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const displayName = React.findDOMNode(this.refs.display_name).value.trim();
|
||||
if (displayName.length < 1) {
|
||||
this.setState({displayNameError: 'This field is required'});
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onSubmitChannel();
|
||||
}
|
||||
handleChange() {
|
||||
const newData = {
|
||||
displayName: React.findDOMNode(this.refs.display_name).value,
|
||||
description: React.findDOMNode(this.refs.channel_desc).value
|
||||
};
|
||||
this.props.onDataChanged(newData);
|
||||
}
|
||||
render() {
|
||||
var displayNameError = null;
|
||||
var serverError = null;
|
||||
var displayNameClass = 'form-group';
|
||||
|
||||
if (this.state.displayNameError) {
|
||||
displayNameError = <p className='input__help error'>{this.state.displayNameError}</p>;
|
||||
displayNameClass += ' has-error';
|
||||
}
|
||||
|
||||
if (this.props.serverError) {
|
||||
serverError = <div className='form-group has-error'><p className='input__help error'>{this.props.serverError}</p></div>;
|
||||
}
|
||||
|
||||
var channelTerm = '';
|
||||
var channelSwitchText = '';
|
||||
switch (this.props.channelType) {
|
||||
case 'P':
|
||||
channelTerm = 'Group';
|
||||
channelSwitchText = (
|
||||
<div className='modal-intro'>
|
||||
{'Create a new private group with restricted membership. '}
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.props.onTypeSwitched}
|
||||
>
|
||||
{'Create a public channel'}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'O':
|
||||
channelTerm = 'Channel';
|
||||
channelSwitchText = (
|
||||
<div className='modal-intro'>
|
||||
{'Create a new public channel anyone can join. '}
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.props.onTypeSwitched}
|
||||
>
|
||||
{'Create a private group'}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const prettyTeamURL = Utils.getShortenedTeamURL();
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.props.onModalDismissed}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>{'New ' + channelTerm}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<form
|
||||
role='form'
|
||||
className='form-horizontal'
|
||||
>
|
||||
<Modal.Body>
|
||||
<div>
|
||||
{channelSwitchText}
|
||||
</div>
|
||||
<div className={displayNameClass}>
|
||||
<label className='col-sm-2 form__label control-label'>{'Name'}</label>
|
||||
<div className='col-sm-10'>
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
type='text'
|
||||
ref='display_name'
|
||||
className='form-control'
|
||||
placeholder='Ex: "Bugs", "Marketing", "办公室恋情"'
|
||||
maxLength='22'
|
||||
value={this.props.channelData.displayName}
|
||||
autoFocus={true}
|
||||
tabIndex='1'
|
||||
/>
|
||||
{displayNameError}
|
||||
<p className='input__help'>
|
||||
{'Channel URL: ' + prettyTeamURL + this.props.channelData.name + ' ('}
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.props.onChangeURLPressed}
|
||||
>
|
||||
{'change this URL'}
|
||||
</a>
|
||||
{')'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group less'>
|
||||
<div className='col-sm-2'>
|
||||
<label className='form__label control-label'>{'Description'}</label>
|
||||
<label className='form__label light'>{'(optional)'}</label>
|
||||
</div>
|
||||
<div className='col-sm-10'>
|
||||
<textarea
|
||||
className='form-control no-resize'
|
||||
ref='channel_desc'
|
||||
rows='4'
|
||||
placeholder='Description'
|
||||
maxLength='1024'
|
||||
value={this.props.channelData.description}
|
||||
onChange={this.handleChange}
|
||||
tabIndex='2'
|
||||
/>
|
||||
<p className='input__help'>
|
||||
{'The purpose of your channel. To help others decide whether to join.'}
|
||||
</p>
|
||||
{serverError}
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.props.onModalDismissed}
|
||||
>
|
||||
{'Cancel'}
|
||||
</button>
|
||||
<button
|
||||
onClick={this.handleSubmit}
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
tabIndex='3'
|
||||
>
|
||||
{'Create New ' + channelTerm}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NewChannelModal.defaultProps = {
|
||||
show: false,
|
||||
channelType: 'O',
|
||||
serverError: ''
|
||||
};
|
||||
NewChannelModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
channelType: React.PropTypes.string.isRequired,
|
||||
channelData: React.PropTypes.object.isRequired,
|
||||
serverError: React.PropTypes.string,
|
||||
onSubmitChannel: React.PropTypes.func.isRequired,
|
||||
onModalDismissed: React.PropTypes.func.isRequired,
|
||||
onTypeSwitched: React.PropTypes.func.isRequired,
|
||||
onChangeURLPressed: React.PropTypes.func.isRequired,
|
||||
onDataChanged: React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -152,7 +152,7 @@ export default class Post extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
id={post.id}
|
||||
id={'post_' + post.id}
|
||||
className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss}
|
||||
>
|
||||
{profilePic}
|
||||
|
||||
@@ -189,9 +189,15 @@ export default class PostList extends React.Component {
|
||||
this.scrollToBottom(true);
|
||||
|
||||
// the user clicked 'load more messages'
|
||||
} else if (this.gotMorePosts) {
|
||||
var lastPost = oldPosts[oldOrder[prevState.numToDisplay]];
|
||||
$('#' + lastPost.id)[0].scrollIntoView();
|
||||
} else if (this.gotMorePosts && oldOrder.length > 0) {
|
||||
let index;
|
||||
if (prevState.numToDisplay >= oldOrder.length) {
|
||||
index = oldOrder.length - 1;
|
||||
} else {
|
||||
index = prevState.numToDisplay;
|
||||
}
|
||||
const lastPost = oldPosts[oldOrder[index]];
|
||||
$('#post_' + lastPost.id)[0].scrollIntoView();
|
||||
this.gotMorePosts = false;
|
||||
} else {
|
||||
this.scrollTo(this.prevScrollTop);
|
||||
|
||||
@@ -12,6 +12,7 @@ var Utils = require('../utils/utils.jsx');
|
||||
var SidebarHeader = require('./sidebar_header.jsx');
|
||||
var SearchBox = require('./search_bar.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var NewChannelFlow = require('./new_channel_flow.jsx');
|
||||
|
||||
export default class Sidebar extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -28,6 +29,7 @@ export default class Sidebar extends React.Component {
|
||||
this.createChannelElement = this.createChannelElement.bind(this);
|
||||
|
||||
this.state = this.getStateFromStores();
|
||||
this.state.modal = '';
|
||||
this.state.loadingDMChannel = -1;
|
||||
}
|
||||
getStateFromStores() {
|
||||
@@ -473,8 +475,18 @@ export default class Sidebar extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
let showChannelModal = false;
|
||||
if (this.state.modal !== '') {
|
||||
showChannelModal = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NewChannelFlow
|
||||
show={showChannelModal}
|
||||
channelType={this.state.modal}
|
||||
onModalDismissed={() => this.setState({modal: ''})}
|
||||
/>
|
||||
<SidebarHeader
|
||||
teamDisplayName={this.props.teamDisplayName}
|
||||
teamType={this.props.teamType}
|
||||
@@ -508,11 +520,9 @@ export default class Sidebar extends React.Component {
|
||||
<a
|
||||
className='add-channel-btn'
|
||||
href='#'
|
||||
data-toggle='modal'
|
||||
data-target='#new_channel'
|
||||
data-channeltype='O'
|
||||
onClick={() => this.setState({modal: 'O'})}
|
||||
>
|
||||
+
|
||||
{'+'}
|
||||
</a>
|
||||
</h4>
|
||||
</li>
|
||||
@@ -537,11 +547,9 @@ export default class Sidebar extends React.Component {
|
||||
<a
|
||||
className='add-channel-btn'
|
||||
href='#'
|
||||
data-toggle='modal'
|
||||
data-target='#new_channel'
|
||||
data-channeltype='P'
|
||||
onClick={() => this.setState({modal: 'P'})}
|
||||
>
|
||||
+
|
||||
{'+'}
|
||||
</a>
|
||||
</h4>
|
||||
</li>
|
||||
|
||||
@@ -32,7 +32,7 @@ export default class SignupUserComplete extends React.Component {
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.state.user.username = React.findDOMNode(this.refs.name).value.trim();
|
||||
this.state.user.username = React.findDOMNode(this.refs.name).value.trim().toLowerCase();
|
||||
if (!this.state.user.username) {
|
||||
this.setState({nameError: 'This field is required', emailError: '', passwordError: '', serverError: ''});
|
||||
return;
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class TeamImportTab extends React.Component {
|
||||
var uploadHelpText = (
|
||||
<div>
|
||||
<p>{'Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p>
|
||||
<p>{'The Slack import to Mattermost is in "Preview". Slack bot posts and channels with underscores do not yet import.'}</p>
|
||||
<p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import.'}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export default class TeamSignupUsernamePage extends React.Component {
|
||||
submitNext(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var name = React.findDOMNode(this.refs.name).value.trim();
|
||||
var name = React.findDOMNode(this.refs.name).value.trim().toLowerCase();
|
||||
|
||||
var usernameError = Utils.isValidUsername(name);
|
||||
if (usernameError === 'Cannot use a reserved word as a username.') {
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class UserSettingsGeneralTab extends React.Component {
|
||||
e.preventDefault();
|
||||
|
||||
var user = this.props.user;
|
||||
var username = this.state.username.trim();
|
||||
var username = this.state.username.trim().toLowerCase();
|
||||
|
||||
var usernameError = utils.isValidUsername(username);
|
||||
if (usernameError === 'Cannot use a reserved word as a username.') {
|
||||
|
||||
@@ -105,6 +105,14 @@ export default class ViewImageModal extends React.Component {
|
||||
this.loadImage(this.state.imgId);
|
||||
}.bind(this));
|
||||
|
||||
$('#' + this.props.modalId).on('hidden.bs.modal', function onModalHide() {
|
||||
if (this.refs.video) {
|
||||
var video = React.findDOMNode(this.refs.video);
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
$(React.findDOMNode(this.refs.modal)).click(function onModalClick(e) {
|
||||
if (e.target === this || e.target === React.findDOMNode(this.refs.imageBody)) {
|
||||
$('.image_modal').modal('hide');
|
||||
@@ -211,6 +219,16 @@ export default class ViewImageModal extends React.Component {
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
} else if (fileType === 'video' || fileType === 'audio') {
|
||||
content = (
|
||||
<video
|
||||
ref='video'
|
||||
data-setup='{}'
|
||||
controls='controls'
|
||||
>
|
||||
<source src={Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename} />
|
||||
</video>
|
||||
);
|
||||
} else {
|
||||
// non-image files include a section providing details about the file
|
||||
var infoString = 'File type ' + fileInfo.ext.toUpperCase();
|
||||
|
||||
@@ -3,22 +3,21 @@
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"autolinker": "^0.18.1",
|
||||
"flux": "^2.1.1",
|
||||
"keymirror": "^0.1.1",
|
||||
"object-assign": "^3.0.0",
|
||||
"react": "^0.13.3",
|
||||
"react-zeroclipboard-mixin": "^0.1.0",
|
||||
"twemoji": "^1.4.1"
|
||||
"autolinker": "0.18.1",
|
||||
"flux": "2.1.1",
|
||||
"keymirror": "0.1.1",
|
||||
"object-assign": "3.0.0",
|
||||
"react-zeroclipboard-mixin": "0.1.0",
|
||||
"twemoji": "1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "^11.0.1",
|
||||
"envify": "^3.4.0",
|
||||
"babelify": "^6.1.3",
|
||||
"uglify-js": "^2.4.24",
|
||||
"watchify": "^3.3.1",
|
||||
"eslint": "^1.3.1",
|
||||
"eslint-plugin-react": "^3.3.1"
|
||||
"browserify": "11.0.1",
|
||||
"envify": "3.4.0",
|
||||
"babelify": "6.1.3",
|
||||
"uglify-js": "2.4.24",
|
||||
"watchify": "3.3.1",
|
||||
"eslint": "1.3.1",
|
||||
"eslint-plugin-react": "3.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "watchify --extension=jsx -o ../static/js/bundle.js -v -d ./**/*.jsx",
|
||||
|
||||
@@ -17,7 +17,6 @@ var RenameChannelModal = require('../components/rename_channel_modal.jsx');
|
||||
var EditPostModal = require('../components/edit_post_modal.jsx');
|
||||
var DeletePostModal = require('../components/delete_post_modal.jsx');
|
||||
var MoreChannelsModal = require('../components/more_channels.jsx');
|
||||
var NewChannelModal = require('../components/new_channel.jsx');
|
||||
var PostDeletedModal = require('../components/post_deleted_modal.jsx');
|
||||
var ChannelNotificationsModal = require('../components/channel_notifications.jsx');
|
||||
var UserSettingsModal = require('../components/user_settings_modal.jsx');
|
||||
@@ -153,11 +152,6 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) {
|
||||
document.getElementById('direct_channel_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<NewChannelModal />,
|
||||
document.getElementById('new_channel_modal')
|
||||
);
|
||||
|
||||
React.render(
|
||||
<PostListContainer />,
|
||||
document.getElementById('post-list')
|
||||
|
||||
@@ -828,14 +828,12 @@ export function isValidUsername(name) {
|
||||
} else if (name.length < 3 || name.length > 15) {
|
||||
error = 'Must be between 3 and 15 characters';
|
||||
} else if (!(/^[a-z0-9\.\-\_]+$/).test(name)) {
|
||||
error = "Must contain only lowercase letters, numbers, and the symbols '.', '-', and '_'.";
|
||||
error = "Must contain only letters, numbers, and the symbols '.', '-', and '_'.";
|
||||
} else if (!(/[a-z]/).test(name.charAt(0))) {
|
||||
error = 'First character must be a letter.';
|
||||
} else {
|
||||
var lowerName = name.toLowerCase().trim();
|
||||
|
||||
for (var i = 0; i < Constants.RESERVED_USERNAMES.length; i++) {
|
||||
if (lowerName === Constants.RESERVED_USERNAMES[i]) {
|
||||
if (name === Constants.RESERVED_USERNAMES[i]) {
|
||||
error = 'Cannot use a reserved word as a username.';
|
||||
break;
|
||||
}
|
||||
@@ -1127,3 +1125,14 @@ export function importSlack(file, success, error) {
|
||||
|
||||
client.importSlack(formData, success, error);
|
||||
}
|
||||
|
||||
export function getTeamURLFromAddressBar() {
|
||||
return window.location.href.split('/channels')[0];
|
||||
}
|
||||
|
||||
export function getShortenedTeamURL() {
|
||||
const teamURL = getTeamURLFromAddressBar();
|
||||
if (teamURL.length > 24) {
|
||||
return teamURL.substring(0, 10) + '...' + teamURL.substring(teamURL.length - 12, teamURL.length - 1) + '/';
|
||||
}
|
||||
}
|
||||
|
||||
33
web/sass-files/sass/partials/_forms.scss
Обычный файл
33
web/sass-files/sass/partials/_forms.scss
Обычный файл
@@ -0,0 +1,33 @@
|
||||
.form-horizontal {
|
||||
.modal-intro {
|
||||
margin: -10px 0 30px;
|
||||
}
|
||||
.form__label {
|
||||
text-align: left;
|
||||
padding-right: 3px;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
&.light {
|
||||
color: #999;
|
||||
font-size: 1.05em;
|
||||
font-style: italic;
|
||||
padding-top: 2px;
|
||||
}
|
||||
}
|
||||
.input__help {
|
||||
color: #777;
|
||||
margin: 10px 0 0 10px;
|
||||
&.error {
|
||||
color: #a94442;
|
||||
}
|
||||
}
|
||||
.form-control {
|
||||
font-weight: normal;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
&.less {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,8 @@
|
||||
li a {
|
||||
padding: 3px 20px;
|
||||
color: #555;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
.dropdown__icon {
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
}
|
||||
.more-channel-table {
|
||||
margin: 0;
|
||||
table-layout: fixed;
|
||||
p {
|
||||
font-size: 0.9em;
|
||||
overflow: hidden;
|
||||
@@ -154,7 +155,7 @@
|
||||
&.td--action {
|
||||
text-align: right;
|
||||
padding: 8px 15px 8px 8px;
|
||||
width: 70px;
|
||||
width: 80px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
}
|
||||
.dropdown-menu {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
max-width: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
.search__form {
|
||||
margin: 0;
|
||||
@@ -49,7 +52,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 72%;
|
||||
color: #777;
|
||||
color: #fff;
|
||||
background: #2389D7;
|
||||
@include border-radius(50px);
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
@import "partials/videos";
|
||||
@import "partials/settings";
|
||||
@import "partials/modal";
|
||||
@import "partials/forms";
|
||||
@import "partials/mentions";
|
||||
@import "partials/command-box";
|
||||
@import "partials/error";
|
||||
|
||||
5
web/static/css/bootstrap-3.3.1.min.css
поставляемый
5
web/static/css/bootstrap-3.3.1.min.css
поставляемый
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
5
web/static/css/bootstrap-3.3.5.min.css
поставляемый
Обычный файл
5
web/static/css/bootstrap-3.3.5.min.css
поставляемый
Обычный файл
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
7
web/static/js/bootstrap-3.3.1.min.js
поставляемый
7
web/static/js/bootstrap-3.3.1.min.js
поставляемый
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
471
web/static/js/bootstrap-3.3.1.js → web/static/js/bootstrap-3.3.5.js
поставляемый
471
web/static/js/bootstrap-3.3.1.js → web/static/js/bootstrap-3.3.5.js
поставляемый
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
7
web/static/js/bootstrap-3.3.5.min.js
поставляемый
Обычный файл
7
web/static/js/bootstrap-3.3.5.min.js
поставляемый
Обычный файл
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
1024
web/static/js/jasny-bootstrap.js
Обычный файл
1024
web/static/js/jasny-bootstrap.js
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
6
web/static/js/jasny-bootstrap.min.js
поставляемый
Обычный файл
6
web/static/js/jasny-bootstrap.min.js
поставляемый
Обычный файл
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
15678
web/static/js/react-bootstrap-0.25.1.js
Обычный файл
15678
web/static/js/react-bootstrap-0.25.1.js
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
14
web/static/js/react-bootstrap-0.25.1.min.js
поставляемый
Обычный файл
14
web/static/js/react-bootstrap-0.25.1.min.js
поставляемый
Обычный файл
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
16
web/static/js/react-with-addons-0.13.1.min.js
поставляемый
16
web/static/js/react-with-addons-0.13.1.min.js
поставляемый
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* React (with addons) v0.13.1
|
||||
* React (with addons) v0.13.3
|
||||
*/
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.React = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
|
||||
/**
|
||||
@@ -710,7 +710,9 @@ var isUnitlessNumber = {
|
||||
columnCount: true,
|
||||
flex: true,
|
||||
flexGrow: true,
|
||||
flexPositive: true,
|
||||
flexShrink: true,
|
||||
flexNegative: true,
|
||||
fontWeight: true,
|
||||
lineClamp: true,
|
||||
lineHeight: true,
|
||||
@@ -723,7 +725,9 @@ var isUnitlessNumber = {
|
||||
|
||||
// SVG-related properties
|
||||
fillOpacity: true,
|
||||
strokeOpacity: true
|
||||
strokeDashoffset: true,
|
||||
strokeOpacity: true,
|
||||
strokeWidth: true
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -3788,6 +3792,7 @@ var HTMLDOMPropertyConfig = {
|
||||
headers: null,
|
||||
height: MUST_USE_ATTRIBUTE,
|
||||
hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
|
||||
high: null,
|
||||
href: null,
|
||||
hrefLang: null,
|
||||
htmlFor: null,
|
||||
@@ -3798,6 +3803,7 @@ var HTMLDOMPropertyConfig = {
|
||||
lang: null,
|
||||
list: MUST_USE_ATTRIBUTE,
|
||||
loop: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
|
||||
low: null,
|
||||
manifest: MUST_USE_ATTRIBUTE,
|
||||
marginHeight: null,
|
||||
marginWidth: null,
|
||||
@@ -3812,6 +3818,7 @@ var HTMLDOMPropertyConfig = {
|
||||
name: null,
|
||||
noValidate: HAS_BOOLEAN_VALUE,
|
||||
open: HAS_BOOLEAN_VALUE,
|
||||
optimum: null,
|
||||
pattern: null,
|
||||
placeholder: null,
|
||||
poster: null,
|
||||
@@ -3825,6 +3832,7 @@ var HTMLDOMPropertyConfig = {
|
||||
rowSpan: null,
|
||||
sandbox: null,
|
||||
scope: null,
|
||||
scoped: HAS_BOOLEAN_VALUE,
|
||||
scrolling: null,
|
||||
seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
|
||||
selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
|
||||
@@ -3866,7 +3874,9 @@ var HTMLDOMPropertyConfig = {
|
||||
itemID: MUST_USE_ATTRIBUTE,
|
||||
itemRef: MUST_USE_ATTRIBUTE,
|
||||
// property is supported for OpenGraph in meta tags.
|
||||
property: null
|
||||
property: null,
|
||||
// IE-only attribute that controls focus behavior
|
||||
unselectable: MUST_USE_ATTRIBUTE
|
||||
},
|
||||
DOMAttributeNames: {
|
||||
acceptCharset: 'accept-charset',
|
||||
@@ -4475,7 +4485,7 @@ if ("production" !== "development") {
|
||||
if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
|
||||
console.debug(
|
||||
'Download the React DevTools for a better development experience: ' +
|
||||
'http://fb.me/react-devtools'
|
||||
'https://fb.me/react-devtools'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4502,7 +4512,7 @@ if ("production" !== "development") {
|
||||
if (!expectedFeatures[i]) {
|
||||
console.error(
|
||||
'One or more ES5 shim/shams expected by React are not available: ' +
|
||||
'http://fb.me/react-warning-polyfills'
|
||||
'https://fb.me/react-warning-polyfills'
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -4510,7 +4520,7 @@ if ("production" !== "development") {
|
||||
}
|
||||
}
|
||||
|
||||
React.version = '0.13.1';
|
||||
React.version = '0.13.3';
|
||||
|
||||
module.exports = React;
|
||||
|
||||
@@ -6229,7 +6239,7 @@ var ReactClass = {
|
||||
("production" !== "development" ? warning(
|
||||
this instanceof Constructor,
|
||||
'Something is calling a React component directly. Use a factory or ' +
|
||||
'JSX instead. See: http://fb.me/react-legacyfactory'
|
||||
'JSX instead. See: https://fb.me/react-legacyfactory'
|
||||
) : null);
|
||||
}
|
||||
|
||||
@@ -6439,20 +6449,38 @@ ReactComponent.prototype.forceUpdate = function(callback) {
|
||||
*/
|
||||
if ("production" !== "development") {
|
||||
var deprecatedAPIs = {
|
||||
getDOMNode: 'getDOMNode',
|
||||
isMounted: 'isMounted',
|
||||
replaceProps: 'replaceProps',
|
||||
replaceState: 'replaceState',
|
||||
setProps: 'setProps'
|
||||
getDOMNode: [
|
||||
'getDOMNode',
|
||||
'Use React.findDOMNode(component) instead.'
|
||||
],
|
||||
isMounted: [
|
||||
'isMounted',
|
||||
'Instead, make sure to clean up subscriptions and pending requests in ' +
|
||||
'componentWillUnmount to prevent memory leaks.'
|
||||
],
|
||||
replaceProps: [
|
||||
'replaceProps',
|
||||
'Instead call React.render again at the top level.'
|
||||
],
|
||||
replaceState: [
|
||||
'replaceState',
|
||||
'Refactor your code to use setState instead (see ' +
|
||||
'https://github.com/facebook/react/issues/3236).'
|
||||
],
|
||||
setProps: [
|
||||
'setProps',
|
||||
'Instead call React.render again at the top level.'
|
||||
]
|
||||
};
|
||||
var defineDeprecationWarning = function(methodName, displayName) {
|
||||
var defineDeprecationWarning = function(methodName, info) {
|
||||
try {
|
||||
Object.defineProperty(ReactComponent.prototype, methodName, {
|
||||
get: function() {
|
||||
("production" !== "development" ? warning(
|
||||
false,
|
||||
'%s(...) is deprecated in plain JavaScript React classes.',
|
||||
displayName
|
||||
'%s(...) is deprecated in plain JavaScript React classes. %s',
|
||||
info[0],
|
||||
info[1]
|
||||
) : null);
|
||||
return undefined;
|
||||
}
|
||||
@@ -6802,6 +6830,14 @@ var ReactCompositeComponentMixin = {
|
||||
'Did you mean to define a state property instead?',
|
||||
this.getName() || 'a component'
|
||||
) : null);
|
||||
("production" !== "development" ? warning(
|
||||
!inst.getDefaultProps ||
|
||||
inst.getDefaultProps.isReactClassApproved,
|
||||
'getDefaultProps was defined on %s, a plain JavaScript class. ' +
|
||||
'This is only supported for classes created using React.createClass. ' +
|
||||
'Use a static property to define defaultProps instead.',
|
||||
this.getName() || 'a component'
|
||||
) : null);
|
||||
("production" !== "development" ? warning(
|
||||
!inst.propTypes,
|
||||
'propTypes was defined as an instance property on %s. Use a static ' +
|
||||
@@ -6838,6 +6874,7 @@ var ReactCompositeComponentMixin = {
|
||||
this._pendingReplaceState = false;
|
||||
this._pendingForceUpdate = false;
|
||||
|
||||
var childContext;
|
||||
var renderedElement;
|
||||
|
||||
var previouslyMounting = ReactLifeCycle.currentlyMountingInstance;
|
||||
@@ -6852,7 +6889,8 @@ var ReactCompositeComponentMixin = {
|
||||
}
|
||||
}
|
||||
|
||||
renderedElement = this._renderValidatedComponent();
|
||||
childContext = this._getValidatedChildContext(context);
|
||||
renderedElement = this._renderValidatedComponent(childContext);
|
||||
} finally {
|
||||
ReactLifeCycle.currentlyMountingInstance = previouslyMounting;
|
||||
}
|
||||
@@ -6866,7 +6904,7 @@ var ReactCompositeComponentMixin = {
|
||||
this._renderedComponent,
|
||||
rootID,
|
||||
transaction,
|
||||
this._processChildContext(context)
|
||||
this._mergeChildContext(context, childContext)
|
||||
);
|
||||
if (inst.componentDidMount) {
|
||||
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
|
||||
@@ -6996,7 +7034,7 @@ var ReactCompositeComponentMixin = {
|
||||
* @return {object}
|
||||
* @private
|
||||
*/
|
||||
_processChildContext: function(currentContext) {
|
||||
_getValidatedChildContext: function(currentContext) {
|
||||
var inst = this._instance;
|
||||
var childContext = inst.getChildContext && inst.getChildContext();
|
||||
if (childContext) {
|
||||
@@ -7021,6 +7059,13 @@ var ReactCompositeComponentMixin = {
|
||||
name
|
||||
) : invariant(name in inst.constructor.childContextTypes));
|
||||
}
|
||||
return childContext;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
_mergeChildContext: function(currentContext, childContext) {
|
||||
if (childContext) {
|
||||
return assign({}, currentContext, childContext);
|
||||
}
|
||||
return currentContext;
|
||||
@@ -7280,6 +7325,10 @@ var ReactCompositeComponentMixin = {
|
||||
return inst.state;
|
||||
}
|
||||
|
||||
if (replace && queue.length === 1) {
|
||||
return queue[0];
|
||||
}
|
||||
|
||||
var nextState = assign({}, replace ? queue[0] : inst.state);
|
||||
for (var i = replace ? 1 : 0; i < queue.length; i++) {
|
||||
var partial = queue[i];
|
||||
@@ -7349,13 +7398,14 @@ var ReactCompositeComponentMixin = {
|
||||
_updateRenderedComponent: function(transaction, context) {
|
||||
var prevComponentInstance = this._renderedComponent;
|
||||
var prevRenderedElement = prevComponentInstance._currentElement;
|
||||
var nextRenderedElement = this._renderValidatedComponent();
|
||||
var childContext = this._getValidatedChildContext();
|
||||
var nextRenderedElement = this._renderValidatedComponent(childContext);
|
||||
if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
|
||||
ReactReconciler.receiveComponent(
|
||||
prevComponentInstance,
|
||||
nextRenderedElement,
|
||||
transaction,
|
||||
this._processChildContext(context)
|
||||
this._mergeChildContext(context, childContext)
|
||||
);
|
||||
} else {
|
||||
// These two IDs are actually the same! But nothing should rely on that.
|
||||
@@ -7371,7 +7421,7 @@ var ReactCompositeComponentMixin = {
|
||||
this._renderedComponent,
|
||||
thisID,
|
||||
transaction,
|
||||
context
|
||||
this._mergeChildContext(context, childContext)
|
||||
);
|
||||
this._replaceNodeWithMarkupByID(prevComponentID, nextMarkup);
|
||||
}
|
||||
@@ -7409,11 +7459,12 @@ var ReactCompositeComponentMixin = {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_renderValidatedComponent: function() {
|
||||
_renderValidatedComponent: function(childContext) {
|
||||
var renderedComponent;
|
||||
var previousContext = ReactContext.current;
|
||||
ReactContext.current = this._processChildContext(
|
||||
this._currentElement._context
|
||||
ReactContext.current = this._mergeChildContext(
|
||||
this._currentElement._context,
|
||||
childContext
|
||||
);
|
||||
ReactCurrentOwner.current = this;
|
||||
try {
|
||||
@@ -7778,6 +7829,7 @@ var ReactDOM = mapObject({
|
||||
|
||||
// SVG
|
||||
circle: 'circle',
|
||||
clipPath: 'clipPath',
|
||||
defs: 'defs',
|
||||
ellipse: 'ellipse',
|
||||
g: 'g',
|
||||
@@ -7927,11 +7979,13 @@ function assertValidProps(props) {
|
||||
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'
|
||||
) : invariant(props.children == null));
|
||||
("production" !== "development" ? invariant(
|
||||
props.dangerouslySetInnerHTML.__html != null,
|
||||
typeof props.dangerouslySetInnerHTML === 'object' &&
|
||||
'__html' in props.dangerouslySetInnerHTML,
|
||||
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
|
||||
'Please visit http://fb.me/react-invariant-dangerously-set-inner-html ' +
|
||||
'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' +
|
||||
'for more information.'
|
||||
) : invariant(props.dangerouslySetInnerHTML.__html != null));
|
||||
) : invariant(typeof props.dangerouslySetInnerHTML === 'object' &&
|
||||
'__html' in props.dangerouslySetInnerHTML));
|
||||
}
|
||||
if ("production" !== "development") {
|
||||
("production" !== "development" ? warning(
|
||||
@@ -8239,6 +8293,8 @@ ReactDOMComponent.Mixin = {
|
||||
if (propKey === STYLE) {
|
||||
if (nextProp) {
|
||||
nextProp = this._previousStyleCopy = assign({}, nextProp);
|
||||
} else {
|
||||
this._previousStyleCopy = null;
|
||||
}
|
||||
if (lastProp) {
|
||||
// Unset styles on `lastProp` but not on `nextProp`.
|
||||
@@ -10721,7 +10777,7 @@ function warnAndMonitorForKeyUse(message, element, parentType) {
|
||||
|
||||
("production" !== "development" ? warning(
|
||||
false,
|
||||
message + '%s%s See http://fb.me/react-warning-keys for more information.',
|
||||
message + '%s%s See https://fb.me/react-warning-keys for more information.',
|
||||
parentOrOwnerAddendum,
|
||||
childOwnerAddendum
|
||||
) : null);
|
||||
@@ -10845,9 +10901,9 @@ function warnForPropsMutation(propName, element) {
|
||||
|
||||
("production" !== "development" ? warning(
|
||||
false,
|
||||
'Don\'t set .props.%s of the React component%s. ' +
|
||||
'Instead, specify the correct value when ' +
|
||||
'initially creating the element.%s',
|
||||
'Don\'t set .props.%s of the React component%s. Instead, specify the ' +
|
||||
'correct value when initially creating the element or use ' +
|
||||
'React.cloneElement to make a new element with updated props.%s',
|
||||
propName,
|
||||
elementInfo,
|
||||
ownerInfo
|
||||
@@ -15236,6 +15292,7 @@ var ReactUpdates = _dereq_(100);
|
||||
var SyntheticEvent = _dereq_(108);
|
||||
|
||||
var assign = _dereq_(29);
|
||||
var emptyObject = _dereq_(130);
|
||||
|
||||
var topLevelTypes = EventConstants.topLevelTypes;
|
||||
|
||||
@@ -15577,6 +15634,9 @@ assign(
|
||||
);
|
||||
|
||||
ReactShallowRenderer.prototype.render = function(element, context) {
|
||||
if (!context) {
|
||||
context = emptyObject;
|
||||
}
|
||||
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
|
||||
this._render(element, transaction, context);
|
||||
ReactUpdates.ReactReconcileTransaction.release(transaction);
|
||||
@@ -15717,7 +15777,7 @@ for (eventType in topLevelTypes) {
|
||||
|
||||
module.exports = ReactTestUtils;
|
||||
|
||||
},{"100":100,"108":108,"16":16,"18":18,"21":21,"29":29,"31":31,"33":33,"43":43,"63":63,"65":65,"72":72,"73":73,"77":77}],96:[function(_dereq_,module,exports){
|
||||
},{"100":100,"108":108,"130":130,"16":16,"18":18,"21":21,"29":29,"31":31,"33":33,"43":43,"63":63,"65":65,"72":72,"73":73,"77":77}],96:[function(_dereq_,module,exports){
|
||||
/**
|
||||
* Copyright 2013-2015, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
@@ -16762,6 +16822,7 @@ var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
|
||||
|
||||
var SVGDOMPropertyConfig = {
|
||||
Properties: {
|
||||
clipPath: MUST_USE_ATTRIBUTE,
|
||||
cx: MUST_USE_ATTRIBUTE,
|
||||
cy: MUST_USE_ATTRIBUTE,
|
||||
d: MUST_USE_ATTRIBUTE,
|
||||
@@ -16807,6 +16868,7 @@ var SVGDOMPropertyConfig = {
|
||||
y: MUST_USE_ATTRIBUTE
|
||||
},
|
||||
DOMAttributeNames: {
|
||||
clipPath: 'clip-path',
|
||||
fillOpacity: 'fill-opacity',
|
||||
fontFamily: 'font-family',
|
||||
fontSize: 'font-size',
|
||||
@@ -19713,6 +19775,7 @@ var shouldWrap = {
|
||||
// Force wrapping for SVG elements because if they get created inside a <div>,
|
||||
// they will be initialized in the wrong namespace (and will not display).
|
||||
'circle': true,
|
||||
'clipPath': true,
|
||||
'defs': true,
|
||||
'ellipse': true,
|
||||
'g': true,
|
||||
@@ -19755,6 +19818,7 @@ var markupWrap = {
|
||||
'th': trWrap,
|
||||
|
||||
'circle': svgWrap,
|
||||
'clipPath': svgWrap,
|
||||
'defs': svgWrap,
|
||||
'ellipse': svgWrap,
|
||||
'g': svgWrap,
|
||||
@@ -20100,6 +20164,7 @@ assign(
|
||||
function isInternalComponentType(type) {
|
||||
return (
|
||||
typeof type === 'function' &&
|
||||
typeof type.prototype !== 'undefined' &&
|
||||
typeof type.prototype.mountComponent === 'function' &&
|
||||
typeof type.prototype.receiveComponent === 'function'
|
||||
);
|
||||
@@ -21355,11 +21420,14 @@ module.exports = traverseAllChildren;
|
||||
* @providesModule update
|
||||
*/
|
||||
|
||||
/* global hasOwnProperty:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assign = _dereq_(29);
|
||||
var keyOf = _dereq_(157);
|
||||
var invariant = _dereq_(150);
|
||||
var hasOwnProperty = {}.hasOwnProperty;
|
||||
|
||||
function shallowCopy(x) {
|
||||
if (Array.isArray(x)) {
|
||||
@@ -21419,7 +21487,7 @@ function update(value, spec) {
|
||||
COMMAND_SET
|
||||
) : invariant(typeof spec === 'object'));
|
||||
|
||||
if (spec.hasOwnProperty(COMMAND_SET)) {
|
||||
if (hasOwnProperty.call(spec, COMMAND_SET)) {
|
||||
("production" !== "development" ? invariant(
|
||||
Object.keys(spec).length === 1,
|
||||
'Cannot have more than one key in an object with %s',
|
||||
@@ -21431,7 +21499,7 @@ function update(value, spec) {
|
||||
|
||||
var nextValue = shallowCopy(value);
|
||||
|
||||
if (spec.hasOwnProperty(COMMAND_MERGE)) {
|
||||
if (hasOwnProperty.call(spec, COMMAND_MERGE)) {
|
||||
var mergeObj = spec[COMMAND_MERGE];
|
||||
("production" !== "development" ? invariant(
|
||||
mergeObj && typeof mergeObj === 'object',
|
||||
@@ -21448,21 +21516,21 @@ function update(value, spec) {
|
||||
assign(nextValue, spec[COMMAND_MERGE]);
|
||||
}
|
||||
|
||||
if (spec.hasOwnProperty(COMMAND_PUSH)) {
|
||||
if (hasOwnProperty.call(spec, COMMAND_PUSH)) {
|
||||
invariantArrayCase(value, spec, COMMAND_PUSH);
|
||||
spec[COMMAND_PUSH].forEach(function(item) {
|
||||
nextValue.push(item);
|
||||
});
|
||||
}
|
||||
|
||||
if (spec.hasOwnProperty(COMMAND_UNSHIFT)) {
|
||||
if (hasOwnProperty.call(spec, COMMAND_UNSHIFT)) {
|
||||
invariantArrayCase(value, spec, COMMAND_UNSHIFT);
|
||||
spec[COMMAND_UNSHIFT].forEach(function(item) {
|
||||
nextValue.unshift(item);
|
||||
});
|
||||
}
|
||||
|
||||
if (spec.hasOwnProperty(COMMAND_SPLICE)) {
|
||||
if (hasOwnProperty.call(spec, COMMAND_SPLICE)) {
|
||||
("production" !== "development" ? invariant(
|
||||
Array.isArray(value),
|
||||
'Expected %s target to be an array; got %s',
|
||||
@@ -21488,7 +21556,7 @@ function update(value, spec) {
|
||||
});
|
||||
}
|
||||
|
||||
if (spec.hasOwnProperty(COMMAND_APPLY)) {
|
||||
if (hasOwnProperty.call(spec, COMMAND_APPLY)) {
|
||||
("production" !== "development" ? invariant(
|
||||
typeof spec[COMMAND_APPLY] === 'function',
|
||||
'update(): expected spec of %s to be a function; got %s.',
|
||||
18
web/static/js/react-with-addons-0.13.3.min.js
поставляемый
Обычный файл
18
web/static/js/react-with-addons-0.13.3.min.js
поставляемый
Обычный файл
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -52,6 +52,7 @@
|
||||
<script>
|
||||
window.setup_channel_page('{{ .Props.TeamDisplayName }}', '{{ .Props.TeamType }}', '{{ .Props.TeamId }}', '{{ .Props.ChannelName }}', '{{ .Props.ChannelId }}');
|
||||
$('body').tooltip( {selector: '[data-toggle=tooltip]'} );
|
||||
$('.modal-body').css('max-height', $(window).height() * 0.7);
|
||||
$('.modal-body').perfectScrollbar();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -18,12 +18,13 @@
|
||||
<link rel="manifest" href="/static/config/manifest.json">
|
||||
<!-- Android add to homescreen -->
|
||||
|
||||
<link rel="stylesheet" href="/static/css/bootstrap-3.3.1.min.css">
|
||||
<link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
|
||||
<link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<script src="/static/js/react-with-addons-0.13.1.js"></script>
|
||||
<script src="/static/js/react-with-addons-0.13.3.js"></script>
|
||||
<script src="/static/js/jquery-1.11.1.js"></script>
|
||||
<script src="/static/js/bootstrap-3.3.1.js"></script>
|
||||
<script src="/static/js/bootstrap-3.3.5.js"></script>
|
||||
<script src="/static/js/react-bootstrap-0.25.1.js"></script>
|
||||
|
||||
<link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon">
|
||||
|
||||
@@ -36,9 +36,14 @@ func TearDown() {
|
||||
func TestStatic(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
resp, _ := http.Get(URL + "/static/images/favicon.ico")
|
||||
// add a short delay to make sure the server is ready to receive requests
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
resp, err := http.Get(URL + "/static/images/favicon.ico")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("got error while trying to get static files %v", err)
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("couldn't get static files %v", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user