Merge pull request #7995 from opf/feature/31935/ee-activation

[31935] WIP: Enterprise activation frontend

[ci skip]
pull/8264/head
Oliver Günther 5 years ago committed by GitHub
commit 639fcf7822
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      Gemfile
  2. 22
      Gemfile.lock
  3. 335
      app/assets/fonts/openproject_icon/openproject-icon-font.svg
  4. BIN
      app/assets/fonts/openproject_icon/openproject-icon-font.ttf
  5. BIN
      app/assets/fonts/openproject_icon/openproject-icon-font.woff
  6. BIN
      app/assets/fonts/openproject_icon/openproject-icon-font.woff2
  7. 148
      app/assets/images/installation_alerts.svg
  8. 1
      app/assets/images/premium_features.svg
  9. BIN
      app/assets/images/security_alerts.jpg
  10. BIN
      app/assets/images/system_maintenance.jpg
  11. 8
      app/assets/stylesheets/content/_enterprise.sass
  12. 4
      app/assets/stylesheets/content/_forms_mobile.sass
  13. 1
      app/assets/stylesheets/content/_index.sass
  14. 73
      app/assets/stylesheets/content/_info_boxes.lsg
  15. 62
      app/assets/stylesheets/content/_info_boxes.sass
  16. 2
      app/assets/stylesheets/content/_modal.sass
  17. 10
      app/assets/stylesheets/content/_widget_box.sass
  18. 670
      app/assets/stylesheets/fonts/_openproject_icon_definitions.scss
  19. 1
      app/assets/stylesheets/fonts/_openproject_icon_font.lsg
  20. 48
      app/controllers/enterprises_controller.rb
  21. 21
      app/helpers/enterprise_trial_helper.rb
  22. 41
      app/models/token/enterprise_trial_key.rb
  23. 8
      app/views/common/upsale.html.erb
  24. 54
      app/views/enterprises/_current.html.erb
  25. 146
      app/views/enterprises/_info.html.erb
  26. 18
      app/views/enterprises/show.html.erb
  27. 37
      app/views/homescreen/blocks/_upsale.html.erb
  28. 2
      app/views/types/form/_form_configuration.html.erb
  29. 11
      config/locales/en.yml
  30. 55
      config/locales/js-en.yml
  31. 3
      config/routes.rb
  32. 10
      frontend/src/app/angular4-modules.ts
  33. 53
      frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-saved-trial.component.ts
  34. 46
      frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.base.ts
  35. 49
      frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.component.html
  36. 2
      frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.component.sass
  37. 109
      frontend/src/app/components/enterprise/enterprise-active-trial/ee-active-trial.component.ts
  38. 23
      frontend/src/app/components/enterprise/enterprise-base.component.html
  39. 7
      frontend/src/app/components/enterprise/enterprise-base.component.sass
  40. 72
      frontend/src/app/components/enterprise/enterprise-base.component.ts
  41. 86
      frontend/src/app/components/enterprise/enterprise-modal/enterprise-trial-form/ee-trial-form.component.html
  42. 88
      frontend/src/app/components/enterprise/enterprise-modal/enterprise-trial-form/ee-trial-form.component.ts
  43. 73
      frontend/src/app/components/enterprise/enterprise-modal/enterprise-trial.modal.html
  44. 9
      frontend/src/app/components/enterprise/enterprise-modal/enterprise-trial.modal.sass
  45. 130
      frontend/src/app/components/enterprise/enterprise-modal/enterprise-trial.modal.ts
  46. 16
      frontend/src/app/components/enterprise/enterprise-trial-waiting/ee-trial-waiting.component.html
  47. 7
      frontend/src/app/components/enterprise/enterprise-trial-waiting/ee-trial-waiting.component.sass
  48. 79
      frontend/src/app/components/enterprise/enterprise-trial-waiting/ee-trial-waiting.component.ts
  49. 166
      frontend/src/app/components/enterprise/enterprise-trial.service.ts
  50. 59
      frontend/src/app/components/enterprise/openproject-enterprise.module.ts
  51. 2
      frontend/src/app/components/homescreen/blocks/new-features.component.sass
  52. 7
      frontend/src/app/global-dynamic-components.const.ts
  53. 5
      lib/open_project/configuration.rb
  54. 9
      lib/open_project/configuration/helpers.rb
  55. 27
      lib/open_project/static/links.rb
  56. 8
      modules/ldap_groups/app/views/ldap_groups/synchronized_groups/upsale.html.erb
  57. 8
      modules/openid_connect/app/views/openid_connect/providers/upsale.html.erb
  58. 2
      spec/controllers/enterprises_controller_spec.rb
  59. 7
      spec/features/admin/enterprise/enterprise_spec.rb
  60. 246
      spec/features/admin/enterprise/enterprise_trial_spec.rb
  61. 1
      spec/rails_helper.rb
  62. 20
      spec/support/browsers/chrome.rb
  63. 23
      spec/support/browsers/firefox.rb
  64. 39
      spec/support/puffing_billy_proxy.rb
  65. 12
      spec/support/webmock.rb
  66. 3
      vendor/openproject-icon-font/src/external-link.svg

@ -221,8 +221,13 @@ group :test do
gem 'fuubar', '~> 2.5.0'
gem 'timecop', '~> 0.9.0'
# Mock backend requests (for ruby tests)
gem 'webmock', '~> 3.8.2', require: false
# Mock selenium requests through proxy (for feature tests)
gem 'puffing-billy', '~> 2.3.1'
gem 'equivalent-xml', '~> 0.6'
gem 'json_spec', '~> 1.1.4'
gem 'shoulda-matchers', '~> 3.1', require: nil

