From a97c51580ee996046725d570bc36b96feb9982b5 Mon Sep 17 00:00:00 2001 From: Chia Yu Pai Date: Fri, 15 Jan 2016 14:22:36 +0800 Subject: Update github.md add default callback URL path --- doc/integration/github.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/integration/github.md b/doc/integration/github.md index a789d2c814f..dbcb2d6a006 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -18,6 +18,7 @@ GitHub will generate an application ID and secret key for you to use. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - Authorization callback URL: 'https://gitlab.company.com/' + - If install from source, default callback URL is '${YOUR_DOMAIN}/import/github/callback' 1. Select "Register application". 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). -- cgit v1.2.1 From da866795edcf77298741f7acc70dd49daeec9eed Mon Sep 17 00:00:00 2001 From: Chia Yu Pai Date: Tue, 1 Mar 2016 18:30:22 +0800 Subject: Update github default callback url --- doc/integration/github.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/integration/github.md b/doc/integration/github.md index dbcb2d6a006..ce2434330ea 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -17,8 +17,7 @@ GitHub will generate an application ID and secret key for you to use. - Application name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - - Authorization callback URL: 'https://gitlab.company.com/' - - If install from source, default callback URL is '${YOUR_DOMAIN}/import/github/callback' + - Default authorization callback URL is '${YOUR_DOMAIN}/import/github/callback' 1. Select "Register application". 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). -- cgit v1.2.1 From b25d42cee9db265afe15d8ef192490a1ce2e3471 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Mon, 21 Mar 2016 21:25:15 -0500 Subject: Update LDAP docs [ci skip] --- doc/README.md | 4 +- doc/administration/auth/README.md | 11 ++ doc/administration/auth/ldap.md | 277 ++++++++++++++++++++++++++++++++++++++ doc/integration/ldap.md | 227 +------------------------------ 4 files changed, 292 insertions(+), 227 deletions(-) create mode 100644 doc/administration/auth/README.md create mode 100644 doc/administration/auth/ldap.md diff --git a/doc/README.md b/doc/README.md index 08d0a6a5bfb..739b7e10194 100644 --- a/doc/README.md +++ b/doc/README.md @@ -19,10 +19,12 @@ ## Administrator documentation +- [Authentication/Authorization](administration/auth/README.md) Configure + external authentication with LDAP, SAML, CAS and additional Omniauth providers. - [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough. - [Install](install/README.md) Requirements, directory structures and installation from source. - [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components -- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. +- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. - [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. - [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. - [Log system](logs/logs.md) Log system. diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md new file mode 100644 index 00000000000..07e548aaabe --- /dev/null +++ b/doc/administration/auth/README.md @@ -0,0 +1,11 @@ +# Authentication and Authorization + +GitLab integrates with the following external authentication and authorization +providers. + +- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP, + and 389 Server +- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, + Bitbucket, Facebook, Shibboleth, Crowd and Azure +- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider +- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md new file mode 100644 index 00000000000..237700bbcd9 --- /dev/null +++ b/doc/administration/auth/ldap.md @@ -0,0 +1,277 @@ +# LDAP + +GitLab integrates with LDAP to support user authentication. +This integration works with most LDAP-compliant directory +servers, including Microsoft Active Directory, Apple Open Directory, Open LDAP, +and 389 Server. GitLab EE includes enhanced integration, including group +membership syncing. + +## Security + +GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email' +or 'userPrincipalName' attribute. An LDAP user who is allowed to change their +email on the LDAP server can potentially +[take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users) +on your GitLab server. + +We recommend against using LDAP integration if your LDAP users are +allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on +the LDAP server. + +### User deletion + +If a user is deleted from the LDAP server, they will be blocked in GitLab, as +well. Users will be immediately blocked from logging in. However, there is an +LDAP check cache time (sync time) of one hour (see note). This means users that +are already logged in or are using Git over SSH will still be able to access +GitLab for up to one hour. Manually block the user in the GitLab Admin area to +immediately block all access. + +>**Note**: GitLab EE supports a configurable sync time, with a default +of one hour. + +## Configuration + +To enable LDAP integration you need to add your LDAP server settings in +`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. + +>**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to +one GitLab server. + +Prior to version 7.4, GitLab used a different syntax for configuring +LDAP integration. The old LDAP integration syntax still works but may be +removed in a future version. If your `gitlab.rb` or `gitlab.yml` file contains +LDAP settings in both the old syntax and the new syntax, only the __old__ +syntax will be used by GitLab. + +The configuration inside `gitlab_rails['ldap_servers']` below is sensitive to +incorrect indentation. Be sure to retain the indentation given in the example. +Copy/paste can sometimes cause problems. + +**Omnibus configuration** + +```ruby +gitlab_rails['ldap_enabled'] = true +gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below +main: # 'main' is the GitLab 'provider ID' of this LDAP server + ## label + # + # A human-friendly name for your LDAP server. It is OK to change the label later, + # for instance if you find out it is too large to fit on the web page. + # + # Example: 'Paris' or 'Acme, Ltd.' + label: 'LDAP' + + host: '_your_ldap_server' + port: 389 + uid: 'sAMAccountName' + method: 'plain' # "tls" or "ssl" or "plain" + bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' + password: '_the_password_of_the_bind_user' + + # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking + # a request if the LDAP server becomes unresponsive. + # A value of 0 means there is no timeout. + timeout: 10 + + # This setting specifies if LDAP server is Active Directory LDAP server. + # For non AD servers it skips the AD specific queries. + # If your LDAP server is not AD, set this to false. + active_directory: true + + # If allow_username_or_email_login is enabled, GitLab will ignore everything + # after the first '@' in the LDAP username submitted by the user on login. + # + # Example: + # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials; + # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'. + # + # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to + # disable this setting, because the userPrincipalName contains an '@'. + allow_username_or_email_login: false + + # To maintain tight control over the number of active users on your GitLab installation, + # enable this setting to keep new users blocked until they have been cleared by the admin + # (default: false). + block_auto_created_users: false + + # Base where we can search for users + # + # Ex. ou=People,dc=gitlab,dc=example + # + base: '' + + # Filter LDAP users + # + # Format: RFC 4515 https://tools.ietf.org/search/rfc4515 + # Ex. (employeeType=developer) + # + # Note: GitLab does not support omniauth-ldap's custom filter syntax. + # + user_filter: '' + + # LDAP attributes that GitLab will use to create an account for the LDAP user. + # The specified attribute can either be the attribute name as a string (e.g. 'mail'), + # or an array of attribute names to try in order (e.g. ['mail', 'email']). + # Note that the user's LDAP login will always be the attribute specified as `uid` above. + attributes: + # The username will be used in paths for the user's own projects + # (like `gitlab.example.com/username/project`) and when mentioning + # them in issues, merge request and comments (like `@username`). + # If the attribute specified for `username` contains an email address, + # the GitLab username will be the part of the email address before the '@'. + username: ['uid', 'userid', 'sAMAccountName'] + email: ['mail', 'email', 'userPrincipalName'] + + # If no full name could be found at the attribute specified for `name`, + # the full name is determined using the attributes specified for + # `first_name` and `last_name`. + name: 'cn' + first_name: 'givenName' + last_name: 'sn' + + ## EE only + + # Base where we can search for groups + # + # Ex. ou=groups,dc=gitlab,dc=example + # + group_base: '' + + # The CN of a group containing GitLab administrators + # + # Ex. administrators + # + # Note: Not `cn=administrators` or the full DN + # + admin_group: '' + + # The LDAP attribute containing a user's public SSH key + # + # Ex. ssh_public_key + # + sync_ssh_keys: false + +# GitLab EE only: add more LDAP servers +# Choose an ID made of a-z and 0-9 . This ID will be stored in the database +# so that GitLab can remember which LDAP server a user belongs to. +# uswest2: +# label: +# host: +# .... +EOS +``` + +**Source configuration** + +Use the same format as `gitlab_rails['ldap_servers']` for the contents under +`servers:` in the example below: + +``` +production: + # snip... + ldap: + enabled: false + servers: + main: # 'main' is the GitLab 'provider ID' of this LDAP server + ## label + # + # A human-friendly name for your LDAP server. It is OK to change the label later, + # for instance if you find out it is too large to fit on the web page. + # + # Example: 'Paris' or 'Acme, Ltd.' + label: 'LDAP' + # snip... +``` + +## Using an LDAP filter to limit access to your GitLab server + +If you want to limit all GitLab access to a subset of the LDAP users on your +LDAP server, the first step should be to narrow the configured `base`. However, +it is sometimes necessary to filter users further. In this case, you can set up +an LDAP user filter. The filter must comply with +[RFC 4515](https://tools.ietf.org/search/rfc4515). + +**Omnibus configuration** + +```ruby +gitlab_rails['ldap_servers'] = YAML.load <<-EOS +main: + # snip... + user_filter: '(employeeType=developer)' +EOS +``` + +**Source configuration** + +```yaml +production: + ldap: + servers: + main: + # snip... + user_filter: '(employeeType=developer)' +``` + +Tip: If you want to limit access to the nested members of an Active Directory +group you can use the following syntax: + +``` +(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com) +``` + +Please note that GitLab does not support the custom filter syntax used by +omniauth-ldap. + +## Enabling LDAP sign-in for existing GitLab users + +When a user signs in to GitLab with LDAP for the first time, and their LDAP +email address is the primary email address of an existing GitLab user, then +the LDAP DN will be associated with the existing user. If the LDAP email +attribute is not found in GitLab's database, a new user is created. + +In other words, if an existing GitLab user wants to enable LDAP sign-in for +themselves, they should check that their GitLab email address matches their +LDAP email address, and then sign into GitLab via their LDAP credentials. + +## Limitations + +### TLS Client Authentication + +Not implemented by `Net::LDAP`. +You should disable anonymous LDAP authentication and enable simple or SASL +authentication. The TLS client authentication setting in your LDAP server cannot +be mandatory and clients cannot be authenticated with the TLS protocol. + +### TLS Server Authentication + +Not supported by GitLab's configuration options. +When setting `method: ssl`, the underlying authentication method used by +`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with +the LDAP server before any LDAP-protocol data is exchanged but no validation of +the LDAP server's SSL certificate is performed. + +## Troubleshooting + +### Invalid credentials when logging in + +- Make sure the user you are binding with has enough permissions to read the user's +tree and traverse it. +- Check that the `user_filter` is not blocking otherwise valid users. +- Run the following check command to make sure that the LDAP settings are + correct and GitLab can see your users: + + ```bash + # For Omnibus installations + sudo gitlab-rake gitlab:ldap:check + + # For installations from source + sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production + ``` + +### Connection Refused + +If you are getting 'Connection Refused' errors when trying to connect to the +LDAP server please double-check the LDAP `port` and `method` settings used by +GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR +`method: 'ssl'` and `port: 636`. diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index cf1f98492ea..fb20308c49c 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -1,228 +1,3 @@ # GitLab LDAP integration -GitLab can be configured to allow your users to sign with their LDAP credentials to integrate with e.g. Active Directory. - -The first time a user signs in with LDAP credentials, GitLab will create a new GitLab user associated with the LDAP Distinguished Name (DN) of the LDAP user. - -GitLab user attributes such as nickname and email will be copied from the LDAP user entry. - -## Security - -GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email' or 'userPrincipalName' attribute. -An LDAP user who is allowed to change their email on the LDAP server can [take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users) on your GitLab server. - -We recommend against using GitLab LDAP integration if your LDAP users are allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on the LDAP server. - -If a user is deleted from the LDAP server, they will be blocked in GitLab as well. -Users will be immediately blocked from logging in. However, there is an LDAP check -cache time of one hour. The means users that are already logged in or are using Git -over SSH will still be able to access GitLab for up to one hour. Manually block -the user in the GitLab Admin area to immediately block all access. - -## Configuring GitLab for LDAP integration - -To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. -In GitLab Enterprise Edition you can have multiple LDAP servers connected to one GitLab server. - -Please note that before version 7.4, GitLab used a different syntax for configuring LDAP integration. -The old LDAP integration syntax still works in GitLab 7.4. -If your `gitlab.rb` or `gitlab.yml` file contains LDAP settings in both the old syntax and the new syntax, only the __old__ syntax will be used by GitLab. - -```ruby -# For omnibus packages -gitlab_rails['ldap_enabled'] = true -gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below -main: # 'main' is the GitLab 'provider ID' of this LDAP server - ## label - # - # A human-friendly name for your LDAP server. It is OK to change the label later, - # for instance if you find out it is too large to fit on the web page. - # - # Example: 'Paris' or 'Acme, Ltd.' - label: 'LDAP' - - host: '_your_ldap_server' - port: 389 - uid: 'sAMAccountName' - method: 'plain' # "tls" or "ssl" or "plain" - bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' - password: '_the_password_of_the_bind_user' - - # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking - # a request if the LDAP server becomes unresponsive. - # A value of 0 means there is no timeout. - timeout: 10 - - # This setting specifies if LDAP server is Active Directory LDAP server. - # For non AD servers it skips the AD specific queries. - # If your LDAP server is not AD, set this to false. - active_directory: true - - # If allow_username_or_email_login is enabled, GitLab will ignore everything - # after the first '@' in the LDAP username submitted by the user on login. - # - # Example: - # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials; - # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'. - # - # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to - # disable this setting, because the userPrincipalName contains an '@'. - allow_username_or_email_login: false - - # To maintain tight control over the number of active users on your GitLab installation, - # enable this setting to keep new users blocked until they have been cleared by the admin - # (default: false). - block_auto_created_users: false - - # Base where we can search for users - # - # Ex. ou=People,dc=gitlab,dc=example - # - base: '' - - # Filter LDAP users - # - # Format: RFC 4515 https://tools.ietf.org/search/rfc4515 - # Ex. (employeeType=developer) - # - # Note: GitLab does not support omniauth-ldap's custom filter syntax. - # - user_filter: '' - - # LDAP attributes that GitLab will use to create an account for the LDAP user. - # The specified attribute can either be the attribute name as a string (e.g. 'mail'), - # or an array of attribute names to try in order (e.g. ['mail', 'email']). - # Note that the user's LDAP login will always be the attribute specified as `uid` above. - attributes: - # The username will be used in paths for the user's own projects - # (like `gitlab.example.com/username/project`) and when mentioning - # them in issues, merge request and comments (like `@username`). - # If the attribute specified for `username` contains an email address, - # the GitLab username will be the part of the email address before the '@'. - username: ['uid', 'userid', 'sAMAccountName'] - email: ['mail', 'email', 'userPrincipalName'] - - # If no full name could be found at the attribute specified for `name`, - # the full name is determined using the attributes specified for - # `first_name` and `last_name`. - name: 'cn' - first_name: 'givenName' - last_name: 'sn' - -# GitLab EE only: add more LDAP servers -# Choose an ID made of a-z and 0-9 . This ID will be stored in the database -# so that GitLab can remember which LDAP server a user belongs to. -# uswest2: -# label: -# host: -# .... -EOS -``` - -If you are getting 'Connection Refused' errors when trying to connect to the LDAP server please double-check the LDAP `port` and `method` settings used by GitLab. -Common combinations are `method: 'plain'` and `port: 389`, OR `method: 'ssl'` and `port: 636`. - -If you are using a GitLab installation from source you can find the LDAP settings in `/home/git/gitlab/config/gitlab.yml`: - -``` -production: - # snip... - ldap: - enabled: false - servers: - main: # 'main' is the GitLab 'provider ID' of this LDAP server - ## label - # - # A human-friendly name for your LDAP server. It is OK to change the label later, - # for instance if you find out it is too large to fit on the web page. - # - # Example: 'Paris' or 'Acme, Ltd.' - label: 'LDAP' - # snip... -``` - -## Enabling LDAP sign-in for existing GitLab users - -When a user signs in to GitLab with LDAP for the first time, and their LDAP email address is the primary email address of an existing GitLab user, then the LDAP DN will be associated with the existing user. - -If the LDAP email attribute is not found in GitLab's database, a new user is created. - -In other words, if an existing GitLab user wants to enable LDAP sign-in for themselves, they should check that their GitLab email address matches their LDAP email address, and then sign into GitLab via their LDAP credentials. - -GitLab recognizes the following LDAP attributes as email addresses: `mail`, `email` and `userPrincipalName`. - -If multiple LDAP email attributes are present, e.g. `mail: foo@bar.com` and `email: foo@example.com`, then the first attribute found wins -- in this case `foo@bar.com`. - -## Using an LDAP filter to limit access to your GitLab server - -If you want to limit all GitLab access to a subset of the LDAP users on your LDAP server you can set up an LDAP user filter. -The filter must comply with [RFC 4515](https://tools.ietf.org/search/rfc4515). - -```ruby -# For omnibus packages; new LDAP server syntax -gitlab_rails['ldap_servers'] = YAML.load <<-EOS -main: - # snip... - user_filter: '(employeeType=developer)' -EOS -``` - -```yaml -# For installations from source; new LDAP server syntax -production: - ldap: - servers: - main: - # snip... - user_filter: '(employeeType=developer)' -``` - -Tip: if you want to limit access to the nested members of an Active Directory group you can use the following syntax: - -``` -(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com) -``` - -Please note that GitLab does not support the custom filter syntax used by omniauth-ldap. - -## Limitations - -GitLab's LDAP client is based on [omniauth-ldap](https://gitlab.com/gitlab-org/omniauth-ldap) -which encapsulates Ruby's `Net::LDAP` class. It provides a pure-Ruby implementation -of the LDAP client protocol. As a result, GitLab is limited by `omniauth-ldap` and may impact your LDAP -server settings. - -### TLS Client Authentication -Not implemented by `Net::LDAP`. -So you should disable anonymous LDAP authentication and enable simple or SASL -authentication. TLS client authentication setting in your LDAP server cannot be -mandatory and clients cannot be authenticated with the TLS protocol. - -### TLS Server Authentication -Not supported by GitLab's configuration options. -When setting `method: ssl`, the underlying authentication method used by -`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with -the LDAP server before any LDAP-protocol data is exchanged but no validation of -the LDAP server's SSL certificate is performed. - -## Troubleshooting - -### Invalid credentials when logging in - -Make sure the user you are binding with has enough permissions to read the user's -tree and traverse it. - -Also make sure that the `user_filter` is not blocking otherwise valid users. - -To make sure that the LDAP settings are correct and GitLab can see your users, -execute the following command: - - -```bash -# For Omnibus installations -sudo gitlab-rake gitlab:ldap:check - -# For installations from source -sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production -``` - +This document was moved under [`administration/auth/ldap`](administration/auth/ldap.md). -- cgit v1.2.1 From d05ec645aef1bd3529902dd6f5e527f9795f1199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Manh=C3=A3es?= Date: Tue, 22 Mar 2016 20:55:19 -0300 Subject: Fix order of steps to prevent PostgreSQL errors when running migration [ci skip] --- doc/update/8.5-to-8.6.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md index 712e9fdf93a..b9abcbd2c12 100644 --- a/doc/update/8.5-to-8.6.md +++ b/doc/update/8.5-to-8.6.md @@ -62,7 +62,26 @@ sudo -u git -H git checkout v0.7.1 sudo -u git -H make ``` -### 6. Install libs, migrations, etc. +### 6. Updates for PostgreSQL Users + +Starting with 8.6 users using GitLab in combination with PostgreSQL are required +to have the `pg_trgm` extension enabled for all GitLab databases. If you're +using GitLab's Omnibus packages there's nothing you'll need to do manually as +this extension is enabled automatically. Users who install GitLab without using +Omnibus (e.g. by building from source) have to enable this extension manually. +To enable this extension run the following SQL command as a PostgreSQL super +user for _every_ GitLab database: + +```sql +CREATE EXTENSION IF NOT EXISTS pg_trgm; +``` + +Certain operating systems might require the installation of extra packages for +this extension to be available. For example, users using Ubuntu will have to +install the `postgresql-contrib` package in order for this extension to be +available. + +### 7. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -84,7 +103,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS ``` -### 7. Update configuration files +### 8. Update configuration files #### New configuration options for `gitlab.yml` @@ -120,25 +139,6 @@ Ensure you're still up-to-date with the latest init script changes: sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab -### 8. Updates for PostgreSQL Users - -Starting with 8.6 users using GitLab in combination with PostgreSQL are required -to have the `pg_trgm` extension enabled for all GitLab databases. If you're -using GitLab's Omnibus packages there's nothing you'll need to do manually as -this extension is enabled automatically. Users who install GitLab without using -Omnibus (e.g. by building from source) have to enable this extension manually. -To enable this extension run the following SQL command as a PostgreSQL super -user for _every_ GitLab database: - -```sql -CREATE EXTENSION IF NOT EXISTS pg_trgm; -``` - -Certain operating systems might require the installation of extra packages for -this extension to be available. For example, users using Ubuntu will have to -install the `postgresql-contrib` package in order for this extension to be -available. - ### 9. Start application sudo service gitlab start -- cgit v1.2.1 From ff29d88dbeea92ce24421dd563424fc5b4a969af Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Thu, 24 Mar 2016 21:45:31 +0100 Subject: add link to forks pages in fork counter button --- app/assets/stylesheets/pages/projects.scss | 2 +- app/views/projects/buttons/_fork.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index c68bd673a67..e3bca6d559f 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -162,7 +162,7 @@ margin-right: 12px; a { - margin: -1px !important; + margin: -1px; } } diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 88cbb7c03c5..5fb5fe5af2f 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -12,7 +12,7 @@ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do = icon('code-fork fw') Fork - %div.count-with-arrow + = link_to namespace_project_forks_path(@project.namespace, @project), class: 'count-with-arrow' do %span.arrow %span.count = @project.forks_count -- cgit v1.2.1 From 8d2ded95c89de74bacdfdeda894636b511e06953 Mon Sep 17 00:00:00 2001 From: connorshea Date: Thu, 24 Mar 2016 20:21:32 -0600 Subject: Upgrade Bullet to 5.0.0 Includes Rails 5 support. See changelog for details (https://github.com/flyerhzm/bullet/blob/master/CHANGELOG.md). --- Gemfile.lock | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 16c09ab6e6d..4f9c404dc6d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,7 +98,7 @@ GEM terminal-table (~> 1.4) browser (1.0.1) builder (3.2.2) - bullet (4.14.10) + bullet (5.0.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.9.0) bundler-audit (0.4.0) @@ -1064,3 +1064,6 @@ DEPENDENCIES web-console (~> 2.0) webmock (~> 1.21.0) wikicloth (= 0.8.1) + +BUNDLED WITH + 1.11.2 -- cgit v1.2.1 From 057244b4c00526ea78d3b83afb04080a18c7e22f Mon Sep 17 00:00:00 2001 From: connorshea Date: Thu, 24 Mar 2016 20:28:24 -0600 Subject: Upgrade font-awesome-rails to 4.5.0.1 Includes Rails 5 support. See changelog for details: https://github.com/bokmann/font-awesome-rails/releases/tag/v4.5.0.1 --- Gemfile.lock | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 16c09ab6e6d..f00de30b475 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -328,8 +328,8 @@ GEM fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) - font-awesome-rails (4.5.0.0) - railties (>= 3.2, < 5.0) + font-awesome-rails (4.5.0.1) + railties (>= 3.2, < 5.1) foreman (0.78.0) thor (~> 0.19.1) formatador (0.2.5) @@ -1064,3 +1064,6 @@ DEPENDENCIES web-console (~> 2.0) webmock (~> 1.21.0) wikicloth (= 0.8.1) + +BUNDLED WITH + 1.11.2 -- cgit v1.2.1 From 2fe8da0b500b78d28dad399bc5552328929fadc4 Mon Sep 17 00:00:00 2001 From: connorshea Date: Thu, 24 Mar 2016 21:29:12 -0600 Subject: Upgrade web-console to 2.3.0 Rails 5 compatibility release. See the Changelog for more information: https://github.com/rails/web-console/releases/tag/v2.3.0 --- Gemfile.lock | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 16c09ab6e6d..40d2f4735a4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -868,7 +868,7 @@ GEM equalizer (~> 0.0, >= 0.0.9) warden (1.2.4) rack (>= 1.0) - web-console (2.2.1) + web-console (2.3.0) activemodel (>= 4.0) binding_of_caller (>= 0.7.2) railties (>= 4.0) @@ -1064,3 +1064,6 @@ DEPENDENCIES web-console (~> 2.0) webmock (~> 1.21.0) wikicloth (= 0.8.1) + +BUNDLED WITH + 1.11.2 -- cgit v1.2.1 From f967d5cf3d85d1fd0d48935ec7f9a5449383c43d Mon Sep 17 00:00:00 2001 From: connorshea Date: Thu, 24 Mar 2016 21:48:06 -0600 Subject: Upgrade Brakeman from 3.1.4 to 3.2.1 Includes support for Rails 5, bug fixes, and various additional vulnerability checks. See Changelog for more information (https://github.com/presidentbeef/brakeman/blob/master/CHANGES) --- Gemfile | 2 +- Gemfile.lock | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Gemfile b/Gemfile index cebd76e7370..dec3ef0785c 100644 --- a/Gemfile +++ b/Gemfile @@ -234,7 +234,7 @@ end group :development do gem "foreman" - gem 'brakeman', '~> 3.1.0', require: false + gem 'brakeman', '~> 3.2.0', require: false gem "annotate", "~> 2.6.0" gem "letter_opener", '~> 1.1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 16c09ab6e6d..87c5ec89348 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,14 +84,12 @@ GEM bootstrap-sass (3.3.6) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) - brakeman (3.1.4) + brakeman (3.2.1) erubis (~> 2.6) - fastercsv (~> 1.5) haml (>= 3.0, < 5.0) highline (>= 1.6.20, < 2.0) - multi_json (~> 1.2) - ruby2ruby (>= 2.1.1, < 2.3.0) - ruby_parser (~> 3.7.0) + ruby2ruby (~> 2.3.0) + ruby_parser (~> 3.8.1) safe_yaml (>= 1.0) sass (~> 3.0) slim (>= 1.3.6, < 4.0) @@ -208,7 +206,6 @@ GEM faraday_middleware-multi_json (0.0.6) faraday_middleware multi_json - fastercsv (1.5.5) ffaker (2.0.0) ffi (1.9.10) fission (0.5.0) @@ -706,10 +703,10 @@ GEM ruby-saml (1.1.2) nokogiri (>= 1.5.10) uuid (~> 2.3) - ruby2ruby (2.2.0) + ruby2ruby (2.3.0) ruby_parser (~> 3.1) sexp_processor (~> 4.0) - ruby_parser (3.7.2) + ruby_parser (3.8.1) sexp_processor (~> 4.1) rubyntlm (0.5.2) rubypants (0.2.0) @@ -718,7 +715,7 @@ GEM safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) - sass (3.4.20) + sass (3.4.21) sass-rails (5.0.4) railties (>= 4.0.0, < 5.0) sass (~> 3.1) @@ -742,7 +739,7 @@ GEM sentry-raven (0.15.6) faraday (>= 0.7.6) settingslogic (2.0.9) - sexp_processor (4.6.0) + sexp_processor (4.7.0) sham_rack (1.3.6) rack shoulda-matchers (2.8.0) @@ -910,7 +907,7 @@ DEPENDENCIES better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) - brakeman (~> 3.1.0) + brakeman (~> 3.2.0) browser (~> 1.0.0) bullet bundler-audit -- cgit v1.2.1 From 6f894bec097a4cfa378cee908d81f3cba67a09e5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 24 Mar 2016 08:09:39 +0100 Subject: Add definition of before action that has been moved Closes #14528 --- CHANGELOG | 1 + app/controllers/ci/projects_controller.rb | 6 +++++- spec/controllers/ci/projects_controller_spec.rb | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 5d9f4961ef5..7c3727a6896 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 8.7.0 (unreleased) v 8.6.2 (unreleased) - Comments on confidential issues don't show up in activity feed to non-members + - Fix NoMethodError when visiting CI root path at `/ci` v 8.6.1 - Add option to reload the schema before restoring a database backup. !2807 diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 081e01a75e0..c0e7f434ff5 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,8 +1,8 @@ module Ci class ProjectsController < Ci::ApplicationController before_action :project - before_action :authorize_read_project!, except: [:badge] before_action :no_cache, only: [:badge] + before_action :authorize_read_project!, except: [:badge, :index] skip_before_action :authenticate_user!, only: [:badge] protect_from_forgery @@ -35,5 +35,9 @@ module Ci response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" end + + def authorize_read_project! + return access_denied! unless can?(current_user, :read_project, project) + end end end diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb index db0748f323f..9a886e4c124 100644 --- a/spec/controllers/ci/projects_controller_spec.rb +++ b/spec/controllers/ci/projects_controller_spec.rb @@ -5,6 +5,16 @@ describe Ci::ProjectsController do let!(:project) { create(:project, visibility, ci_id: 1) } let(:ci_id) { project.ci_id } + describe '#index' do + let(:user) { create(:user) } + before { sign_in(user) } + before { get(:index) } + + it 'returns 200' do + expect(response.status).to eq 200 + end + end + ## # Specs for *deprecated* CI badge # -- cgit v1.2.1 From f4bdefdff1861c0d0e2e6ae3418be969c2600b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 25 Mar 2016 12:31:43 +0100 Subject: Ensure private project snippets are not viewable by unauthorized people Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/14607. --- app/controllers/projects/snippets_controller.rb | 6 +- app/models/ability.rb | 10 ++ .../projects/snippets_controller_spec.rb | 107 +++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 spec/controllers/projects/snippets_controller_spec.rb diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index b578b419a46..383b86b68e0 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -3,7 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read any snippet - before_action :authorize_read_project_snippet! + before_action :authorize_read_project_snippet!, except: [:index] # Allow write(create) snippet before_action :authorize_create_project_snippet!, only: [:new, :create] @@ -81,6 +81,10 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet ||= @project.snippets.find(params[:id]) end + def authorize_read_project_snippet! + return render_404 unless can?(current_user, :read_project_snippet, @snippet) + end + def authorize_update_project_snippet! return render_404 unless can?(current_user, :update_project_snippet, @snippet) end diff --git a/app/models/ability.rb b/app/models/ability.rb index fa2345f6faa..5f326729433 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -27,6 +27,8 @@ class Ability case true when subject.is_a?(PersonalSnippet) anonymous_personal_snippet_abilities(subject) + when subject.is_a?(ProjectSnippet) + anonymous_project_snippet_abilities(subject) when subject.is_a?(CommitStatus) anonymous_commit_status_abilities(subject) when subject.is_a?(Project) || subject.respond_to?(:project) @@ -100,6 +102,14 @@ class Ability end end + def anonymous_project_snippet_abilities(snippet) + if snippet.public? + [:read_project_snippet] + else + [] + end + end + def global_abilities(user) rules = [] rules << :create_group if user.can_create_group diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb new file mode 100644 index 00000000000..0f32a30f18b --- /dev/null +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +describe Projects::SnippetsController do + let(:project) { create(:project_empty_repo, :public, snippets_enabled: true) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + + before do + project.team << [user, :master] + project.team << [user2, :master] + end + + describe 'GET #index' do + context 'when the project snippet is private' do + let!(:project_snippet) { create(:project_snippet, :private, project: project, author: user) } + + context 'when anonymous' do + it 'does not include the private snippet' do + get :index, namespace_id: project.namespace.path, project_id: project.path + + expect(assigns(:snippets)).not_to include(project_snippet) + expect(response.status).to eq(200) + end + end + + context 'when signed in as the author' do + before { sign_in(user) } + + it 'renders the snippet' do + get :index, namespace_id: project.namespace.path, project_id: project.path + + expect(assigns(:snippets)).to include(project_snippet) + expect(response.status).to eq(200) + end + end + + context 'when signed in as a project member' do + before { sign_in(user2) } + + it 'renders the snippet' do + get :index, namespace_id: project.namespace.path, project_id: project.path + + expect(assigns(:snippets)).to include(project_snippet) + expect(response.status).to eq(200) + end + end + end + end + + %w[show raw].each do |action| + describe "GET ##{action}" do + context 'when the project snippet is private' do + let(:project_snippet) { create(:project_snippet, :private, project: project, author: user) } + + context 'when anonymous' do + it 'responds with status 404' do + get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param + + expect(response.status).to eq(404) + end + end + + context 'when signed in as the author' do + before { sign_in(user) } + + it 'renders the snippet' do + get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param + + expect(assigns(:snippet)).to eq(project_snippet) + expect(response.status).to eq(200) + end + end + + context 'when signed in as a project member' do + before { sign_in(user2) } + + it 'renders the snippet' do + get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param + + expect(assigns(:snippet)).to eq(project_snippet) + expect(response.status).to eq(200) + end + end + end + + context 'when the project snippet does not exist' do + context 'when anonymous' do + it 'responds with status 404' do + get action, namespace_id: project.namespace.path, project_id: project.path, id: 42 + + expect(response.status).to eq(404) + end + end + + context 'when signed in' do + before { sign_in(user) } + + it 'responds with status 404' do + get action, namespace_id: project.namespace.path, project_id: project.path, id: 42 + + expect(response.status).to eq(404) + end + end + end + end + end +end -- cgit v1.2.1 From 16ba5eb4d89f903d89fbad33086013a2f69717dd Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 11 Mar 2016 00:28:10 -0500 Subject: Updates sidebar milestone to use new dropdowns --- app/assets/javascripts/api.js.coffee | 21 +++++++++++++++++++++ app/assets/javascripts/gl_dropdown.js.coffee | 3 ++- app/assets/javascripts/milestone_select.js.coffee | 21 ++++++++++++--------- app/controllers/projects/issues_controller.rb | 1 + app/views/projects/merge_requests/_show.html.haml | 2 +- app/views/shared/issuable/_filter.html.haml | 4 +++- app/views/shared/issuable/_sidebar.html.haml | 6 +++--- config/environments/development.rb | 2 +- 8 files changed, 44 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index f3ed9a66715..b259c6556ae 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -5,6 +5,27 @@ group_projects_path: "/api/:version/groups/:id/projects.json" projects_path: "/api/:version/projects.json" labels_path: "/api/:version/projects/:id/labels" + issues_paths: + update : "/api/:version/projects/:id/issues/:issue_id" + merge_request_path: "/api/:version/issues/:id.json" + + issues: + update: (project_id, issue_id, data, callback) -> + url = Api.buildUrl(Api.issues_paths.update) + url = url + .replace(":id", project_id) + .replace(":issue_id", issue_id) + if not data? + data = {} + data.private_token = gon.api_token + $.ajax( + url: url + type: "PUT" + data: data + dataType: "json" + ).done (issue) -> + if callback? + callback(issue) group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 4b78bcde774..6cd39f5f0b9 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -143,10 +143,11 @@ class GitLabDropdown selector = ".dropdown-page-one .dropdown-content a" @dropdown.on "click", selector, (e) -> + e.preventDefault() self.rowClicked $(@) if self.options.clicked - self.options.clicked() + self.options.clicked.call(@,e) toggleLoading: -> $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index e17a1adb648..18647e25c55 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -1,5 +1,6 @@ class @MilestoneSelect - constructor: -> + constructor: (@opts) -> + opts = @opts $('.js-milestone-select').each (i, dropdown) -> $dropdown = $(dropdown) projectId = $dropdown.data('project-id') @@ -9,6 +10,7 @@ class @MilestoneSelect showAny = $dropdown.data('show-any') useId = $dropdown.data('use-id') defaultLabel = $dropdown.data('default-label') + issuableId = $dropdown.data('issuable-id') $dropdown.glDropdown( data: (term, callback) -> @@ -53,13 +55,14 @@ class @MilestoneSelect milestone.id isSelected: (milestone) -> milestone.title is selectedMilestone - clicked: -> - page = $('body').data 'page' - isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is page is 'projects:merge_requests:index' - if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - Issues.filterResults $dropdown.closest('form') - else if $dropdown.hasClass 'js-filter-submit' - $dropdown.closest('form').submit() + clicked: (e) -> + if $(dropdown).hasClass "js-filter-submit" && opts.submitForm + $(dropdown).parents('form').submit() + else + milestoneVal = $(@) + .closest('.selectbox') + .find('input[type="hidden"]') + .val() + Api.issues.update(projectId, issuableId, milestone_id: milestoneVal, (data) => console.log 'data', data) ) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 877b39c9b1b..b3b098c5153 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -58,6 +58,7 @@ class Projects::IssuesController < Projects::ApplicationController end def edit + puts params respond_with(@issue) end diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index ee5b9fd95a8..1dd8f721f7e 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -10,7 +10,7 @@ .merge-request{'data-url' => merge_request_path(@merge_request)} = render "projects/merge_requests/show/mr_title" - .merge-request-details.issuable-details + .merge-request-details.issuable-details{data: {id: @merge_request.project.id}} = render "projects/merge_requests/show/mr_box" .append-bottom-default.mr-source-target.prepend-top-default - if @merge_request.open? diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index f91ff0e3694..6eb5e5f867e 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -55,7 +55,9 @@ :javascript new UsersSelect(); new LabelsSelect(); - new MilestoneSelect(); + new MilestoneSelect({ + submitForm: true + }); new IssueStatusSelect(); $('form.filter-form').on('submit', function (event) { event.preventDefault(); diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 0e20e86356d..4147eb613aa 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -62,10 +62,9 @@ = issuable.milestone.title - else .light None + .selectbox.hide-collapsed - = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }}) - = hidden_field_tag :issuable_context - = f.submit class: 'btn hide' + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search milestones", data: { show_no: true, field_name: "milestone_id", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true }}) - if issuable.project.labels.any? .block.labels @@ -117,4 +116,5 @@ :javascript new Subscription('.subscription'); + new MilestoneSelect(); new IssuableContext(); diff --git a/config/environments/development.rb b/config/environments/development.rb index 689694a3480..78fc91bc3cc 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -36,7 +36,7 @@ Rails.application.configure do # For having correct urls in mails config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } # Open sent mails in browser - config.action_mailer.delivery_method = :letter_opener + config.action_mailer.delivery_method = :test # Don't make a mess when bootstrapping a development environment config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1') -- cgit v1.2.1 From 1c8fff965cca617ba0dbb5bfde1c76fc360c5661 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 11 Mar 2016 10:48:20 -0500 Subject: Adds milestone API JS call --- app/assets/javascripts/api.js.coffee | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index b259c6556ae..51b04051741 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -8,6 +8,24 @@ issues_paths: update : "/api/:version/projects/:id/issues/:issue_id" merge_request_path: "/api/:version/issues/:id.json" + milestones_paths: + index: "/api/:version/projects/:id/milestones" + + milestones: + index: (project_id, callback) -> + data = {} + url = Api.buildUrl(Api.milestones_paths.index) + url = url + .replace(":id", project_id) + data.private_token = gon.api_token + $.ajax( + url: url + type: "GET" + data: data + dataType: "json" + ).done (milestones) -> + if callback? + callback(milestones) issues: update: (project_id, issue_id, data, callback) -> -- cgit v1.2.1 From ff86138d7688d3b4f49fabea28ca75dd7b4db3ef Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 11 Mar 2016 14:20:44 -0500 Subject: Sidebar milestone updatable via JSON --- app/assets/javascripts/milestone_select.js.coffee | 22 +++++++++++++++------- app/controllers/projects/issues_controller.rb | 5 +---- app/controllers/projects/milestones_controller.rb | 1 - app/views/shared/issuable/_filter.html.haml | 4 +--- app/views/shared/issuable/_sidebar.html.haml | 2 +- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 18647e25c55..3a791ae5153 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -1,10 +1,10 @@ class @MilestoneSelect - constructor: (@opts) -> - opts = @opts + constructor: () -> $('.js-milestone-select').each (i, dropdown) -> $dropdown = $(dropdown) projectId = $dropdown.data('project-id') milestonesUrl = $dropdown.data('milestones') + issueUpdateURL = $dropdown.data('issueUpdate') selectedMilestone = $dropdown.data('selected') showNo = $dropdown.data('show-no') showAny = $dropdown.data('show-any') @@ -57,12 +57,20 @@ class @MilestoneSelect milestone.title is selectedMilestone clicked: (e) -> - if $(dropdown).hasClass "js-filter-submit" && opts.submitForm - $(dropdown).parents('form').submit() + if $dropdown.hasClass "js-filter-submit" + $dropdown.parents('form').submit() else - milestoneVal = $(@) + selected = $dropdown .closest('.selectbox') .find('input[type="hidden"]') .val() - Api.issues.update(projectId, issuableId, milestone_id: milestoneVal, (data) => console.log 'data', data) - ) + + $.ajax( + type: 'PUT' + url: issueUpdateURL + data: + issue: + milestone_id: selected + ).done (data) -> + console.log 'databack', data + ) \ No newline at end of file diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b3b098c5153..7a15f299cb5 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -108,10 +108,7 @@ class Projects::IssuesController < Projects::ApplicationController end end format.json do - render json: { - saved: @issue.valid?, - assignee_avatar_url: @issue.assignee.try(:avatar_url) - } + render json: @issue end end end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index b2e974eff17..5b0a63a933c 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -19,7 +19,6 @@ class Projects::MilestonesController < Projects::ApplicationController end @milestones = @milestones.includes(:project) - respond_to do |format| format.html do @milestones = @milestones.page(params[:page]) diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 6eb5e5f867e..f91ff0e3694 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -55,9 +55,7 @@ :javascript new UsersSelect(); new LabelsSelect(); - new MilestoneSelect({ - submitForm: true - }); + new MilestoneSelect(); new IssueStatusSelect(); $('form.filter-form').on('submit', function (event) { event.preventDefault(); diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 4147eb613aa..0a8300cc513 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -64,7 +64,7 @@ .light None .selectbox.hide-collapsed - = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search milestones", data: { show_no: true, field_name: "milestone_id", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true }}) + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search milestones", data: { show_no: true, field_name: "milestone_id", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), use_id: true }}) - if issuable.project.labels.any? .block.labels -- cgit v1.2.1 From a76ee508fbf203c3708277d1d09e91d5a7b5adfa Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Sat, 12 Mar 2016 06:52:50 -0500 Subject: Ajax call to milestones via new dropdowns --- app/assets/javascripts/issuable_context.js.coffee | 5 ++++- app/assets/javascripts/milestone_select.js.coffee | 19 +++++++++++++++++-- app/assets/stylesheets/pages/issuable.scss | 8 ++++++++ app/controllers/projects/issues_controller.rb | 2 +- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index d6d09b36d8d..ddc7de602e4 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -14,7 +14,10 @@ class @IssuableContext block = $(@).parents('.block') block.find('.selectbox').show() block.find('.value').hide() - block.find('.js-select2').select2("open") + setTimeout (-> + block.find('.dropdown-menu-toggle').trigger 'click' + ), 0 + $(".right-sidebar").niceScroll() diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 3a791ae5153..f67e12f9ad1 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -11,6 +11,9 @@ class @MilestoneSelect useId = $dropdown.data('use-id') defaultLabel = $dropdown.data('default-label') issuableId = $dropdown.data('issuable-id') + $selectbox = $dropdown.closest('.selectbox') + $block = $selectbox.closest('.block') + $value = $block.find('.value') $dropdown.glDropdown( data: (term, callback) -> @@ -64,7 +67,7 @@ class @MilestoneSelect .closest('.selectbox') .find('input[type="hidden"]') .val() - + console.log 'gonna ajax it with', url: issueUpdateURL, data: issue: milestone_id: selected $.ajax( type: 'PUT' url: issueUpdateURL @@ -72,5 +75,17 @@ class @MilestoneSelect issue: milestone_id: selected ).done (data) -> - console.log 'databack', data + $selectbox.hide() + href = $value + .show() + .find('.milestone-title') + .text(data.milestone.title) + .end() + .find('a') + .attr('href') + splitHref = href.split('/') + splitHref[splitHref.length - 1] = data.id + $value + .find('a') + .attr('href',splitHref.join('/')) ) \ No newline at end of file diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 5300bb52a1b..b2cd0a41b78 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -252,6 +252,14 @@ text-decoration: none; } } + + .dropdown-menu-toggle { + width: 100%; + } + + .open .dropdown-menu { + width: 100%; + } } .btn-default.gutter-toggle { diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 7a15f299cb5..2280ad54cd7 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -108,7 +108,7 @@ class Projects::IssuesController < Projects::ApplicationController end end format.json do - render json: @issue + render json: @issue.to_json(include: :milestone ) end end end -- cgit v1.2.1 From aa4da384357c025ce6e363eebd9182ffb39b7bd4 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Sat, 12 Mar 2016 11:34:26 -0500 Subject: Make label search work with JSON data --- app/assets/javascripts/milestone_select.js.coffee | 19 ++++++++++++------- app/views/shared/issuable/_filter.html.haml | 1 - app/views/shared/issuable/_sidebar.html.haml | 1 + 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index f67e12f9ad1..680a24a3c04 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -14,6 +14,7 @@ class @MilestoneSelect $selectbox = $dropdown.closest('.selectbox') $block = $selectbox.closest('.block') $value = $block.find('.value') + $loading = $block.find('.block-loading').fadeOut() $dropdown.glDropdown( data: (term, callback) -> @@ -67,7 +68,10 @@ class @MilestoneSelect .closest('.selectbox') .find('input[type="hidden"]') .val() - console.log 'gonna ajax it with', url: issueUpdateURL, data: issue: milestone_id: selected + # need inline-block here instead of show, + # which will default to the element's style in this case inline. + $loading + .fadeIn() $.ajax( type: 'PUT' url: issueUpdateURL @@ -75,14 +79,15 @@ class @MilestoneSelect issue: milestone_id: selected ).done (data) -> + $loading.fadeOut() $selectbox.hide() href = $value - .show() - .find('.milestone-title') - .text(data.milestone.title) - .end() - .find('a') - .attr('href') + .show() + .find('.milestone-title') + .text(data.milestone.title) + .end() + .find('a') + .attr('href') splitHref = href.split('/') splitHref[splitHref.length - 1] = data.id $value diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index f91ff0e3694..a7840f1b393 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -23,7 +23,6 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown" - .pull-right = render 'shared/sort_dropdown' diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 0a8300cc513..d44ab935831 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -54,6 +54,7 @@ No .title.hide-collapsed Milestone + =icon('spinner spin', class: 'block-loading') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed -- cgit v1.2.1 From 024dc8073ec1f315d3ded16b7d112cd5686c1c26 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Sat, 12 Mar 2016 15:37:02 -0500 Subject: Add milestone sidebar update via JSON --- app/assets/javascripts/gl_dropdown.js.coffee | 6 ++- app/assets/javascripts/labels_select.js.coffee | 49 ++++++++++++++++++++++- app/assets/javascripts/milestone_select.js.coffee | 6 +-- app/assets/stylesheets/pages/issuable.scss | 1 + app/controllers/projects/issues_controller.rb | 2 +- app/views/shared/issuable/_sidebar.html.haml | 28 +++++++++++-- 6 files changed, 83 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 6cd39f5f0b9..f6de0136316 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -185,7 +185,8 @@ class GitLabDropdown if @options.filterable @dropdown.find(".dropdown-input-field").focus() - hidden: => + hidden: (e) => + if @options.filterable @dropdown .find(".dropdown-input-field") @@ -196,6 +197,9 @@ class GitLabDropdown if @dropdown.find(".dropdown-toggle-page").length $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS + if @options.hidden + @options.hidden.call(@,e) + # Render the full menu renderMenu: (html) -> diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index e08648d583b..7b60bfd8614 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -4,6 +4,7 @@ class @LabelsSelect $dropdown = $(dropdown) projectId = $dropdown.data('project-id') labelUrl = $dropdown.data('labels') + issueUpdateURL = $dropdown.data('issueUpdate') selectedLabel = $dropdown.data('selected') if selectedLabel selectedLabel = selectedLabel.toString().split(',') @@ -12,6 +13,23 @@ class @LabelsSelect showNo = $dropdown.data('show-no') showAny = $dropdown.data('show-any') defaultLabel = $dropdown.data('default-label') + $selectbox = $dropdown.closest('.selectbox') + $block = $selectbox.closest('.block') + $value = $block.find('.value') + $loading = $block.find('.block-loading').fadeOut() + issueURLSplit = issueUpdateURL.split('/') + labelHTMLTemplate = _.template( + '<% _.each(labels, function(label){ %>'+ + ''+ + ''+ + '<%= label.title %>'+ + ''+ + ''+ + '<% }); %>'); if newLabelField.length $newLabelCreateButton = $('.js-new-label-btn') @@ -133,6 +151,7 @@ class @LabelsSelect search: fields: ['title'] selectable: true + toggleLabel: (selected) -> if selected and selected.title isnt 'Any Label' selected.title @@ -142,8 +161,15 @@ class @LabelsSelect id: (label) -> if label.isAny? '' - else + else if $dropdown.hasClass "js-filter-submit" label.title + else + label.id + + hidden: -> + $selectbox.hide() + $value.show() + clicked: -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' @@ -153,4 +179,25 @@ class @LabelsSelect Issues.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() + else + selected = $dropdown + .closest('.selectbox') + .find('input[type="hidden"]') + .val() + # need inline-block here instead of show, + # which will default to the element's style in this case inline. + $loading + .fadeIn() + $.ajax( + type: 'PUT' + url: issueUpdateURL + data: + issue: + label_ids: [selected] + ).done (data) -> + $loading.fadeOut() + $selectbox.hide() + href = $value + .show() + .html(labelHTMLTemplate(data)) ) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 680a24a3c04..14d1c74f835 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -59,7 +59,9 @@ class @MilestoneSelect milestone.id isSelected: (milestone) -> milestone.title is selectedMilestone - + hidden: -> + $selectbox.hide() + $value.show() clicked: (e) -> if $dropdown.hasClass "js-filter-submit" $dropdown.parents('form').submit() @@ -68,8 +70,6 @@ class @MilestoneSelect .closest('.selectbox') .find('input[type="hidden"]') .val() - # need inline-block here instead of show, - # which will default to the element's style in this case inline. $loading .fadeIn() $.ajax( diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index b2cd0a41b78..459c1840ec1 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -255,6 +255,7 @@ .dropdown-menu-toggle { width: 100%; + padding-top: 0; } .open .dropdown-menu { diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 2280ad54cd7..3255cde0464 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -108,7 +108,7 @@ class Projects::IssuesController < Projects::ApplicationController end end format.json do - render json: @issue.to_json(include: :milestone ) + render json: @issue.to_json(include: [:milestone, :labels]) end end end diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index d44ab935831..3696f9fafd9 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -75,6 +75,7 @@ = issuable.labels.count .title.hide-collapsed Labels + =icon('spinner spin', class: 'block-loading') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) } @@ -84,8 +85,29 @@ - else .light None .selectbox.hide-collapsed - = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" } + .dropdown + %button.dropdown-menu-toggle.js-label-select{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: (@project.id if @project), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} + %span.dropdown-toggle-text + Label + = icon('chevron-down') + .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable + .dropdown-page-one + = dropdown_title("Filter by label") + = dropdown_filter("Search labels") + = dropdown_content + - if @project + = dropdown_footer do + %ul.dropdown-footer-list + - if can? current_user, :admin_label, @project + %li + %a.dropdown-toggle-page{href: "#"} + Create new + %li + = link_to namespace_project_labels_path(@project.namespace, @project) do + - if can? current_user, :admin_label, @project + Manage labels + - else + View labels = render "shared/issuable/participants", participants: issuable.participants(current_user) - if current_user @@ -116,6 +138,6 @@ = clipboard_button(clipboard_text: project_ref) :javascript - new Subscription('.subscription'); new MilestoneSelect(); + new LabelsSelect(); new IssuableContext(); -- cgit v1.2.1 From 3753ace9bacb5036d7fe9b3a647f5f92d53a51be Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Sun, 13 Mar 2016 15:08:42 -0400 Subject: Add labels as proper POST arrays using new dropdown --- app/assets/javascripts/labels_select.js.coffee | 73 +++++++++++++---------- app/assets/javascripts/milestone_select.js.coffee | 26 ++++---- app/controllers/projects/issues_controller.rb | 8 ++- app/views/shared/issuable/_filter.html.haml | 3 +- app/views/shared/issuable/_sidebar.html.haml | 2 +- 5 files changed, 63 insertions(+), 49 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 7b60bfd8614..59496043eed 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -6,8 +6,8 @@ class @LabelsSelect labelUrl = $dropdown.data('labels') issueUpdateURL = $dropdown.data('issueUpdate') selectedLabel = $dropdown.data('selected') - if selectedLabel - selectedLabel = selectedLabel.toString().split(',') + if selectedLabel? + selectedLabel = selectedLabel.split(',') newLabelField = $('#new_label_name') newColorField = $('#new_label_color') showNo = $dropdown.data('show-no') @@ -17,19 +17,6 @@ class @LabelsSelect $block = $selectbox.closest('.block') $value = $block.find('.value') $loading = $block.find('.block-loading').fadeOut() - issueURLSplit = issueUpdateURL.split('/') - labelHTMLTemplate = _.template( - '<% _.each(labels, function(label){ %>'+ - ''+ - ''+ - '<%= label.title %>'+ - ''+ - ''+ - '<% }); %>'); if newLabelField.length $newLabelCreateButton = $('.js-new-label-btn') @@ -39,6 +26,24 @@ class @LabelsSelect # Suggested colors in the dropdown to chose from pre-chosen colors $('.suggest-colors-dropdown a').on 'click', (e) -> + + issueURLSplit = issueURL.split('/') if issueURL? + if issueURL + labelHTMLTemplate = _.template( + '<% _.each(labels, function(label){ %>'+ + ''+ + ''+ + '<%= label.title %>'+ + ''+ + ''+ + '<% }); %>'); + + if newLabelField.length and $dropdown.hasClass 'js-extra-options' + $('.suggest-colors-dropdown a').on "click", (e) -> e.preventDefault() e.stopPropagation() newColorField @@ -113,22 +118,23 @@ class @LabelsSelect $.ajax( url: labelUrl ).done (data) -> - if showNo - data.unshift( - id: 0 - title: 'No Label' - ) - - if showAny - data.unshift( - isAny: true - title: 'Any Label' - ) - - if data.length > 2 - data.splice 2, 0, 'divider' - + if $dropdown.hasClass 'js-extra-options' + if showNo + data.unshift( + id: 0 + title: 'No Label' + ) + + if showAny + data.unshift( + isAny: true + title: 'Any Label' + ) + + if data.length > 2 + data.splice 2, 0, 'divider' callback data + renderRow: (label) -> if $.isArray(selectedLabel) selected = '' @@ -184,16 +190,19 @@ class @LabelsSelect .closest('.selectbox') .find('input[type="hidden"]') .val() + console.log 'selected', selected # need inline-block here instead of show, # which will default to the element's style in this case inline. + selected = if selected? then [ selected ] else [''] + console.log 'selected', selected $loading .fadeIn() $.ajax( type: 'PUT' - url: issueUpdateURL + url: issueURL data: issue: - label_ids: [selected] + label_ids: selected ).done (data) -> $loading.fadeOut() $selectbox.hide() diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 14d1c74f835..876b9d1ae86 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -21,21 +21,21 @@ class @MilestoneSelect $.ajax( url: milestonesUrl ).done (data) -> - if showNo - data.unshift( - id: '0' - title: 'No Milestone' - ) + if $dropdown.hasClass "js-extra-options" + if showNo + data.unshift( + id: '0' + title: 'No Milestone' + ) - if showAny - data.unshift( - isAny: true - title: 'Any Milestone' - ) - - if data.length > 2 - data.splice 2, 0, 'divider' + if showAny + data.unshift( + isAny: true + title: 'Any Milestone' + ) + if data.length > 2 + data.splice 2, 0, 'divider' callback(data) filterable: true search: diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 3255cde0464..ecd6ed3375e 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -69,7 +69,13 @@ class Projects::IssuesController < Projects::ApplicationController @merge_requests = @issue.referenced_merge_requests(current_user) @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch) - respond_with(@issue) + respond_to do |format| + format.html + format.json do + render json: @issue.to_json(include: [:milestone, :labels]) + end + end + end def create diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index a7840f1b393..1fce7d159e3 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -40,8 +40,7 @@ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) .filter-item.inline - = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", - placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 3696f9fafd9..b92c7aaf0e4 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -86,7 +86,7 @@ .light None .selectbox.hide-collapsed .dropdown - %button.dropdown-menu-toggle.js-label-select{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: (@project.id if @project), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} %span.dropdown-toggle-text Label = icon('chevron-down') -- cgit v1.2.1 From fdb703d790e1eb8e91bed1ffb2bba202d80708d3 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 14 Mar 2016 15:37:16 -0400 Subject: Fix up commit message --- app/assets/javascripts/labels_select.js.coffee | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 59496043eed..4e1718c32e4 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -41,6 +41,7 @@ class @LabelsSelect ''+ ''+ '<% }); %>'); + labelNoneHTMLTemplate = _.template('
None
') if newLabelField.length and $dropdown.hasClass 'js-extra-options' $('.suggest-colors-dropdown a').on "click", (e) -> @@ -176,6 +177,8 @@ class @LabelsSelect $selectbox.hide() $value.show() + multiSelect: $dropdown.hasClass 'js-multiselect' + clicked: -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' @@ -190,11 +193,9 @@ class @LabelsSelect .closest('.selectbox') .find('input[type="hidden"]') .val() - console.log 'selected', selected # need inline-block here instead of show, # which will default to the element's style in this case inline. - selected = if selected? then [ selected ] else [''] - console.log 'selected', selected + selected = if selected? then selected.split(',') else [''] $loading .fadeIn() $.ajax( @@ -206,7 +207,11 @@ class @LabelsSelect ).done (data) -> $loading.fadeOut() $selectbox.hide() + if not data.labels.length + template = labelNoneHTMLTemplate() + else + template = labelHTMLTemplate(data) href = $value .show() - .html(labelHTMLTemplate(data)) + .html(template) ) -- cgit v1.2.1 From ed517c5ab77c09fddb980d3b2933ea1488a449b0 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 14 Mar 2016 22:08:07 -0400 Subject: Removes bugs with selection. Adds deselect still a WIP. --- app/assets/javascripts/gl_dropdown.js.coffee | 13 +++++++++++-- app/views/shared/issuable/_sidebar.html.haml | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index f6de0136316..92d44321fcb 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -263,15 +263,24 @@ class GitLabDropdown rowClicked: (el) -> fieldName = @options.fieldName - field = @dropdown.parent().find("input[name='#{fieldName}']") + field = @dropdown.parent().find("input[name='#{fieldName}']") + selectedIndex = el.parent().index() + if @renderedData + selectedObject = @renderedData[selectedIndex] + value = if @options.id then @options.id(selectedObject, el) else selectedObject.id + field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") if el.hasClass(ACTIVE_CLASS) - field.remove() + if @options.multiSelect + console.log field.val(), value + else + field.remove() else fieldName = @options.fieldName selectedIndex = el.parent().index() if @renderedData selectedObject = @renderedData[selectedIndex] + selectedObject.selected = true value = if @options.id then @options.id(selectedObject, el) else selectedObject.id if !value? diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index b92c7aaf0e4..0f0d0e4234a 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -85,8 +85,9 @@ - else .light None .selectbox.hide-collapsed + = f.hidden_field 'label_name', value: issuable.labels.map(&:id).join(',') .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "issue[label_name]", show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} %span.dropdown-toggle-text Label = icon('chevron-down') -- cgit v1.2.1 From c81b261374085051e6d70cb8b80cfe5fbcc27ce6 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 15 Mar 2016 01:06:14 -0400 Subject: Fix bugs with select dropdown and multiselection --- app/assets/javascripts/gl_dropdown.js.coffee | 31 +++++++++++++++------------- app/views/shared/issuable/_sidebar.html.haml | 6 ++++-- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 92d44321fcb..3d4c8f7726c 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -231,6 +231,13 @@ class GitLabDropdown html = @options.renderRow(data) else selected = if @options.isSelected then @options.isSelected(data) else false + if not selected + value = if @options.id then @options.id(data) else data.id + fieldName = @options.fieldName + field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") + if field.length + selected = true + url = if @options.url then @options.url(data) else "#" text = if @options.text then @options.text(data) else "" cssClass = ""; @@ -263,7 +270,7 @@ class GitLabDropdown rowClicked: (el) -> fieldName = @options.fieldName - field = @dropdown.parent().find("input[name='#{fieldName}']") + field = @dropdown.parent().find("input[name='#{fieldName}']") selectedIndex = el.parent().index() if @renderedData selectedObject = @renderedData[selectedIndex] @@ -271,10 +278,11 @@ class GitLabDropdown field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") if el.hasClass(ACTIVE_CLASS) - if @options.multiSelect - console.log field.val(), value - else - field.remove() + console.log 'has ACTIVE_CLASS' + # if @options.multiSelect + # console.log field.val(), value + # else + field.remove() else fieldName = @options.fieldName selectedIndex = el.parent().index() @@ -286,11 +294,7 @@ class GitLabDropdown if !value? field.remove() - if @options.multiSelect - oldValue = field.val() - if oldValue - value = "#{oldValue},#{value}" - else + if not @options.multiSelect @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS # Toggle active class for the tick mark @@ -299,14 +303,13 @@ class GitLabDropdown # Toggle the dropdown label if @options.toggleLabel $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) - if value? if !field.length # Create hidden input for form - input = "" + input = "" @dropdown.before input - - @dropdown.parent().find("input[name='#{fieldName}']").val value + else + console.log 'has field???' selectFirstRow: -> selector = '.dropdown-content li:first-child a' diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 0f0d0e4234a..daee1cdccf5 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -65,7 +65,8 @@ .light None .selectbox.hide-collapsed - = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search milestones", data: { show_no: true, field_name: "milestone_id", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), use_id: true }}) + = f.hidden_field 'milestone_id', value: issuable.milestone.id, id: nil + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search milestones", data: { show_no: true, field_name: "issue[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), use_id: true }}) - if issuable.project.labels.any? .block.labels @@ -85,7 +86,8 @@ - else .light None .selectbox.hide-collapsed - = f.hidden_field 'label_name', value: issuable.labels.map(&:id).join(',') + - issuable.labels.each do |label| + = f.hidden_field 'label_name', value: label.id, id: nil .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "issue[label_name]", show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} %span.dropdown-toggle-text -- cgit v1.2.1 From d17b549935ad09581e5af4a81137a8f25c76bf16 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 15 Mar 2016 01:29:41 -0400 Subject: Make remove multi and remove single work. Make add single and add multi work properly. Multiple inputs for multiselect --- app/assets/javascripts/gl_dropdown.js.coffee | 8 +------- app/assets/javascripts/labels_select.js.coffee | 8 +++++--- app/views/shared/issuable/_sidebar.html.haml | 4 ++-- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 3d4c8f7726c..a4a7bacfda2 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -270,7 +270,6 @@ class GitLabDropdown rowClicked: (el) -> fieldName = @options.fieldName - field = @dropdown.parent().find("input[name='#{fieldName}']") selectedIndex = el.parent().index() if @renderedData selectedObject = @renderedData[selectedIndex] @@ -278,10 +277,6 @@ class GitLabDropdown field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") if el.hasClass(ACTIVE_CLASS) - console.log 'has ACTIVE_CLASS' - # if @options.multiSelect - # console.log field.val(), value - # else field.remove() else fieldName = @options.fieldName @@ -296,6 +291,7 @@ class GitLabDropdown if not @options.multiSelect @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS + @dropdown.parent().find("input[name='#{fieldName}']").remove() # Toggle active class for the tick mark el.toggleClass "is-active" @@ -308,8 +304,6 @@ class GitLabDropdown # Create hidden input for form input = "" @dropdown.before input - else - console.log 'has field???' selectFirstRow: -> selector = '.dropdown-content li:first-child a' diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 4e1718c32e4..c05daca4afb 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -191,11 +191,13 @@ class @LabelsSelect else selected = $dropdown .closest('.selectbox') - .find('input[type="hidden"]') - .val() + .find("input[name='#{$dropdown.data('field-name')}']") + .map(-> + @value + ).get() # need inline-block here instead of show, # which will default to the element's style in this case inline. - selected = if selected? then selected.split(',') else [''] + selected = if selected.length then selected else [''] $loading .fadeIn() $.ajax( diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index daee1cdccf5..0f488614cf9 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -87,9 +87,9 @@ .light None .selectbox.hide-collapsed - issuable.labels.each do |label| - = f.hidden_field 'label_name', value: label.id, id: nil + = hidden_field_tag 'issue[label_names][]', label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "issue[label_name]", show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} %span.dropdown-toggle-text Label = icon('chevron-down') -- cgit v1.2.1 From 9da87709c46f5b2d14dceaae8f97b38f94189b89 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 15 Mar 2016 10:11:47 -0400 Subject: Remove api calls from api.js.coffee. Using respond to instead --- app/assets/javascripts/api.js.coffee | 39 ------------------------------------ 1 file changed, 39 deletions(-) diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 51b04051741..f3ed9a66715 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -5,45 +5,6 @@ group_projects_path: "/api/:version/groups/:id/projects.json" projects_path: "/api/:version/projects.json" labels_path: "/api/:version/projects/:id/labels" - issues_paths: - update : "/api/:version/projects/:id/issues/:issue_id" - merge_request_path: "/api/:version/issues/:id.json" - milestones_paths: - index: "/api/:version/projects/:id/milestones" - - milestones: - index: (project_id, callback) -> - data = {} - url = Api.buildUrl(Api.milestones_paths.index) - url = url - .replace(":id", project_id) - data.private_token = gon.api_token - $.ajax( - url: url - type: "GET" - data: data - dataType: "json" - ).done (milestones) -> - if callback? - callback(milestones) - - issues: - update: (project_id, issue_id, data, callback) -> - url = Api.buildUrl(Api.issues_paths.update) - url = url - .replace(":id", project_id) - .replace(":issue_id", issue_id) - if not data? - data = {} - data.private_token = gon.api_token - $.ajax( - url: url - type: "PUT" - data: data - dataType: "json" - ).done (issue) -> - if callback? - callback(issue) group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) -- cgit v1.2.1 From 74c7a537eda71ba8b08f56edacee56e09d5c1004 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 15 Mar 2016 18:26:50 -0400 Subject: Adding dropdown for users --- app/assets/javascripts/users_select.js.coffee | 2 ++ app/views/shared/issuable/_sidebar.html.haml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 84193400890..fe4a54e07e1 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -71,6 +71,8 @@ class @UsersSelect Issues.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() + else + console.log 'else' renderRow: (user) -> username = if user.username then "@#{user.username}" else "" avatar = if user.avatar_url then user.avatar_url else false diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 0f488614cf9..097e1cee140 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -42,7 +42,7 @@ .light None .selectbox.hide-collapsed - = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true) + = dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-author-search", title: "Filter by user", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author", placeholder: "Search users", data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id" } }) .block.milestone .sidebar-collapsed-icon -- cgit v1.2.1 From e8d84110165d1358d90ebafb72ec91a441624527 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 16 Mar 2016 16:52:07 -0400 Subject: Adds new dropdown ajax to user selection --- app/assets/javascripts/labels_select.js.coffee | 5 ++-- app/assets/javascripts/users_select.js.coffee | 38 +++++++++++++++++++++++++- app/controllers/projects/issues_controller.rb | 2 +- app/views/shared/issuable/_sidebar.html.haml | 4 ++- 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index c05daca4afb..464e6cb456a 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -198,14 +198,13 @@ class @LabelsSelect # need inline-block here instead of show, # which will default to the element's style in this case inline. selected = if selected.length then selected else [''] - $loading - .fadeIn() + $loading.fadeIn() $.ajax( type: 'PUT' url: issueURL data: issue: - label_ids: selected + assignee_id: selected ).done (data) -> $loading.fadeOut() $selectbox.hide() diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index fe4a54e07e1..2bfe6b72432 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -12,6 +12,11 @@ class @UsersSelect firstUser = $dropdown.data('first-user') selectedId = $dropdown.data('selected') defaultLabel = $dropdown.data('default-label') + issueURL = $dropdown.data('issueUpdate') + $selectbox = $dropdown.closest('.selectbox') + $block = $selectbox.closest('.block') + $value = $block.find('.value') + $loading = $block.find('.block-loading').fadeOut() $dropdown.glDropdown( data: (term, callback) => @@ -57,11 +62,17 @@ class @UsersSelect fields: ['name', 'username'] selectable: true fieldName: $dropdown.data('field-name') + toggleLabel: (selected) -> if selected && 'id' of selected selected.name else defaultLabel + + hidden: -> + $selectbox.hide() + $value.show() + clicked: -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' @@ -72,7 +83,32 @@ class @UsersSelect else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() else - console.log 'else' + selected = $dropdown + .closest('.selectbox') + .find("input[name='#{$dropdown.data('field-name')}']").val() + $loading + .fadeIn() + $.ajax( + type: 'PUT' + url: issueURL + data: + issue: + assignee_id: selected + ).done (data) -> + $loading.fadeOut() + $selectbox.hide() + href = $value + .show() + .find('.author') + .text(data.assignee.name) + .end() + .find('a') + .attr('href') + splitHref = href.split('/') + splitHref[splitHref.length - 1] = data.assignee.username + $value + .find('a') + .attr('href',splitHref.join('/')) renderRow: (user) -> username = if user.username then "@#{user.username}" else "" avatar = if user.avatar_url then user.avatar_url else false diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index ecd6ed3375e..ec43e56f0ba 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -114,7 +114,7 @@ class Projects::IssuesController < Projects::ApplicationController end end format.json do - render json: @issue.to_json(include: [:milestone, :labels]) + render json: @issue.to_json(include: [:milestone, :labels, :assignee]) end end end diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 097e1cee140..6b42293ec4b 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -28,6 +28,7 @@ = icon('user') .title.hide-collapsed Assignee + =icon('spinner spin', class: 'block-loading') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed @@ -42,7 +43,8 @@ .light None .selectbox.hide-collapsed - = dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-author-search", title: "Filter by user", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author", placeholder: "Search users", data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id" } }) + = f.hidden_field 'assignee_id', value: issuable.assignee.id, id: nil + = dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-author-search", title: "Filter by user", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author", placeholder: "Search users", data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "issue[assignee_id]", issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json) } }) .block.milestone .sidebar-collapsed-icon -- cgit v1.2.1 From 22aef628085b486db02804246e71e154e6516156 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 17 Mar 2016 09:14:46 -0400 Subject: New dropdowns work on merge requests too! --- app/assets/javascripts/gl_dropdown.js.coffee | 6 ++++++ app/assets/javascripts/labels_select.js.coffee | 14 ++++++++------ app/assets/javascripts/milestone_select.js.coffee | 8 +++++--- app/assets/javascripts/users_select.js.coffee | 9 ++++++--- app/controllers/projects/merge_requests_controller.rb | 5 +---- app/views/shared/issuable/_sidebar.html.haml | 16 ++++++++++------ 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index a4a7bacfda2..a1a0e1ecb08 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -126,8 +126,10 @@ class GitLabDropdown @selectFirstRow() # Event listeners + @dropdown.on "shown.bs.dropdown", @opened @dropdown.on "hidden.bs.dropdown", @hidden + @dropdown.on "click", ".dropdown-menu", @shouldPropagate if @dropdown.find(".dropdown-toggle-page").length @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) => @@ -177,6 +179,10 @@ class GitLabDropdown @appendMenu(full_html) + shouldPropagate: (e) => + if @options.multiSelect + e.stopPropagation() + opened: => contentHtml = $('.dropdown-content', @dropdown).html() if @remote && contentHtml is "" diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 464e6cb456a..0ec92764567 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -13,6 +13,7 @@ class @LabelsSelect showNo = $dropdown.data('show-no') showAny = $dropdown.data('show-any') defaultLabel = $dropdown.data('default-label') + abilityName = $dropdown.data('ability-name') $selectbox = $dropdown.closest('.selectbox') $block = $selectbox.closest('.block') $value = $block.find('.value') @@ -195,16 +196,17 @@ class @LabelsSelect .map(-> @value ).get() - # need inline-block here instead of show, - # which will default to the element's style in this case inline. - selected = if selected.length then selected else [''] + console.log 'selected', selected + data = {} + data[abilityName] = {} + data[abilityName].label_ids = selected + if not selected.length + data[abilityName].label_ids = [''] $loading.fadeIn() $.ajax( type: 'PUT' url: issueURL - data: - issue: - assignee_id: selected + data: data ).done (data) -> $loading.fadeOut() $selectbox.hide() diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 876b9d1ae86..6c7a3a5aa78 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -11,6 +11,7 @@ class @MilestoneSelect useId = $dropdown.data('use-id') defaultLabel = $dropdown.data('default-label') issuableId = $dropdown.data('issuable-id') + abilityName = $dropdown.data('ability-name') $selectbox = $dropdown.closest('.selectbox') $block = $selectbox.closest('.block') $value = $block.find('.value') @@ -70,14 +71,15 @@ class @MilestoneSelect .closest('.selectbox') .find('input[type="hidden"]') .val() + data = {} + data[abilityName] = {} + data[abilityName].milestone_id = selected $loading .fadeIn() $.ajax( type: 'PUT' url: issueUpdateURL - data: - issue: - milestone_id: selected + data: data ).done (data) -> $loading.fadeOut() $selectbox.hide() diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 2bfe6b72432..057ad954346 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -15,6 +15,7 @@ class @UsersSelect issueURL = $dropdown.data('issueUpdate') $selectbox = $dropdown.closest('.selectbox') $block = $selectbox.closest('.block') + abilityName = $dropdown.data('ability-name') $value = $block.find('.value') $loading = $block.find('.block-loading').fadeOut() @@ -86,14 +87,16 @@ class @UsersSelect selected = $dropdown .closest('.selectbox') .find("input[name='#{$dropdown.data('field-name')}']").val() + data = {} + data[abilityName] = {} + data[abilityName].assignee_id = selected + console.log 'data',data $loading .fadeIn() $.ajax( type: 'PUT' url: issueURL - data: - issue: - assignee_id: selected + data: data ).done (data) -> $loading.fadeOut() $selectbox.hide() diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index b830d777752..6189de09f27 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -154,10 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.target_project, @merge_request]) end format.json do - render json: { - saved: @merge_request.valid?, - assignee_avatar_url: @merge_request.assignee.try(:avatar_url) - } + render json: @merge_request.to_json(include: [:milestone, :labels, :assignee]) end end else diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 6b42293ec4b..ad76655205e 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,6 +1,10 @@ %aside.right-sidebar{ class: sidebar_gutter_collapsed_class } .issuable-sidebar .block.issuable-sidebar-header + - if issuable.to_ability_name == 'merge_request' + - issuable_url = namespace_project_merge_request_path(@project.namespace, @project, issuable.id, :json) + - else + - issuable_url = namespace_project_issue_path(@project.namespace, @project, issuable.id, :json) %span.issuable-count.hide-collapsed.pull-left = issuable.iid of @@ -43,8 +47,8 @@ .light None .selectbox.hide-collapsed - = f.hidden_field 'assignee_id', value: issuable.assignee.id, id: nil - = dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-author-search", title: "Filter by user", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author", placeholder: "Search users", data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "issue[assignee_id]", issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json) } }) + = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: nil + = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Filter by user', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_url, ability_name: issuable.to_ability_name } }) .block.milestone .sidebar-collapsed-icon @@ -67,8 +71,8 @@ .light None .selectbox.hide-collapsed - = f.hidden_field 'milestone_id', value: issuable.milestone.id, id: nil - = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search milestones", data: { show_no: true, field_name: "issue[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), use_id: true }}) + = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil + = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_url, use_id: true }}) - if issuable.project.labels.any? .block.labels @@ -89,9 +93,9 @@ .light None .selectbox.hide-collapsed - issuable.labels.each do |label| - = hidden_field_tag 'issue[label_names][]', label.id, id: nil + = hidden_field_tag "{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: namespace_project_issue_path(@project.namespace, @project, issuable.id, :json), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: issuable_url, labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} %span.dropdown-toggle-text Label = icon('chevron-down') -- cgit v1.2.1 From 8c1e3c77cd4da2fff126e46bc40b70fef798ac79 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 17 Mar 2016 15:15:47 -0400 Subject: Add multi select stay open functionality --- app/assets/javascripts/gl_dropdown.js.coffee | 9 ++- app/assets/javascripts/labels_select.js.coffee | 77 +++++++++++++++++--------- app/assets/javascripts/users_select.js.coffee | 1 - app/helpers/dropdowns_helper.rb | 2 +- 4 files changed, 58 insertions(+), 31 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index a1a0e1ecb08..3694407c31a 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -129,7 +129,7 @@ class GitLabDropdown @dropdown.on "shown.bs.dropdown", @opened @dropdown.on "hidden.bs.dropdown", @hidden - @dropdown.on "click", ".dropdown-menu", @shouldPropagate + @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate if @dropdown.find(".dropdown-toggle-page").length @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) => @@ -181,7 +181,12 @@ class GitLabDropdown shouldPropagate: (e) => if @options.multiSelect - e.stopPropagation() + $target = $(e.target) + if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') + e.stopPropagation() + return false + else + return true opened: => contentHtml = $('.dropdown-content', @dropdown).html() diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 0ec92764567..01847ad2c31 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -82,6 +82,23 @@ class @LabelsSelect # This allows us to enable the button when ready enableLabelCreateButton = -> if newLabelField.val() isnt '' and newColorField.val() isnt '' + $newLabelError.hide() + $('.js-new-label-btn').disable() + + # Create new label with API + Api.newLabel projectId, { + name: newLabelField.val() + color: newColorField.val() + }, (label) -> + $('.js-new-label-btn').enable() + + if label.message? + $newLabelError + .text label.message + .show() + else + $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + $newLabelCreateButton.enable() else $newLabelCreateButton.disable() @@ -115,6 +132,35 @@ class @LabelsSelect else $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + saveLabelData = -> + selected = $dropdown + .closest('.selectbox') + .find("input[name='#{$dropdown.data('field-name')}']") + .map(-> + @value + ).get() + data = {} + data[abilityName] = {} + data[abilityName].label_ids = selected + if not selected.length + data[abilityName].label_ids = [''] + $loading.fadeIn() + $.ajax( + type: 'PUT' + url: issueURL + data: data + ).done (data) -> + $loading.fadeOut() + $selectbox.hide() + if not data.labels.length + template = labelNoneHTMLTemplate() + else + template = labelHTMLTemplate(data) + href = $value + .show() + .html(template) +>>>>>>> Add multi select stay open functionality + $dropdown.glDropdown( data: (term, callback) -> $.ajax( @@ -190,31 +236,8 @@ class @LabelsSelect else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() else - selected = $dropdown - .closest('.selectbox') - .find("input[name='#{$dropdown.data('field-name')}']") - .map(-> - @value - ).get() - console.log 'selected', selected - data = {} - data[abilityName] = {} - data[abilityName].label_ids = selected - if not selected.length - data[abilityName].label_ids = [''] - $loading.fadeIn() - $.ajax( - type: 'PUT' - url: issueURL - data: data - ).done (data) -> - $loading.fadeOut() - $selectbox.hide() - if not data.labels.length - template = labelNoneHTMLTemplate() - else - template = labelHTMLTemplate(data) - href = $value - .show() - .html(template) + if $dropdown.hasClass 'js-multiselect' + return + else + saveLabelData() ) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 057ad954346..d0e408325b4 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -90,7 +90,6 @@ class @UsersSelect data = {} data[abilityName] = {} data[abilityName].assignee_id = selected - console.log 'data',data $loading .fadeIn() $.ajax( diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 316a10b7da3..14697f774cc 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -60,7 +60,7 @@ module DropdownsHelper title_output << content_tag(:span, title) title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do - icon('times') + icon('times', class: 'dropdown-menu-close-icon') end title_output.html_safe -- cgit v1.2.1 From 38cece12fbdd79f82207f5283812bbe745602bc0 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 17 Mar 2016 15:30:23 -0400 Subject: Rename header for dropdowns --- app/views/shared/issuable/_sidebar.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index ad76655205e..2c8d449c3e4 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -48,7 +48,7 @@ .selectbox.hide-collapsed = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: nil - = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Filter by user', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_url, ability_name: issuable.to_ability_name } }) + = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign user', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_url, ability_name: issuable.to_ability_name } }) .block.milestone .sidebar-collapsed-icon @@ -101,7 +101,7 @@ = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable .dropdown-page-one - = dropdown_title("Filter by label") + = dropdown_title("Assign labels") = dropdown_filter("Search labels") = dropdown_content - if @project -- cgit v1.2.1 From ad06bc66ec9148ef7c4edc31678059dd47e8cb97 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 17 Mar 2016 17:44:52 -0500 Subject: Add missing # --- app/views/shared/issuable/_sidebar.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 2c8d449c3e4..33361a05a46 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -95,7 +95,7 @@ - issuable.labels.each do |label| = hidden_field_tag "{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: issuable_url, labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: issuable_url, labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} %span.dropdown-toggle-text Label = icon('chevron-down') -- cgit v1.2.1 From deee55f0bbbfbe394acef9f8f5d7074578b57dbb Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 17 Mar 2016 23:07:37 -0500 Subject: Remove unnecessary sentence --- app/controllers/projects/issues_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index ec43e56f0ba..629ff43bee9 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -58,7 +58,6 @@ class Projects::IssuesController < Projects::ApplicationController end def edit - puts params respond_with(@issue) end -- cgit v1.2.1 From a971880e5f0c97c478a56bb32ba3372524745387 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 18 Mar 2016 13:35:39 -0500 Subject: Fix endpoint --- app/assets/javascripts/labels_select.js.coffee | 1 + app/assets/javascripts/users_select.js.coffee | 1 + app/views/shared/issuable/_sidebar.html.haml | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 01847ad2c31..78ee0ba467a 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -148,6 +148,7 @@ class @LabelsSelect $.ajax( type: 'PUT' url: issueURL + dataType: 'JSON' data: data ).done (data) -> $loading.fadeOut() diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index d0e408325b4..5fe8f73ce2d 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -94,6 +94,7 @@ class @UsersSelect .fadeIn() $.ajax( type: 'PUT' + dataType: 'json' url: issueURL data: data ).done (data) -> diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 33361a05a46..d702c8ec1fe 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -2,9 +2,9 @@ .issuable-sidebar .block.issuable-sidebar-header - if issuable.to_ability_name == 'merge_request' - - issuable_url = namespace_project_merge_request_path(@project.namespace, @project, issuable.id, :json) + - issuable_url = namespace_project_merge_request_path(@project.namespace, @project, issuable.iid) - else - - issuable_url = namespace_project_issue_path(@project.namespace, @project, issuable.id, :json) + - issuable_url = namespace_project_issue_path(@project.namespace, @project, issuable.iid) %span.issuable-count.hide-collapsed.pull-left = issuable.iid of @@ -93,7 +93,7 @@ .light None .selectbox.hide-collapsed - issuable.labels.each do |label| - = hidden_field_tag "{issuable.to_ability_name}[label_names][]", label.id, id: nil + = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: issuable_url, labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} %span.dropdown-toggle-text -- cgit v1.2.1 From 531331f79d63a736efd1d40d7ad40b5a277327fd Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 18 Mar 2016 13:36:08 -0500 Subject: Fix specs --- spec/features/issues/filter_by_milestone_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb index f6e33f651c4..d8e2ecb9feb 100644 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -31,7 +31,7 @@ feature 'Issue filtering by Milestone', feature: true do def filter_by_milestone(title) find(".js-milestone-select").click sleep 0.5 - find(".milestone-filter a", text: title).click + find(".milestone-filter .dropdown-content a", text: title).click sleep 1 end end -- cgit v1.2.1 From 506878970b2040be7446fc1a341d7abc61f9c6ec Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 24 Mar 2016 09:29:00 -0700 Subject: Don't attempt to look up an avatar in repo if repo directory does not exist Closes #14580 --- CHANGELOG | 1 + app/models/repository.rb | 2 ++ spec/models/project_spec.rb | 6 ++++++ spec/models/repository_spec.rb | 6 ++++++ 4 files changed, 15 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 5d9f4961ef5..18893feff06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) + - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu) - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) - Fix avatar stretching by providing a cropping feature diff --git a/app/models/repository.rb b/app/models/repository.rb index 13154eb4205..b69d5f239a0 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -877,6 +877,8 @@ class Repository end def avatar + return nil unless exists? + @avatar ||= cache.fetch(:avatar) do AVATAR_FILES.find do |file| blob_at_branch('master', file) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 20f06f4b7e1..55f1c665b86 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -422,6 +422,12 @@ describe Project, models: true do it { should eq "http://localhost#{avatar_path}" } end + + context 'when git repo is empty' do + let(:project) { create(:empty_project) } + + it { should eq nil } + end end describe :ci_commit do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 7eac70ae948..b30a6e7ae3d 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -725,6 +725,12 @@ describe Repository, models: true do end describe '#avatar' do + it 'returns nil if repo does not exist' do + expect(repository).to receive(:exists?).and_return(false) + + expect(repository.avatar).to eq(nil) + end + it 'returns the first avatar file found in the repository' do expect(repository).to receive(:blob_at_branch). with('master', 'logo.png'). -- cgit v1.2.1 From 6f48cb860957676273d1e7bfc1195195599ab0f1 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Sat, 19 Mar 2016 11:39:34 -0400 Subject: Fix broken functionality in sidebar after merge. Added nice animations too. --- app/assets/javascripts/gl_dropdown.js.coffee | 2 +- app/assets/javascripts/labels_select.js.coffee | 18 ++++++++++++++---- app/assets/javascripts/lib/animate.js.coffee | 13 +++++++++++++ app/assets/javascripts/milestone_select.js.coffee | 16 +++++++--------- app/assets/javascripts/users_select.js.coffee | 3 +++ app/assets/stylesheets/application.scss | 1 + app/assets/stylesheets/pages/issuable.scss | 2 +- app/views/shared/issuable/_sidebar.html.haml | 4 ++-- vendor/assets/stylesheets/animate.css | 11 +++++++++++ 9 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 app/assets/javascripts/lib/animate.js.coffee create mode 100644 vendor/assets/stylesheets/animate.css diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 3694407c31a..d1438aed7f6 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -286,8 +286,8 @@ class GitLabDropdown selectedObject = @renderedData[selectedIndex] value = if @options.id then @options.id(selectedObject, el) else selectedObject.id field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") - if el.hasClass(ACTIVE_CLASS) + el.removeClass(ACTIVE_CLASS) field.remove() else fieldName = @options.fieldName diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 78ee0ba467a..285e15feffe 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -28,8 +28,8 @@ class @LabelsSelect # Suggested colors in the dropdown to chose from pre-chosen colors $('.suggest-colors-dropdown a').on 'click', (e) -> - issueURLSplit = issueURL.split('/') if issueURL? - if issueURL + issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL? + if issueUpdateURL labelHTMLTemplate = _.template( '<% _.each(labels, function(label){ %>'+ ' $loading.fadeOut() $selectbox.hide() - href = $value + $milestoneLink = $value .show() - .find('.milestone-title') - .text(data.milestone.title) - .end() .find('a') + href = $milestoneLink + .text(data.milestone.title) .attr('href') + splitHref = href.split('/') - splitHref[splitHref.length - 1] = data.id - $value - .find('a') + splitHref[splitHref.length - 1] = data.milestone.iid + $milestoneLink .attr('href',splitHref.join('/')) ) \ No newline at end of file diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 5fe8f73ce2d..2f6d365237b 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -105,6 +105,9 @@ class @UsersSelect .find('.author') .text(data.assignee.name) .end() + .find('.username') + .text("@#{data.assignee.username}") + .end() .find('a') .attr('href') splitHref = href.split('/') diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index e2d590f4df4..69b3b6586de 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -10,6 +10,7 @@ *= require dropzone/basic *= require cal-heatmap *= require cropper.css + *= require animate */ /* diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 459c1840ec1..467df4c3653 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -255,7 +255,7 @@ .dropdown-menu-toggle { width: 100%; - padding-top: 0; + padding-top: 6px; } .open .dropdown-menu { diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index d702c8ec1fe..46a7f325441 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -2,9 +2,9 @@ .issuable-sidebar .block.issuable-sidebar-header - if issuable.to_ability_name == 'merge_request' - - issuable_url = namespace_project_merge_request_path(@project.namespace, @project, issuable.iid) + - issuable_url = namespace_project_merge_request_path(@project.namespace, @project, issuable.iid, :json) - else - - issuable_url = namespace_project_issue_path(@project.namespace, @project, issuable.iid) + - issuable_url = namespace_project_issue_path(@project.namespace, @project, issuable.iid, :json) %span.issuable-count.hide-collapsed.pull-left = issuable.iid of diff --git a/vendor/assets/stylesheets/animate.css b/vendor/assets/stylesheets/animate.css new file mode 100644 index 00000000000..b6f61295392 --- /dev/null +++ b/vendor/assets/stylesheets/animate.css @@ -0,0 +1,11 @@ +@charset "UTF-8"; + +/*! + * animate.css -http://daneden.me/animate + * Version - 3.5.1 + * Licensed under the MIT license - http://opensource.org/licenses/MIT + * + * Copyright (c) 2016 Daniel Eden + */ + +.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.hinge{-webkit-animation-duration:2s;animation-duration:2s}.animated.bounceIn,.animated.bounceOut,.animated.flipOutX,.animated.flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}@keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}.bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.pulse{-webkit-animation-name:pulse;animation-name:pulse}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.shake{-webkit-animation-name:shake;animation-name:shake}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}@keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}.wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.bounceIn{-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}.bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.bounceOut{-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animated.flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.flipOutX{-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.flipOutY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}.lightSpeedIn{-webkit-animation-name:lightSpeedIn;animation-name:lightSpeedIn;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.lightSpeedOut{-webkit-animation-name:lightSpeedOut;animation-name:lightSpeedOut;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}.rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn}@-webkit-keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft}@-webkit-keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight}@-webkit-keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft}@-webkit-keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight}@-webkit-keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut}@-webkit-keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft}@-webkit-keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight}@-webkit-keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft}@-webkit-keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight}@-webkit-keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.hinge{-webkit-animation-name:hinge;animation-name:hinge}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}.rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}.zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}.zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}.zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp} \ No newline at end of file -- cgit v1.2.1 From 43a8e3a693efbbb43f4871332af6235786f18adc Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Sat, 19 Mar 2016 12:44:58 -0400 Subject: Fix subscription button by hooking it back up. --- app/assets/javascripts/subscription.js.coffee | 1 + app/views/shared/issuable/_sidebar.html.haml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index 084f0e0dc65..78ed819fb4b 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -7,6 +7,7 @@ class @Subscription @subscribe_button.unbind('click').click(@toggleSubscription) toggleSubscription: (event) => + console.log 'toggleSubscription' btn = $(event.currentTarget) action = btn.find('span').text() current_status = @subscription_status.attr('data-status') diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 46a7f325441..fe6bb77fdcc 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -150,3 +150,4 @@ new MilestoneSelect(); new LabelsSelect(); new IssuableContext(); + new Subscription('.subscription') \ No newline at end of file -- cgit v1.2.1 From 3fc887ea02e42e1475b920769c142006ac98a249 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Sat, 19 Mar 2016 13:58:14 -0400 Subject: Remove console log --- app/assets/javascripts/subscription.js.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index 78ed819fb4b..084f0e0dc65 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -7,7 +7,6 @@ class @Subscription @subscribe_button.unbind('click').click(@toggleSubscription) toggleSubscription: (event) => - console.log 'toggleSubscription' btn = $(event.currentTarget) action = btn.find('span').text() current_status = @subscription_status.attr('data-status') -- cgit v1.2.1 From f649126095481dc39ef7e8606672ae77509a5516 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Sat, 19 Mar 2016 14:25:57 -0400 Subject: Add js-extras so show any and show no for milestones show up. --- app/views/shared/issuable/_milestone_dropdown.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 0434506c8d7..1c79494f816 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,6 +1,6 @@ - if params[:milestone_title] = hidden_field_tag(:milestone_title, params[:milestone_title]) -= dropdown_tag(h(params[:milestone_title].presence || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", += dropdown_tag(h(params[:milestone_title].presence || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit js-extra-options', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - if @project %ul.dropdown-footer-list -- cgit v1.2.1 From 5db2622aeafe40bf6027bb45592f2fda5bfec396 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Sat, 19 Mar 2016 20:19:51 -0400 Subject: Assign current user when no user is assigned link --- app/assets/javascripts/labels_select.js.coffee | 21 ++++--- app/assets/javascripts/users_select.js.coffee | 81 +++++++++++++++++--------- app/assets/stylesheets/pages/issuable.scss | 6 ++ app/views/shared/issuable/_sidebar.html.haml | 9 ++- 4 files changed, 77 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 285e15feffe..d77490cae4e 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -31,17 +31,16 @@ class @LabelsSelect issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL? if issueUpdateURL labelHTMLTemplate = _.template( - '<% _.each(labels, function(label){ %>'+ - ''+ - ''+ - '<%= label.title %>'+ - ''+ - ''+ - '<% }); %>'); + '<% _.each(labels, function(label){ %> + "> + + <%= label.title %> + + + <% }); %>' + ); labelNoneHTMLTemplate = _.template('
None
') if newLabelField.length and $dropdown.hasClass 'js-extra-options' diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 2f6d365237b..cb3e16a6007 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -19,6 +19,27 @@ class @UsersSelect $value = $block.find('.value') $loading = $block.find('.block-loading').fadeOut() + noAssigneeTemplate = _.template( + '<% if (username) { %> + + <% if( avatar ) { %> + + <% } %> + <%= name %> + + @<%= username %> + + + <% } else { %> + + No assignee - + + assign yourself + + + <% } %>' + ) + $dropdown.glDropdown( data: (term, callback) => @users term, (users) => @@ -100,21 +121,21 @@ class @UsersSelect ).done (data) -> $loading.fadeOut() $selectbox.hide() - href = $value - .show() - .find('.author') - .text(data.assignee.name) - .end() - .find('.username') - .text("@#{data.assignee.username}") - .end() - .find('a') - .attr('href') - splitHref = href.split('/') - splitHref[splitHref.length - 1] = data.assignee.username - $value - .find('a') - .attr('href',splitHref.join('/')) + + if data.assignee + user = + name: data.assignee.name + username: data.assignee.username + avatar: data.assignee.avatar.url + else + user = + name: 'Unassigned' + username: '' + avatar: '' + + $value.html(noAssigneeTemplate(user)) + $value.find('a').attr('href') + renderRow: (user) -> username = if user.username then "@#{user.username}" else "" avatar = if user.avatar_url then user.avatar_url else false @@ -131,17 +152,25 @@ class @UsersSelect if avatar img = "" - "
  • - - #{img} - - #{user.name} - - - #{username} - - -
  • " + # split into three parts so we can remove the username section if nessesary + listWithName = "
  • + + #{img} + + #{user.name} + " + + listWithUserName = " + #{username} + " + listClosingTags = " +
  • " + + + if username is '' + listWithUserName = '' + + listWithName + listWithUserName + listClosingTags ) $('.ajax-users-select').each (i, select) => diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 467df4c3653..70a8fc5a9e2 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -133,6 +133,12 @@ .value { line-height: 1; + + .assign-yourself { + margin-top: 10px; + font-weight: normal; + display: block; + } } .bold { diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index fe6bb77fdcc..162c7787c55 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -44,11 +44,14 @@ %span.username = issuable.assignee.to_reference - else - .light None + %span.assign-yourself + No assignee - + %a.js-assign-yourself{href:'#'} + assign yourself .selectbox.hide-collapsed - = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: nil - = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign user', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_url, ability_name: issuable.to_ability_name } }) + = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id' + = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign user', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_url, ability_name: issuable.to_ability_name, null_user: true } }) .block.milestone .sidebar-collapsed-icon -- cgit v1.2.1 From 201101dfff831fc3c05475b7219d2870ca700fa5 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Sat, 19 Mar 2016 20:49:36 -0400 Subject: Add Assign to me button and unassigned. --- app/assets/javascripts/issuable_context.js.coffee | 3 +- app/assets/javascripts/users_select.js.coffee | 65 +++++++++++++---------- app/views/shared/issuable/_sidebar.html.haml | 2 +- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index ddc7de602e4..1e804d25c14 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -1,8 +1,7 @@ class @IssuableContext constructor: -> @initParticipants() - - new UsersSelect() + new UsersSelect(currentUser) $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) $(".issuable-sidebar .inline-update").on "change", "select", -> diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index cb3e16a6007..76eb2b1d696 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -1,7 +1,8 @@ class @UsersSelect - constructor: -> + constructor: (currentUser) -> @usersPath = "/autocomplete/users.json" @userPath = "/autocomplete/users/:id.json" + @currentUser = JSON.parse(currentUser) $('.js-user-search').each (i, dropdown) => $dropdown = $(dropdown) @@ -19,6 +20,40 @@ class @UsersSelect $value = $block.find('.value') $loading = $block.find('.block-loading').fadeOut() + $block.on('click','.js-assign-yourself' , (e) => + e.preventDefault() + assignTo(@currentUser.id) + ) + + assignTo = (selected) -> + data = {} + data[abilityName] = {} + data[abilityName].assignee_id = selected + $loading + .fadeIn() + $.ajax( + type: 'PUT' + dataType: 'json' + url: issueURL + data: data + ).done (data) -> + $loading.fadeOut() + $selectbox.hide() + + if data.assignee + user = + name: data.assignee.name + username: data.assignee.username + avatar: data.assignee.avatar.url + else + user = + name: 'Unassigned' + username: '' + avatar: '' + + $value.html(noAssigneeTemplate(user)) + $value.find('a').attr('href') + noAssigneeTemplate = _.template( '<% if (username) { %> @@ -108,33 +143,7 @@ class @UsersSelect selected = $dropdown .closest('.selectbox') .find("input[name='#{$dropdown.data('field-name')}']").val() - data = {} - data[abilityName] = {} - data[abilityName].assignee_id = selected - $loading - .fadeIn() - $.ajax( - type: 'PUT' - dataType: 'json' - url: issueURL - data: data - ).done (data) -> - $loading.fadeOut() - $selectbox.hide() - - if data.assignee - user = - name: data.assignee.name - username: data.assignee.username - avatar: data.assignee.avatar.url - else - user = - name: 'Unassigned' - username: '' - avatar: '' - - $value.html(noAssigneeTemplate(user)) - $value.find('a').attr('href') + assignTo(selected) renderRow: (user) -> username = if user.username then "@#{user.username}" else "" diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 162c7787c55..d6328216dae 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -152,5 +152,5 @@ :javascript new MilestoneSelect(); new LabelsSelect(); - new IssuableContext(); + new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}'); new Subscription('.subscription') \ No newline at end of file -- cgit v1.2.1 From cf0c6e87eabcc637a145d1bc7fc89c914e7c0781 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 21 Mar 2016 09:07:53 +0000 Subject: Fixed some colours in sidebar --- app/assets/stylesheets/pages/issuable.scss | 8 ++++++-- app/views/shared/issuable/_sidebar.html.haml | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 70a8fc5a9e2..88c1b614c74 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -30,6 +30,10 @@ } .issuable-sidebar { + a { + color: inherit; + } + .block { @include clearfix; padding: $gl-padding 0; @@ -89,7 +93,7 @@ } .cross-project-reference { - color: $gl-link-color; + color: inherit; span { white-space: nowrap; @@ -258,7 +262,7 @@ text-decoration: none; } } - + .dropdown-menu-toggle { width: 100%; padding-top: 6px; diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index d6328216dae..70de11fe136 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -44,7 +44,7 @@ %span.username = issuable.assignee.to_reference - else - %span.assign-yourself + %span.assign-yourself No assignee - %a.js-assign-yourself{href:'#'} assign yourself @@ -153,4 +153,4 @@ new MilestoneSelect(); new LabelsSelect(); new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}'); - new Subscription('.subscription') \ No newline at end of file + new Subscription('.subscription') -- cgit v1.2.1 From 139c255a929596a722c2c394a4650381feab34c8 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 21 Mar 2016 11:20:21 +0000 Subject: Fixed JS error that was failing the builds --- app/assets/javascripts/users_select.js.coffee | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 76eb2b1d696..99322ffd807 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -2,7 +2,8 @@ class @UsersSelect constructor: (currentUser) -> @usersPath = "/autocomplete/users.json" @userPath = "/autocomplete/users/:id.json" - @currentUser = JSON.parse(currentUser) + if currentUser? + @currentUser = JSON.parse(currentUser) $('.js-user-search').each (i, dropdown) => $dropdown = $(dropdown) @@ -20,7 +21,7 @@ class @UsersSelect $value = $block.find('.value') $loading = $block.find('.block-loading').fadeOut() - $block.on('click','.js-assign-yourself' , (e) => + $block.on('click', '.js-assign-yourself', (e) => e.preventDefault() assignTo(@currentUser.id) ) @@ -39,7 +40,7 @@ class @UsersSelect ).done (data) -> $loading.fadeOut() $selectbox.hide() - + if data.assignee user = name: data.assignee.name @@ -67,7 +68,7 @@ class @UsersSelect <% } else { %> - No assignee - + No assignee - assign yourself @@ -175,7 +176,7 @@ class @UsersSelect listClosingTags = " " - + if username is '' listWithUserName = '' -- cgit v1.2.1 From 42389ea8d22096fe260a57bd9bf7b763d0578628 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 22 Mar 2016 15:18:29 -0400 Subject: Fix features until tests pass. Milestones needed a none selector Assignee needed unassign. --- app/assets/javascripts/gl_dropdown.js.coffee | 3 + app/assets/javascripts/milestone_select.js.coffee | 26 ++++--- app/assets/javascripts/users_select.js.coffee | 2 + app/views/shared/issuable/_sidebar.html.haml | 4 +- spec/features/issues_spec.rb | 82 ++++++++++------------- 5 files changed, 59 insertions(+), 58 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index d1438aed7f6..3476cf97b32 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -314,6 +314,9 @@ class GitLabDropdown if !field.length # Create hidden input for form input = "" + if @options.inputId? + input = $(input) + .attr('id', @options.inputId) @dropdown.before input selectFirstRow: -> diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 52fa5740c49..b6983b93cc7 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -1,5 +1,8 @@ class @MilestoneSelect - constructor: () -> + constructor: (currentProject) -> + if currentProject? + _this = @ + @currentProject = JSON.parse(currentProject) $('.js-milestone-select').each (i, dropdown) -> $dropdown = $(dropdown) projectId = $dropdown.data('project-id') @@ -17,6 +20,13 @@ class @MilestoneSelect $value = $block.find('.value') $loading = $block.find('.block-loading').fadeOut() + if issueUpdateURL + milestoneLinkTemplate = _.template( + '<%= title %>' + ) + + milestoneLinkNoneTemplate = '
    None
    ' + $dropdown.glDropdown( data: (term, callback) -> $.ajax( @@ -85,12 +95,10 @@ class @MilestoneSelect $milestoneLink = $value .show() .find('a') - href = $milestoneLink - .text(data.milestone.title) - .attr('href') - - splitHref = href.split('/') - splitHref[splitHref.length - 1] = data.milestone.iid - $milestoneLink - .attr('href',splitHref.join('/')) + if data.milestone? + data.milestone.namespace = _this.currentProject.namespace + data.milestone.path = _this.currentProject.path + $value.html(milestoneLinkTemplate(data.milestone)) + else + $value.html(milestoneLinkNoneTemplate) ) \ No newline at end of file diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 99322ffd807..a12e6d60e90 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -127,6 +127,8 @@ class @UsersSelect else defaultLabel + inputId: 'issue_assignee_id' + hidden: -> $selectbox.hide() $value.show() diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 70de11fe136..c6e6aacbb5c 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -75,7 +75,7 @@ .selectbox.hide-collapsed = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil - = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_url, use_id: true }}) + = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_url, use_id: true }}) - if issuable.project.labels.any? .block.labels @@ -150,7 +150,7 @@ = clipboard_button(clipboard_text: project_ref) :javascript - new MilestoneSelect(); + new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); new LabelsSelect(); new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}'); new Subscription('.subscription') diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index e844e681ebf..d4939584ab6 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -35,17 +35,17 @@ describe 'Issues', feature: true do fill_in 'issue_description', with: 'bug description' end - it 'does not change issue count' do - expect { click_button 'Save changes' }.to_not change { Issue.count } - end + # it 'does not change issue count' do + # expect { click_button 'Save changes' }.to_not change { Issue.count } + # end - it 'should update issue fields' do - click_button 'Save changes' + # it 'should update issue fields' do + # click_button 'Save changes' - expect(page).to have_content @user.name - expect(page).to have_content 'bug 345' - expect(page).to have_content project.name - end + # expect(page).to have_content @user.name + # expect(page).to have_content 'bug 345' + # expect(page).to have_content project.name + # end end end @@ -70,7 +70,7 @@ describe 'Issues', feature: true do click_button 'Save changes' page.within('.assignee') do - expect(page).to have_content 'None' + expect(page).to have_content 'No assignee - assign yourself' end expect(issue.reload.assignee).to be_nil @@ -198,20 +198,26 @@ describe 'Issues', feature: true do end describe 'update assignee from issue#show' do - let(:issue) { create(:issue, project: project, author: @user) } + let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } context 'by autorized user' do - it 'with dropdown menu' do + it 'allows user to select unassigned', js: true do visit namespace_project_issue_path(project.namespace, project, issue) - find('.issuable-sidebar #issue_assignee_id'). - set project.team.members.first.id - click_button 'Update Issue' + page.within('.assignee') do + expect(page).to have_content "#{@user.name}" + end + + find('.block.assignee .edit-link').click + sleep 2 # wait for ajax stuff to complete + first('.dropdown-menu-user-link').click + sleep 2 + page.within('.assignee') do + expect(page).to have_content 'No assignee' + end - expect(page).to have_content 'Assignee' - has_select?('issue_assignee_id', - selected: project.team.members.first.name) + expect(issue.reload.assignee).to be_nil end end @@ -221,8 +227,6 @@ describe 'Issues', feature: true do before :each do project.team << [[guest], :guest] - issue.assignee = @user - issue.save end it 'shows assignee text', js: true do @@ -241,20 +245,23 @@ describe 'Issues', feature: true do context 'by authorized user' do - it 'with dropdown menu' do - visit namespace_project_issue_path(project.namespace, project, issue) - find('.issuable-sidebar'). - select(milestone.title, from: 'issue_milestone_id') - click_button 'Update Issue' + it 'allows user to select unassigned', js: true do + visit namespace_project_issue_path(project.namespace, project, issue) - expect(page).to have_content "Milestone changed to #{milestone.title}" + page.within('.milestone') do + expect(page).to have_content "None" + end + find('.block.milestone .edit-link').click + sleep 2 # wait for ajax stuff to complete + first('.dropdown-content li').click + sleep 2 page.within('.milestone') do - expect(page).to have_content milestone.title + expect(page).to have_content 'None' end - has_select?('issue_assignee_id', selected: milestone.title) + expect(issue.reload.milestone).to be_nil end end @@ -283,25 +290,6 @@ describe 'Issues', feature: true do issue.assignee = user2 issue.save end - - it 'allows user to remove assignee', js: true do - visit namespace_project_issue_path(project.namespace, project, issue) - - page.within('.assignee') do - expect(page).to have_content user2.name - end - - find('.assignee .edit-link').click - sleep 2 # wait for ajax stuff to complete - first('.user-result').click - - page.within('.assignee') do - expect(page).to have_content 'None' - end - - sleep 2 # wait for ajax stuff to complete - expect(issue.reload.assignee).to be_nil - end end end -- cgit v1.2.1 From 6835f19c2b173e8fe3b81d43ef24b0b35350c59a Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Tue, 22 Mar 2016 20:10:56 -0400 Subject: Add back in currentUser --- app/assets/javascripts/issuable_context.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index 1e804d25c14..acc2a387f41 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -1,5 +1,5 @@ class @IssuableContext - constructor: -> + constructor: (currentUser) -> @initParticipants() new UsersSelect(currentUser) $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) -- cgit v1.2.1 From 53a831124e4e704cc494511ed4d04969a223db4e Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 24 Mar 2016 18:29:35 -0400 Subject: Edit button leaves modal open Add user avatar to user selection --- app/assets/javascripts/gl_dropdown.js.coffee | 1 - app/assets/javascripts/issuable_context.js.coffee | 19 +++++++++++++------ app/assets/javascripts/users_select.js.coffee | 4 ++-- app/controllers/projects/issues_controller.rb | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 3476cf97b32..2b56ab2e6de 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -197,7 +197,6 @@ class GitLabDropdown @dropdown.find(".dropdown-input-field").focus() hidden: (e) => - if @options.filterable @dropdown .find(".dropdown-input-field") diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index acc2a387f41..6fc924d3d66 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -10,12 +10,19 @@ class @IssuableContext $(this).submit() $(document).on "click",".edit-link", (e) -> - block = $(@).parents('.block') - block.find('.selectbox').show() - block.find('.value').hide() - setTimeout (-> - block.find('.dropdown-menu-toggle').trigger 'click' - ), 0 + $block = $(@).parents('.block') + $selectbox = $block.find('.selectbox') + if $selectbox.is(':visible') + $selectbox.hide() + $block.find('.value').show() + else + $selectbox.show() + $block.find('.value').hide() + + if $selectbox.is(':visible') + setTimeout (-> + $block.find('.dropdown-menu-toggle').trigger 'click' + ), 0 $(".right-sidebar").niceScroll() diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index a12e6d60e90..59dac4efa65 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -45,7 +45,7 @@ class @UsersSelect user = name: data.assignee.name username: data.assignee.username - avatar: data.assignee.avatar.url + avatar: data.assignee.avatar_url else user = name: 'Unassigned' @@ -129,7 +129,7 @@ class @UsersSelect inputId: 'issue_assignee_id' - hidden: -> + hidden: (e) -> $selectbox.hide() $value.show() diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 629ff43bee9..6d649e72f84 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -113,7 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController end end format.json do - render json: @issue.to_json(include: [:milestone, :labels, :assignee]) + render json: @issue.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }]) end end end -- cgit v1.2.1 From 1cc6d786e4bfd22b5c94dae516ef04c0f32cb379 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 25 Mar 2016 08:00:16 -0400 Subject: Fix minor code style issues Fix underscore template error. Fix `=` spacing --- app/assets/javascripts/labels_select.js.coffee | 7 +++---- app/helpers/issuables_helper.rb | 10 ++++++++++ app/views/shared/issuable/_sidebar.html.haml | 18 +++++++----------- config/environments/development.rb | 2 +- spec/features/issues_spec.rb | 13 ------------- 5 files changed, 21 insertions(+), 29 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index d77490cae4e..16cbffa6818 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -32,10 +32,8 @@ class @LabelsSelect if issueUpdateURL labelHTMLTemplate = _.template( '<% _.each(labels, function(label){ %> - "> - + issues?label_name=<%= label.title %>"> + <%= label.title %> @@ -152,6 +150,7 @@ class @LabelsSelect ).done (data) -> $loading.fadeOut() $selectbox.hide() + data.issueURLSplit = issueURLSplit if not data.labels.length template = labelNoneHTMLTemplate() else diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 81df2094392..62050691a39 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -16,6 +16,16 @@ module IssuablesHelper base_issuable_scope(issuable).where('iid > ?', issuable.iid).last end + def issuable_json_path(issuable) + project = issuable.project + + if issuable.kind_of?(MergeRequest) + namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json) + else + namespace_project_issue_path(project.namespace, project, issuable.iid, :json) + end + end + def prev_issuable_for(issuable) base_issuable_scope(issuable).where('iid < ?', issuable.iid).first end diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c6e6aacbb5c..b606f454eeb 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,10 +1,6 @@ %aside.right-sidebar{ class: sidebar_gutter_collapsed_class } .issuable-sidebar .block.issuable-sidebar-header - - if issuable.to_ability_name == 'merge_request' - - issuable_url = namespace_project_merge_request_path(@project.namespace, @project, issuable.iid, :json) - - else - - issuable_url = namespace_project_issue_path(@project.namespace, @project, issuable.iid, :json) %span.issuable-count.hide-collapsed.pull-left = issuable.iid of @@ -32,7 +28,7 @@ = icon('user') .title.hide-collapsed Assignee - =icon('spinner spin', class: 'block-loading') + = icon('spinner spin', class: 'block-loading') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed @@ -46,12 +42,12 @@ - else %span.assign-yourself No assignee - - %a.js-assign-yourself{href:'#'} + %a.js-assign-yourself{ href: '#' } assign yourself .selectbox.hide-collapsed = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id' - = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign user', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_url, ability_name: issuable.to_ability_name, null_user: true } }) + = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign user', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }) .block.milestone .sidebar-collapsed-icon @@ -63,7 +59,7 @@ No .title.hide-collapsed Milestone - =icon('spinner spin', class: 'block-loading') + = icon('spinner spin', class: 'block-loading') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed @@ -75,7 +71,7 @@ .selectbox.hide-collapsed = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil - = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_url, use_id: true }}) + = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) - if issuable.project.labels.any? .block.labels @@ -85,7 +81,7 @@ = issuable.labels.count .title.hide-collapsed Labels - =icon('spinner spin', class: 'block-loading') + = icon('spinner spin', class: 'block-loading') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) } @@ -98,7 +94,7 @@ - issuable.labels.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: issuable_url, labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} %span.dropdown-toggle-text Label = icon('chevron-down') diff --git a/config/environments/development.rb b/config/environments/development.rb index 78fc91bc3cc..689694a3480 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -36,7 +36,7 @@ Rails.application.configure do # For having correct urls in mails config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } # Open sent mails in browser - config.action_mailer.delivery_method = :test + config.action_mailer.delivery_method = :letter_opener # Don't make a mess when bootstrapping a development environment config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1') diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index d4939584ab6..db46657c36a 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -34,20 +34,7 @@ describe 'Issues', feature: true do fill_in 'issue_title', with: 'bug 345' fill_in 'issue_description', with: 'bug description' end - - # it 'does not change issue count' do - # expect { click_button 'Save changes' }.to_not change { Issue.count } - # end - - # it 'should update issue fields' do - # click_button 'Save changes' - - # expect(page).to have_content @user.name - # expect(page).to have_content 'bug 345' - # expect(page).to have_content project.name - # end end - end describe 'Editing issue assignee' do -- cgit v1.2.1 From 1445bfa18d583d2816e75df0ee626697615b6864 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 25 Mar 2016 12:44:33 -0400 Subject: Use hidden inputs instead of `data-selected` --- app/assets/javascripts/labels_select.js.coffee | 15 ++++++--------- app/views/shared/issuable/_sidebar.html.haml | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 16cbffa6818..b5c7af9a8ad 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -191,19 +191,16 @@ class @LabelsSelect callback data renderRow: (label) -> - if $.isArray(selectedLabel) - selected = '' - $.each selectedLabel, (i, selectedLbl) -> - selectedLbl = selectedLbl.trim() - if selected is '' and label.title is selectedLbl - selected = 'is-active' - else - selected = if label.title is selectedLabel then 'is-active' else '' + selectedClass = '' + if $selectbox.find("input[type='hidden']\ + [name='#{$dropdown.data('field-name')}']\ + [value='#{label.id}']").length + selectedClass = 'is-active' color = if label.color? then "" else "" "
  • - + #{color} #{label.title} diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index b606f454eeb..a0e6cba0702 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -94,7 +94,7 @@ - issuable.labels.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", selected: issuable.label_names.join(","), project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} %span.dropdown-toggle-text Label = icon('chevron-down') -- cgit v1.2.1 From 4f07c0a107b86ea23834a6797989963f1a63f5c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 25 Mar 2016 18:51:17 +0100 Subject: Ensure project snippets have their own access level --- app/controllers/projects/snippets_controller.rb | 2 +- app/models/ability.rb | 46 +++++++---- .../project/snippet/internal_access_spec.rb | 78 ++++++++++++++++++ .../project/snippet/private_access_spec.rb | 63 +++++++++++++++ .../security/project/snippet/public_access_spec.rb | 93 ++++++++++++++++++++++ 5 files changed, 266 insertions(+), 16 deletions(-) create mode 100644 spec/features/security/project/snippet/internal_access_spec.rb create mode 100644 spec/features/security/project/snippet/private_access_spec.rb create mode 100644 spec/features/security/project/snippet/public_access_spec.rb diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 383b86b68e0..6d2901a24a4 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -3,7 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read any snippet - before_action :authorize_read_project_snippet!, except: [:index] + before_action :authorize_read_project_snippet!, except: [:new, :create, :index] # Allow write(create) snippet before_action :authorize_create_project_snippet!, only: [:new, :create] diff --git a/app/models/ability.rb b/app/models/ability.rb index 5f326729433..c0bf6def7c5 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -348,24 +348,22 @@ class Ability end end - [:note, :project_snippet].each do |name| - define_method "#{name}_abilities" do |user, subject| - rules = [] - - if subject.author == user - rules += [ - :"read_#{name}", - :"update_#{name}", - :"admin_#{name}" - ] - end + def note_abilities(user, note) + rules = [] - if subject.respond_to?(:project) && subject.project - rules += project_abilities(user, subject.project) - end + if note.author == user + rules += [ + :read_note, + :update_note, + :admin_note + ] + end - rules + if note.respond_to?(:project) && note.project + rules += project_abilities(user, note.project) end + + rules end def personal_snippet_abilities(user, snippet) @@ -386,6 +384,24 @@ class Ability rules end + def project_snippet_abilities(user, snippet) + rules = [] + + if snippet.author == user || user.admin? + rules += [ + :read_project_snippet, + :update_project_snippet, + :admin_project_snippet + ] + end + + if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user)) + rules << :read_project_snippet + end + + rules + end + def group_member_abilities(user, subject) rules = [] target_user = subject.user diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb new file mode 100644 index 00000000000..db53a9cec97 --- /dev/null +++ b/spec/features/security/project/snippet/internal_access_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +describe "Internal Project Snippets Access", feature: true do + include AccessMatchers + + let(:project) { create(:project, :internal) } + + let(:owner) { project.owner } + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } + let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: owner) } + let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) } + + before do + project.team << [master, :master] + project.team << [developer, :developer] + project.team << [reporter, :reporter] + project.team << [guest, :guest] + end + + describe "GET /:project_path/snippets" do + subject { namespace_project_snippets_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/new" do + subject { new_namespace_project_snippet_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/:id for an internal snippet" do + subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/:id for a private snippet" do + subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end +end diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb new file mode 100644 index 00000000000..d23d645c8e5 --- /dev/null +++ b/spec/features/security/project/snippet/private_access_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe "Private Project Snippets Access", feature: true do + include AccessMatchers + + let(:project) { create(:project, :private) } + + let(:owner) { project.owner } + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } + let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) } + + before do + project.team << [master, :master] + project.team << [developer, :developer] + project.team << [reporter, :reporter] + project.team << [guest, :guest] + end + + describe "GET /:project_path/snippets" do + subject { namespace_project_snippets_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/new" do + subject { new_namespace_project_snippet_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/:id for a private snippet" do + subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end +end diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb new file mode 100644 index 00000000000..e3665b6116a --- /dev/null +++ b/spec/features/security/project/snippet/public_access_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe "Public Project Snippets Access", feature: true do + include AccessMatchers + + let(:project) { create(:project, :public) } + + let(:owner) { project.owner } + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } + let(:public_snippet) { create(:project_snippet, :public, project: project, author: owner) } + let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: owner) } + let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) } + + before do + project.team << [master, :master] + project.team << [developer, :developer] + project.team << [reporter, :reporter] + project.team << [guest, :guest] + end + + describe "GET /:project_path/snippets" do + subject { namespace_project_snippets_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } + it { is_expected.to be_allowed_for :visitor } + end + + describe "GET /:project_path/snippets/new" do + subject { new_namespace_project_snippet_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/:id for a public snippet" do + subject { namespace_project_snippet_path(project.namespace, project, public_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } + it { is_expected.to be_allowed_for :visitor } + end + + describe "GET /:project_path/snippets/:id for an internal snippet" do + subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/:id for a private snippet" do + subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end +end -- cgit v1.2.1 From 07d87d15f095e3dc92ed78bbd3fe0b05bfccfde9 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 25 Mar 2016 16:42:28 -0400 Subject: Fix filter so it works with new multi select drop-downs. --- app/views/shared/issuable/_filter.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 1fce7d159e3..88a65e272dc 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -9,13 +9,13 @@ .filter-item.inline - if params[:author_id] = hidden_field_tag(:author_id, params[:author_id]) - = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author", + = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - if params[:assignee_id] = hidden_field_tag(:assignee_id, params[:assignee_id]) - = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee", + = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) .filter-item.inline.milestone-filter @@ -37,10 +37,10 @@ %li %a{href: "#", data: {id: "close"}} Closed .filter-item.inline - = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) .filter-item.inline - = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline -- cgit v1.2.1 From 26aa2a6e67ae25ac1f6f7d49241ddba3e835f69c Mon Sep 17 00:00:00 2001 From: Grant Douglas Date: Fri, 25 Mar 2016 20:24:22 +0000 Subject: short project names result in unusable fuzz search UI elements, so added a min-width property to the css Added a space between property colon and value to satisfy linter and pass build. --- app/assets/stylesheets/framework/selects.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index fa7944cdabe..e82d052f45a 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -44,6 +44,7 @@ @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0); @include border-radius ($border-radius-default); border: none; + min-width: 175px; } .select2-results .select2-result-label { -- cgit v1.2.1 From e664d95437d4fa9d5fbde929799fcc38eae7800d Mon Sep 17 00:00:00 2001 From: connorshea Date: Fri, 25 Mar 2016 15:48:13 -0600 Subject: Upgrade Teaspoon from 1.0.1 to 1.1.5. Includes compatibility with Rails 5. See the changelog for more information (https://github.com/modeset/teaspoon/blob/master/CHANGELOG.md). --- Gemfile | 2 +- Gemfile.lock | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index cebd76e7370..fecb2a25093 100644 --- a/Gemfile +++ b/Gemfile @@ -279,7 +279,7 @@ group :development, :test do gem 'capybara-screenshot', '~> 1.0.0' gem 'poltergeist', '~> 1.9.0' - gem 'teaspoon', '~> 1.0.0' + gem 'teaspoon', '~> 1.1.0' gem 'teaspoon-jasmine', '~> 2.2.0' gem 'spring', '~> 1.6.4' diff --git a/Gemfile.lock b/Gemfile.lock index 16c09ab6e6d..a1f663e976e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -806,8 +806,8 @@ GEM systemu (2.6.5) task_list (1.0.2) html-pipeline - teaspoon (1.0.2) - railties (>= 3.2.5, < 5) + teaspoon (1.1.5) + railties (>= 3.2.5, < 6) teaspoon-jasmine (2.2.0) teaspoon (>= 1.0.0) temple (0.7.6) @@ -1048,7 +1048,7 @@ DEPENDENCIES sprockets (~> 3.3.5) state_machines-activerecord (~> 0.3.0) task_list (~> 1.0.2) - teaspoon (~> 1.0.0) + teaspoon (~> 1.1.0) teaspoon-jasmine (~> 2.2.0) test_after_commit (~> 0.4.2) thin (~> 1.6.1) @@ -1064,3 +1064,6 @@ DEPENDENCIES web-console (~> 2.0) webmock (~> 1.21.0) wikicloth (= 0.8.1) + +BUNDLED WITH + 1.11.2 -- cgit v1.2.1 From 88007c3d77416601205dfb1b32b462da1ea16995 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Sat, 26 Mar 2016 09:30:37 -0400 Subject: Update, so filtering works properly. Bulk update works as well --- app/assets/javascripts/milestone_select.js.coffee | 5 ++++- app/assets/javascripts/users_select.js.coffee | 2 ++ app/views/shared/issuable/_filter.html.haml | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index b6983b93cc7..d1746c38e74 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -74,7 +74,10 @@ class @MilestoneSelect $selectbox.hide() $value.show() clicked: (e) -> - if $dropdown.hasClass "js-filter-submit" + if $dropdown.hasClass 'js-filter-bulk-update' + return + + if $dropdown.hasClass 'js-filter-submit' $dropdown.parents('form').submit() else selected = $selectbox diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 59dac4efa65..3262d8b8c90 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -137,6 +137,8 @@ class @UsersSelect page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' isMRIndex = page is page is 'projects:merge_requests:index' + if $dropdown.hasClass('js-filter-bulk-update') + return if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) Issues.filterResults $dropdown.closest('form') diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 88a65e272dc..c99da92be9f 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -37,10 +37,10 @@ %li %a{href: "#", data: {id: "close"}} Closed .filter-item.inline - = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) .filter-item.inline - = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline -- cgit v1.2.1 From feee6dab1086a05681a430bb397b14e4557514b7 Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Sun, 27 Mar 2016 16:15:07 +0100 Subject: fix right sidebar overllaping build section in merge request --- app/assets/stylesheets/pages/merge_requests.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index cee5c47cfb2..7ff63ca20b6 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -230,3 +230,9 @@ } } } + +.builds { + .table-holder { + overflow-x: scroll; + } +} -- cgit v1.2.1 From ac7d4a4084661d7439068c446e548ae69523e1bb Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 27 Mar 2016 18:10:10 +0000 Subject: Fix typos in webhooks docs. Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/14660 --- doc/web_hooks/web_hooks.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index afdf1a682e2..22e207b6d32 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -58,13 +58,13 @@ X-Gitlab-Event: Push Hook "path_with_namespace":"mike/diaspora", "default_branch":"master", "homepage":"http://example.com/mike/diaspora", - "url":"git@example.com:mike/diasporadiaspora.git", + "url":"git@example.com:mike/diaspora.git", "ssh_url":"git@example.com:mike/diaspora.git", "http_url":"http://example.com/mike/diaspora.git" }, "repository":{ "name": "Diaspora", - "url": "git@example.com:mike/diasporadiaspora.git", + "url": "git@example.com:mike/diaspora.git", "description": "", "homepage": "http://example.com/mike/diaspora", "git_http_url":"http://example.com/mike/diaspora.git", @@ -113,7 +113,6 @@ Triggered when you create (or delete) tags to the repository. X-Gitlab-Event: Tag Push Hook ``` - **Request body:** ```json @@ -143,7 +142,7 @@ X-Gitlab-Event: Tag Push Hook "http_url":"http://example.com/jsmith/example.git" }, "repository":{ - "name": "jsmith", + "name": "Example", "url": "ssh://git@example.com/jsmith/example.git", "description": "", "homepage": "http://example.com/jsmith/example", @@ -478,7 +477,7 @@ X-Gitlab-Event: Note Hook }, "repository":{ "name":"diaspora", - "url":"git@example.com:mike/diasporadiaspora.git", + "url":"git@example.com:mike/diaspora.git", "description":"", "homepage":"http://example.com/mike/diaspora" }, -- cgit v1.2.1 From 5f6166a256561a9767b28b309cff118bb299500d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 28 Mar 2016 01:35:53 -0500 Subject: Fix cancel link --- app/views/shared/issuable/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 6a42a6e3f58..178223fb463 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -127,7 +127,7 @@ for this project. - if issuable.new_record? - = link_to 'Cancel', polymorphic_path([@project.namespace, @project, issuable.class]), class: 'btn btn-cancel' + = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' - else .pull-right - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) -- cgit v1.2.1 From 6e86ef845246a70cb33d69a2aa6cfe451e9202b2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Mar 2016 11:26:45 +0200 Subject: Add border bottom and margin for readme holder Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/files.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index ad0e88cda86..a26ace5cc19 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -3,12 +3,10 @@ * */ .file-holder { - border: none; border: 1px solid $border-color; &.readme-holder { - margin-top: 10px; - border-bottom: 0; + margin: $gl-padding-top 0; } table { -- cgit v1.2.1 From 3bb47bb6e0cbef022299598a6e67105f44ba3beb Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Mar 2016 11:31:48 +0200 Subject: Fix vertical align for files tab top navigation Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/projects.scss | 2 +- app/views/projects/find_file/show.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 71bde1174ee..ee7c6a3d55f 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -222,7 +222,7 @@ padding: 0; background: transparent; border: none; - line-height: 42px; + line-height: 36px; margin: 0; > li + li:before { diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 905f6bbbd48..1fe1d98bf13 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -2,7 +2,7 @@ - header_title project_title(@project, "Files", project_files_path(@project)) .file-finder-holder.tree-holder.clearfix - .gray-content-block.top-block + .nav-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'find_file', path: @path %ul.breadcrumb.repo-breadcrumb -- cgit v1.2.1 From ef4ca663fd341898b19ba25231fd89b70ae34989 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Mar 2016 11:34:29 +0200 Subject: Use standard button size for new file Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/tree/_tree_header.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index ba69569b1e7..1c5f8b3928b 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -15,11 +15,11 @@ - if current_user %li - if !on_top_of_branch? - %span.btn.btn-sm.add-to-tree.disabled.has-tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }} + %span.btn.add-to-tree.disabled.has-tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }} = icon('plus') - else %span.dropdown - %a.dropdown-toggle.btn.btn-sm.add-to-tree{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"} = icon('plus') %ul.dropdown-menu - if can_edit_tree? -- cgit v1.2.1 From 0f00a75df047f5760c1a838c1173c2215424fbf9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 28 Mar 2016 11:38:06 +0200 Subject: Dont use small font size for commits list Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/pages/commits.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 5e91496679a..b6011fe7679 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -93,7 +93,6 @@ li.commit { .commit-row-info { color: $gl-gray; line-height: 24px; - font-size: 13px; a { color: $gl-gray; -- cgit v1.2.1 From 473b261261ab89fb921d12f05926ad9e9de5faee Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Fri, 25 Mar 2016 22:21:50 +0100 Subject: Back dating of issues when creating throught the API --- CHANGELOG | 1 + doc/api/issues.md | 1 + lib/api/issues.rb | 16 ++++++++++------ spec/requests/api/issues_spec.rb | 11 +++++++++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5d9f4961ef5..ef40c367dee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) + - Allow back dating on issues when created through the API - Fix avatar stretching by providing a cropping feature v 8.6.2 (unreleased) diff --git a/doc/api/issues.md b/doc/api/issues.md index 18d64c41986..cc6355d34ef 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -237,6 +237,7 @@ POST /projects/:id/issues | `assignee_id` | integer | no | The ID of a user to assign issue | | `milestone_id` | integer | no | The ID of a milestone to assign issue | | `labels` | string | no | Comma-separated label names for an issue | +| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` | ```bash curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug diff --git a/lib/api/issues.rb b/lib/api/issues.rb index e5ae88eb96f..1fee1dee1a6 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -111,17 +111,21 @@ module API # Create a new project issue # # Parameters: - # id (required) - The ID of a project - # title (required) - The title of an issue - # description (optional) - The description of an issue - # assignee_id (optional) - The ID of a user to assign issue + # id (required) - The ID of a project + # title (required) - The title of an issue + # description (optional) - The description of an issue + # assignee_id (optional) - The ID of a user to assign issue # milestone_id (optional) - The ID of a milestone to assign issue - # labels (optional) - The labels of an issue + # labels (optional) - The labels of an issue + # created_at (optional) - The date # Example Request: # POST /projects/:id/issues post ":id/issues" do required_attributes! [:title] - attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id] + + keys = [:title, :description, :assignee_id, :milestone_id] + keys << :created_at if current_user.admin? || user_project.owner == current_user + attrs = attributes_for_keys(keys) # Validate label names in advance if (errors = validate_label_params(params)).any? diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index ce55cb7b0ae..822d3ad3017 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -318,6 +318,17 @@ describe API::API, api: true do 'is too long (maximum is 255 characters)' ]) end + + context 'when an admin or owner makes the request' do + it "accepts the creation date to be set" do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', labels: 'label, label2', created_at: 2.weeks.ago + + expect(response.status).to eq(201) + # this take about a second, so probably not equal + expect(Time.parse(json_response['created_at'])).to be <= 2.weeks.ago + end + end end describe 'POST /projects/:id/issues with spam filtering' do -- cgit v1.2.1 From 9e0890790f7c0250e790d2c7548c1a0543a61d15 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 28 Mar 2016 13:59:19 -0500 Subject: Remove background Fixes note's action bar background color when note is highlighted --- app/assets/stylesheets/pages/note_form.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index daf2651425f..655f88b0c2c 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -71,8 +71,6 @@ } .note-form-actions { - background: #fff; - .note-form-option { margin-top: 8px; margin-left: 30px; -- cgit v1.2.1 From 5449296fd60b0d620eef64a875a0796712c7ac79 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 28 Mar 2016 15:49:21 -0400 Subject: Fix small style issue --- app/assets/stylesheets/framework/dropdowns.scss | 6 +++--- app/views/shared/issuable/_sidebar.html.haml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 2d616fc660c..11e1e4275ac 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -117,7 +117,7 @@ padding-left: 10px; padding-right: 10px; color: $dropdown-link-color; - line-height: 34px; + line-height: 16px; text-overflow: ellipsis; border-radius: 2px; white-space: nowrap; @@ -167,13 +167,13 @@ } .dropdown-menu-user-link { - padding-top: 7px; + padding-top: 10px; padding-bottom: 7px; } .dropdown-menu-user-full-name { display: block; - font-weight: 600; + font-weight: 500; line-height: 16px; text-overflow: ellipsis; overflow: hidden; diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index a0e6cba0702..451c64da2c4 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -47,7 +47,7 @@ .selectbox.hide-collapsed = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id' - = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign user', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }) + = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }) .block.milestone .sidebar-collapsed-icon -- cgit v1.2.1 From 8406318d87b902c7805b1597f729a1e30c920bae Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 28 Mar 2016 17:04:11 -0400 Subject: Remove tabstop from the WIP toggle links [ci skip] --- app/views/shared/issuable/_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 178223fb463..e2a9e5bfb92 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -14,7 +14,7 @@ - if issuable.is_a?(MergeRequest) %p.help-block .js-wip-explanation - %a.js-toggle-wip{href: ""} + %a.js-toggle-wip{href: "", tabindex: -1} Remove the %code WIP: prefix from the title @@ -22,7 +22,7 @@ %strong Work In Progress merge request to be merged when it's ready. .js-no-wip-explanation - %a.js-toggle-wip{href: ""} + %a.js-toggle-wip{href: "", tabindex: -1} Start the title with %code WIP: to prevent a -- cgit v1.2.1 From 47a539dfd7733968b25b73e6b8b61348926207af Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 28 Mar 2016 18:03:29 -0400 Subject: Use raw SQL instead of Rails models in migrations where appropriate Closes #14694. --- ...30218141258_convert_closed_to_state_in_issue.rb | 18 +++++++++++------- ...327_convert_closed_to_state_in_merge_request.rb | 22 +++++++++++++--------- ...8141344_convert_closed_to_state_in_milestone.rb | 18 +++++++++++------- ...125544_convert_merge_status_in_merge_request.rb | 22 ++++++++++++---------- .../20130419190306_allow_merges_for_forks.rb | 8 +++++++- 5 files changed, 54 insertions(+), 34 deletions(-) diff --git a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb index 9fa96203ffd..99289166e81 100644 --- a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb +++ b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb @@ -1,14 +1,18 @@ class ConvertClosedToStateInIssue < ActiveRecord::Migration + include Gitlab::Database + def up - Issue.transaction do - Issue.where(closed: true).update_all(state: :closed) - Issue.where(closed: false).update_all(state: :opened) - end + execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}" + execute "UPDATE #{table_name} SET state = 'opened' WHERE closed = #{false_value}" end def down - Issue.transaction do - Issue.where(state: :closed).update_all(closed: true) - end + execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'closed'" + end + + private + + def table_name + Issue.table_name end end diff --git a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb index ebb7ae585e6..bd1e016d679 100644 --- a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb +++ b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb @@ -1,16 +1,20 @@ class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration + include Gitlab::Database + def up - MergeRequest.transaction do - MergeRequest.where(closed: true, merged: true).update_all(state: :merged) - MergeRequest.where(closed: true, merged: false).update_all(state: :closed) - MergeRequest.where(closed: false).update_all(state: :opened) - end + execute "UPDATE #{table_name} SET state = 'merged' WHERE closed = #{true_value} AND merged = #{true_value}" + execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value} AND merged = #{false_value}" + execute "UPDATE #{table_name} SET state = 'opened' WHERE closed = #{false_value}" end def down - MergeRequest.transaction do - MergeRequest.where(state: :closed).update_all(closed: true) - MergeRequest.where(state: :merged).update_all(closed: true, merged: true) - end + execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'closed'" + execute "UPDATE #{table_name} SET closed = #{true_value}, merged = #{true_value} WHERE state = 'merged'" + end + + private + + def table_name + MergeRequest.table_name end end diff --git a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb index 1978ea89153..d1174bc3d98 100644 --- a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb +++ b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb @@ -1,14 +1,18 @@ class ConvertClosedToStateInMilestone < ActiveRecord::Migration + include Gitlab::Database + def up - Milestone.transaction do - Milestone.where(closed: true).update_all(state: :closed) - Milestone.where(closed: false).update_all(state: :active) - end + execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}" + execute "UPDATE #{table_name} SET state = 'active' WHERE closed = #{false_value}" end def down - Milestone.transaction do - Milestone.where(state: :closed).update_all(closed: true) - end + execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'cloesd'" + end + + private + + def table_name + Milestone.table_name end end diff --git a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb index b310b35e373..1c758c56ffe 100644 --- a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb +++ b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb @@ -1,17 +1,19 @@ class ConvertMergeStatusInMergeRequest < ActiveRecord::Migration def up - MergeRequest.transaction do - MergeRequest.where(merge_status: 1).update_all("new_merge_status = 'unchecked'") - MergeRequest.where(merge_status: 2).update_all("new_merge_status = 'can_be_merged'") - MergeRequest.where(merge_status: 3).update_all("new_merge_status = 'cannot_be_merged'") - end + execute "UPDATE #{table_name} SET new_merge_status = 'unchecked' WHERE merge_status = 1" + execute "UPDATE #{table_name} SET new_merge_status = 'can_be_merged' WHERE merge_status = 2" + execute "UPDATE #{table_name} SET new_merge_status = 'cannot_be_merged' WHERE merge_status = 3" end def down - MergeRequest.transaction do - MergeRequest.where(new_merge_status: :unchecked).update_all("merge_status = 1") - MergeRequest.where(new_merge_status: :can_be_merged).update_all("merge_status = 2") - MergeRequest.where(new_merge_status: :cannot_be_merged).update_all("merge_status = 3") - end + execute "UPDATE #{table_name} SET merge_status = 1 WHERE new_merge_status = 'unchecked'" + execute "UPDATE #{table_name} SET merge_status = 2 WHERE new_merge_status = 'can_be_merged'" + execute "UPDATE #{table_name} SET merge_status = 3 WHERE new_merge_status = 'cannot_be_merged'" + end + + private + + def table_name + MergeRequest.table_name end end diff --git a/db/migrate/20130419190306_allow_merges_for_forks.rb b/db/migrate/20130419190306_allow_merges_for_forks.rb index 56ce58a846d..56ea97e8561 100644 --- a/db/migrate/20130419190306_allow_merges_for_forks.rb +++ b/db/migrate/20130419190306_allow_merges_for_forks.rb @@ -1,7 +1,7 @@ class AllowMergesForForks < ActiveRecord::Migration def self.up add_column :merge_requests, :target_project_id, :integer, :null => true - MergeRequest.update_all("target_project_id = project_id") + execute "UPDATE #{table_name} SET target_project_id = project_id" change_column :merge_requests, :target_project_id, :integer, :null => false rename_column :merge_requests, :project_id, :source_project_id end @@ -10,4 +10,10 @@ class AllowMergesForForks < ActiveRecord::Migration remove_column :merge_requests, :target_project_id rename_column :merge_requests, :source_project_id,:project_id end + + private + + def table_name + MergeRequest.table_name + end end -- cgit v1.2.1 From 7b8a4dd35199f4e9d2e631da92255a44cde7af28 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 28 Mar 2016 17:52:20 -0500 Subject: Set properly line heights --- app/assets/stylesheets/framework/dropdowns.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 11e1e4275ac..6c870ed927e 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -117,7 +117,7 @@ padding-left: 10px; padding-right: 10px; color: $dropdown-link-color; - line-height: 16px; + line-height: 34px; text-overflow: ellipsis; border-radius: 2px; white-space: nowrap; @@ -136,6 +136,10 @@ background-color: $dropdown-empty-row-bg; } } + + &.dropdown-menu-user-link { + line-height: 16px; + } } } -- cgit v1.2.1 From b7685b57d3fc9a2e27096c5984d92fd104ff71b7 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 24 Mar 2016 14:16:12 +0100 Subject: Redirect to root path when visiting `/ci` --- app/controllers/ci/projects_controller.rb | 4 ++++ app/views/ci/projects/index.html.haml | 20 -------------------- spec/controllers/ci/projects_controller_spec.rb | 21 ++++++++++++++++----- 3 files changed, 20 insertions(+), 25 deletions(-) delete mode 100644 app/views/ci/projects/index.html.haml diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index c0e7f434ff5..8bf71a1adbb 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -6,6 +6,10 @@ module Ci skip_before_action :authenticate_user!, only: [:badge] protect_from_forgery + def index + redirect_to root_path + end + def show # Temporary compatibility with CI badges pointing to CI project page redirect_to namespace_project_path(project.namespace, project) diff --git a/app/views/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml deleted file mode 100644 index 9c2290bc4a5..00000000000 --- a/app/views/ci/projects/index.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.wiki - %h1 - GitLab CI is now integrated in GitLab UI - %h2 For existing projects - - %p - Check the following pages to find the CI status you're looking for: - - %ul - %li Projects page - shows CI status for each project. - %li Project commits page - show CI status for each commit. - - - - %h2 For new projects - - %p - If you want to enable CI for a new project it is easy as adding - = link_to ".gitlab-ci.yml", "http://doc.gitlab.com/ce/ci/yaml/README.html" - file to your repository diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb index 9a886e4c124..5022a3e2c80 100644 --- a/spec/controllers/ci/projects_controller_spec.rb +++ b/spec/controllers/ci/projects_controller_spec.rb @@ -6,12 +6,23 @@ describe Ci::ProjectsController do let(:ci_id) { project.ci_id } describe '#index' do - let(:user) { create(:user) } - before { sign_in(user) } - before { get(:index) } + context 'user signed in' do + before do + sign_in(create(:user)) + get(:index) + end + + it 'redirects to /' do + expect(response).to redirect_to(root_path) + end + end + + context 'user not signed in' do + before { get(:index) } - it 'returns 200' do - expect(response.status).to eq 200 + it 'redirects to sign in page' do + expect(response).to redirect_to(new_user_session_path) + end end end -- cgit v1.2.1