Merge pull request #11544 from opf/feature-44516-enterprise-page-on-the-notification-centre

[44516] Enterprise page on the Notification Centre
feature/ee-date-alerts
Oliver Günther 2 years ago committed by GitHub
commit d38aa8c379
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      app/helpers/icons_helper.rb
  2. 3
      app/services/authorization/enterprise_service.rb
  3. 2
      app/views/common/upsale.html.erb
  4. 4
      app/views/homescreen/blocks/_upsale.html.erb
  5. 12
      config/initializers/menus.rb
  6. 2
      config/locales/en.yml
  7. 1
      frontend/src/app/features/in-app-notifications/center/menu/menu.component.ts
  8. 4
      frontend/src/app/shared/components/enterprise-banner/enterprise-banner.component.html
  9. 6
      frontend/src/app/shared/components/sidemenu/sidemenu.component.html
  10. 3
      frontend/src/app/shared/components/sidemenu/sidemenu.component.sass
  11. 5
      frontend/src/app/shared/components/sidemenu/sidemenu.component.ts
  12. BIN
      frontend/src/assets/fonts/openproject_icon/openproject-icon-font.ttf
  13. BIN
      frontend/src/assets/fonts/openproject_icon/openproject-icon-font.woff
  14. BIN
      frontend/src/assets/fonts/openproject_icon/openproject-icon-font.woff2
  15. 3
      lib/redmine/menu_manager/menu_helper.rb
  16. 4
      lib/redmine/menu_manager/menu_item.rb
  17. 3
      modules/boards/lib/open_project/boards/engine.rb
  18. 3
      modules/ldap_groups/lib/open_project/ldap_groups/engine.rb
  19. 2
      modules/openid_connect/app/controllers/openid_connect/providers_controller.rb
  20. 3
      modules/openid_connect/lib/open_project/openid_connect/engine.rb
  21. 191
      modules/openid_connect/spec/controllers/providers_controller_spec.rb
  22. 4
      modules/openid_connect/spec/requests/openid_connect_spec.rb
  23. 3
      modules/team_planner/lib/open_project/team_planner/engine.rb
  24. 3
      modules/two_factor_authentication/lib/open_project/two_factor_authentication/engine.rb

@ -36,6 +36,11 @@ module IconsHelper
%(<i class="#{classnames}" #{title} aria-hidden="true"></i>).html_safe
end
def spot_icon(icon_name, title: nil)
classnames = "spot-icon spot-icon_#{icon_name}"
content_tag(:span, title, class: classnames.to_s)
end
##
# Icon wrapper with an invisible label
def icon_wrapper(icon_class, label)

@ -46,6 +46,7 @@ class Authorization::EnterpriseService
grid_widget_wp_graph
placeholder_users
team_planner_view
openid_providers
).freeze
def initialize(token)
@ -68,7 +69,7 @@ class Authorization::EnterpriseService
def process(action)
# Every non-expired token
GUARDED_ACTIONS.include?(action)
GUARDED_ACTIONS.include?(action.to_sym)
end
def result(bool)

@ -52,7 +52,7 @@
aria: {label: t('admin.enterprise.buttons.upgrade')},
target: '_blank',
title: t('admin.enterprise.buttons.upgrade')}) do %>
<%= op_icon('button--icon icon-medal') %>
<%= spot_icon('enterprise-badge') %>
<span class="button--text"><%= t('admin.enterprise.buttons.upgrade') %></span>
<% end %>
<free-trial-button></free-trial-button>

@ -3,7 +3,7 @@
<div class="widget-box--blocks--upsale-description">
<div class="widget-box--blocks--upsale-title">
<%= op_icon('button--icon icon-medal') %>
<%= spot_icon('enterprise-badge') %>
<span><%= t('homescreen.blocks.upsale.title') %></span>
<p class="widget-box--blocks--upsale-description">
@ -37,7 +37,7 @@
aria: {label: t('admin.enterprise.buttons.upgrade')},
target: '_blank',
title: t('admin.enterprise.buttons.upgrade')}) do %>
<%= op_icon('button--icon icon-medal') %>
<%= spot_icon('enterprise-badge') %>
<span class="button--text"><%= t('admin.enterprise.buttons.upgrade') %></span>
<% end %>