@ -358,6 +358,7 @@ GEM
compare-xml (0.66)
nokogiri (~> 1.8)
concurrent-ruby (1.1.6)
cookiejar (0.3.3)
cork (0.3.0)
colored2 (~> 3.1)
crack (0.4.3)
@ -458,6 +459,16 @@ GEM
dry-equalizer (~> 0.3)
dry-inflector (~> 0.1, >= 0.1.2)
dry-logic (~> 1.0, >= 1.0.2)
em-http-request (1.1.5)
addressable (>= 2.3.4)
cookiejar (!= 0.3.1)
em-socksify (>= 0.3)
eventmachine (>= 1.0.3)
http_parser.rb (>= 0.6.0)
em-socksify (0.3.2)
eventmachine (>= 1.0.0.beta.4)
em-synchrony (1.0.6)
eventmachine (>= 1.0.0.beta.1)
equivalent-xml (0.6.0)
nokogiri (>= 1.4.3)
erbse (0.1.4)
@ -465,6 +476,7 @@ GEM
erubi (1.9.0)
escape_utils (1.2.1)
eventmachine (1.2.7)
eventmachine_httpserver (0.2.1)
excon (0.72.0)
execjs (2.7.0)
factory_bot (5.1.1)
@ -533,6 +545,7 @@ GEM
http-accept (1.7.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
http_parser.rb (0.6.0)
httpclient (2.8.3)
i18n (1.8.2)
concurrent-ruby (~> 1.0)
@ -689,6 +702,14 @@ GEM
binding_of_caller (>= 0.7)
pry (>= 0.9.11)
public_suffix (4.0.3)
puffing-billy (2.3.1)
addressable (~> 2.5)
em-http-request (~> 1.1, >= 1.1.0)
em-synchrony
eventmachine (~> 1.2)
eventmachine_httpserver
http_parser.rb (~> 0.6.0)
multi_json
puma (4.3.3)
nio4r (~> 2.0)
rack (2.2.2)
@ -1056,6 +1077,7 @@ DEPENDENCIES
pry-rails (~> 0.3.6)
pry-rescue (~> 1.5.0)
pry-stack_explorer (~> 0.4.9.2)
puffing-billy (~> 2.3.1)
puma (~> 4.3.1)
rack-attack (~> 6.2.2)
rack-mini-profiler

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 143 KiB

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:#FFFFFF;}
.st2{fill:#0070BA;}
.st3{fill:#66CB92;}
.st4{fill:#9FCDE0;}
</style>
<g>
<circle class="st0" cx="271" cy="356.7" r="89.4"/>
<path class="st0" d="M564.7,209c28.5,0,51.6-23.1,51.6-51.6c0-28.5-23.1-51.6-51.6-51.6c-28.5,0-51.6,23.1-51.6,51.6
C513.1,185.9,536.3,209,564.7,209z"/>
<path class="st0" d="M786.8,629.8l23.8,0c0,0,0,0,0,0c0-2.7,2.2-4.9,4.9-5l-0.8-28.5L786.8,629.8z"/>
<path class="st0" d="M582.3,594.6l-1.8,30.2c2.6,0.2,4.7,2.3,4.7,5c0,0,0,0,0,0l26.4,0L582.3,594.6z"/>
<path class="st0" d="M450.8,178.2l23.1,1.7l1,3.4c2,7.1,4.9,14,8.6,20.5l1.7,3l-15,17.6c-0.1,0.3,0,0.7,0.3,1l26.4,26.4
c0.2,0.2,0.6,0.3,0.9,0.2l17.7-15l3,1.7c6.4,3.7,13.3,6.6,20.4,8.6l3.3,1l1.7,23.1c0.2,0.3,0.5,0.5,0.9,0.5h39.9h0
c0.3,0,0.7-0.2,0.8-0.5l1.7-23.1l3.4-1c7.1-2,14-4.9,20.5-8.6l3-1.7l17.6,15c0.3,0.1,0.7,0,1-0.3l26.4-26.4
c0.2-0.2,0.3-0.6,0.2-0.9l-15-17.6l1.7-3c3.7-6.4,6.6-13.3,8.6-20.5l1-3.4l23.1-1.7c0.3-0.2,0.5-0.5,0.5-0.9v-39.9
c0-0.3-0.2-0.7-0.5-0.8l-23.1-1.7l-1-3.4c-2-7.1-4.9-14-8.6-20.5l-1.7-3l15-17.6c0.1-0.3,0-0.7-0.2-0.9l-26.4-26.4
c-0.2-0.2-0.6-0.3-0.9-0.2l-17.6,15l-3-1.7c-6.4-3.7-13.3-6.6-20.5-8.6l-3.4-1l-1.7-23.1c-0.2-0.3-0.5-0.5-0.8-0.5h-39.9
c-0.3,0-0.6,0.2-0.8,0.5l-1.6,23.1l-3.4,1c-7.1,2-14,4.9-20.5,8.6l-3,1.7l-17.7-15c-0.3-0.1-0.7,0-1,0.3l-26.4,26.4
c-0.2,0.2-0.3,0.6-0.2,0.9l15,17.6l-1.7,3.1c-3.7,6.4-6.6,13.3-8.6,20.5l-1,3.4l-23.1,1.7c-0.3,0.2-0.5,0.5-0.5,0.8v39.9
C450.3,177.7,450.5,178.1,450.8,178.2z M564.7,95.9c34,0,61.6,27.6,61.6,61.6S598.7,219,564.7,219c-34,0-61.6-27.6-61.6-61.6
S530.8,95.9,564.7,95.9z"/>
<path class="st1" d="M860.9,782.6H301.5c-11.6,0-21,9.4-21,21v95.8c0,11.6,9.4,21,21,21H358h119.6h207.2h119.6h56.5
c11.6,0,21-9.4,21-21v-95.8C881.9,792.1,872.5,782.6,860.9,782.6z M365.6,885.5c-17.7,0-32.2-14.4-32.2-32.2s14.4-32.2,32.2-32.2
c17.7,0,32.2,14.4,32.2,32.2S383.3,885.5,365.6,885.5z M491,854.1c0,6.7-5.5,12.2-12.2,12.2h-1.5c-6.7,0-12.2-5.5-12.2-12.2v-1.5
c0-6.7,5.5-12.2,12.2-12.2h1.5c6.7,0,12.2,5.5,12.2,12.2V854.1z M815.8,854.1c0,6.7-5.5,12.2-12.2,12.2H573.4
c-6.7,0-12.2-5.5-12.2-12.2v-1.5c0-6.7,5.5-12.2,12.2-12.2h230.2c6.7,0,12.2,5.5,12.2,12.2V854.1z"/>
<path class="st1" d="M478.8,847.4h-1.5c-2.9,0-5.2,2.3-5.2,5.2v1.5c0,2.9,2.3,5.2,5.2,5.2h1.5c2.9,0,5.2-2.3,5.2-5.2v-1.5
C484,849.7,481.7,847.4,478.8,847.4z"/>
<path class="st1" d="M803.6,847.4H573.4c-2.9,0-5.2,2.3-5.2,5.2v1.5c0,2.9,2.3,5.2,5.2,5.2h230.2c2.9,0,5.2-2.3,5.2-5.2v-1.5
C808.8,849.7,806.5,847.4,803.6,847.4z"/>
<path class="st2" d="M877.7,777.6c8.5-5.5,14.2-15.1,14.2-26v-95.8c0-10.9-5.6-20.5-14.2-26c8.5-5.5,14.2-15.1,14.2-26V508
c0-17.1-13.9-31-31-31h-49.2c-2.8,0-5,2.2-5,5s2.2,5,5,5h0.1h49.1c11.6,0,21,9.4,21,21v95.8c0,11.6-9.4,21-21,21h-45.3
c0,0-0.1,0-0.1,0c-2.7,0.1-4.9,2.3-4.9,5c0,0,0,0,0,0c0,2.8,2.2,5,5,5h45.1c0.1,0,0.1,0,0.2,0c11.6,0,21,9.4,21,21v95.8
c0,11.6-9.4,21-21,21H301.5c-11.6,0-21-9.4-21-21v-95.8c0-11.6,9.4-21,21-21h219.8c0.1,0,0.1,0,0.2,0h58.8c2.8,0,5-2.2,5-5
c0,0,0,0,0,0c0-2.7-2.1-4.8-4.7-5c-0.1,0-0.2,0-0.3,0H301.5c-11.6,0-21-9.4-21-21V508c0-11.6,9.4-21,21-21h287.1h0.3
c2.8,0,5-2.2,5-5s-2.2-5-5-5H301.5c-17.1,0-31,13.9-31,31v95.8c0,10.9,5.6,20.5,14.2,26c-8.5,5.5-14.2,15.1-14.2,26v95.8
c0,10.9,5.7,20.5,14.2,26c-8.5,5.5-14.2,15.1-14.2,26v95.8c0,17.1,13.9,31,31,31H353v7.8c0,16.5,13.5,30,30,30h69.6
c16.5,0,30-13.5,30-30v-7.8h197.2v7.8c0,16.5,13.5,30,30,30h69.6c16.5,0,30-13.5,30-30v-7.8h51.5c17.1,0,31-13.9,31-31v-95.8
C891.9,792.8,886.3,783.1,877.7,777.6z M881.9,899.4c0,11.6-9.4,21-21,21h-56.5H684.8H477.6H358h-56.5c-11.6,0-21-9.4-21-21v-95.8
c0-11.6,9.4-21,21-21h559.4c11.6,0,21,9.4,21,21V899.4z M799.4,938.2c0,11-9,20-20,20h-69.6c-11,0-20-9-20-20v-7.8h109.6V938.2z
M472.6,938.2c0,11-9,20-20,20H383c-11,0-20-9-20-20v-7.8h109.6V938.2z"/>
<path class="st3" d="M365.6,828.2c-13.9,0-25.2,11.3-25.2,25.2s11.3,25.2,25.2,25.2c13.9,0,25.2-11.3,25.2-25.2
S379.5,828.2,365.6,828.2z"/>
<path class="st2" d="M365.6,821.2c-17.7,0-32.2,14.4-32.2,32.2s14.4,32.2,32.2,32.2c17.7,0,32.2-14.4,32.2-32.2
S383.3,821.2,365.6,821.2z M365.6,878.5c-13.9,0-25.2-11.3-25.2-25.2s11.3-25.2,25.2-25.2c13.9,0,25.2,11.3,25.2,25.2
S379.5,878.5,365.6,878.5z"/>
<path class="st2" d="M803.6,840.4H573.4c-6.7,0-12.2,5.5-12.2,12.2v1.5c0,6.7,5.5,12.2,12.2,12.2h230.2c6.7,0,12.2-5.5,12.2-12.2
v-1.5C815.8,845.9,810.3,840.4,803.6,840.4z M808.8,854.1c0,2.9-2.3,5.2-5.2,5.2H573.4c-2.9,0-5.2-2.3-5.2-5.2v-1.5
c0-2.9,2.3-5.2,5.2-5.2h230.2c2.9,0,5.2,2.3,5.2,5.2V854.1z"/>
<path class="st2" d="M478.8,840.4h-1.5c-6.7,0-12.2,5.5-12.2,12.2v1.5c0,6.7,5.5,12.2,12.2,12.2h1.5c6.7,0,12.2-5.5,12.2-12.2v-1.5
C491,845.9,485.5,840.4,478.8,840.4z M484,854.1c0,2.9-2.3,5.2-5.2,5.2h-1.5c-2.9,0-5.2-2.3-5.2-5.2v-1.5c0-2.9,2.3-5.2,5.2-5.2
h1.5c2.9,0,5.2,2.3,5.2,5.2V854.1z"/>
<path class="st1" d="M710.7,721.3c-0.6,0.7-1.2,1.3-1.9,1.9c-2.7,2.3-6,3.5-9.5,3.5c-0.5,0-0.9,0-1.4-0.1c-4-0.4-7.6-2.2-10.1-5.3
l-76.1-91.5l-26.4,0c0,2.8-2.2,5-5,5h-58.8c-0.1,0-0.1,0-0.2,0H301.5c-11.6,0-21,9.4-21,21v95.8c0,11.6,9.4,21,21,21h559.4
c11.6,0,21-9.4,21-21v-95.8c0-11.6-9.4-21-21-21c-0.1,0-0.1,0-0.2,0h-45.1c-2.8,0-5-2.2-5-5l-23.8,0L710.7,721.3z M365.6,737.7
c-17.7,0-32.2-14.4-32.2-32.2c0-17.7,14.4-32.2,32.2-32.2c17.7,0,32.2,14.4,32.2,32.2C397.8,723.3,383.3,737.7,365.6,737.7z
M491,706.2c0,6.7-5.5,12.2-12.2,12.2h-1.5c-6.7,0-12.2-5.5-12.2-12.2v-1.5c0-6.7,5.5-12.2,12.2-12.2h1.5c6.7,0,12.2,5.5,12.2,12.2
V706.2z M647.1,718.5h-72.9c-7.1,0-12.9-5.8-13-12.9c0-7.1,5.8-13,13-13h60.2c1.9,0,3.5,1.6,3.5,3.5s-1.6,3.5-3.5,3.5h-60.2
c-3.3,0-6,2.7-6,5.9c0,3.3,2.7,5.9,6,5.9h72.9c1.9,0,3.5,1.6,3.5,3.5S649,718.5,647.1,718.5z M815.8,705.5c0,7.1-5.8,12.9-13,12.9
h-58.2c-1.9,0-3.5-1.6-3.5-3.5s1.6-3.5,3.5-3.5h58.2c3.3,0,5.9-2.7,6-5.9c0-3.3-2.7-5.9-6-5.9h-41.1c-1.9,0-3.5-1.6-3.5-3.5
s1.6-3.5,3.5-3.5h41.1C810,692.6,815.8,698.4,815.8,705.5z"/>
<path class="st1" d="M478.8,699.6h-1.5c-2.9,0-5.2,2.3-5.2,5.2v1.5c0,2.9,2.3,5.2,5.2,5.2h1.5c2.9,0,5.2-2.3,5.2-5.2v-1.5
C484,701.9,481.7,699.6,478.8,699.6z"/>
<path class="st3" d="M365.6,680.4c-13.9,0-25.2,11.3-25.2,25.2s11.3,25.2,25.2,25.2c13.9,0,25.2-11.3,25.2-25.2
S379.5,680.4,365.6,680.4z"/>
<path class="st2" d="M365.6,673.4c-17.7,0-32.2,14.4-32.2,32.2c0,17.7,14.4,32.2,32.2,32.2c17.7,0,32.2-14.4,32.2-32.2
C397.8,687.8,383.3,673.4,365.6,673.4z M365.6,730.7c-13.9,0-25.2-11.3-25.2-25.2s11.3-25.2,25.2-25.2c13.9,0,25.2,11.3,25.2,25.2
S379.5,730.7,365.6,730.7z"/>
<path class="st2" d="M647.1,711.5h-72.9c-3.3,0-5.9-2.7-6-5.9c0-3.3,2.7-5.9,6-5.9h60.2c1.9,0,3.5-1.6,3.5-3.5s-1.6-3.5-3.5-3.5
h-60.2c-7.1,0-13,5.8-13,13c0,7.1,5.8,12.9,13,12.9h72.9c1.9,0,3.5-1.6,3.5-3.5S649,711.5,647.1,711.5z"/>
<path class="st2" d="M758.2,696.1c0,1.9,1.6,3.5,3.5,3.5h41.1c3.3,0,6,2.7,6,5.9c0,3.3-2.7,5.9-6,5.9h-58.2c-1.9,0-3.5,1.6-3.5,3.5
s1.6,3.5,3.5,3.5h58.2c7.1,0,12.9-5.8,13-12.9c0-7.1-5.8-12.9-13-12.9h-41.1C759.8,692.6,758.2,694.1,758.2,696.1z"/>
<path class="st2" d="M478.8,692.6h-1.5c-6.7,0-12.2,5.5-12.2,12.2v1.5c0,6.7,5.5,12.2,12.2,12.2h1.5c6.7,0,12.2-5.5,12.2-12.2v-1.5
C491,698,485.5,692.6,478.8,692.6z M484,706.2c0,2.9-2.3,5.2-5.2,5.2h-1.5c-2.9,0-5.2-2.3-5.2-5.2v-1.5c0-2.9,2.3-5.2,5.2-5.2h1.5
c2.9,0,5.2,2.3,5.2,5.2V706.2z"/>
<path class="st1" d="M280.5,508v95.8c0,11.6,9.4,21,21,21h278.8c0.1,0,0.2,0,0.3,0l1.8-30.2l-32-38.4c-2.2-2.7-3.4-6-3.4-9.5
c0-4,1.5-7.7,4.4-10.6c2.8-2.8,6.6-4.4,10.5-4.4H586l2.6-44.8H301.5C289.9,487,280.5,496.4,280.5,508z M465.1,557
c0-6.7,5.5-12.2,12.2-12.2h1.5c6.7,0,12.2,5.5,12.2,12.2v1.5c0,6.7-5.5,12.2-12.2,12.2h-1.5c-6.7,0-12.2-5.5-12.2-12.2V557z
M365.6,525.5c17.7,0,32.2,14.4,32.2,32.2s-14.4,32.2-32.2,32.2c-17.7,0-32.2-14.4-32.2-32.2S347.9,525.5,365.6,525.5z"/>
<path class="st1" d="M477.3,563.6h1.5c2.9,0,5.2-2.3,5.2-5.2V557c0-2.9-2.3-5.2-5.2-5.2h-1.5c-2.9,0-5.2,2.3-5.2,5.2v1.5
C472.1,561.3,474.4,563.6,477.3,563.6z"/>
<path class="st1" d="M846.1,535.2c6.3,5.3,7.2,14.7,1.9,21l-33.3,40.1l0.8,28.5c0,0,0.1,0,0.1,0h45.3c11.6,0,21-9.4,21-21V508
c0-11.6-9.4-21-21-21h-49.1l1.2,44.7h23.6C840,531.7,843.4,532.9,846.1,535.2z"/>
<path class="st3" d="M340.4,557.7c0,13.9,11.3,25.2,25.2,25.2c13.9,0,25.2-11.3,25.2-25.2s-11.3-25.2-25.2-25.2
C351.7,532.5,340.4,543.8,340.4,557.7z"/>
<path class="st2" d="M365.6,589.9c17.7,0,32.2-14.4,32.2-32.2s-14.4-32.2-32.2-32.2c-17.7,0-32.2,14.4-32.2,32.2
S347.9,589.9,365.6,589.9z M390.8,557.7c0,13.9-11.3,25.2-25.2,25.2c-13.9,0-25.2-11.3-25.2-25.2s11.3-25.2,25.2-25.2
C379.5,532.5,390.8,543.8,390.8,557.7z"/>
<path class="st2" d="M477.3,570.6h1.5c6.7,0,12.2-5.5,12.2-12.2V557c0-6.7-5.5-12.2-12.2-12.2h-1.5c-6.7,0-12.2,5.5-12.2,12.2v1.5
C465.1,565.2,470.6,570.6,477.3,570.6z M472.1,557c0-2.9,2.3-5.2,5.2-5.2h1.5c2.9,0,5.2,2.3,5.2,5.2v1.5c0,2.9-2.3,5.2-5.2,5.2
h-1.5c-2.9,0-5.2-2.3-5.2-5.2V557z"/>
<path class="st4" d="M363,938.2c0,11,9,20,20,20h69.6c11,0,20-9,20-20v-7.8H363V938.2z"/>
<path class="st4" d="M689.8,938.2c0,11,9,20,20,20h69.6c11,0,20-9,20-20v-7.8H689.8V938.2z"/>
<path class="st4" d="M585.4,541.8h-23.6c-1.3,0-2.5,0.5-3.5,1.4c-0.9,0.9-1.4,2.2-1.4,3.5c0,1.1,0.4,2.2,1.1,3.1l25.1,30.2
l41.5,49.8l70.8,85.1c0.8,1,2,1.6,3.3,1.8c1.3,0.1,2.6-0.3,3.6-1.1c0.2-0.2,0.4-0.4,0.6-0.6l70.8-85.1l40.5-48.7l26.1-31.3
c1.7-2.1,1.5-5.2-0.6-6.9c-0.9-0.7-2-1.1-3.2-1.1h-23.3h-43.5V313.6c0-2.7-2.2-4.9-4.9-4.9H633.5c0,0,0,0,0,0
c-1.3,0-2.6,0.5-3.5,1.4c-0.9,0.9-1.4,2.2-1.4,3.5l-5,0h5v228.1H585.4z"/>
<path class="st2" d="M551.3,536.1c-2.8,2.8-4.4,6.6-4.4,10.6c0,3.5,1.2,6.8,3.4,9.5l32,38.4l29.3,35.2l76.1,91.5
c2.6,3.1,6.1,5,10.1,5.3c0.5,0,0.9,0.1,1.4,0.1c3.5,0,6.8-1.2,9.5-3.5c0.7-0.6,1.3-1.2,1.9-1.9l76.1-91.5l27.9-33.6l33.3-40.1
c5.3-6.3,4.4-15.8-1.9-21c-2.7-2.2-6.1-3.5-9.5-3.5H813h-33.2V313.6c0-8.2-6.7-14.9-14.9-14.9H633.5c0,0,0,0,0,0
c-4,0-7.7,1.5-10.5,4.4c-2.8,2.8-4.4,6.6-4.4,10.6v218.1H586h-24.2C557.9,531.8,554.1,533.3,551.3,536.1z M623.6,313.6l5,0
c0-1.3,0.5-2.5,1.4-3.5c0.9-0.9,2.2-1.4,3.5-1.4c0,0,0,0,0,0h131.3c2.7,0,4.9,2.2,4.9,4.9v228.1h43.5h23.3c1.1,0,2.3,0.4,3.2,1.1
c2.1,1.7,2.4,4.9,0.6,6.9l-26.1,31.3l-40.5,48.7L703,714.9c-0.2,0.2-0.4,0.4-0.6,0.6c-1,0.8-2.3,1.2-3.6,1.1
c-1.3-0.1-2.5-0.7-3.3-1.8l-70.8-85.1L583.2,580l-25.1-30.2c-0.7-0.9-1.1-2-1.1-3.1c0-1.3,0.5-2.6,1.4-3.5c0.9-0.9,2.2-1.4,3.5-1.4
h23.6h43.2V313.6H623.6z"/>
<path class="st2" d="M227,509.9c-12.2-3.5-23.9-8.4-34.9-14.7l-3-1.7l-28,23.9c-1.9,0.9-4.1,0.6-5.6-0.9l-44.1-44.1
c-1.5-1.5-1.8-3.7-0.9-5.6l23.9-28.1l-1.7-3c-6.3-11-11.2-22.7-14.7-34.8l-1-3.4l-36.8-2.7c-2-0.7-3.3-2.5-3.3-4.7v-66.6
c0-2.1,1.4-4,3.3-4.7l36.8-2.7l1-3.4c3.5-12.1,8.4-23.9,14.7-34.8l1.7-3l-23.9-28c-0.9-1.9-0.6-4.1,0.9-5.6l44.1-44.1
c1.5-1.5,3.8-1.8,5.6-0.9l28,23.9l3-1.7c11-6.3,22.7-11.2,34.9-14.7l3.4-1l2.7-36.7c0.7-2,2.5-3.3,4.7-3.3h66.6
c2.1,0,4,1.4,4.7,3.3l2.7,36.7l3.4,1c12.2,3.5,23.9,8.4,34.9,14.7l3,1.7l28-23.9c1.9-0.9,4.1-0.6,5.6,0.9l44.1,44.1
c1.5,1.5,1.8,3.8,0.9,5.6l-23.9,28l1.7,3.1c6.3,11,11.2,22.7,14.7,34.8l1,3.3l36.7,2.7c2,0.7,3.3,2.5,3.3,4.7V390
c0,2.1-1.4,4-3.3,4.7l-36.7,2.7l-1,3.4c-3.5,12.1-8.5,23.9-14.7,34.8c-1.4,2.4-0.5,5.5,1.9,6.8c2.4,1.4,5.5,0.5,6.8-1.9
c6.1-10.7,11-22,14.7-33.7l30.8-2.2l0.4-0.1c6.6-1.7,11.2-7.6,11.2-14.4v-66.6c0-6.8-4.6-12.8-11.2-14.4l-0.4-0.1l-30.8-2.3
c-3.3-10.6-7.6-20.9-12.9-30.6l20-23.5l0.2-0.4c3.4-5.8,2.5-13.3-2.3-18.1l-44.1-44.1c-4.8-4.8-12.2-5.7-18.1-2.3l-0.4,0.2
l-23.5,20c-9.8-5.3-20.1-9.6-30.7-12.9l-2.3-30.8l-0.1-0.4c-1.7-6.6-7.6-11.2-14.4-11.2h-66.6c-6.8,0-12.8,4.6-14.4,11.2l-0.1,0.4
l-2.3,30.8c-10.6,3.3-20.9,7.6-30.7,12.9l-23.5-20l-0.4-0.2c-5.8-3.4-13.3-2.5-18.1,2.3L104.2,234c-4.8,4.8-5.7,12.2-2.3,18.1
l0.2,0.4l20,23.5c-5.3,9.7-9.6,20-12.9,30.6l-30.8,2.3l-0.4,0.1c-6.6,1.7-11.2,7.6-11.2,14.4V390c0,6.8,4.6,12.8,11.2,14.4l0.4,0.1
l30.8,2.3c3.3,10.6,7.6,20.9,12.9,30.6l-20,23.5l-0.2,0.4c-3.4,5.8-2.5,13.3,2.3,18.1l44.1,44.1c2.9,2.9,6.7,4.4,10.5,4.4
c2.6,0,5.2-0.7,7.5-2l0.4-0.2l23.5-20c10.8,5.9,22.2,10.5,34,13.9c2.7,0.8,5.4-0.8,6.2-3.4C231.2,513.4,229.6,510.6,227,509.9z"/>
<path class="st2" d="M271,257.3c-54.8,0-99.4,44.6-99.4,99.4s44.6,99.4,99.4,99.4s99.4-44.6,99.4-99.4S325.8,257.3,271,257.3z
M271,446.1c-49.3,0-89.4-40.1-89.4-89.4c0-49.3,40.1-89.4,89.4-89.4s89.4,40.1,89.4,89.4C360.4,406,320.3,446.1,271,446.1z"/>
<path class="st2" d="M564.7,219c34,0,61.6-27.6,61.6-61.6s-27.6-61.6-61.6-61.6c-34,0-61.6,27.6-61.6,61.6S530.8,219,564.7,219z
M564.7,105.9c28.5,0,51.6,23.1,51.6,51.6c0,28.5-23.1,51.6-51.6,51.6c-28.5,0-51.6-23.1-51.6-51.6
C513.1,129,536.3,105.9,564.7,105.9z"/>
<path class="st2" d="M448.5,188l0.4,0.1l17.3,1.3c1.8,5.6,4.1,11,6.8,16.2l-11.2,13.2l-0.2,0.4c-2.5,4.3-1.8,9.7,1.7,13.2
l26.5,26.5c3.5,3.5,8.9,4.2,13.2,1.7l0.4-0.2l13.2-11.2c5.2,2.7,10.6,5,16.2,6.8l1.3,17.3l0.1,0.4c1.3,4.8,5.6,8.2,10.6,8.2h39.9
c0,0,0,0,0,0c5,0,9.4-3.4,10.6-8.2l0.1-0.4l1.3-17.3c5.6-1.8,11-4.1,16.2-6.8l13.2,11.2l0.4,0.2c4.3,2.5,9.7,1.8,13.2-1.7
l26.5-26.5c3.5-3.5,4.2-8.9,1.7-13.2l-0.2-0.4l-11.2-13.2c2.7-5.2,5-10.6,6.8-16.2l17.3-1.3l0.4-0.1c4.8-1.3,8.2-5.6,8.2-10.6
v-39.9c0-5-3.4-9.4-8.2-10.6l-0.4-0.1l-17.3-1.3c-1.8-5.6-4.1-11-6.8-16.2l11.2-13.2l0.2-0.4c2.5-4.3,1.8-9.8-1.7-13.3l-26.4-26.4
c-3.5-3.5-9-4.2-13.3-1.7l-0.4,0.2l-13.2,11.2c-5.2-2.7-10.6-5-16.2-6.8l-1.3-17.3l-0.1-0.4c-1.2-4.8-5.6-8.2-10.6-8.2c0,0,0,0,0,0
h-40c-4.9,0-9.3,3.4-10.6,8.2l-0.1,0.5l-1.2,17.3c-5.6,1.8-11,4.1-16.2,6.8l-13.2-11.2l-0.4-0.2c-4.3-2.5-9.7-1.8-13.2,1.7
l-26.5,26.5c-3.5,3.5-4.2,8.9-1.7,13.2l0.2,0.4l11.2,13.2c-2.7,5.2-5,10.6-6.8,16.2l-17.3,1.3l-0.4,0.1c-4.9,1.2-8.2,5.6-8.2,10.6
v40C440.3,182.4,443.7,186.7,448.5,188z M450.3,137.5c0-0.3,0.2-0.7,0.5-0.8l23.1-1.7l1-3.4c2-7.1,4.9-14,8.6-20.5l1.7-3.1
l-15-17.6c-0.1-0.3,0-0.7,0.2-0.9l26.4-26.4c0.3-0.3,0.6-0.3,1-0.3l17.7,15l3-1.7c6.4-3.7,13.3-6.6,20.5-8.6l3.4-1l1.6-23.1
c0.2-0.3,0.5-0.5,0.8-0.5h39.9c0.3,0,0.7,0.2,0.8,0.5l1.7,23.1l3.4,1c7.1,2,14,4.9,20.5,8.6l3,1.7l17.6-15c0.3-0.1,0.7,0,0.9,0.2
l26.4,26.4c0.2,0.2,0.3,0.6,0.2,0.9l-15,17.6l1.7,3c3.7,6.4,6.6,13.3,8.6,20.5l1,3.4l23.1,1.7c0.3,0.2,0.5,0.5,0.5,0.8v39.9
c0,0.4-0.2,0.7-0.5,0.9l-23.1,1.7l-1,3.4c-2,7.1-5,14-8.6,20.5l-1.7,3l15,17.6c0.1,0.3,0,0.7-0.2,0.9l-26.4,26.4
c-0.3,0.3-0.6,0.3-1,0.3l-17.6-15l-3,1.7c-6.4,3.7-13.3,6.6-20.5,8.6l-3.4,1l-1.7,23.1c-0.2,0.3-0.5,0.5-0.8,0.5h0h-39.9
c-0.4,0-0.7-0.2-0.9-0.5l-1.7-23.1l-3.3-1c-7.1-2-14-5-20.4-8.6l-3-1.7l-17.7,15c-0.3,0.1-0.7,0-0.9-0.2l-26.4-26.4
c-0.3-0.3-0.3-0.6-0.3-1l15-17.6l-1.7-3c-3.7-6.4-6.6-13.3-8.6-20.5l-1-3.4l-23.1-1.7c-0.3-0.2-0.5-0.5-0.5-0.8V137.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><defs><style>.cls-1{fill:#9fcde0;}.cls-1,.cls-3{stroke:#0070ba;}.cls-1,.cls-5{stroke-miterlimit:10;}.cls-1,.cls-3,.cls-4{stroke-width:10px;}.cls-2,.cls-3,.cls-4{fill:#fff;}.cls-3,.cls-4{stroke-linecap:round;stroke-linejoin:round;}.cls-4,.cls-5{stroke:#66cb92;}.cls-5{fill:none;stroke-width:7px;stroke-dasharray:20.32 32.51;}.cls-6{fill:#66cb92;}</style></defs><g id="Ebene_1" data-name="Ebene 1"><path class="cls-1" d="M217.55,873.35,313.26,850l27.65,94.54a12.74,12.74,0,0,0,23.27,2.79l329.2-570.2L532.71,284.4,203.5,854.6A12.74,12.74,0,0,0,217.55,873.35Z"/><path class="cls-1" d="M782.45,873.35,686.74,850l-27.65,94.54a12.74,12.74,0,0,1-23.27,2.79l-329.2-570.2L467.29,284.4,796.5,854.6A12.74,12.74,0,0,1,782.45,873.35Z"/><path class="cls-2" d="M818.63,444.45l-.14.1a58.26,58.26,0,0,0-19,71l0,.06a58.26,58.26,0,0,1-47.27,81.78h0a58.26,58.26,0,0,0-52.06,52.06h0a58.25,58.25,0,0,1-81.78,47.26l-.06,0a58.26,58.26,0,0,0-71,19l-.1.14a58.25,58.25,0,0,1-94.4,0l-.1-.14a58.26,58.26,0,0,0-71-19l-.06,0a58.25,58.25,0,0,1-81.78-47.26h0a58.26,58.26,0,0,0-52.06-52.06h0a58.26,58.26,0,0,1-47.27-81.78l0-.06a58.26,58.26,0,0,0-19-71l-.14-.1a58.26,58.26,0,0,1,0-94.41l.14-.1a58.25,58.25,0,0,0,19-71l0-.05a58.27,58.27,0,0,1,47.27-81.79h0a58.26,58.26,0,0,0,52.06-52h0a58.26,58.26,0,0,1,81.78-47.27l.06,0a58.26,58.26,0,0,0,71-19l.1-.14a58.25,58.25,0,0,1,94.4,0l.1.14a58.26,58.26,0,0,0,71,19l.06,0A58.26,58.26,0,0,1,700.17,145h0a58.26,58.26,0,0,0,52.06,52h0a58.27,58.27,0,0,1,47.27,81.79l0,.05a58.25,58.25,0,0,0,19,71l.14.1A58.26,58.26,0,0,1,818.63,444.45Z"/><path class="cls-3" d="M818.63,444.45l-.14.1a58.26,58.26,0,0,0-19,71l0,.06a58.26,58.26,0,0,1-47.27,81.78h0a58.26,58.26,0,0,0-52.06,52.06h0a58.25,58.25,0,0,1-81.78,47.26l-.06,0a58.26,58.26,0,0,0-71,19l-.1.14a58.25,58.25,0,0,1-94.4,0l-.1-.14a58.26,58.26,0,0,0-71-19l-.06,0a58.25,58.25,0,0,1-81.78-47.26h0a58.26,58.26,0,0,0-52.06-52.06h0a58.26,58.26,0,0,1-47.27-81.78l0-.06a58.26,58.26,0,0,0-19-71l-.14-.1a58.26,58.26,0,0,1,0-94.41l.14-.1a58.25,58.25,0,0,0,19-71l0-.05a58.27,58.27,0,0,1,47.27-81.79h0a58.26,58.26,0,0,0,52.06-52h0a58.26,58.26,0,0,1,81.78-47.27l.06,0a58.26,58.26,0,0,0,71-19l.1-.14a58.25,58.25,0,0,1,94.4,0l.1.14a58.26,58.26,0,0,0,71,19l.06,0A58.26,58.26,0,0,1,700.17,145h0a58.26,58.26,0,0,0,52.06,52h0a58.27,58.27,0,0,1,47.27,81.79l0,.05a58.25,58.25,0,0,0,19,71l.14.1A58.26,58.26,0,0,1,818.63,444.45Z"/><circle class="cls-4" cx="500" cy="397.25" r="239.31"/><circle class="cls-5" cx="500" cy="397.25" r="193.4"/><path class="cls-6" d="M535.36,356.58l93.49,3.69a4.79,4.79,0,0,1,2.63,8.65L555.86,424l25.37,90.06a4.78,4.78,0,0,1-7.41,5.17l-75.76-54.9-77.81,52a4.78,4.78,0,0,1-7.21-5.45l28.8-89-73.46-57.94a4.78,4.78,0,0,1,2.95-8.54l93.57-.12,32.4-87.77a4.78,4.78,0,0,1,9,.17Z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@ -14,3 +14,11 @@
.token-form textarea
font-family: "Lucida Console", Monaco, monospace
max-width: 560px
.upsale--actions
display: flex
flex-wrap: wrap
align-items: baseline
.openproject--static-link
margin-left: 1rem

@ -54,8 +54,8 @@
form.-wide-labels .op-modal--modal-body
.form--label,
.form--field-container
flex-basis: 100%
max-width: 100%
flex-basis: 100% !important
max-width: 100% !important
autocomplete-select-decoration
width: 100%

@ -26,6 +26,7 @@
@import content/icon_control
@import content/drag_and_drop
@import content/boxes
@import content/info_boxes
@import content/headings
@import content/watchers
@import content/simple_filters

@ -0,0 +1,73 @@
# Info boxes
## Simple info boxes
```
<div class="info-boxes">
<h2 class="info-boxes--title">Heading</h2>
<div class="info-boxes--container">
<div class="info-boxes--item">
<h3 class="info-boxes--item-title">Box 1</h3>
<div class="info-boxes--item-content">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</div>
</div>
<div class="info-boxes--item">
<h3 class="info-boxes--item-title">Box 2</h3>
<div class="info-boxes--item-content">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</div>
</div>
<div class="info-boxes--item">
<h3 class="info-boxes--item-title">Box 3</h3>
<div class="info-boxes--item-content">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</div>
</div>
</div>
</div>
```
## Centered with image and links
```
<div class="info-boxes -centered">
<h2 class="info-boxes--title">Heading</h2>
<div class="info-boxes--container">
<div class="info-boxes--item">
<img src="https://via.placeholder.com/250x200" class="info-boxes--teaser-image">
<h3 class="info-boxes--item-title">Box 1</h3>
<div class="info-boxes--item-content">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<ul class="widget-box--arrow-links">
<li>
<a href="" target="_blank">Learn more</a>
</li>
</ul>
</div>
</div>
<div class="info-boxes--item">
<img src="https://via.placeholder.com/250x200" class="info-boxes--teaser-image">
<h3 class="info-boxes--item-title">Box 2</h3>
<div class="info-boxes--item-content">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<ul class="widget-box--arrow-links">
<li>
<a href="" target="_blank">Learn more</a>
</li>
</ul>
</div>
</div>
<div class="info-boxes--item">
<img src="https://via.placeholder.com/250x200" class="info-boxes--teaser-image">
<h3 class="info-boxes--item-title">Box 3</h3>
<div class="info-boxes--item-content">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<ul class="widget-box--arrow-links">
<li>
<a href="" target="_blank">Learn more</a>
</li>
</ul>
</div>
</div>
</div>
</div>
```

@ -0,0 +1,62 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2020 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2017 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
//++
.info-boxes
width: 100%
max-width: 1140px
.info-boxes--title,
.info-boxes--item-title
margin: 20px auto
font-weight: bold
&.-centered
margin: auto
padding: 0 15px
.info-boxes--title
text-align: center
.info-boxes--container
display: grid
grid-gap: 20px
grid-template-columns: repeat(auto-fit,minmax(200px,1fr))
.info-boxes--item
.info-boxes--teaser-image
display: block
margin: auto
max-width: 150px
.info-boxes--item-title
white-space: nowrap
border-bottom: none
.info-boxes--item-content
.widget-box--arrow-links
text-transform: uppercase
font-size: .875rem

@ -126,6 +126,7 @@ $modal-footer-height: $modal-header-height
h2, h3
@include text-shortener
margin: 0
.op-modal--modal-close-button
right: 10px
@ -191,3 +192,4 @@ ul.export-options
.wp-table--configuration-modal,
.op-modal--modal-container
width: 90vw
max-width: unset

@ -101,11 +101,11 @@ $widget-box--enumeration-width: 20px
@include varprop(color, alternative-color)
width: $widget-box--enumeration-width
.widget-box--teaser-image
max-width: 30%
margin-left: auto
margin-right: auto
.widget-box--description
display: grid
grid-template-columns: auto auto
grid-column-gap: 20px
margin-bottom: 20px
.widget-box--header
font-weight: bold

@ -90,6 +90,7 @@
<li><span class="icon icon-export-xls-with-relations"></span>export-xls-with-relations</li>
<li><span class="icon icon-export-xls"></span>export-xls</li>
<li><span class="icon icon-export"></span>export</li>
<li><span class="icon icon-external-link"></span>external-link</li>
<li><span class="icon icon-faq"></span>faq</li>
<li><span class="icon icon-filter"></span>filter</li>
<li><span class="icon icon-flag"></span>flag</li>

@ -26,25 +26,46 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
class EnterprisesController < ApplicationController
include EnterpriseTrialHelper
layout 'admin'
menu_item :enterprise
helper_method :gon
before_action :augur_content_security_policy
before_action :chargebee_content_security_policy
before_action :youtube_content_security_policy
before_action :require_admin
before_action :check_user_limit, only: [:show]
def show
@current_token = EnterpriseToken.current
@token = @current_token || EnterpriseToken.new
write_augur_to_gon
if !@current_token.present?
write_trial_key_to_gon
end
end
def create
@token = EnterpriseToken.current || EnterpriseToken.new
saved_encoded_token = @token.encoded_token
@token.encoded_token = params[:enterprise_token][:encoded_token]
if @token.save
flash[:notice] = t(:notice_successful_update)
redirect_to action: :show
respond_to do |format|
format.html { redirect_to action: :show }
format.json { head :no_content }
end
else
# restore the old token
if saved_encoded_token
@token.encoded_token = saved_encoded_token
@current_token = @token || EnterpriseToken.new
end
render action: :show
end
end
@ -54,16 +75,37 @@ class EnterprisesController < ApplicationController
if token
token.destroy
flash[:notice] = t(:notice_successful_delete)
trial_key = Token::EnterpriseTrialKey.find_by(user_id: User.system.id)
trial_key&.destroy
redirect_to action: :show
else
render_404
end
end
def save_trial_key
Token::EnterpriseTrialKey.create(user_id: User.system.id, value: params[:trial_key])
end
private
def write_trial_key_to_gon
@trial_key = Token::EnterpriseTrialKey.find_by(user_id: User.system.id)
if @trial_key
gon.ee_trial_key = {
value: @trial_key.value
}
end
end
def write_augur_to_gon
gon.augur_url = OpenProject::Configuration.enterprise_trial_creation_host
end
def default_breadcrumb
t(:label_enterprise)
t(:label_enterprise_edition)
end
def show_local_breadcrumb

@ -0,0 +1,21 @@
module EnterpriseTrialHelper
def augur_content_security_policy
append_content_security_policy_directives(
connect_src: [OpenProject::Configuration.enterprise_trial_creation_host]
)
end
def chargebee_content_security_policy
append_content_security_policy_directives(
script_src: %w(js.chargebee.com),
style_src: %w(js.chargebee.com openproject-enterprise-test.chargebee.com),
frame_src: %w(js.chargebee.com openproject-enterprise-test.chargebee.com)
)
end
def youtube_content_security_policy
append_content_security_policy_directives(
frame_src: %w(https://www.youtube.com)
)
end
end

@ -0,0 +1,41 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require_dependency 'token/base'
module Token
class EnterpriseTrialKey < Base
include ExpirableToken
def self.validity_time
1.days
end
end
end

@ -10,18 +10,18 @@
<%= feature_description %>
</p>
<p><%= t('homescreen.blocks.upsale.description') %></p>
<p><%= t('js.admin.enterprise.upsale.benefits.description') %></p>
<ul class="">
<li>
<%= t('homescreen.blocks.upsale.additional_features') %>
<%= t('js.admin.enterprise.upsale.benefits.premium_features_text') %>
</li>
<li>
<%= t('homescreen.blocks.upsale.professional_support') %>
<%= t('js.admin.enterprise.upsale.benefits.professional_support_text') %>
</li>
</ul>
<p>
<b><%= t('homescreen.blocks.upsale.become_hero') %></b> <%= t('homescreen.blocks.upsale.you_contribute') %>
<b><%= t('js.admin.enterprise.upsale.become_hero') %></b> <%= t('js.admin.enterprise.upsale.you_contribute') %>
</p>
<%= link_to( "#{OpenProject::Static::Links.links[:upsale][:href]}/?utm_source=unknown&utm_medium=community-edition&utm_campaign=#{feature_reference}",
{ class: 'button -alt-highlight',

@ -1,47 +1,13 @@
<div class="enterprise--active-token">
<div class="attributes-group">
<div class="attributes-key-value">
<div class="attributes-key-value--key"><%= EnterpriseToken.human_attribute_name(:subscriber) %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span><%= @current_token.subscriber %></span>
</div>
</div>
<div class="attributes-key-value--key"><%= EnterpriseToken.human_attribute_name(:mail) %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span><%= @current_token.mail %></span>
</div>
</div>
<% Hash(@current_token.restrictions).each do |key, value| %>
<div class="attributes-key-value--key"><%= EnterpriseToken.human_attribute_name("#{key}_restriction") %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span><%= value %></span>
</div>
</div>
<% end %>
<div class="attributes-key-value--key"><%= EnterpriseToken.human_attribute_name(:starts_at) %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span><%= format_date @current_token.starts_at %></span>
</div>
</div>
<% if @current_token.will_expire? %>
<div class="attributes-key-value--key"><%= EnterpriseToken.human_attribute_name(:expires_at) %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span><%= format_date @current_token.expires_at %></span>
</div>
</div>
<% end %>
</div>
</div>
</div>
<enterprise-active-saved-trial data-subscriber="<%= @current_token.subscriber %>"
data-email="<%= @current_token.mail %>"
data-user-count="<%= @current_token.restrictions.nil? ? t('js.admin.enterprise.upsale.unlimited') : @current_token.restrictions[:active_user_count] %>"
data-starts-at="<%= format_date @current_token.starts_at %>"
data-expires-at="<%= (!@current_token.will_expire?) ? t('js.admin.enterprise.upsale.unlimited') : (format_date @current_token.expires_at) %>">
</enterprise-active-saved-trial>
<%= form_tag({ action: :destroy },
method: :delete) do %>
<%= form_tag({}, method: :delete) do %>
<confirm-form-submit></confirm-form-submit>
<%= styled_button_tag t(:button_delete), type: 'submit', class: '-with-icon icon-delete' %>
<%= styled_button_tag t(:button_delete),
method: :delete,
class: '-with-icon icon-delete' %>
<% end %>

@ -1,28 +1,122 @@
<div class="notification-box upsale-notification">
<div class="notification-box--content">
<h3><%= t('admin.enterprise.upgrade_to_ee') %></h3>
<%= image_tag "enterprise_edition.png", class: "widget-box--teaser-image" %>
<p><%= t('homescreen.blocks.upsale.description') %></p>
<ul class="">
<li>
<%= t('homescreen.blocks.upsale.additional_features') %>
</li>
<li>
<%= t('homescreen.blocks.upsale.professional_support') %>
</li>
</ul>
<p>
<b><%= t('homescreen.blocks.upsale.become_hero') %></b> <%= t('homescreen.blocks.upsale.you_contribute') %>
</p>
<%= link_to( "#{OpenProject::Static::Links.links[:upsale][:href]}/?utm_source=unknown&utm_medium=community-edition&utm_campaign=enterprise-admin",
{ class: 'button -alt-highlight',
target: '_blank',
aria: {label: t('admin.enterprise.order')},
title: t('admin.enterprise.order')}) do %>
<%= op_icon('button--icon icon-add') %>
<span class="button--text"><%= t('admin.enterprise.order') %></span>
<% end %>
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 2012-2020 the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2017 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See docs/COPYRIGHT.rdoc for more details.
++#%>
<% content_for :header_tags do %>
<script src="<%= OpenProject::Static::Links.links[:chargebee][:href] %>"
data-cb-site="openproject-enterprise-test">
</script>
<% end %>
<enterprise-base></enterprise-base>
<div class='upsale--actions'>
<a href="#"
class="button -highlight"
data-cb-type="checkout"
data-cb-plan-id="enterprise-edition---annual-user-license">
<%= t('admin.enterprise.book_now') %>
</a>
<% quote_link = OpenProject::Static::Links.links.fetch :upsale_get_quote %>
<%= link_to t(quote_link[:label]),
quote_link[:href],
target: '_blank',
class: 'button -highlight'%>
<%= static_link_to :contact %>
</div>
<p>
<b><%= t('js.admin.enterprise.upsale.confidence') %></b>
</p>
<div class="info-boxes upsale-benefits">
<h3 class="info-boxes--title -no-border"><%= t('js.admin.enterprise.upsale.benefits.description') %></h3>
<div class="info-boxes--container">
<div class="info-boxes--item">
<%= image_tag "installation_alerts.svg",
class: "info-boxes--teaser-image",
title: t('js.admin.enterprise.upsale.benefits.installation'),
alt: t('js.admin.enterprise.upsale.benefits.installation') %>
<h4 class="info-boxes--item-title"><%= t('js.admin.enterprise.upsale.benefits.installation') %></h4>
<div class="info-boxes--item-content">
<p><%= t('js.admin.enterprise.upsale.benefits.installation_text') %></p>
<ul class="widget-box--arrow-links">
<li>
<%= static_link_to :upsale_benefits_installation %>
</li>
</ul>
</div>
</div>
<div class="info-boxes--item">
<%= image_tag "system_maintenance.jpg",
class: "info-boxes--teaser-image",
title: t('js.admin.enterprise.upsale.benefits.professional_support'),
alt: t('js.admin.enterprise.upsale.benefits.professional_support') %>
<h4 class="info-boxes--item-title"><%= t('js.admin.enterprise.upsale.benefits.professional_support') %></h4>
<div class="info-boxes--item-content">
<p><%= t('js.admin.enterprise.upsale.benefits.professional_support_text') %></p>
<ul class="widget-box--arrow-links">
<li>
<%= static_link_to :upsale_benefits_support %>
</li>
</ul>
</div>
</div>
<div class="info-boxes--item">
<%= image_tag "premium_features.svg",
class: "info-boxes--teaser-image",
title: t('js.admin.enterprise.upsale.benefits.premium_features'),
alt: t('js.admin.enterprise.upsale.benefits.premium_features') %>
<h4 class="info-boxes--item-title"><%= t('js.admin.enterprise.upsale.benefits.premium_features') %></h4>
<div class="info-boxes--item-content">
<p><%= t('js.admin.enterprise.upsale.benefits.premium_features_text') %></p>
<ul class="widget-box--arrow-links">
<li>
<%= static_link_to :upsale_benefits_features %>
</li>
</ul>
</div>
</div>
<div class="info-boxes--item">
<%= image_tag "security_alerts.jpg",
class: "info-boxes--teaser-image",
title: t('js.admin.enterprise.upsale.benefits.high_security'),
alt: t('js.admin.enterprise.upsale.benefits.high_security') %>
<h4 class="info-boxes--item-title"><%= t('js.admin.enterprise.upsale.benefits.high_security') %></h4>
<div class="info-boxes--item-content">
<p><%= t('js.admin.enterprise.upsale.benefits.high_security_text') %></p>
<ul class="widget-box--arrow-links">
<li>
<%= static_link_to :upsale_benefits_security %>
</li>
</ul>
</div>
</div>
</div>
</div>

@ -1,17 +1,9 @@
<% html_title t(:label_administration), t(:label_enterprise_edition) %>
<% html_title t(:label_administration), t("admin.enterprise.upgrade_to_ee") %>
<%= toolbar title: t(:label_enterprise_edition) do %>
<% if EnterpriseToken.show_banners? %>
<li class="toolbar-item">
<%= link_to( "#{OpenProject::Static::Links.links[:upsale][:href]}/?utm_source=unknown&utm_medium=community-edition&utm_campaign=enterprise-admin",
{ class: 'button -alt-highlight',
target: '_blank',
title: t('admin.enterprise.order')}) do %>
<%= op_icon('button--icon icon-add') %>
<span class="button--text"><%= t('admin.enterprise.order') %></span>
<% end %>
</li>
<% end %>
<%= toolbar title: t("admin.enterprise.upgrade_to_ee") %>
<%= nonced_javascript_tag do %>
<%= include_gon(need_tag: false) -%>
<% end %>
<%= error_messages_for 'token' %>

@ -1,28 +1,31 @@
<%= render 'homescreen/blocks/header', title: t('homescreen.blocks.upsale.title') %>
<%= image_tag "enterprise_edition.png", class: "widget-box--teaser-image" %>
<p><%= t('homescreen.blocks.upsale.description') %></p>
<ul class="widget-box--feature-list">
<li>
<%= t('homescreen.blocks.upsale.additional_features') %>
</li>
<li>
<%= t('homescreen.blocks.upsale.professional_support') %>
</li>
</ul>
<p>
<b><%= t('homescreen.blocks.upsale.become_hero') %></b> <%= t('homescreen.blocks.upsale.you_contribute') %>
</p>
<div class="widget-box--description">
<%= image_tag "enterprise_edition.png", class: "widget-box--teaser-image" %>
<p>
<%= t('js.admin.enterprise.upsale.text') %>
</p>
</div>
<div class="widget-box--blocks--buttons">
<% if current_user.admin? %>
<%= link_to t('js.admin.enterprise.upsale.button_start_trial'), enterprise_path, class: 'button -alt-highlight' %>
<% end %>
<%= link_to "#{OpenProject::Static::Links.links[:upsale][:href]}/?utm_source=unknown&utm_medium=community-edition&utm_campaign=home-screen",
{ class: 'button -alt-highlight',
{ class: 'button -highlight',
aria: {label: t('homescreen.blocks.upsale.more_info')},
target: '_blank',
title: t('homescreen.blocks.upsale.more_info')} do %>
<%= op_icon('button--icon icon-info2') %>
<span class="button--text"><%= t('homescreen.blocks.upsale.more_info') %></span>
<%= op_icon('button--icon icon-external-link') %>
<% end %>
</div>
<span><b><%= t('js.admin.enterprise.upsale.become_hero') %></b></span>
<p><%= t('js.admin.enterprise.upsale.you_contribute') %></p>
<p>
<b><%= t('js.admin.enterprise.upsale.confidence') %></b>
</p>

@ -59,7 +59,7 @@ See docs/COPYRIGHT.rdoc for more details.
</ul>
<p>
<br/>
<b><%= t('homescreen.blocks.upsale.become_hero') %></b> <%= t('homescreen.blocks.upsale.you_contribute') %>
<b><%= t('js.admin.enterprise.upsale.become_hero') %></b> <%= t('js.admin.enterprise.upsale.you_contribute') %>
</p>
<%= link_to("#{OpenProject::Static::Links.links[:upsale][:href]}/?utm_source=unknown&utm_medium=community-edition&utm_campaign=form-configuration",
{ class: 'button -alt-highlight',

@ -72,13 +72,16 @@ en:
main-menu-bg-color: "Left side menu's background color."
theme_warning: Changing the theme will overwrite you custom style. The design will then be lost. Are you sure you want to continue?
enterprise:
upgrade_to_ee: "Upgrade to Enterprise Edition"
upgrade_to_ee: "Upgrade to the Enterprise Edition"
add_token: "Upload an Enterprise Edition support token"
replace_token: "Replace your current support token"
order: "Order Enterprise Edition"
paste: "Paste your Enterprise Edition support token"
required_for_feature: "This feature is only available with an active Enterprise Edition support token."
enterprise_link: "For more information, click here."
start_trial: 'Start free trial'
book_now: 'Book now'
get_quote: 'Get a quote'
announcements:
show_until: Show until
@ -1179,13 +1182,8 @@ en:
blocks:
community: "OpenProject community"
upsale:
become_hero: "Become a hero!"
title: "Upgrade to Enterprise Edition"
description: "What are the benefits?"
more_info: "More information"
additional_features: "Additional powerful premium features"
professional_support: "Professional support from the OpenProject experts"
you_contribute: "Developers need to pay their bills, too. With Enterprise Edition you substantially contribute to this Open-Source community effort."
links:
upgrade_enterprise_edition: "Upgrade to Enterprise Edition"
postgres_migration: "Migrating your installation to PostgreSQL"
@ -1202,6 +1200,7 @@ en:
links:
configuration_guide: 'Configuration guide'
get_in_touch: "You have questions? Get in touch with us."
instructions_after_registration: "You can sign in as soon as your account has been activated by clicking %{signin}."
instructions_after_logout: "You can sign in again by clicking %{signin}."

@ -181,6 +181,60 @@ en:
edit_query: 'Edit query'
new_group: 'New group'
reset_to_defaults: 'Reset to defaults'
enterprise:
trial:
confirmation: "Confirmation of email address"
confirmation_info: "We sent you an email. Please check your emails and click the confirmation link provided to start your 14 days trial."
form:
general_consent: >
I agree with the <a target="_blank" href="%{link_terms}">terms of service</a>
and the <a target="_blank" href="%{link_privacy}">privacy policy</a>.
invalid_email: "Invalid email address"
label_company: "Company"
label_first_name: "First name"
label_last_name: "Last name"
label_email: "Email"
label_domain: "Domain"
label_subscriber: "Subscriber"
label_maximum_users: "Maximum active users"
label_starts_at: "Starts at"
label_expires_at: "Expires at"
receive_newsletter: I want to receive the OpenProject <a target="_blank" href="%{link}">newsletter</a>.
email_not_received: "You did not receive an email? You can resend the email with the link on the right."
try_another_email: "Or try it with another email address."
try_another_email_hint: "Please be aware that your previous email address gets invalid if you try again with a new one."
next_steps: "Next steps"
resend_link: "Resend"
resend_success: "Email has been resent. Please check your emails and click the confirmation link provided."
resend_warning: "Could not resend email."
session_timeout: "Your session timed out. Please try to reload the page or resend email."
status_label: "Status:"
status_confirmed: "confirmed"
status_waiting: "email sent - waiting for confirmation"
test_ee: "Test the Enterprise Edition 14 days for free"
quick_overview: "Get a quick overview of project management and team collaboration with OpenProject Enterprise Edition."
upsale:
become_hero: "Become a hero!"
benefits:
description: "What are the benefits?"
high_security: "Security features"
high_security_text: "Single sign on (SAML, OpenID Connect, CAS), two-factor authentication and automatic sync of LDAP groups."
installation: "Installation support"
installation_text: "Experienced software engineers guide you through the complete installation and setup process in your own infrastructure."
premium_features: "Premium features"
premium_features_text: "Agile boards, custom theme and logo, graphs, intelligent workflows with custom actions, full text search for work package attachments and multi-select custom fields."
professional_support: "Professional support"
professional_support_text: "Get reliable, high-touch support from senior support engineers with expert knowledge about running OpenProject in business-critical environments."
button_start_trial: "Start free trial"
button_book_now: "Book now"
confidence: >
We deliver the confidence of a tested and supported enterprise-class project management software - with Open Source and an open mind.
link_quote: "Get a quote"
text: >
The OpenProject Enterprise Edition builds on top of the Community Edition.
It includes premium features and professional support mainly aimed at organizations with more than 10 users that manage business critical projects with OpenProject.
unlimited: "Unlimited"
you_contribute: "Developers need to pay their bills, too. With the Enterprise Edition, you substantially contribute to this Open Source community effort."
custom_actions:
date:
@ -326,6 +380,7 @@ en:
label_import: "Import"
label_latest_activity: "Latest activity"
label_last_updated_on: "Last updated on"
label_learn_more_link: "Learn more"
label_less_or_equal: "<="
label_less_than_ago: "less than days ago"
label_loading: "Loading..."

@ -340,6 +340,9 @@ OpenProject::Application.routes.draw do
resource :announcements, only: %i[edit update]
constraints(Enterprise) do
resource :enterprise, only: %i[show create destroy]
scope controller: 'enterprises' do
post 'enterprise/save_trial_key' => 'enterprises#save_trial_key'
end
end
resources :enumerations

@ -27,6 +27,7 @@
// ++
import {APP_INITIALIZER, ApplicationRef, Injector, NgModule} from '@angular/core';
import {ReactiveFormsModule} from '@angular/forms';
import {OpenprojectHalModule} from 'core-app/modules/hal/openproject-hal.module';
import {OpContextMenuTrigger} from 'core-components/op-context-menu/handlers/op-context-menu-trigger.directive';
@ -73,6 +74,7 @@ import {OpenprojectProjectsModule} from "core-app/modules/projects/openproject-p
import {KeyboardShortcutService} from "core-app/modules/a11y/keyboard-shortcut-service";
import {globalDynamicComponents} from "core-app/global-dynamic-components.const";
import {OpenprojectMembersModule} from "core-app/modules/members/members.module";
import {OpenprojectEnterpriseModule} from "core-components/enterprise/openproject-enterprise.module";
@NgModule({
imports: [
@ -119,6 +121,7 @@ import {OpenprojectMembersModule} from "core-app/modules/members/members.module"
// Admin module
OpenprojectAdminModule,
OpenprojectEnterpriseModule,
// Plugin hooks and modules
OpenprojectPluginsModule,
@ -127,9 +130,12 @@ import {OpenprojectMembersModule} from "core-app/modules/members/members.module"
// Members
OpenprojectMembersModule,
// Angular Forms
ReactiveFormsModule
],
providers: [
{provide: States, useValue: new States()},
{ provide: States, useValue: new States() },
{ provide: APP_INITIALIZER, useFactory: initializeServices, deps: [Injector], multi: true },
PaginationService,
OpenProjectFileUploadService,
@ -177,7 +183,7 @@ export class OpenProjectModule {
const hookService = (appRef as any)._injector.get(HookService);
hookService
.call('openProjectAngularBootstrap')
.forEach((results:{selector:string, cls:any}[]) => {
.forEach((results:{ selector:string, cls:any }[]) => {
DynamicBootstrapper.bootstrapOptionalDocument(appRef, document, results);
});
}

@ -0,0 +1,53 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2020 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {Component, ElementRef} from "@angular/core";
import {I18nService} from "app/modules/common/i18n/i18n.service";
import {EEActiveTrialBase} from "core-components/enterprise/enterprise-active-trial/ee-active-trial.base";
export const enterpriseActiveSavedTrialSelector = 'enterprise-active-saved-trial';
@Component({
selector: enterpriseActiveSavedTrialSelector,
templateUrl: './ee-active-trial.component.html',
styleUrls: ['./ee-active-trial.component.sass']
})
export class EEActiveSavedTrialComponent extends EEActiveTrialBase {
public subscriber = this.elementRef.nativeElement.dataset['subscriber'];
public email = this.elementRef.nativeElement.dataset['email'];
public userCount = this.elementRef.nativeElement.dataset['userCount'];
public startsAt = this.elementRef.nativeElement.dataset['startsAt'];
public expiresAt = this.elementRef.nativeElement.dataset['expiresAt'];
public company:string;
public domain:string;
constructor(readonly elementRef:ElementRef,
readonly I18n:I18nService) {
super(I18n);
}
}

@ -0,0 +1,46 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2020 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixin";
import {I18nService} from "app/modules/common/i18n/i18n.service";
export class EEActiveTrialBase extends UntilDestroyedMixin {
public text = {
label_email: this.I18n.t('js.admin.enterprise.trial.form.label_email'),
label_expires_at: this.I18n.t('js.admin.enterprise.trial.form.label_expires_at'),
label_maximum_users: this.I18n.t('js.admin.enterprise.trial.form.label_maximum_users'),
label_company: this.I18n.t('js.admin.enterprise.trial.form.label_company'),
label_domain: this.I18n.t('js.admin.enterprise.trial.form.label_domain'),
label_starts_at: this.I18n.t('js.admin.enterprise.trial.form.label_starts_at'),
label_subscriber: this.I18n.t('js.admin.enterprise.trial.form.label_subscriber')
};
constructor(readonly I18n:I18nService) {
super();
}
}

@ -0,0 +1,49 @@
<div class="enterprise--active-token">
<div class="attributes-group">
<div class="attributes-key-value">
<div class="attributes-key-value--key">{{ text.label_subscriber }}</div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span>{{ subscriber }}</span>
</div>
</div>
<div class="attributes-key-value--key">{{ text.label_email }}</div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span>{{ email }}</span>
</div>
</div>
<div *ngIf="company" class="attributes-key-value--key">{{ text.label_company }}</div>
<div *ngIf="company" class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span>{{ company }}</span>
</div>
</div>
<div *ngIf="domain" class="attributes-key-value--key">{{ text.label_domain }}</div>
<div *ngIf="domain" class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span>{{ domain }}</span>
</div>
</div>
<div *ngIf="userCount" class="attributes-key-value--key">{{ text.label_maximum_users }}</div>
<div *ngIf="userCount" class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span>{{ userCount }}</span>
</div>
</div>
<div *ngIf="startsAt" class="attributes-key-value--key">{{ text.label_starts_at }}</div>
<div *ngIf="startsAt" class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span>{{ startsAt }}</span>
</div>
</div>
<!-- only show if token expires -->
<div *ngIf="expiresAt" class="attributes-key-value--key">{{ text.label_expires_at }}</div>
<div *ngIf="expiresAt" class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span>{{ expiresAt }}</span>
</div>
</div>
</div>
</div>
</div>

@ -0,0 +1,109 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2020 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {ChangeDetectorRef, Component, ElementRef, OnInit} from "@angular/core";
import {distinctUntilChanged} from "rxjs/operators";
import {I18nService} from "app/modules/common/i18n/i18n.service";
import {EnterpriseTrialService} from "app/components/enterprise/enterprise-trial.service";
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
import {EEActiveTrialBase} from "core-components/enterprise/enterprise-active-trial/ee-active-trial.base";
import {GonService} from "core-app/modules/common/gon/gon.service";
@Component({
selector: 'enterprise-active-trial',
templateUrl: './ee-active-trial.component.html',
styleUrls: ['./ee-active-trial.component.sass']
})
export class EEActiveTrialComponent extends EEActiveTrialBase implements OnInit {
public subscriber:string;
public email:string;
public userCount:string;
public startsAt:string;
public expiresAt:string;
public company:string;
public domain:string;
constructor(readonly elementRef:ElementRef,
readonly cdRef:ChangeDetectorRef,
readonly I18n:I18nService,
readonly http:HttpClient,
readonly Gon:GonService,
public eeTrialService:EnterpriseTrialService) {
super(I18n);
}
ngOnInit() {
if (!this.subscriber) {
this.eeTrialService.userData$
.pipe(
distinctUntilChanged(),
this.untilDestroyed()
)
.subscribe(userForm => {
this.formatUserData(userForm);
this.cdRef.detectChanges();
});
this.initialize();
}
}
private initialize():void {
let eeTrialKey = this.Gon.get('ee_trial_key') as any;
if (eeTrialKey && !this.eeTrialService.userData) {
// after reload: get data from Augur using the trial key saved in gon
this.eeTrialService.trialLink = this.eeTrialService.baseUrlAugur + '/public/v1/trials/' + eeTrialKey.value;
this.getUserDataFromAugur();
}
}
// use the trial key saved in the db
// to get the user data from Augur
private getUserDataFromAugur() {
this.http
.get<any>(this.eeTrialService.trialLink + '/details')
.toPromise()
.then((userForm:any) => {
this.formatUserData(userForm);
this.eeTrialService.retryConfirmation();
})
.catch((error:HttpErrorResponse) => {
// Check whether the mail has been confirmed by now
this.eeTrialService.getToken();
});
}
private formatUserData(userForm:any) {
this.subscriber = userForm.first_name + ' ' + userForm.last_name;
this.email = userForm.email;
this.company = userForm.company;
this.domain = userForm.domain;
}
}

@ -0,0 +1,23 @@
<div *ngIf="noTrialRequested; else alreadyRequested">
<p>
<b>{{ text.text }}</b>
</p>
<p>
<b>{{ text.become_hero }}</b><br>
{{ text.you_contribute }}
</p>
<button class="button -alt-highlight" (click)="openTrialModal()">
{{ text.button_trial }}
</button>
</div>
<ng-template #alreadyRequested>
<enterprise-trial-waiting></enterprise-trial-waiting>
<p class="confirmation-hint">{{ text.email_not_received }}
<a (click)="openTrialModal()">{{ text.try_another_email }}</a><br>
{{ text.try_another_email_hint }}
</p>
</ng-template>

@ -0,0 +1,7 @@
.button
float: left
.confirmation-hint
font-size: 0.9em
font-style: italic
margin-bottom: 2rem

@ -0,0 +1,72 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {Component, Injector} from "@angular/core";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {DynamicBootstrapper} from "core-app/globals/dynamic-bootstrapper";
import {EnterpriseTrialModal} from "core-components/enterprise/enterprise-modal/enterprise-trial.modal";
import {OpModalService} from "core-components/op-modals/op-modal.service";
import {EnterpriseTrialService} from "core-components/enterprise/enterprise-trial.service";
export const enterpriseBaseSelector = 'enterprise-base';
@Component({
selector: enterpriseBaseSelector,
templateUrl: './enterprise-base.component.html',
styleUrls: ['./enterprise-base.component.sass']
})
export class EnterpriseBaseComponent {
public text = {
button_trial: this.I18n.t('js.admin.enterprise.upsale.button_start_trial'),
button_book: this.I18n.t('js.admin.enterprise.upsale.button_book_now'),
link_quote: this.I18n.t('js.admin.enterprise.upsale.link_quote'),
become_hero: this.I18n.t('js.admin.enterprise.upsale.become_hero'),
you_contribute: this.I18n.t('js.admin.enterprise.upsale.you_contribute'),
email_not_received: this.I18n.t('js.admin.enterprise.trial.email_not_received'),
text: this.I18n.t('js.admin.enterprise.upsale.text'),
try_another_email: this.I18n.t('js.admin.enterprise.trial.try_another_email'),
try_another_email_hint: this.I18n.t('js.admin.enterprise.trial.try_another_email_hint')
};
constructor(protected I18n:I18nService,
protected opModalService:OpModalService,
readonly injector:Injector,
public eeTrialService:EnterpriseTrialService) {
}
public openTrialModal() {
// cancel request and open first modal window
this.eeTrialService.cancelled = true;
this.eeTrialService.modalOpen = true;
this.opModalService.show(EnterpriseTrialModal, this.injector);
}
public get noTrialRequested() {
return this.eeTrialService.status === undefined;
}
}

@ -0,0 +1,86 @@
<form id="enterprise-trial-form" class="form" [formGroup]="trialForm">
<div class="form--field -wide-label -required">
<label class="form--label" for="trial-company-name">{{ text.label_company }}</label>
<div class="form--field-container">
<div class="form--text-field-container">
<input type="text"
id="trial-company-name"
class="form--text-field"
formControlName="company">
</div>
</div>
</div>
<div class="form--field -wide-label -required">
<label class="form--label" for="trial-first-name">{{ text.label_first_name }}</label>
<div class="form--field-container">
<div class="form--text-field-container">
<input type="text"
id="trial-first-name"
class="form--text-field"
formControlName="first_name">
</div>
</div>
</div>
<div class="form--field -wide-label -required">
<label class="form--label" for="trial-last-name">{{ text.label_last_name }}</label>
<div class="form--field-container">
<div class="form--text-field-container">
<input type="text"
id="trial-last-name"
class="form--text-field"
formControlName="last_name">
</div>
</div>
</div>
<div class="form--field -wide-label -required" [ngClass]="{ '-error': eeTrialService.errorMsg }">
<label class="form--label" for="trial-email">{{ text.label_email }}</label>
<div class="form--field-container">
<div class="form--text-field-container"
[ngClass]="{ '-required-highlighting' : eeTrialService.errorMsg }">
<input type="email"
class="form--text-field"
id="trial-email"
formControlName="email" (blur)="checkMailField()">
</div>
</div>
<div *ngIf="eeTrialService.errorMsg" class="form--field-instructions">{{ eeTrialService.errorMsg }}</div>
</div>
<div class="form--field -wide-label -required">
<label class="form--label" for="trial-domain-name">{{ text.label_domain }}</label>
<div class="form--field-container">
<div class="form--text-field-container">
<input type="text"
id="trial-domain-name"
class="form--text-field"
formControlName="domain">
</div>
</div>
</div>
<div class="form--field -required">
<div class="form--field-container">
<label class="form--label-with-check-box -no-ellipsis" for="trial-general-consent">
<div class="form--check-box-container">
<input type="checkbox"
id="trial-general-consent"
class="form--check-box"
formControlName="general_consent"
required>
</div>
<span [innerHTML]="text.general_consent"></span>
</label>
</div>
</div>
<div class="form--field">
<div class="form--field-container">
<label class="form--label-with-check-box -no-ellipsis" for="trial-newsletter-consent">
<div class="form--check-box-container">
<input type="checkbox"
id="trial-newsletter-consent"
class="form--check-box"
formControlName="newsletter_consent">
</div>
<span [innerHtml]="text.receive_newsletter"></span>
</label>
</div>
</div>
</form>

@ -0,0 +1,88 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2020 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {Component, ElementRef} from "@angular/core";
import {FormBuilder, Validators} from "@angular/forms";
import {I18nService} from "app/modules/common/i18n/i18n.service";
import {EnterpriseTrialService} from "core-components/enterprise/enterprise-trial.service";
const termsOfServiceURL = 'https://www.openproject.com/terms-of-service/';
const legalNoticeURL = 'https://www.openproject.com/legal-notice/';
const newsletterURL = 'https://www.openproject.com/newsletter/';
@Component({
selector: 'enterprise-trial-form',
templateUrl: './ee-trial-form.component.html'
})
export class EETrialFormComponent {
// enterprise trial form
trialForm = this.formBuilder.group({
company: ['', Validators.required],
first_name: ['', Validators.required],
last_name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
domain: ['', Validators.required],
general_consent: [null, Validators.required],
newsletter_consent: null,
});
public text = {
general_consent: this.I18n.t('js.admin.enterprise.trial.form.general_consent', {
link_terms: termsOfServiceURL,
link_privacy: legalNoticeURL
}),
invalid_email: this.I18n.t('js.admin.enterprise.trial.form.invalid_email'),
label_test_ee: this.I18n.t('js.admin.enterprise.trial.form.test_ee'),
label_company: this.I18n.t('js.admin.enterprise.trial.form.label_company'),
label_first_name: this.I18n.t('js.admin.enterprise.trial.form.label_first_name'),
label_last_name: this.I18n.t('js.admin.enterprise.trial.form.label_last_name'),
label_email: this.I18n.t('js.admin.enterprise.trial.form.label_email'),
label_domain: this.I18n.t('js.admin.enterprise.trial.form.label_domain'),
privacy_policy: this.I18n.t('js.admin.enterprise.trial.form.privacy_policy'),
receive_newsletter: this.I18n.t('js.admin.enterprise.trial.form.receive_newsletter',{ link: newsletterURL }),
terms_of_service: this.I18n.t('js.admin.enterprise.trial.form.terms_of_service')
};
constructor(readonly elementRef:ElementRef,
readonly I18n:I18nService,
private formBuilder:FormBuilder,
public eeTrialService:EnterpriseTrialService) {
}
// checks if mail is valid after input field was edited by the user
// displays message for user
public checkMailField() {
if (this.trialForm.value.email !== '' && this.trialForm.controls.email.errors) {
this.eeTrialService.errorMsg = this.text.invalid_email;
} else {
this.eeTrialService.errorMsg = undefined;
}
}
}

@ -0,0 +1,73 @@
<div class="op-modal--portal">
<div class="op-modal--modal-container"
data-indicator-name="modal"
tabindex="0">
<div class="op-modal--modal-header">
<h3 [textContent]="headerText()"></h3>
<a *ngIf="showClose" class="op-modal--modal-close-button">
<i class="icon-close" (click)="closeModal($event)" [attr.title]="text.close_popup">
</i>
</a>
</div>
<div [ngSwitch]="openWindow()" class="op-modal--modal-body">
<!-- first modal window -->
<div *ngSwitchCase="1">
<enterprise-trial-form></enterprise-trial-form>
</div>
<!-- second modal window -->
<div *ngSwitchCase="2">
<enterprise-trial-waiting></enterprise-trial-waiting>
</div>
<!-- third modal window -->
<div *ngSwitchCase="3">
<div class="onboarding--video-block">
<div class="onboarding--video-text">
<span>{{ text.quick_overview }}</span>
</div>
<div class="onboarding--video iframe-target-wrapper">
<iframe frameborder="0"
height="400"
width="100%"
[src]="trustedEEVideoURL"
allowfullscreen>
</iframe>
</div>
</div>
</div>
</div>
<div class="op-modal--modal-footer">
<div *ngIf="!eeTrialService.status || eeTrialService.cancelled; else mailSubmitted">
<button class="confirm-form-submit--continue button -highlight"
(click)="onSubmit()"
[disabled]="!trialForm || trialForm.invalid"
[textContent]="text.button_submit"
[attr.title]="text.button_submit"
[hidden]="eeTrialService.mailSubmitted && !eeTrialService.cancelled">
</button>
<button class="confirm-form-submit--cancel button"
(click)="closeModal($event)"
[textContent]="text.button_cancel"
[attr.title]="text.button_cancel">
</button>
</div>
<ng-template #mailSubmitted>
<button class="confirm-form-submit--continue button -highlight"
(click)="startEnterpriseTrial()"
[textContent]="text.button_continue"
[attr.title]="text.button_continue"
[disabled]="!eeTrialService.confirmed"
[hidden]="eeTrialService.trialStarted">
</button>
<button *ngIf="eeTrialService.trialStarted"
class="confirm-form-submit--continue button -highlight"
(click)="closeModal($event)"
[textContent]="text.button_continue"
[attr.title]="text.button_continue">
</button>
</ng-template>
</div>
</div>
</div>

@ -0,0 +1,9 @@
.op-modal--modal-body
padding: 0
.onboarding--video-text
margin-bottom: 1.25rem
.op-modal--modal-footer
margin: 0
padding: 0

@ -0,0 +1,130 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2020 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {AfterViewInit, ChangeDetectorRef, Component, ElementRef, Inject, Input, ViewChild} from "@angular/core";
import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser";
import {FormControl, FormGroup} from "@angular/forms";
import {OpModalComponent} from "app/components/op-modals/op-modal.component";
import {OpModalLocalsToken} from "app/components/op-modals/op-modal.service";
import {OpModalLocalsMap} from "app/components/op-modals/op-modal.types";
import {I18nService} from "app/modules/common/i18n/i18n.service";
import {EETrialFormComponent} from "core-components/enterprise/enterprise-modal/enterprise-trial-form/ee-trial-form.component";
import {EnterpriseTrialService} from "core-components/enterprise/enterprise-trial.service";
export const eeOnboardingVideoURL = 'https://www.youtube.com/embed/zLMSydhFSkw?autoplay=1';
@Component({
selector: 'enterprise-trial-modal',
templateUrl: './enterprise-trial.modal.html',
styleUrls: ['./enterprise-trial.modal.sass']
})
export class EnterpriseTrialModal extends OpModalComponent implements AfterViewInit {
@ViewChild(EETrialFormComponent, { static: false }) formComponent:EETrialFormComponent;
@Input() public opReferrer:string;
public trialForm:FormGroup;
public errorMsg:string|undefined;
// modal configuration
public showClose = true;
public closeOnEscape = false;
public closeOnOutsideClick = false;
public trustedEEVideoURL:SafeResourceUrl;
public text = {
button_submit: this.I18n.t('js.modals.button_submit'),
button_cancel: this.I18n.t('js.modals.button_cancel'),
button_continue: this.I18n.t('js.button_continue'),
close_popup: this.I18n.t('js.close_popup_title'),
heading_confirmation: this.I18n.t('js.admin.enterprise.trial.confirmation'),
heading_next_steps: this.I18n.t('js.admin.enterprise.trial.next_steps'),
heading_test_ee: this.I18n.t('js.admin.enterprise.trial.test_ee'),
quick_overview: this.I18n.t('js.admin.enterprise.trial.quick_overview')
};
constructor(readonly elementRef:ElementRef,
@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap,
readonly cdRef:ChangeDetectorRef,
readonly I18n:I18nService,
readonly domSanitizer:DomSanitizer,
public eeTrialService:EnterpriseTrialService) {
super(locals, cdRef, elementRef);
this.trustedEEVideoURL = this.trustedURL(eeOnboardingVideoURL);
}
ngAfterViewInit() {
this.trialForm = this.formComponent.trialForm;
}
// checks if form is valid and submits it
public onSubmit() {
if (this.trialForm.valid) {
this.trialForm.addControl('_type', new FormControl('enterprise-trial'));
this.eeTrialService.sendForm(this.trialForm);
}
}
public startEnterpriseTrial() {
// open onboarding modal screen
this.eeTrialService.setStartTrialStatus();
}
public headerText() {
if (this.eeTrialService.mailSubmitted) {
return this.text.heading_confirmation;
} else if (this.eeTrialService.trialStarted) {
return this.text.heading_next_steps;
} else {
return this.text.heading_test_ee;
}
}
public closeModal(event:any) {
this.closeMe(event);
// refresh page to show enterprise trial
if (this.eeTrialService.trialStarted || this.eeTrialService.confirmed) {
window.location.reload();
}
this.eeTrialService.modalOpen = false;
}
public trustedURL(url:string) {
return this.domSanitizer.bypassSecurityTrustResourceUrl(url);
}
public openWindow():number {
if (!this.eeTrialService.status || this.eeTrialService.cancelled) {
return 1;
} else if (this.eeTrialService.mailSubmitted && !this.eeTrialService.cancelled) {
return 2;
} else {
return 3;
}
}
}

@ -0,0 +1,16 @@
<enterprise-active-trial></enterprise-active-trial>
<p>{{ text.confirmation_info }}</p>
<p>
<span>{{ text.status_label }} </span>
<span *ngIf="!eeTrialService.confirmed; else confirmedStatus" class="status--waiting">
{{ text.status_waiting }}
<a id="resend-link" (click)="resendMail()">{{ text.resend }}</a>
<p *ngIf="eeTrialService.cancelled">{{ text.session_timeout }}</p>
</span>
<ng-template #confirmedStatus>
<span class="status--confirmed icon-yes">{{ text.status_confirmed }}</span>
</ng-template>
</p>

@ -0,0 +1,7 @@
#resend-link
float: right
.status--confirmed
color: var(--button--alt-highlight-background-color)
.status--waiting
color: orange

@ -0,0 +1,79 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2020 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {Component, ElementRef} from "@angular/core";
import {I18nService} from "app/modules/common/i18n/i18n.service";
import {EnterpriseTrialService} from "app/components/enterprise/enterprise-trial.service";
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
import {NotificationsService} from "core-app/modules/common/notifications/notifications.service";
@Component({
selector: 'enterprise-trial-waiting',
templateUrl: './ee-trial-waiting.component.html',
styleUrls: ['./ee-trial-waiting.component.sass']
})
export class EETrialWaitingComponent {
public text = {
confirmation_info: this.I18n.t('js.admin.enterprise.trial.confirmation_info'),
resend: this.I18n.t('js.admin.enterprise.trial.resend_link'),
resend_success: this.I18n.t('js.admin.enterprise.trial.resend_success'),
resend_warning: this.I18n.t('js.admin.enterprise.trial.resend_warning'),
session_timeout: this.I18n.t('js.admin.enterprise.trial.session_timeout'),
status_confirmed: this.I18n.t('js.admin.enterprise.trial.status_confirmed'),
status_label: this.I18n.t('js.admin.enterprise.trial.status_label'),
status_waiting: this.I18n.t('js.admin.enterprise.trial.status_waiting')
};
constructor(readonly elementRef:ElementRef,
readonly I18n:I18nService,
protected http:HttpClient,
protected notificationsService:NotificationsService,
public eeTrialService:EnterpriseTrialService) {
}
// resend mail if resend link has been clicked
public resendMail() {
this.eeTrialService.cancelled = false;
this.http.post(this.eeTrialService.resendLink, {})
.toPromise()
.then(() => {
this.notificationsService.addSuccess(this.text.resend_success);
this.eeTrialService.retryConfirmation();
})
.catch(() => {
if (this.eeTrialService.trialLink) {
// Check whether the mail has been confirmed by now
this.eeTrialService.getToken();
} else {
this.notificationsService.addError(this.text.resend_warning);
}
});
}
}

@ -0,0 +1,166 @@
import {Injectable} from "@angular/core";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
import {NotificationsService} from "core-app/modules/common/notifications/notifications.service";
import {FormGroup} from "@angular/forms";
import {BehaviorSubject} from 'rxjs';
@Injectable()
export class EnterpriseTrialService {
// user data needs to be sync in ee-active-trial.component.ts
private userDataSubject = new BehaviorSubject<any>({});
public userData$ = this.userDataSubject.asObservable();
public userData:any;
public baseUrlAugur:string;
public trialLink:string;
public resendLink:string;
public modalOpen = false;
public confirmed:boolean;
public cancelled = false;
public status:'mailSubmitted'|'startTrial'|undefined;
public errorMsg:string|undefined;
constructor(readonly I18n:I18nService,
protected http:HttpClient,
readonly pathHelper:PathHelperService,
protected notificationsService:NotificationsService) {
let gon = (window as any).gon;
this.baseUrlAugur = gon.augur_url;
if ((window as any).gon.ee_trial_key) {
this.setMailSubmittedStatus();
}
}
// send POST request with form object
// receive an enterprise trial link to access a token
public sendForm(form:FormGroup) {
this.userData = form.value;
this.userDataSubject.next(this.userData);
this.cancelled = false;
this.http.post(this.baseUrlAugur + '/public/v1/trials', form.value)
.toPromise()
.then((enterpriseTrial:any) => {
this.trialLink = enterpriseTrial._links.self.href;
this.saveTrialKey(this.trialLink);
this.retryConfirmation();
})
.catch((error:HttpErrorResponse) => {
// mail is invalid or user already created a trial
if (error.status === 422 || error.status === 400) {
this.errorMsg = error.error.description;
} else {
this.notificationsService.addWarning(error.error.description || I18n.t('js.error.internal'));
}
});
}
// get a token from the trial link if user confirmed mail
public getToken() {
// 2) GET /public/v1/trials/:id
this.http
.get<any>(this.trialLink)
.toPromise()
.then((res:any) => {
// show confirmed status and enable continue btn
this.confirmed = true;
// returns token if mail was confirmed
// -> if token is new (token_retrieved: false) save token in backend
if (!res.token_retrieved) {
this.saveToken(res.token);
}
// load page if mail was confirmed and modal window is not open
if (!this.modalOpen) {
setTimeout(() => { // display confirmed status before reloading
window.location.reload();
}, 500);
}
})
.catch((error:HttpErrorResponse) => {
// returns error 422 while waiting of confirmation
if (error.status === 422 && error.error.identifier === 'waiting_for_email_verification') {
// get resend button link
this.resendLink = error.error._links.resend.href;
// save a key for the requested trial
if (!this.status || this.cancelled) { // only do it once
this.saveTrialKey(this.resendLink);
}
// open next modal window -> status waiting
this.setMailSubmittedStatus();
this.confirmed = false;
} else if (_.get(error, 'error._type') === 'Error') {
this.notificationsService.addError(error.error.message);
} else {
this.notificationsService.addError(error.error || I18n.t('js.error.internal'));
}
});
}
// save a part of the resend link in db
// which allows to remember if a user has already requested a trial token
// and to ask for the corresponding user data saved in Augur
private saveTrialKey(resendlink:string) {
// extract token from resend link
let trialKey = resendlink.split('/')[6];
return this.http.post(
this.pathHelper.api.v3.appBasePath + '/admin/enterprise/save_trial_key',
{ trial_key: trialKey },
{ withCredentials: true }
)
.toPromise()
.catch((e:any) => {
this.notificationsService.addError(e.error.message || e.message || e);
});
}
// save received token in controller
private saveToken(token:string) {
this.http.post(
this.pathHelper.api.v3.appBasePath + '/admin/enterprise',
{ enterprise_token: { encoded_token: token } },
{ withCredentials: true }
)
.toPromise()
.catch((error:HttpErrorResponse) => {
this.notificationsService.addError(error.error.description || I18n.t('js.error.internal'));
});
}
// retry request while waiting for mail confirmation
public retryConfirmation(delay:number = 5000, retries:number = 60) {
if (this.cancelled || this.confirmed) {
return;
} else if (retries === 0) {
this.cancelled = true;
} else {
this.getToken();
setTimeout( () => {
this.retryConfirmation(delay, retries - 1);
}, delay);
}
}
public setStartTrialStatus() {
this.status = 'startTrial';
}
public setMailSubmittedStatus() {
this.status = 'mailSubmitted';
}
public get trialStarted():boolean {
return this.status === 'startTrial';
}
public get mailSubmitted():boolean {
return this.status === 'mailSubmitted';
}
}

@ -0,0 +1,59 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2020 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {NgModule} from '@angular/core';
import {OpenprojectCommonModule} from "core-app/modules/common/openproject-common.module";
import {EnterpriseTrialService} from "core-components/enterprise/enterprise-trial.service";
import {EnterpriseBaseComponent} from "core-components/enterprise/enterprise-base.component";
import {EnterpriseTrialModal} from "core-components/enterprise/enterprise-modal/enterprise-trial.modal";
import {EETrialFormComponent} from "core-components/enterprise/enterprise-modal/enterprise-trial-form/ee-trial-form.component";
import {EETrialWaitingComponent} from "core-components/enterprise/enterprise-trial-waiting/ee-trial-waiting.component";
import {EEActiveTrialComponent} from "core-components/enterprise/enterprise-active-trial/ee-active-trial.component";
import {EEActiveSavedTrialComponent} from "core-components/enterprise/enterprise-active-trial/ee-active-saved-trial.component";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
@NgModule({
imports: [
OpenprojectCommonModule,
FormsModule,
ReactiveFormsModule
],
providers: [
EnterpriseTrialService
],
declarations: [
EnterpriseBaseComponent,
EnterpriseTrialModal,
EETrialFormComponent,
EETrialWaitingComponent,
EEActiveTrialComponent,
EEActiveSavedTrialComponent,
]
})
export class OpenprojectEnterpriseModule {
}

@ -1,6 +1,4 @@
.widget-box--description
display: flex
.widget-box--teaser-image
background-image: var(--new-feature-teaser-image)
min-width: 170px

@ -126,6 +126,11 @@ import {
MembersAutocompleterComponent,
membersAutocompleterSelector
} from "core-app/modules/members/members-autocompleter.component";
import {EnterpriseBaseComponent, enterpriseBaseSelector} from "core-components/enterprise/enterprise-base.component";
import {
EEActiveSavedTrialComponent,
enterpriseActiveSavedTrialSelector
} from "core-components/enterprise/enterprise-active-trial/ee-active-saved-trial.component";
export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
{ selector: appBaseSelector, cls: ApplicationBaseComponent },
@ -161,6 +166,8 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
{ selector: globalSearchSelector, cls: GlobalSearchInputComponent },
{ selector: collapsibleSectionAugmentSelector, cls: CollapsibleSectionComponent },
{ selector: enterpriseBannerSelector, cls: EnterpriseBannerBootstrapComponent },
{ selector: enterpriseBaseSelector, cls: EnterpriseBaseComponent },
{ selector: enterpriseActiveSavedTrialSelector, cls: EEActiveSavedTrialComponent },
{ selector: projectMenuAutocompleteSelector, cls: ProjectMenuAutocompleteComponent },
{ selector: remoteFieldUpdaterSelector, cls: RemoteFieldUpdaterComponent },
{ selector: wpEmbeddedTableEntrySelector, cls: WorkPackageEmbeddedTableEntryComponent },

@ -167,7 +167,10 @@ module OpenProject
'sentry_dsn' => nil,
# Allow error reporting for frontend errors
'sentry_report_js' => false,
'sentry_host' => 'https://sentry.openproject.com'
'sentry_host' => 'https://sentry.openproject.com',
# Allow connection to Augur
'enterprise_trial_creation_host' => 'https://augur.openproject-edge.com'
}
@config = nil

@ -41,6 +41,15 @@ module OpenProject
(self['attachments_storage'] || 'file').to_sym
end
# Augur connect host
def enterprise_trial_creation_host
if Rails.env.production?
self['enterprise_trial_creation_host']
else
'https://augur.openproject-edge.com'
end
end
def file_storage?
attachments_storage == :file
end

@ -77,6 +77,26 @@ module OpenProject
href: 'https://www.openproject.org/enterprise-edition',
label: 'homescreen.links.upgrade_enterprise_edition'
},
upsale_benefits_features: {
href: 'https://www.openproject.org/enterprise-edition/#premium-features',
label: 'noscript_learn_more'
},
upsale_benefits_installation: {
href: 'https://www.openproject.org/enterprise-edition/#installation',
label: 'noscript_learn_more'
},
upsale_benefits_security: {
href: 'https://www.openproject.org/enterprise-edition/#security-features',
label: 'noscript_learn_more'
},
upsale_benefits_support: {
href: 'https://www.openproject.org/enterprise-edition/#professional-support',
label: 'noscript_learn_more'
},
upsale_get_quote: {
href: 'https://www.openproject.org/upgrade-enterprise-edition/',
label: 'admin.enterprise.get_quote'
},
user_guides: {
href: 'https://docs.openproject.org/user-guide/',
label: 'homescreen.links.user_guides'
@ -93,6 +113,10 @@ module OpenProject
href: 'https://www.openproject.org/operations/configuration/',
label: 'links.configuration_guide'
},
contact: {
href: 'https://www.openproject.org/contact-us/',
label: 'links.get_in_touch'
},
glossary: {
href: 'https://www.openproject.org/help/glossary/',
label: 'homescreen.links.glossary'
@ -162,6 +186,9 @@ module OpenProject
},
security_badge_documentation: {
href: 'https://docs.openproject.org/system-admin-guide/information/#security-badge'
},
chargebee: {
href: 'https://js.chargebee.com/v2/chargebee.js',
}
}
end

@ -6,18 +6,18 @@
<h3><%= t('admin.enterprise.upgrade_to_ee') %></h3>
<%= image_tag "enterprise_edition.png", class: "widget-box--teaser-image" %>
<p><%= t('homescreen.blocks.upsale.description') %></p>
<p><%= t('js.admin.enterprise.upsale.benefits.description') %></p>
<ul class="">
<li>
<%= t('homescreen.blocks.upsale.additional_features') %>
<%= t('js.admin.enterprise.upsale.benefits.premium_features_text') %>
</li>
<li>
<%= t('homescreen.blocks.upsale.professional_support') %>
<%= t('js.admin.enterprise.upsale.benefits.professional_support_text') %>
</li>
</ul>
<p>
<b><%= t('homescreen.blocks.upsale.become_hero') %></b> <%= t('homescreen.blocks.upsale.you_contribute') %>
<b><%= t('js.admin.enterprise.upsale.become_hero') %></b> <%= t('js.admin.enterprise.upsale.you_contribute') %>
</p>
<%= link_to( "#{OpenProject::Static::Links.links[:upsale][:href]}/?utm_source=unknown&utm_medium=community-edition&utm_campaign=enterprise-ldap-groups",
{ class: 'button -alt-highlight',

@ -7,18 +7,18 @@
<h3><%= t('admin.enterprise.upgrade_to_ee') %></h3>
<%= image_tag "enterprise_edition.png", class: "widget-box--teaser-image" %>
<p><%= t('homescreen.blocks.upsale.description') %></p>
<p><%= t('js.admin.enterprise.upsale.benefits.description') %></p>
<ul class="">
<li>
<%= t('homescreen.blocks.upsale.additional_features') %>
<%= t('js.admin.enterprise.upsale.benefits.premium_features_text') %>
</li>
<li>
<%= t('homescreen.blocks.upsale.professional_support') %>
<%= t('js.admin.enterprise.upsale.benefits.professional_support_text') %>
</li>
</ul>
<p>
<b><%= t('homescreen.blocks.upsale.become_hero') %></b> <%= t('homescreen.blocks.upsale.you_contribute') %>
<b><%= t('js.admin.enterprise.upsale.become_hero') %></b> <%= t('js.admin.enterprise.upsale.you_contribute') %>
</p>
<%= link_to( "#{OpenProject::Static::Links.links[:upsale][:href]}/?utm_source=unknown&utm_medium=community-edition&utm_campaign=enterprise-openid-connect",
{ class: 'button -alt-highlight',

@ -73,7 +73,7 @@ describe EnterprisesController, type: :controller do
it 'still renders #show with form' do
expect(response).not_to render_template partial: 'enterprises/_current'
expect(response.body).to have_selector '.upsale-notification'
expect(response.body).to have_selector '.upsale-benefits'
end
end
end

@ -52,7 +52,8 @@ describe 'Enterprise token', type: :feature, js: true do
end
it 'shows a teaser and token form without a token' do
expect(page).to have_selector('.upsale-notification a', text: 'Order Enterprise Edition')
expect(page).to have_selector('.button', text: 'Start free trial')
expect(page).to have_selector('.button', text: 'Book now')
expect(textarea.value).to be_empty
textarea.set 'foobar'
@ -78,9 +79,9 @@ describe 'Enterprise token', type: :feature, js: true do
expect(page).to have_selector('.enterprise--active-token')
expect(page.all('.attributes-key-value--key').map(&:text))
.to eq ['Subscriber', 'Email', 'Valid since']
.to eq ['Subscriber', 'Email', 'Maximum active users', 'Starts at', 'Expires at']
expect(page.all('.attributes-key-value--value').map(&:text))
.to eq ['Foobar', 'foo@example.org', format_date(Date.today)]
.to eq ['Foobar', 'foo@example.org', 'Unlimited', format_date(Date.today), 'Unlimited']
expect(page).to have_selector('.button.icon-delete', text: I18n.t(:button_delete))

@ -0,0 +1,246 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe 'Enterprise trial management',
type: :feature,
skip: true,
driver: :headless_firefox_billy do
let(:admin) { FactoryBot.create(:admin) }
let(:trial_id) { '1b6486b4-5a30-4042-8714-99d7c8e6b637' }
let(:created_body) do
{
_type: "enterprise-trial",
id: trial_id,
_links:
{
self:
{
href: "https://augur.openproject-edge.com/public/v1/trials/#{trial_id}"
},
details:
{
href: "https://augur.openproject-edge.com/public/v1/trials/#{trial_id}/details"
}
}
}
end
let(:waiting_body) do
{
_type: "error",
code: 422,
identifier: "waiting_for_email_verification",
description: "User has to confirm their email address",
_links: {
resend: {
href: "https://augur.openproject-edge.com/public/v1/trials/#{trial_id}/resend",
method: "POST"
},
details: {
href: "https://augur.openproject-edge.com/public/v1/trials/#{trial_id}/details"
}
}
}
end
let(:expired_token) do
<<~EOS
-----BEGIN OPENPROJECT-EE TOKEN-----
eyJkYXRhIjoiTE02OG5UWjJ1cTY4dnlKNWo4NEk0ZnZGdHlFcUtEU1ZxVGd5
WnBicTUzTlA5VFFOa3NSc3haOGl1KzZpXG5VTEhuQmhnWjc5c3pYRzhTV2lt
Tlg3QnpLdkh2MlFLeXFqOCtkQ2dzNHNhQUEvV21aRWZ3YmtPVExTSTBcblVY
eTYxMmFnKzY0OXVOT2dOdTZmTm5mQndoTnNZdnFGRmxDZjJZd1VQU0ROZUhQ
dWF2bDJEa3hlTTlLdlxuaWRNbC8wU3BxdWpzMVk4VjlLazhEejRJNUViQU1E
K1NOMzE1eHplOWc2MDduN2p4c3FKS3k3RVVrUTI5XG5RRG5DSTVZSTJ6bTJv
dkpaXG4iLCJrZXkiOiJSaXlnRTE0RWswdi9qVFZkOW9HRWJOcldudStQQlN2
K0xDTEVpUWZadEczY2g2djN1TERWdWVZeG8xV2NcbjRXdUFGUkdKOFEvejhn
OG01NWpyMkRKdGh6UUdoVjRYa2t4ZlN2ZUdaaUVzRWJFMmh5NzQ2cDRHNjl1
b1xuaFFOQmtqZ1FqWUZwTW9yUVBSRmhXRTNjbkp1dGFKOGU1dUVTbkZPYUFD
RDdsdkNvMUhMY2J4NWduMm96XG5NcXllbC96NytBdSt5QUNtT2poSlRaUW9L
M25ZenVuZ1FXbXJiZm93ZGUzVVN6c1lraEdyRHlBNXJSWmlcbk9TaXpqSnNE
MXBIRmZ4aVhEMnYzVlNuMWJMNXpJWFZNMDBUSFJGUHZLODVYY3IzTEVFNTZy
TVBCMytnRlxuYUptcVFUYVJOWFowamJKZ3cwNFdqUEtTbGxxaDVIVWZ5Umk2
ZjErRGxxYWlsMmcvOUZpTUpPc29QOFhhXG5IYm9oUURBY1drOHBGVE9Fci8z
NTNSdU4rejE3SklJdVdsM0Z2ZmhiQVFGZVdHQ0paR0JzTnJ4WUV3QzBcbmN1
WC80NFZKem9kcVkrRHlSUWFYNFlkcytCOUJVRnB2MHRaSnUrZmNza1MyVnc5
MGtCM3hQZTBmYTFjV1xuZTVSdHFucklFNXRQMzlEQ25YZWxwSmxGRXh5YzhX
ekZiL3BlVEVqUWtCWnM0ZUNKMzhQT1djQXh1R2s2XG5iRFppYWlEQitJSFV4
QTBuYXhSa2R5OWJvbHBNa1RBNjk1a3Q3TnhSTDJ2WU1PZFFEY3pIekFNQmpV
TE5cbnFQd3FuQWlreDgrazJqTStHWDFmTXgrUzcyb3FQTjd6M0ZqaU85S1BV
VVAveTlNMG1RV1hCZEY0bVJUR1xuM2RuUDNoOXErSHNnTkFHTUNxSHBTRzNv
L05ONTRQSCs1NCsyWk5MVDFzZ1ZubjBsQ1hVdlh0Vkt6b1hhXG5TdXlZNzBV
R3NJTUdDYmZhdnlYREVpQzU2SWtJVzNTSU1CVHVQdisxQ1J4TCtIcEEzRE5x
R09BVjVMbFFcbkUrdGNqZlNpUlVzRXcxeWkxWUZPODVEM2ZVTXdLZzFKclZB
WEV1YVdvbjUxMzNVRnZNZjBNbFhkSUQ1L1xuNEtXczNGeEdmWVJJRUQ5VlhR
eFNYdEQ3cWYzSlFFbHdHSGVMdUtVYkRmMWEzTEVKMUFKb2FOV0phcG9xXG4v
QlU3ZHJoM28zSDFXYVBpeUhpUlQrVExTa2cxUXhxY2p0eUVuK1JiNnBKVmwr
eVJXWHMrR1pkcGFDY09cbnh2ZWRIam12NzNsUjc5WFpVNVh6UlhwY3E1d1pm
T2FVaHZ1NHllQysyR0FpYlIrcGowbTA0UzRXbjI0elxucjUvZGtnSG5Xek9B
V2lZb0MxOEZpckhTSnVGM1FHWHJUK1JyT2c4QVdwTDlHMGZQQlpveTJvNFZj
V004XG51UXJvSDUwT3Rtcm00cW53QUU3TEFyc3g3bWxOblBGMmpyejZMeWkz
UlhDN1ZrSE9FVXhiUHNjZHJiRlhcbkd3cTlvNU5LNi9sb2RVTTAzeklyaTBs
TVdKSlpUU3BNMnVzU0VxWUpoS05uSGI1a3lYcy9MRkhOWW05c1xuN2hBOVdS
RUxWQi9Tc2x5RjJQczNzSHJQaGtZM1BGZElSeU9Kb2JxdnZoaUpPTVA5dDVu
MUxUeTFjbkhGXG5DbTBDM0U1bWFjTi9hOE5OSXk2dGhia3JJVE5XK2I4K2Jw
VDN3OGkxSDVYNCtodlJ5T1g0Y0JEVWhNN2pcbnN3Wkw0citmWlRmaGlNQkZi
K2NmSUZ0U2lyMVBpdz09XG4iLCJpdiI6IjFxbEZqRWM4QzcrMjg4QWR6cXdL
OEE9PVxuIn0=
-----END OPENPROJECT-EE TOKEN-----
EOS
end
let (:confirmed_body) do
{
_type: "enterprise-trial",
id: trial_id,
token: expired_token,
token_retrieved: false,
_links: {
self: {
href: "https://augur.openproject-edge.com/public/v1/trials/#{trial_id}"
}
}
}
end
let(:mail_in_use_body) do
{
_type: "error",
code: 422,
identifier: "user_already_created_trial",
description: "Each user can only create one trial."
}
end
before do
login_as(admin)
visit enterprise_path
end
def fill_out_modal(mail: 'foo@foocorp.example')
fill_in 'Company', with: 'Foo Corp.'
fill_in 'First name', with: 'Foo'
fill_in 'Last name', with: 'Bar'
fill_in 'Email', with: mail
fill_in 'Domain', with: 'foo.example.com'
find('#trial-general-consent').check
end
it 'blocks the request assuming the mail was used' do
proxy.stub('https://augur.openproject-edge.com:443/public/v1/trials', method: 'post')
.and_return(headers: {'Access-Control-Allow-Origin' => '*'}, code: 422, body: mail_in_use_body.to_json)
find('.button', text: 'Start free trial').click
fill_out_modal
find('.button:not(:disabled)', text: 'Submit').click
expect(page).to have_selector('.form--field.-error #trial-email')
expect(page).to have_text 'Each user can only create one trial.'
expect(page).to have_no_text 'email sent - waiting for confirmation'
end
context 'with a waiting request pending' do
before do
proxy.stub('https://augur.openproject-edge.com:443/public/v1/trials', method: 'post')
.and_return(headers: {'Access-Control-Allow-Origin' => '*'}, code: 200, body: created_body.to_json)
proxy.stub("https://augur.openproject-edge.com:443/public/v1/trials/#{trial_id}")
.and_return(headers: {'Access-Control-Allow-Origin' => '*'}, code: 422, body: waiting_body.to_json)
find('.button', text: 'Start free trial').click
fill_out_modal
find('.button:not(:disabled)', text: 'Submit').click
expect(page).to have_text 'foo@foocorp.example'
expect(page).to have_text 'email sent - waiting for confirmation'
end
it 'can get the trial if reloading the page' do
# We need to go to another page to stop the request cycle
visit info_admin_index_path
# Stub with successful body
# Stub the proxy to a successful return
# which marks the user has confirmed the mail link
proxy.stub("https://augur.openproject-edge.com:443/public/v1/trials/#{trial_id}")
.and_return(headers: {'Access-Control-Allow-Origin' => '*'}, code: 200, body: confirmed_body.to_json)
# Stub the details URL to still return 403
proxy.stub("https://augur.openproject-edge.com:443/public/v1/trials/#{trial_id}/details")
.and_return(headers: {'Access-Control-Allow-Origin' => '*'}, code: 403)
visit enterprise_path
expect(page).to have_selector('.attributes-key-value--value-container', text: 'OpenProject Test', wait: 20)
expect(page).to have_selector('.attributes-key-value--value-container', text: '01/01/2020')
expect(page).to have_selector('.attributes-key-value--value-container', text: '01/02/2020')
expect(page).to have_selector('.attributes-key-value--value-container', text: '5')
# Generated expired token has different mail
expect(page).to have_selector('.attributes-key-value--value-container', text: 'info@openproject.com')
end
it 'can confirm that trial regularly' do
# Stub resend method
proxy.stub("https://augur.openproject-edge.com:443/public/v1/trials/#{trial_id}/resend")
.and_return(headers: {'Access-Control-Allow-Origin' => '*'}, code: 200, body: waiting_body.to_json)
find('.op-modal--modal-body #resend-link', text: 'Resend').click
expect(page).to have_text 'Email has been resent.'
expect(page).to have_text 'foo@foocorp.example'
expect(page).to have_text 'email sent - waiting for confirmation'
# Stub the proxy to a successful return
# which marks the user has confirmed the mail link
proxy.stub("https://augur.openproject-edge.com:443/public/v1/trials/#{trial_id}")
.and_return(headers: {'Access-Control-Allow-Origin' => '*'}, code: 200, body: confirmed_body.to_json)
# Wait until the next request
expect(page).to have_selector '.status--confirmed', text: 'confirmed', wait: 20
# advance to video
click_on 'Continue'
# advance to close
click_on 'Continue'
expect(page).to have_selector('.flash.notice', text: 'Successful update.', wait: 10)
expect(page).to have_selector('.attributes-key-value--value-container', text: 'OpenProject Test')
expect(page).to have_selector('.attributes-key-value--value-container', text: '01/01/2020')
expect(page).to have_selector('.attributes-key-value--value-container', text: '01/02/2020')
expect(page).to have_selector('.attributes-key-value--value-container', text: '5')
# Generated expired token has different mail
expect(page).to have_selector('.attributes-key-value--value-container', text: 'info@openproject.com')
end
end
end

@ -33,6 +33,7 @@ require 'rspec/rails'
require 'shoulda/matchers'
require 'test_prof/recipes/rspec/before_all'
# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end

@ -6,9 +6,7 @@ if ENV['CI']
::Webdrivers::Chromedriver.update
end
def register_chrome_headless(language)
name = :"chrome_headless_#{language}"
def register_chrome_headless(language, name: :"chrome_headless_#{language}")
Capybara.register_driver name do |app|
options = Selenium::WebDriver::Chrome::Options.new
@ -37,6 +35,12 @@ def register_chrome_headless(language)
options.add_preference(:browser, set_download_behavior: { behavior: 'allow' })
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
loggingPrefs: { browser: 'ALL' }
)
yield(options, capabilities) if block_given?
client = Selenium::WebDriver::Remote::Http::Default.new
client.read_timeout = 180
client.open_timeout = 180
@ -44,6 +48,7 @@ def register_chrome_headless(language)
driver = Capybara::Selenium::Driver.new(
app,
browser: :chrome,
desired_capabilities: capabilities,
http_client: client,
options: options
)
@ -68,3 +73,12 @@ end
register_chrome_headless 'en'
# Register german locale for custom field decimal test
register_chrome_headless 'de'
# Register mocking proxy driver
register_chrome_headless 'en', name: :headless_chrome_billy do |options, capabilities|
options.add_argument("--proxy-server=#{Billy.proxy.host}:#{Billy.proxy.port}")
options.add_argument('--proxy-bypass-list=127.0.0.1;localhost')
capabilities[:acceptInsecureCerts] = true
end

@ -7,9 +7,7 @@ if ENV['CI']
end
def register_firefox_headless(language)
name = :"firefox_headless_#{language}"
def register_firefox_headless(language, name: :"firefox_headless_#{language}")
require 'selenium/webdriver'
Capybara.register_driver name do |app|
@ -38,6 +36,12 @@ def register_firefox_headless(language)
options = Selenium::WebDriver::Firefox::Options.new(profile: profile)
capabilities = Selenium::WebDriver::Remote::Capabilities.firefox(
loggingPrefs: { browser: 'ALL' }
)
yield(profile, options, capabilities) if block_given?
unless ActiveRecord::Type::Boolean.new.cast(ENV['OPENPROJECT_TESTING_NO_HEADLESS'])
options.args << "--headless"
end
@ -49,6 +53,8 @@ def register_firefox_headless(language)
app,
browser: :firefox,
options: options,
desired_capabilities: capabilities,
http_client: client,
)
@ -64,6 +70,17 @@ register_firefox_headless 'en'
# Register german locale for custom field decimal test
register_firefox_headless 'de'
# Register mocking proxy driver
register_firefox_headless 'en', name: :headless_firefox_billy do |profile, options, capabilities|
profile.assume_untrusted_certificate_issuer = false
profile.proxy = Selenium::WebDriver::Proxy.new(
http: "#{Billy.proxy.host}:#{Billy.proxy.port}",
ssl: "#{Billy.proxy.host}:#{Billy.proxy.port}")
capabilities[:accept_insecure_certs] = true
end
# Resize window if firefox
RSpec.configure do |config|
config.before(:each, driver: Proc.new { |val| val.to_s.start_with? 'firefox_headless_' }) do

@ -0,0 +1,39 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
# puffing-billy is a gem that creates a middleman proxy between the browser controlled
# by capybara/selenium and the spec execution.
#
# This allows us to stub requests to external APIs to guarantee responses regardless of
# their availability.
#
# In order to use the proxied server, you need to use `driver: headless_firefox_billy` in your examples
#
# See https://github.com/oesmith/puffing-billy for more information
require 'billy/capybara/rspec'

@ -36,17 +36,17 @@ RSpec.configure do |config|
WebMock.disable!
end
# When we enable webmock, no connections other than stubbed ones are allowed.
# We will exempt local connections from this block, since selenium etc.
# uses localhost to communicate with the browser.
# Leaving this off will randomly fail some specs with WebMock::NetConnectNotAllowedError
WebMock.disable_net_connect!(allow_localhost: true)
config.around(:example, webmock: true) do |example|
begin
# When we enable webmock, no connections other than stubbed ones are allowed.
# We will exempt local connections from this block, since selenium etc.
# uses localhost to communicate with the browser.
# Leaving this off will randomly fail some specs with WebMock::NetConnectNotAllowedError
WebMock.disable_net_connect!(allow_localhost: true)
WebMock.enable!
example.run
ensure
WebMock.allow_net_connect!
WebMock.disable!
end
end

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 512 512" width="512" height="512"><defs><path d="M12 68.89C12 108.71 12 427.29 12 467.11C12 498.2 37.8 524 68.89 524C108.71 524 427.29 524 467.11 524C498.2 524 524 498.2 524 467.11C524 453.84 524 387.47 524 268L467.11 268L467.11 467.11L68.89 467.11L68.89 68.89L268 68.89L268 12L68.89 12C30.96 29.2 12 48.16 12 68.89ZM324.89 68.89L426.89 68.89L162.56 333.22L202.78 373.44L467.11 109.11L467.11 211.11L524 211.11L524 12L324.89 12L324.89 68.89Z" id="a4ClflbBbj"></path><path d="M12 20C12 25.6 12 70.4 12 76C12 80.37 15.63 84 20 84C25.6 84 70.4 84 76 84C80.37 84 84 80.37 84 76C84 74.13 84 64.8 84 48L76 48L76 76L20 76L20 20L48 20L48 12L20 12C14.67 14.42 12 17.09 12 20ZM56 20L70.34 20L33.17 57.17L38.83 62.83L76 25.66L76 40L84 40L84 12L56 12L56 20Z" id="aH6E9REk8"></path></defs><g><g><g><use xlink:href="#a4ClflbBbj" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#a4ClflbBbj" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#aH6E9REk8" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#aH6E9REk8" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Loading…
Cancel
Save