Replace show_banners with hide_banners

Encode features in the configuration API, instead of relying on a boolean yes/no flag
This commit is contained in:
Oliver Günther
2025-03-26 16:31:57 +01:00
parent 681bd56afb
commit edc0a2a47e
47 changed files with 217 additions and 206 deletions
@@ -46,7 +46,7 @@ module EnterpriseEdition
description: nil,
link_title: nil,
href: nil,
skip_render: !EnterpriseToken.show_banners?(feature: feature_key),
skip_render: EnterpriseToken.hide_banners?,
**system_arguments)
@system_arguments = system_arguments
@system_arguments[:test_selector] = "op-enterprise-banner-#{feature_key.to_s.tr('_', '-')}"
@@ -28,7 +28,7 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%=
render(EnterpriseEdition::BannerComponent.new(:automatic_subject_generation, mb: 3))
render(EnterpriseEdition::BannerComponent.new(:work_package_subject_generation, mb: 3))
%>
<%=
+3 -1
View File
@@ -242,7 +242,9 @@ module ApplicationHelper
css << ("action-#{action_name}")
end
css << "ee-banners-#{EnterpriseToken.show_banners? ? 'visible' : 'hidden'}"
if EnterpriseToken.hide_banners?
css << "ee-banners-hidden"
end
css << "env-#{Rails.env}"
+1 -1
View File
@@ -54,7 +54,7 @@ module HomescreenHelper
##
# Determine whether we should render the links on homescreen?
def show_homescreen_links?
EnterpriseToken.show_banners? || OpenProject::Configuration.show_community_links?
OpenProject::Configuration.show_community_links?
end
##
+3 -6
View File
@@ -45,12 +45,8 @@ class EnterpriseToken < ApplicationRecord
current && !current.expired?
end
def show_banners?(feature: nil)
if feature
OpenProject::Configuration.ee_manager_visible? && (!active? || !allows_to?(feature))
else
OpenProject::Configuration.ee_manager_visible? && !active?
end
def hide_banners?
!OpenProject::Configuration.ee_manager_visible?
end
def banner_type_for(feature:)
@@ -88,6 +84,7 @@ class EnterpriseToken < ApplicationRecord
:reprieve_days,
:reprieve_days_left,
:restrictions,
:available_features,
:plan,
:features,
:version,
@@ -33,7 +33,8 @@ See COPYRIGHT and LICENSE files for more details.
<div class="form--field">
<label class="form--label" for="project_enabled_module_names_<%= name %>">
<%= l_or_humanize(name, prefix: "project_module_") %>
<% if EnterpriseToken.show_banners? && OpenProject::AccessControl.module_enterprise_feature?(name) %>
<% feature = OpenProject::AccessControl.module_enterprise_feature?(name) %>
<% if feature && !EnterpriseToken.allows_to?(feature) %>
<%= spot_icon(
"enterprise-addons",
inline: true,
@@ -37,7 +37,7 @@ See COPYRIGHT and LICENSE files for more details.
render(EnterpriseEdition::BannerComponent.new(:form_configuration, mb: 3))
%>
<% if EnterpriseToken.show_banners? %>
<% unless EnterpriseToken.allows_to?(:form_configuration) %>
<%= angular_component_tag "opce-no-results",
inputs: {
title: t("text_form_configuration") + t("text_custom_field_hint_activate_per_project")
+2 -2
View File
@@ -56,7 +56,7 @@ OpenProject::Static::Homescreen.manage :blocks do |blocks|
},
{
partial: "community",
if: Proc.new { EnterpriseToken.show_banners? || OpenProject::Configuration.show_community_links? }
if: Proc.new { OpenProject::Configuration.show_community_links? }
},
{
partial: "administration",
@@ -64,7 +64,7 @@ OpenProject::Static::Homescreen.manage :blocks do |blocks|
},
{
partial: "upsale",
if: Proc.new { EnterpriseToken.show_banners? }
if: Proc.new { !EnterpriseToken.hide_banners? }
}
)
end
+1 -1
View File
@@ -2044,7 +2044,7 @@ en:
plan_name: "%{plan} enterprise plan"
plan_text_html: "Available only through the %{plan_name}."
link_title: "More information"
automatic_subject_generation:
work_package_subject_generation:
description: "Create automatically generated subjects using referenced attributes and text."
customize_life_cycle:
description: "Create and organize different project phases than the ones provided by PM2 project cycle planning."
+1 -1
View File
@@ -131,7 +131,7 @@ pricing:
progress_tracking_docs:
href: https://www.openproject.org/docs/user-guide/time-and-costs/progress-tracking/
enterprise_docs:
automatic_subject_generation:
work_package_subject_generation:
href: https://www.openproject.org/docs/system-admin-guide/manage-work-packages/work-package-types/#work-package-subject-configuration-enterprise-add-on
form_configuration:
href: https://www.openproject.org/docs/system-admin-guide/manage-work-packages/work-package-types/#work-package-form-configuration-enterprise-add-on
@@ -146,6 +146,10 @@ export class ConfigurationService {
return this.systemPreference<string[]>('activeFeatureFlags');
}
public get availableFeatures():string[] {
return this.systemPreference<string[]>('availableFeatures');
}
private loadConfiguration() {
return this
.apiV3Service
@@ -29,17 +29,21 @@
import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { enterpriseEditionUrl } from 'core-app/core/setup/globals/constants.const';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
@Injectable({ providedIn: 'root' })
export class BannersService {
private readonly _banners:boolean = true;
private readonly _bannersHidden:boolean = true;
constructor(@Inject(DOCUMENT) protected documentElement:Document) {
this._banners = documentElement.body.classList.contains('ee-banners-visible');
constructor(
@Inject(DOCUMENT) protected documentElement:Document,
protected configuration:ConfigurationService,
) {
this._bannersHidden = documentElement.body.classList.contains('ee-banners-hidden');
}
public get eeShowBanners():boolean {
return this._banners;
public showBannerFor(feature:string):boolean {
return !(this._bannersHidden || this.configuration.availableFeatures.includes(feature));
}
public getEnterPriseEditionUrl({ referrer, hash }:{ referrer?:string, hash?:string } = {}) {
@@ -55,11 +59,15 @@ export class BannersService {
return url.toString();
}
public conditional(bannersVisible?:() => void, bannersNotVisible?:() => void) {
this._banners ? this.callMaybe(bannersVisible) : this.callMaybe(bannersNotVisible);
public conditional(feature:string, bannersVisible?:() => void, bannersNotVisible?:() => void) {
if (this.showBannerFor(feature)) {
this.callMaybe(bannersVisible);
} else {
this.callMaybe(bannersNotVisible);
}
}
private callMaybe(func?:Function) {
private callMaybe(func?:() => unknown) {
func && func();
}
}
@@ -68,6 +68,7 @@ export class GroupEditInPlaceComponent implements OnInit {
startEditing():void {
this.bannerService.conditional(
'work_package_query_relation_columns',
() => this.bannerService.showEEOnlyHint(),
() => {
this.editing = true;
@@ -3,13 +3,19 @@ import { BannersService } from 'core-app/core/enterprise/banners.service';
import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { ConfirmDialogService } from 'core-app/shared/components/modals/confirm-dialog/confirm-dialog.service';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
@Injectable()
export class TypeBannerService extends BannersService {
constructor(@Inject(DOCUMENT) protected documentElement:Document,
private confirmDialog:ConfirmDialogService,
private I18n:I18nService) {
super(documentElement);
showBanners = this.showBannerFor('edit_attribute_groups');
constructor(
@Inject(DOCUMENT) protected documentElement:Document,
protected confirmDialog:ConfirmDialogService,
protected I18n:I18nService,
protected configuration:ConfigurationService,
) {
super(documentElement, configuration);
}
showEEOnlyHint():void {
@@ -23,6 +29,7 @@ export class TypeBannerService extends BannersService {
}).then(() => {
window.location.href = 'https://www.openproject.org/enterprise-edition/?utm_source=unknown&utm_medium=community-edition&utm_campaign=form-configuration';
})
.catch(() => {});
.catch(() => {
});
}
}
@@ -182,6 +182,7 @@ export class TypeFormConfigurationComponent extends UntilDestroyedMixin implemen
editQuery(group:TypeGroup):void {
this.typeBanner.conditional(
'edit_attribute_groups',
() => this.typeBanner.showEEOnlyHint(),
() => {
// Disable display mode and timeline for now since we don't want users to enable it
@@ -200,7 +201,8 @@ export class TypeFormConfigurationComponent extends UntilDestroyedMixin implemen
}
deleteGroup(group:TypeGroup):void {
this.typeBanner.conditional(
void this.typeBanner.conditional(
'edit_attribute_groups',
() => this.typeBanner.showEEOnlyHint(),
() => {
if (group.type === 'attribute') {
@@ -10,7 +10,7 @@
</button>
</li>
<li
*ngIf="!typeBanner.eeShowBanners"
*ngIf="!typeBanner.showBanners"
class="toolbar-item drop-down">
<a class="form-configuration--add-group button -primary" aria-haspopup="true">
<op-icon icon-classes="button--icon icon-add"></op-icon>
@@ -30,7 +30,7 @@ export class BoardActionsRegistryService {
icon: '',
description: '',
image: '',
disabled: this.bannersService.eeShowBanners,
disabled: this.bannersService.showBannerFor('board_view'),
}));
}
@@ -91,7 +91,7 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements
showHiddenListWarning:boolean = false;
needEnterpriseEdition = this.Banner.eeShowBanners;
needEnterpriseEdition = this.Banner.showBannerFor('board_view');
private currentQueryUpdatedMonitoring:Subscription;
@@ -129,7 +129,7 @@ readonly I18n:I18nService,
);
this.board$.subscribe((board) => {
this.needEnterpriseEdition = this.Banner.eeShowBanners && !board.isFree;
this.needEnterpriseEdition = this.Banner.showBannerFor('board_view') && !board.isFree;
});
this.Boards.currentBoard$.next(id);
@@ -119,7 +119,7 @@ export class ProjectSelectionComponent implements OnInit {
}
private setPlaceholderOption():void {
if (this.bannersService.eeShowBanners) {
if (this.bannersService.showBannerFor('placeholder_users')) {
this.typeOptions.push({
value: PrincipalType.Placeholder,
title: this.I18n.t('js.invite_user_modal.type.placeholder.title_no_ee'),
@@ -140,7 +140,7 @@ export class NotificationsSettingsPageComponent extends UntilDestroyedMixin impl
ngOnInit():void {
this.form.disable();
this.eeShowBanners = this.bannersService.eeShowBanners;
this.eeShowBanners = this.bannersService.showBannerFor('date_alerts');
this
.currentUserService
@@ -81,7 +81,7 @@ export class NotificationSettingsTableComponent implements OnInit {
) {}
ngOnInit():void {
this.eeShowBanners = this.bannersService.eeShowBanners;
this.eeShowBanners = this.bannersService.showBannerFor('date_alerts');
}
projectLink(href:string) {
@@ -93,7 +93,6 @@ export class QueryFilterComponent implements OnInit {
readonly schemaCache:SchemaCacheService,
readonly I18n:I18nService,
readonly currentProject:CurrentProjectService,
readonly bannerService:BannersService,
) {
}
@@ -116,7 +115,6 @@ export class QueryFilterComponent implements OnInit {
}
ngOnInit() {
this.eeShowBanners = this.bannerService.eeShowBanners;
this.availableOperators = this.schemaCache.of(this.filter).availableOperators;
this.showValuesInput = this.showValues();
this.baselineIncompatibleFilter = this.wpTableBaseline.isActive() && this.wpTableBaseline.isIncompatibleFilter(this.filter.id);
@@ -98,7 +98,7 @@ export class OpBaselineComponent extends UntilDestroyedMixin implements OnInit {
public tooltipPosition = SpotDropAlignmentOption.TopRight;
eeShowBanners = this.Banner.eeShowBanners;
eeShowBanners = this.Banner.showBannerFor('baseline_comparison');
public text = {
toggle_title: this.I18n.t('js.baseline.toggle_title'),
@@ -52,7 +52,7 @@ import { CollectionResource } from 'core-app/features/hal/resources/collection-r
export class WorkPackageShareButtonComponent extends UntilDestroyedMixin implements OnInit {
@Input() public workPackage:WorkPackageResource;
showEnterpriseIcon = this.bannersService.eeShowBanners;
showEnterpriseIcon = this.bannersService.showBannerFor('work_package_sharing');
shareCount$:Observable<number>;
@@ -1,5 +1,5 @@
<ng-container
*ngIf="!this.bannersService.eeShowBanners"
*ngIf="showBanners"
>
<wp-custom-action
*ngFor="let action of actions; trackBy: trackByHref"
@@ -48,6 +48,8 @@ export class WpCustomActionsComponent extends UntilDestroyedMixin implements OnI
actions:CustomActionResource[] = [];
showBanners = this.bannersService.showBannerFor('custom_actions');
constructor(
readonly apiV3Service:ApiV3Service,
readonly cdRef:ChangeDetectorRef,
@@ -51,7 +51,7 @@ export class WpTableConfigurationColumnsTabComponent implements TabComponent, On
}
ngOnInit() {
this.eeShowBanners = this.bannerService.eeShowBanners;
this.eeShowBanners = this.bannerService.showBannerFor('work_package_query_relation_columns');
this.selectedColumns.forEach((c:DraggableOption) => {
this.selectedColumnMap[c.id] = true;
});
@@ -1,16 +1,23 @@
import { Component, Injector } from '@angular/core';
import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { TabComponent } from 'core-app/features/work-packages/components/wp-table/configuration-modal/tab-portal-outlet';
import { WorkPackageFiltersService } from 'core-app/features/work-packages/components/filters/wp-filters/wp-filters.service';
import { WorkPackageViewFiltersService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service';
import {
TabComponent,
} from 'core-app/features/work-packages/components/wp-table/configuration-modal/tab-portal-outlet';
import {
WorkPackageFiltersService,
} from 'core-app/features/work-packages/components/filters/wp-filters/wp-filters.service';
import {
WorkPackageViewFiltersService,
} from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service';
import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource';
import { BannersService } from 'core-app/core/enterprise/banners.service';
@Component({
templateUrl: './filters-tab.component.html',
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'wp-table-config-filters-tab',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WpTableConfigurationFiltersTab implements TabComponent {
export class WpTableConfigurationFiltersTabComponent implements TabComponent, OnInit {
public filters:QueryFilterInstanceResource[] = [];
public eeShowBanners = false;
@@ -24,15 +31,15 @@ export class WpTableConfigurationFiltersTab implements TabComponent {
upsaleRelationColumnsLink: this.I18n.t('js.modals.upsale_relation_columns_link'),
};
constructor(readonly injector:Injector,
constructor(
readonly injector:Injector,
readonly I18n:I18nService,
readonly wpTableFilters:WorkPackageViewFiltersService,
readonly wpFiltersService:WorkPackageFiltersService,
readonly bannerService:BannersService) {
) {
}
ngOnInit() {
this.eeShowBanners = this.bannerService.eeShowBanners;
this.wpTableFilters
.onReady()
.then(() => this.filters = this.wpTableFilters.current);
@@ -74,7 +74,7 @@ export class WpTableConfigurationHighlightingTabComponent implements TabComponen
this.setSelectedValues();
this.eeShowBanners = this.Banners.eeShowBanners;
this.eeShowBanners = this.Banners.showBannerFor('conditional_highlighting');
this.updateMode(this.wpTableHighlight.current.mode);
if (this.eeShowBanners) {
@@ -3,7 +3,7 @@ import { I18nService } from 'core-app/core/i18n/i18n.service';
import { WpTableConfigurationDisplaySettingsTabComponent } from 'core-app/features/work-packages/components/wp-table/configuration-modal/tabs/display-settings-tab.component';
import { TabInterface } from 'core-app/features/work-packages/components/wp-table/configuration-modal/tab-portal-outlet';
import { WpTableConfigurationColumnsTabComponent } from 'core-app/features/work-packages/components/wp-table/configuration-modal/tabs/columns-tab.component';
import { WpTableConfigurationFiltersTab } from 'core-app/features/work-packages/components/wp-table/configuration-modal/tabs/filters-tab.component';
import { WpTableConfigurationFiltersTabComponent } from 'core-app/features/work-packages/components/wp-table/configuration-modal/tabs/filters-tab.component';
import { WpTableConfigurationSortByTabComponent } from 'core-app/features/work-packages/components/wp-table/configuration-modal/tabs/sort-by-tab.component';
import { WpTableConfigurationTimelinesTabComponent } from 'core-app/features/work-packages/components/wp-table/configuration-modal/tabs/timelines-tab.component';
import { WpTableConfigurationHighlightingTabComponent } from 'core-app/features/work-packages/components/wp-table/configuration-modal/tabs/highlighting-tab.component';
@@ -21,7 +21,7 @@ export class WpTableConfigurationService {
{
id: 'filters',
name: this.I18n.t('js.work_packages.query.filters'),
componentClass: WpTableConfigurationFiltersTab,
componentClass: WpTableConfigurationFiltersTabComponent,
},
{
id: 'sort-by',
@@ -138,7 +138,7 @@ import {
WpTableConfigurationDisplaySettingsTabComponent,
} from 'core-app/features/work-packages/components/wp-table/configuration-modal/tabs/display-settings-tab.component';
import {
WpTableConfigurationFiltersTab,
WpTableConfigurationFiltersTabComponent,
} from 'core-app/features/work-packages/components/wp-table/configuration-modal/tabs/filters-tab.component';
import {
WpTableConfigurationSortByTabComponent,
@@ -604,7 +604,7 @@ import { OpWpDatePickerModalComponent } from 'core-app/shared/components/datepic
WpTableConfigurationModalComponent,
WpTableConfigurationColumnsTabComponent,
WpTableConfigurationDisplaySettingsTabComponent,
WpTableConfigurationFiltersTab,
WpTableConfigurationFiltersTabComponent,
WpTableConfigurationSortByTabComponent,
WpTableConfigurationTimelinesTabComponent,
WpTableConfigurationHighlightingTabComponent,
@@ -661,7 +661,7 @@ import { OpWpDatePickerModalComponent } from 'core-app/shared/components/datepic
// Modals
WpTableConfigurationModalComponent,
WpTableConfigurationFiltersTab,
WpTableConfigurationFiltersTabComponent,
// Needed so that e.g. IFC can access it.
WorkPackageCreateButtonComponent,
@@ -6,14 +6,18 @@ import { BannersService } from 'core-app/core/enterprise/banners.service';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { WorkPackageCollectionResource } from 'core-app/features/hal/resources/wp-collection-resource';
import { QuerySchemaResource } from 'core-app/features/hal/resources/query-schema-resource';
import { WorkPackageViewHighlight } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-table-highlight';
import {
WorkPackageViewHighlight,
} from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-table-highlight';
import { WorkPackageQueryStateService } from './wp-view-base.service';
@Injectable()
export class WorkPackageViewHighlightingService extends WorkPackageQueryStateService<WorkPackageViewHighlight> {
public constructor(readonly states:States,
public constructor(
readonly states:States,
readonly Banners:BannersService,
readonly querySpace:IsolatedQuerySpace) {
readonly querySpace:IsolatedQuerySpace,
) {
super(querySpace);
}
@@ -28,7 +32,7 @@ export class WorkPackageViewHighlightingService extends WorkPackageQueryStateSer
*/
public shouldHighlightInline(name:string):boolean {
// 1. Are we in inline mode or unable to render?
if (!this.isInline || this.Banners.eeShowBanners) {
if (!this.isInline || this.Banners.showBannerFor('conditional_highlighting')) {
return false;
}
@@ -82,10 +86,13 @@ export class WorkPackageViewHighlightingService extends WorkPackageQueryStateSer
value.selectedAttributes = undefined;
}
this.Banners.conditional(() => {
value.mode = 'none';
value.selectedAttributes = undefined;
});
void this.Banners.conditional(
'conditional_highlighting',
() => {
value.mode = 'none';
value.selectedAttributes = undefined;
},
);
return value;
}
@@ -49,7 +49,7 @@ export class AddGridWidgetModalComponent extends OpModalComponent implements OnI
ngOnInit() {
super.ngOnInit();
this.eeShowBanners = this.bannerService.eeShowBanners;
this.eeShowBanners = this.bannerService.showBannerFor('grid_widget_wp_graph');
this.fetchSchema();
}
@@ -98,6 +98,11 @@ module API
.map { |flag| flag.camelize(:lower) }
}
property :available_features,
getter: ->(*) {
EnterpriseToken.current&.available_features || []
}
property :allowed_link_protocols,
getter: ->(*) { Setting::AllowedLinkProtocols.all }
@@ -1,3 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -82,13 +84,13 @@ module Redmine::MenuManager::TopMenu::HelpMenu
controller.render_to_string(partial: "onboarding/menu_item")
end
def render_help_and_support(result)
def render_help_and_support(result) # rubocop:disable Metrics/AbcSize
result << content_tag(:li, class: "op-menu--item") do
content_tag :span, I18n.t("top_menu.help_and_support"),
class: "op-menu--headline",
title: I18n.t("top_menu.help_and_support")
end
if EnterpriseToken.show_banners?
unless EnterpriseToken.hide_banners? && EnterpriseToken.active?
result << static_link_item(:upsale,
href_suffix: "/?utm_source=unknown&utm_medium=op-instance&utm_campaign=ee-upsale-help-menu")
end
@@ -111,7 +113,7 @@ module Redmine::MenuManager::TopMenu::HelpMenu
result << content_tag(:hr, "", class: "op-menu--separator")
end
def render_additional_resources(result)
def render_additional_resources(result) # rubocop:disable Metrics/AbcSize
result << content_tag(:li, class: "op-menu--item") do
content_tag :span,
I18n.t("top_menu.additional_resources"),
@@ -51,9 +51,6 @@ module OpenProject
# ```
#
# The href is inferred from `OpenProject::Static::Links.enterprise_docs[feature_key][:href]`.
#
# The value of `EnterpriseToken.show_banners?` is used to determine whether the banner should be shown. For this
# example, that value is overwritten as the banner might otherwise not show up in the preview.
def default
render(
::EnterpriseEdition::BannerComponent
@@ -81,7 +81,7 @@ module OpenProject::Plugins
def self.filtered_strategy?(_strategy_key, provider)
name = provider[:name]&.to_s
!EnterpriseToken.show_banners? || name == "developer"
EnterpriseToken.allows_to?(:sso_auth_providers) || name == "developer"
end
def self.strategy_key(strategy)
@@ -59,7 +59,6 @@ RSpec.describe "Switching work package view",
before do
wp_1
wp_2
allow(EnterpriseToken).to receive(:show_banners?).and_return(false)
login_as(user)
wp_page.visit!
@@ -36,7 +36,9 @@ module OpenProject::TeamPlanner
author_url: "https://www.openproject.org",
bundled: true,
settings: {} do
project_module :team_planner_view, dependencies: :work_package_tracking, enterprise_feature: true do
project_module :team_planner_view,
dependencies: :work_package_tracking,
enterprise_feature: "team_planner_view" do
permission :view_team_planner,
{ "team_planner/team_planner": %i[index show upsale overview],
"team_planner/menus": %i[show] },
@@ -67,9 +67,6 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do
end
before do
allow(EnterpriseToken)
.to receive(:show_banners?)
.and_return(ee_show_banners)
allow(OpenProject::Static::Links)
.to receive(:links)
.and_return(static_links)
@@ -175,24 +172,4 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do
expect { render_component_in_mo }.to raise_error(RuntimeError)
end
end
context "if banners are hidden" do
let(:ee_show_banners) { false }
it "hides the component" do
render_component_in_mo
expect(page).to have_no_css ".op-ee-banner"
end
end
context "if banners are hidden but skip_render is overwritten" do
let(:ee_show_banners) { false }
let(:render_component) do
render_inline(described_class.new(:some_enterprise_feature,
skip_render: false))
end
it_behaves_like "renders the component"
end
end
@@ -68,7 +68,6 @@ RSpec.describe "Help menu items", :js do
include_context "support links"
def fake_an_enterprise_token
allow(EnterpriseToken).to receive(:show_banners?).and_return(false)
allow(EnterpriseToken).to receive(:active?).and_return(true)
end
@@ -46,7 +46,6 @@ RSpec.describe "Switching work package view on mobile", :js, :selenium do
before do
wp_1
wp_2
allow(EnterpriseToken).to receive(:show_banners?).and_return(false)
login_as(user)
wp_table.visit!
@@ -53,7 +53,6 @@ RSpec.describe "Work Package highlighting fields",
# Ensure Rails and Capybara caches are cleared
Rails.cache.clear
Capybara.reset!
allow(EnterpriseToken).to receive(:show_banners?).and_return(false)
login_as(user)
wp_table.visit_query query
wp_table.expect_work_package_listed wp_1, wp_2
@@ -245,6 +245,24 @@ RSpec.describe API::V3::Configuration::ConfigurationRepresenter do
end
end
end
describe "availableFeatures" do
context "without any features" do
it "is an empty array" do
expect(subject)
.to be_json_eql([].to_json)
.at_path("availableFeatures")
end
end
context "with certain features allowed", with_ee: %i[some_value foobar] do
it "is an array of strings of those flags" do
expect(subject)
.to be_json_eql(%w(some_value foobar).to_json)
.at_path("availableFeatures")
end
end
end
end
describe "_embedded" do
+68 -98
View File
@@ -1,10 +1,40 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 COPYRIGHT and LICENSE files for more details.
#++
require "spec_helper"
RSpec.describe EnterpriseToken do
let(:object) { OpenProject::Token.new domain: Setting.host_name }
let(:ee_manager_visible) { true }
subject { EnterpriseToken.new(encoded_token: "foo") }
subject { described_class.new(encoded_token: "foo") }
before do
RequestStore.delete :current_ee_token
@@ -39,81 +69,20 @@ RSpec.describe EnterpriseToken do
end
end
describe ".show_banners?" do
before do
allow(described_class).to receive(:allows_to?).with(:active_feature).and_return(true)
allow(described_class).to receive(:allows_to?).with(:inactive_feature).and_return(false)
end
describe ".hide_banners?" do
context "when ee manager is visible" do
let(:ee_manager_visible) { true }
context "when token is active" do
before { allow(described_class).to receive(:active?).and_return(true) }
it "returns false when requesting without a feature" do
expect(described_class).not_to be_show_banners
end
it "returns false when requesting with a feature that is enabled in the token" do
expect(described_class).not_to be_show_banners(feature: :active_feature)
end
it "returns true when requesting with a feature that is disabled in the token" do
expect(described_class).to be_show_banners(feature: :inactive_feature)
end
end
context "when token is inactive" do
before { allow(described_class).to receive(:active?).and_return(false) }
it "returns true when requesting without a feature" do
expect(described_class).to be_show_banners
end
it "returns true when requesting with a feature that is enabled in the token" do
expect(described_class).to be_show_banners(feature: :active_feature)
end
it "returns true when requesting with a feature that is disabled in the token" do
expect(described_class).to be_show_banners(feature: :inactive_feature)
end
it "returns true" do
expect(described_class).to be_show_banners
end
end
context "when ee manager is not visible" do
let(:ee_manager_visible) { false }
context "when token is active" do
before { allow(described_class).to receive(:active?).and_return(true) }
it "returns false when requesting without a feature" do
expect(described_class).not_to be_show_banners
end
it "returns false when requesting with a feature that is enabled in the token" do
expect(described_class).not_to be_show_banners(feature: :active_feature)
end
it "returns false when requesting with a feature that is disabled in the token" do
expect(described_class).not_to be_show_banners(feature: :inactive_feature)
end
end
context "when token is inactive" do
before { allow(described_class).to receive(:active?).and_return(false) }
it "returns false when requesting without a feature" do
expect(described_class).not_to be_show_banners
end
it "returns false when requesting with a feature that is enabled in the token" do
expect(described_class).not_to be_show_banners(feature: :active_feature)
end
it "returns false when requesting with a feature that is disabled in the token" do
expect(described_class).not_to be_show_banners(feature: :inactive_feature)
end
it "returns false" do
expect(described_class).not_to be_show_banners
end
end
end
@@ -157,39 +126,38 @@ RSpec.describe EnterpriseToken do
describe "existing token" do
before do
allow_any_instance_of(EnterpriseToken).to receive(:token_object).and_return(object)
allow_any_instance_of(described_class).to receive(:token_object).and_return(object) # rubocop:disable RSpec/AnyInstance
subject.save!(validate: false)
end
context "when inner token is active" do
it "has an active token" do
expect(object).to receive(:expired?).and_return(false)
expect(EnterpriseToken.count).to eq(1)
expect(EnterpriseToken.current).to eq(subject)
expect(EnterpriseToken.current.encoded_token).to eq("foo")
expect(EnterpriseToken.show_banners?).to be(false)
allow(object).to receive(:expired?).and_return(false)
expect(described_class.count).to eq(1)
expect(described_class.current).to eq(subject)
expect(described_class.current.encoded_token).to eq("foo")
# Deleting it updates the current token
EnterpriseToken.current.destroy!
described_class.current.destroy!
expect(EnterpriseToken.count).to eq(0)
expect(EnterpriseToken.current).to be_nil
expect(described_class.count).to eq(0)
expect(described_class.current).to be_nil
end
it "delegates to the token object" do
allow(object).to receive_messages(
subscriber: "foo",
mail: "bar",
starts_at: Date.today,
issued_at: Date.today,
starts_at: Time.zone.today,
issued_at: Time.zone.today,
expires_at: "never",
restrictions: { foo: :bar }
)
expect(subject.subscriber).to eq("foo")
expect(subject.mail).to eq("bar")
expect(subject.starts_at).to eq(Date.today)
expect(subject.issued_at).to eq(Date.today)
expect(subject.starts_at).to eq(Time.zone.today)
expect(subject.issued_at).to eq(Time.zone.today)
expect(subject.expires_at).to eq("never")
expect(subject.restrictions).to eq(foo: :bar)
end
@@ -198,38 +166,40 @@ RSpec.describe EnterpriseToken do
let(:service_double) { Authorization::EnterpriseService.new(subject) }
before do
expect(Authorization::EnterpriseService)
.to receive(:new).twice.with(subject).and_return(service_double)
allow(Authorization::EnterpriseService)
.to receive(:new)
.with(subject)
.and_return(service_double)
end
it "forwards to EnterpriseTokenService for checks" do
expect(service_double)
allow(service_double)
.to receive(:call)
.with(:forbidden_action)
.and_return double("ServiceResult", result: false)
expect(service_double)
.and_return ServiceResult.success(result: true)
allow(service_double)
.to receive(:call)
.with(:allowed_action)
.and_return double("ServiceResult", result: true)
.and_return ServiceResult.success(result: true)
expect(EnterpriseToken.allows_to?(:forbidden_action)).to be false
expect(EnterpriseToken.allows_to?(:allowed_action)).to be true
expect(described_class.allows_to?(:forbidden_action)).to be false
expect(described_class.allows_to?(:allowed_action)).to be true
end
end
end
context "when inner token is expired" do
before do
expect(object).to receive(:expired?).and_return(true)
allow(object).to receive(:expired?).and_return(true)
end
it "has an expired token" do
expect(EnterpriseToken.current).to eq(subject)
expect(EnterpriseToken.show_banners?).to be(true)
expect(described_class.current).to eq(subject)
expect(described_class).not_to be_active
end
end
context "updating it with an invalid token" do
context "when updating it with an invalid token" do
it "fails validations" do
subject.encoded_token = "bar"
expect(subject.save).to be_falsey
@@ -239,22 +209,22 @@ RSpec.describe EnterpriseToken do
describe "no token" do
it do
expect(EnterpriseToken.current).to be_nil
expect(EnterpriseToken.show_banners?).to be(true)
expect(described_class.current).to be_nil
expect(described_class).not_to be_active
end
end
describe "invalid token" do
it "appears as if no token is shown" do
expect(EnterpriseToken.current).to be_nil
expect(EnterpriseToken.show_banners?).to be(true)
expect(described_class.current).to be_nil
expect(described_class).not_to be_active
end
end
describe "Configuration file has `ee_manager_visible` set to false" do
it "does not show banners promoting EE" do
expect(OpenProject::Configuration).to receive(:ee_manager_visible?).and_return(false)
expect(EnterpriseToken.show_banners?).to be_falsey
allow(OpenProject::Configuration).to receive(:ee_manager_visible?).and_return(false)
expect(described_class).to be_hide_banners
end
end
end
@@ -27,7 +27,7 @@
require "spec_helper"
RSpec.describe Users::RegisterUserService do
RSpec.describe Users::RegisterUserService, with_ee: %i[sso_auth_providers] do
let(:user) { build(:user) }
let(:instance) { described_class.new(user) }
let(:call) { instance.call }
@@ -83,9 +83,6 @@ RSpec.describe Users::RegisterUserService do
before do
allow(user).to receive(:activate)
allow(user).to receive(:save).and_return true
# required so that the azure provider is visible (ee feature)
allow(EnterpriseToken).to receive(:show_banners?).and_return false
end
it "tries to activate that user regardless of settings" do
+13 -2
View File
@@ -1,3 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -48,13 +50,22 @@ RSpec.configure do |config|
if allowed.present?
allowed = aggregate_parent_array(example, allowed.to_set)
token_double = instance_double(EnterpriseToken)
token_object_double = instance_double(OpenProject::Token)
allow(EnterpriseToken).to receive(:allows_to?).and_call_original
allow(token_object_double).to receive(:has_feature?).and_return(false)
allowed.each do |enterprise_feature|
allow(EnterpriseToken).to receive(:allows_to?).with(enterprise_feature).and_return(true)
allow(token_object_double).to receive(:has_feature?).with(enterprise_feature).and_return(true)
end
# Also disable banners to signal the frontend we're on EE
allow(EnterpriseToken).to receive(:show_banners?).and_return(allowed.empty?)
# Also signal available features
allow(EnterpriseToken).to receive(:current).and_return(token_double)
allow(token_double)
.to receive_messages(token_object: token_object_double,
available_features: allowed.to_a,
expired?: false,
restrictions: {})
end
end
end