@ -201,7 +201,8 @@ Redmine::MenuManager.map :admin_menu do |menu|
{ controller: '/placeholder_users' },
if: Proc.new { User.current.admin? },
caption: :label_placeholder_user_plural,
parent: :users_and_permissions
parent: :users_and_permissions,
enterprise_feature: 'placeholder_users'
menu.push :groups,
{ controller: '/groups' },
@ -263,13 +264,15 @@ Redmine::MenuManager.map :admin_menu do |menu|
{ controller: '/custom_actions' },
if: Proc.new { User.current.admin? },
caption: :'custom_actions.plural',
parent: :admin_work_packages
parent: :admin_work_packages,
enterprise_feature: 'custom_actions'
menu.push :attribute_help_texts,
{ controller: '/attribute_help_texts' },
caption: :'attribute_help_texts.label_plural',
icon: 'icon2 icon-help2',
if: Proc.new { User.current.admin? }
if: Proc.new { User.current.admin? },
enterprise_feature: 'attribute_help_texts'
menu.push :enumerations,
{ controller: '/enumerations' },
@ -388,7 +391,8 @@ Redmine::MenuManager.map :admin_menu do |menu|
{ controller: '/custom_styles', action: :show },
if: Proc.new { User.current.admin? },
caption: :label_custom_style,
icon: 'icon2 icon-design'
icon: 'icon2 icon-design',
enterprise_feature: 'define_custom_style'
menu.push :colors,
{ controller: '/colors', action: 'index' },

@ -89,7 +89,7 @@ en:
buttons:
upgrade: "Upgrade now"
contact: "Contact us for a demo"
enterprise_info_html: "is an Enterprise <strong class='icon-medal'></strong> feature."
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-badge'></span> feature."
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
journal_aggregation:
explanation:

@ -96,6 +96,7 @@ export class IanMenuComponent implements OnInit {
key: 'dateAlert',
title: this.I18n.t('js.notifications.menu.date_alert'),
icon: 'date-alert',
isEnterprise: true,
...getUiLinkForFilters({ filter: 'reason', name: 'dateAlert' }),
},
];

@ -8,7 +8,7 @@
<img [src]="image.enterprise_edition" class="hidden-for-mobile op-enterprise-banner--image">
<div class="op-toast--content">
<div class="op-enterprise-banner--header">
<span class="spot-icon spot-icon_medal"></span>
<span class="spot-icon spot-icon_enterprise-badge"></span>
<span [textContent]="text.enterpriseFeature" class="op-enterprise-banner--title"></span>
<button
*ngIf="collapsible"
@ -44,7 +44,7 @@
[href]="pricingUrl"
target=”_blank”
class="button -highlight">
<span class="spot-icon spot-icon_medal"></span>
<span class="spot-icon spot-icon_enterprise-badge"></span>
{{ text.upgrade }}
</a>

@ -70,6 +70,10 @@
class="op-sidemenu--item-icon"
[class]="'icon-' + item.icon"
></span>
<span class="op-sidemenu--item-title">{{ item.title }}</span>
<span
class="op-sidemenu--item-title"
>{{ item.title }}
<span class="op-sidemenu--enterprise-badge spot-icon spot-icon_enterprise-badge" *ngIf="noEEToken && item.isEnterprise"></span>
</span>
<span class="op-bubble op-bubble_alt_highlighting" *ngIf="item.count">{{ item.count }}</span>
</ng-template>

@ -53,6 +53,9 @@
line-height: 30px
text-decoration: none
&--enterprise-badge
margin-left: 0.25rem
&--item-icon
font-size: 24px
margin-right: 8px

@ -6,6 +6,7 @@ import {
HostBinding,
ElementRef,
} from '@angular/core';
import { BannersService } from 'core-app/core/enterprise/banners.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
export interface IOpSidemenuItem {
@ -18,6 +19,7 @@ export interface IOpSidemenuItem {
uiOptions?:unknown;
children?:IOpSidemenuItem[];
collapsible?:boolean;
isEnterprise?:boolean;
}
export const sidemenuSelector = 'op-sidemenu';
@ -41,10 +43,13 @@ export class OpSidemenuComponent {
public collapsed = false;
noEEToken = this.Banner.eeShowBanners;
constructor(
readonly elementRef:ElementRef,
readonly cdRef:ChangeDetectorRef,
readonly I18n:I18nService,
readonly Banner:BannersService,
) {
}

@ -170,6 +170,9 @@ module Redmine::MenuManager::MenuHelper
lang: menu_item_locale(item)) do
''.html_safe + caption + badge_for(item)
end
if item.enterprise_feature.present? && !EnterpriseToken.allows_to?(item.enterprise_feature)
link_text << (' '.html_safe + spot_icon('enterprise-badge'))
end
link_text << (' '.html_safe + op_icon(item.icon_after)) if item.icon_after.present?
html_options = item.html_options(selected:)
html_options[:title] ||= selected ? t(:description_current_position) + caption : caption

@ -37,7 +37,8 @@ class Redmine::MenuManager::MenuItem < Redmine::MenuManager::TreeNode
:child_menus,
:last,
:partial,
:engine
:engine,
:enterprise_feature
def initialize(name, url, options)
raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
@ -55,6 +56,7 @@ class Redmine::MenuManager::MenuItem < Redmine::MenuManager::TreeNode
@param = options[:param] || :project_id
@icon = options[:icon]
@icon_after = options[:icon_after]
@enterprise_feature = options[:enterprise_feature]
@caption = options[:caption]
@context = options[:context]
@html_options = options[:html].nil? ? {} : options[:html].dup

@ -43,7 +43,8 @@ module OpenProject::Boards
{ controller: '/boards/boards', action: :index },
caption: :'boards.label_boards',
after: :work_packages,
icon: 'icon2 icon-boards'
icon: 'icon2 icon-boards',
enterprise_feature: 'board_view'
menu :project_menu,
:board_menu,

@ -15,7 +15,8 @@ module OpenProject::LdapGroups
{ controller: '/ldap_groups/synchronized_groups', action: :index },
parent: :authentication,
last: true,
caption: ->(*) { I18n.t('ldap_groups.label_menu_item') }
caption: ->(*) { I18n.t('ldap_groups.label_menu_item') },
enterprise_feature: 'ldap_groups'
end
add_cron_jobs { LdapGroups::SynchronizationJob }

@ -55,7 +55,7 @@ module OpenIDConnect
private
def check_ee
if EnterpriseToken.show_banners?
unless EnterpriseToken.allows_to?(:openid_providers)
render template: '/openid_connect/providers/upsale'
false
end

@ -15,7 +15,8 @@ module OpenProject::OpenIDConnect
:plugin_openid_connect,
:openid_connect_providers_path,
parent: :authentication,
caption: ->(*) { I18n.t('openid_connect.menu_title') }
caption: ->(*) { I18n.t('openid_connect.menu_title') },
enterprise_feature: 'openid_providers'
end
assets %w(

@ -30,7 +30,6 @@ require 'spec_helper'
describe ::OpenIDConnect::ProvidersController, type: :controller do
let(:user) { build_stubbed :admin }
let(:ee) { true }
let(:valid_params) do
{
@ -42,129 +41,135 @@ describe ::OpenIDConnect::ProvidersController, type: :controller do
before do
login_as user
allow(EnterpriseToken).to receive(:show_banners?).and_return(!ee)
without_enterprise_token
end
context 'when not ee' do
let(:ee) { false }
context 'without an EE token' do
it 'renders upsale' do
get :index
expect(response.status).to eq 200
expect(response).to have_http_status(:ok)
expect(response).to render_template 'openid_connect/providers/upsale'
end
end
context 'when not admin' do
let(:user) { build_stubbed :user }
it 'renders 403' do
get :index
expect(response.status).to eq 403
context 'with an EE token' do
before do
login_as user
with_enterprise_token :openid_providers
end
end
context 'when not logged in' do
let(:user) { User.anonymous }
context 'when not admin' do
let(:user) { build_stubbed(:user) }
it 'renders 403' do
get :index
expect(response.status).to redirect_to(signin_url(back_url: openid_connect_providers_url))
it 'renders 403' do
get :index
expect(response).to have_http_status(:forbidden)
end
end
end
describe '#index' do
it 'renders the index page' do
get :index
expect(response).to be_successful
expect(response).to render_template 'index'
end
end
context 'when not logged in' do
let(:user) { User.anonymous }
describe '#new' do
it 'renders the new page' do
get :new
expect(response).to be_successful
expect(assigns[:provider]).to be_new_record
expect(response).to render_template 'new'
it 'renders 403' do
get :index
expect(response.status).to redirect_to(signin_url(back_url: openid_connect_providers_url))
end
end
it 'redirects to the index page if no provider available', with_settings: {
plugin_openproject_openid_connect: {
"providers" => OpenIDConnect::Provider::ALLOWED_TYPES.inject({}) do |accu, name|
accu.merge(name => { "identifier" => "IDENTIFIER", "secret" => "SECRET" })
end
}
} do
get :new
expect(response).to be_redirect
describe '#index' do
it 'renders the index page' do
get :index
expect(response).to be_successful
expect(response).to render_template 'index'
end
end
end
describe '#create' do
it 'is successful if valid params' do
post :create, params: { openid_connect_provider: valid_params }
expect(flash[:notice]).to eq(I18n.t(:notice_successful_create))
expect(Setting.plugin_openproject_openid_connect["providers"]).to have_key("azure")
expect(response).to be_redirect
end
describe '#new' do
it 'renders the new page' do
get :new
expect(response).to be_successful
expect(assigns[:provider]).to be_new_record
expect(response).to render_template 'new'
end
it 'renders an error if invalid params' do
post :create, params: { openid_connect_provider: valid_params.merge(identifier: "") }
expect(response).to render_template 'new'
it 'redirects to the index page if no provider available', with_settings: {
plugin_openproject_openid_connect: {
"providers" => OpenIDConnect::Provider::ALLOWED_TYPES.inject({}) do |accu, name|
accu.merge(name => { "identifier" => "IDENTIFIER", "secret" => "SECRET" })
end
}
} do
get :new
expect(response).to be_redirect
end
end
end
describe '#edit' do
context 'when found', with_settings: {
plugin_openproject_openid_connect: {
"providers" => { "azure" => { "identifier" => "IDENTIFIER", "secret" => "SECRET" } }
}
} do
it 'renders the edit page' do
get :edit, params: { id: 'azure' }
expect(response).to be_successful
expect(assigns[:provider]).to be_present
expect(response).to render_template 'edit'
describe '#create' do
it 'is successful if valid params' do
post :create, params: { openid_connect_provider: valid_params }
expect(flash[:notice]).to eq(I18n.t(:notice_successful_create))
expect(Setting.plugin_openproject_openid_connect["providers"]).to have_key("azure")
expect(response).to be_redirect
end
it 'renders an error if invalid params' do
post :create, params: { openid_connect_provider: valid_params.merge(identifier: "") }
expect(response).to render_template 'new'
end
end
context 'when not found' do
it 'renders 404' do
get :edit, params: { id: 'doesnoexist' }
expect(response).not_to be_successful
expect(response.status).to eq 404
describe '#edit' do
context 'when found', with_settings: {
plugin_openproject_openid_connect: {
"providers" => { "azure" => { "identifier" => "IDENTIFIER", "secret" => "SECRET" } }
}
} do
it 'renders the edit page' do
get :edit, params: { id: 'azure' }
expect(response).to be_successful
expect(assigns[:provider]).to be_present
expect(response).to render_template 'edit'
end
end
context 'when not found' do
it 'renders 404' do
get :edit, params: { id: 'doesnoexist' }
expect(response).not_to be_successful
expect(response).to have_http_status(:not_found)
end
end
end
end
describe '#update' do
context 'when found', with_settings: {
plugin_openproject_openid_connect: {
"providers" => { "azure" => { "identifier" => "IDENTIFIER", "secret" => "SECRET" } }
}
} do
it 'successfully updates the provider configuration' do
put :update, params: { id: "azure", openid_connect_provider: valid_params.merge(secret: "NEWSECRET") }
expect(response).to be_redirect
expect(flash[:notice]).to be_present
provider = OpenProject::OpenIDConnect.providers.find { |item| item.name == "azure" }
expect(provider.secret).to eq("NEWSECRET")
describe '#update' do
context 'when found', with_settings: {
plugin_openproject_openid_connect: {
"providers" => { "azure" => { "identifier" => "IDENTIFIER", "secret" => "SECRET" } }
}
} do
it 'successfully updates the provider configuration' do
put :update, params: { id: "azure", openid_connect_provider: valid_params.merge(secret: "NEWSECRET") }
expect(response).to be_redirect
expect(flash[:notice]).to be_present
provider = OpenProject::OpenIDConnect.providers.find { |item| item.name == "azure" }
expect(provider.secret).to eq("NEWSECRET")
end
end
end
end
describe '#destroy' do
context 'when found', with_settings: {
plugin_openproject_openid_connect: {
"providers" => { "azure" => { "identifier" => "IDENTIFIER", "secret" => "SECRET" } }
}
} do
it 'removes the provider' do
delete :destroy, params: { id: "azure" }
expect(response).to be_redirect
expect(flash[:notice]).to be_present
expect(OpenProject::OpenIDConnect.providers).to be_empty
describe '#destroy' do
context 'when found', with_settings: {
plugin_openproject_openid_connect: {
"providers" => { "azure" => { "identifier" => "IDENTIFIER", "secret" => "SECRET" } }
}
} do
it 'removes the provider' do
delete :destroy, params: { id: "azure" }
expect(response).to be_redirect
expect(flash[:notice]).to be_present
expect(OpenProject::OpenIDConnect.providers).to be_empty
end
end
end
end

@ -48,7 +48,7 @@ describe 'OpenID Connect',
end
before do
allow(EnterpriseToken).to receive(:show_banners?).and_return(false)
with_enterprise_token :openid_providers
# The redirect will include an authorisation code.
# Since we don't actually get a valid code in the test we will stub the resulting AccessToken.
@ -149,7 +149,7 @@ describe 'OpenID Connect',
end
it 'will show no option unless EE' do
allow(EnterpriseToken).to receive(:show_banners?).and_return(true)
without_enterprise_token
get '/login'
expect(response.body).not_to match /Google/i
expect(response.body).not_to match /Azure/i

@ -43,7 +43,8 @@ module OpenProject::TeamPlanner
{ controller: '/team_planner/team_planner', action: :index },
caption: :'team_planner.label_team_planner_plural',
after: :work_packages,
icon: 'icon2 icon-team-planner'
icon: 'icon2 icon-team-planner',
enterprise_feature: 'team_planner_view'
menu :project_menu,
:team_planner_menu,

@ -34,7 +34,8 @@ module OpenProject::TwoFactorAuthentication
{ controller: '/two_factor_authentication/two_factor_settings', action: :show },
caption: ->(*) { I18n.t('two_factor_authentication.label_two_factor_authentication') },
parent: :authentication,
if: ->(*) { ::OpenProject::TwoFactorAuthentication::TokenStrategyManager.configurable_by_ui? }
if: ->(*) { ::OpenProject::TwoFactorAuthentication::TokenStrategyManager.configurable_by_ui? },
enterprise_feature: 'two_factor_authentication'
end
patches %i[User]

Loading…
Cancel
Save