Merge pull request #19045 from opf/implementation/64263-add-trial-enterprise-banner

[#64263] Add trial enterprise banner
This commit is contained in:
Oliver Günther
2025-06-11 10:10:18 +02:00
committed by GitHub
39 changed files with 288 additions and 121 deletions
@@ -44,7 +44,7 @@ module EnterpriseEdition
VARIANT_OPTIONS = %i[inline medium large].freeze
# @param feature_key [Symbol, NilClass] The key of the feature to show the banner for.
# @param variant [Symbol, NilClass] The variant of the banner comopnent.
# @param variant [Symbol, NilClass] The variant of the banner component.
# @param image [String, NilClass] Path to the image to show on the banner, or nil.
# Only applicable and required when variant is :medium.
# @param video [String, NilClass] Path to the video to show on the banner, or nil.
@@ -55,7 +55,7 @@ module EnterpriseEdition
# @param show_always [boolean] Always show the banner, regardless of the dismissed or feature state.
# @param dismiss_key [String] Provide a string to identify this banner when being dismissed. Defaults to feature_key
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
def initialize(feature_key, # rubocop:disable Metrics/AbcSize
def initialize(feature_key,
variant: DEFAULT_VARIANT,
image: nil,
video: nil,
@@ -68,12 +68,15 @@ module EnterpriseEdition
@image = image
@video = video
@dismissable = dismissable
@dismiss_key = dismiss_key
@dismiss_key = dismiss_key.to_s
@show_always = show_always
self.feature_key = feature_key
self.i18n_scope = i18n_scope
trial_overrides! if trial_feature?
if @variant == :medium && @image.nil?
raise ArgumentError, "The 'image' parameter is required when the variant is :medium."
end
@@ -82,17 +85,7 @@ module EnterpriseEdition
raise ArgumentError, "The 'video' parameter is required when the variant is :large."
end
@system_arguments = system_arguments
@system_arguments[:tag] = :div
@system_arguments[:mb] ||= 2
@system_arguments[:id] = "op-enterprise-banner-#{feature_key.to_s.tr('_', '-')}"
@system_arguments[:test_selector] = "op-enterprise-banner"
@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"op-enterprise-banner",
@variant == :medium ? "op-enterprise-banner_medium" : nil,
@variant == :large ? "op-enterprise-banner_large" : nil
)
set_system_arguments(system_arguments, feature_key)
super
end
@@ -115,15 +108,39 @@ module EnterpriseEdition
end
def wrapper_key
"enterprise_banner_#{feature_key}"
"enterprise_banner_#{@dismiss_key}"
end
private
def set_system_arguments(system_arguments, feature_key)
@system_arguments = system_arguments
@system_arguments[:tag] = :div
@system_arguments[:mb] ||= 2
@system_arguments[:id] = "op-enterprise-banner-#{feature_key.to_s.tr('_', '-')}"
@system_arguments[:test_selector] = "op-enterprise-banner"
@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"op-enterprise-banner",
"op-enterprise-banner_medium" => @variant == :medium,
"op-enterprise-banner_large" => @variant == :large,
"op-enterprise-banner_trial" => trial_feature?
)
end
def trial_overrides!
@dismissable = true
@dismiss_key += "_trial" unless @dismiss_key.end_with?("_trial")
@variant = :inline
end
def render?
return true if @show_always
return false if dismissed?
return true if feature_available? && trial_feature?
return false if EnterpriseToken.hide_banners?
!(EnterpriseToken.hide_banners? || feature_available? || dismissed?)
!feature_available?
end
def feature_available?
@@ -135,5 +152,9 @@ module EnterpriseEdition
User.current.pref.dismissed_banner?(@dismiss_key)
end
def trial_feature?
EnterpriseToken.trialling?(feature_key)
end
end
end
@@ -58,6 +58,7 @@ module EnterpriseEdition
def buttons
[
buy_now_button,
free_trial_button,
upgrade_now_button,
more_info_button
@@ -74,6 +75,20 @@ module EnterpriseEdition
)
end
def buy_now_button
return unless EnterpriseToken.active?
return unless User.current.admin?
render(Primer::Beta::Button.new(
classes: "upsell-colored-background",
tag: :a,
href: OpenProject::Enterprise.upgrade_path,
align_self: :center
)) do
I18n.t("ee.upsell.buy_now_button")
end
end
# Allow providing a custom upgrade now button
def upgrade_now_button
nil
@@ -31,7 +31,14 @@ See COPYRIGHT and LICENSE files for more details.
component_wrapper do
flex_layout(data: wrapper_data_attributes) do |flex|
flex.with_row do
if allowed_to_customize_life_cycle?
render EnterpriseEdition::BannerComponent.new(:customize_life_cycle,
variant: :medium,
image: "enterprise/project-lifecycle.png",
mb: 3)
end
if allowed_to_customize_life_cycle?
flex.with_row do
render(Primer::OpenProject::SubHeader.new) do |subheader|
subheader.with_filter_input(
name: "border-box-filter",
@@ -57,11 +64,6 @@ See COPYRIGHT and LICENSE files for more details.
I18n.t("settings.project_phase_definitions.label_add")
end
end
else
render EnterpriseEdition::BannerComponent.new(:customize_life_cycle,
variant: :medium,
image: "enterprise/project-lifecycle.png",
mb: 3)
end
end
@@ -7,7 +7,8 @@
end
end
render(strategy.manage_shares_component(modal_content:, errors:))
render(strategy.upsell_banner(modal_content:))
render(strategy.manage_shares_component(modal_content:, errors:)) if strategy.allow_feature?
end
end
%>
@@ -1,5 +1,7 @@
<%=
component_wrapper(tag: "turbo-frame") do
render(EnterpriseEdition::BannerComponent.new(:work_package_sharing))
modal_content.with_row do
render(EnterpriseEdition::BannerComponent.new(:work_package_sharing))
end
end
%>
@@ -33,6 +33,14 @@ module Shares
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
def initialize(modal_content:)
super
@modal_content = modal_content
end
attr_reader :modal_content
def self.wrapper_key
"share_modal_body"
end
@@ -47,9 +47,13 @@ class My::EnterpriseBannersController < ApplicationController
def dismiss
pref = User.current.pref
pref.dismiss_banner(@feature_key)
pref.dismiss_banner(@dismiss_key)
if pref.save
remove_via_turbo_stream(component: EnterpriseEdition::BannerComponent.new(@feature_key))
remove_via_turbo_stream(component: EnterpriseEdition::BannerComponent.new(
@feature_key,
dismiss_key: @dismiss_key,
show_always: true
))
respond_with_turbo_streams
else
respond_with_flash_error(message: call.message)
@@ -59,7 +63,11 @@ class My::EnterpriseBannersController < ApplicationController
private
def get_feature_key
@feature_key = params[:feature_key].to_sym
raw_key = params[:feature_key]
@dismiss_key = raw_key
@feature_key = raw_key.gsub(/_trial$/, "").to_sym
render_400 unless OpenProject::Token.lowest_plan_for(@feature_key)
end
end
+2 -4
View File
@@ -86,10 +86,8 @@ module Statuses
}
)
if readonly_work_packages_restricted?
statuses_form.html_content do
render(EnterpriseEdition::BannerComponent.new(:readonly_work_packages))
end
statuses_form.html_content do
render(EnterpriseEdition::BannerComponent.new(:readonly_work_packages))
end
statuses_form.check_box(
+13
View File
@@ -45,6 +45,18 @@ class EnterpriseToken < ApplicationRecord
current && !current.expired?
end
def available_features
EnterpriseToken.current&.available_features || []
end
def trialling_features
available_features.select { |feature| trialling?(feature) }
end
def trialling?(feature)
allows_to?(feature) && EnterpriseToken.current.trial?
end
def hide_banners?
OpenProject::Configuration.ee_hide_banners?
end
@@ -88,6 +100,7 @@ class EnterpriseToken < ApplicationRecord
:plan,
:features,
:version,
:trial?,
to: :token_object
def token_object
@@ -105,18 +105,19 @@ module SharingStrategies
end
end
def manage_shares_component(modal_content:, errors:)
if EnterpriseToken.allows_to?(:project_list_sharing)
super
else
Shares::ProjectQueries::UpsellComponent.new(modal_content:)
end
end
def title
I18n.t(:label_share_project_list)
end
def upsell_banner(modal_content:)
Shares::ProjectQueries::UpsellComponent.new(modal_content:)
end
def allow_feature?
EnterpriseToken.allows_to?(:project_list_sharing) ||
EnterpriseToken.trialling?(:project_list_sharing)
end
private
def virtual_owner_share
@@ -99,18 +99,19 @@ module SharingStrategies
Shares::WorkPackages::DeleteContract
end
def modal_body_component(errors)
if EnterpriseToken.allows_to?(:work_package_sharing)
super
else
Shares::WorkPackages::ModalUpsellComponent.new
end
end
def title
I18n.t(:label_share_work_package)
end
def upsell_banner(modal_content:)
Shares::WorkPackages::ModalUpsellComponent.new(modal_content:)
end
def allow_feature?
EnterpriseToken.allows_to?(:work_package_sharing) ||
EnterpriseToken.trialling?(:work_package_sharing)
end
private
def project_member?(share)
+1 -1
View File
@@ -46,7 +46,7 @@ See COPYRIGHT and LICENSE files for more details.
<% content_controller "admin--custom-fields",
dynamic: true,
"admin--custom-fields-format-config-value": OpenProject::CustomFieldFormatDependent.stimulus_config,
"admin--custom-fields-enterprise-edition-value": EnterpriseToken.allows_to?(:custom_field_hierarchies) %>
"admin--custom-fields-hierarchy-enabled-value": EnterpriseToken.allows_to?(:custom_field_hierarchies) %>
<%= labelled_tabular_form_for @custom_field, as: :custom_field,
url: custom_fields_path,
+1 -1
View File
@@ -2094,12 +2094,12 @@ en:
readonly_work_packages: Readonly Work Packages
sso_auth_providers: Single sign-on
team_planner_view: Team Planner View
two_factor_authentication: 2FA Authentication
virus_scanning: Antivirus Scanning
work_package_query_relation_columns: Work Package Query Relation Columns
work_package_sharing: Share work packages with external users
work_package_subject_generation: Work Package Subject Generation
upsell:
buy_now_button: "Buy now"
plans_title: "Enterprise plans"
title: "Enterprise add-on"
plan_title: "Enterprise %{plan} add-on"
@@ -150,6 +150,10 @@ export class ConfigurationService {
return this.systemPreference<string[]>('availableFeatures');
}
public get triallingFeatures():string[] {
return this.systemPreference<string[]>('triallingFeatures');
}
private loadConfiguration() {
return this
.apiV3Service
@@ -43,7 +43,19 @@ export class BannersService {
}
public showBannerFor(feature:string):boolean {
return !(this._bannersHidden || this.configuration.availableFeatures.includes(feature));
if (this._bannersHidden) {
return false;
}
return !this.allowsTo(feature) || this.trialling(feature);
}
public allowsTo(feature:string):boolean {
return this.configuration.availableFeatures.includes(feature);
}
public trialling(feature:string):boolean {
return this.configuration.triallingFeatures.includes(feature);
}
public getEnterPriseEditionUrl({ referrer, hash }:{ referrer?:string, hash?:string } = {}) {
@@ -59,13 +71,13 @@ export class BannersService {
return url.toString();
}
public async conditional(feature:string, bannersVisible?:() => void, bannersNotVisible?:() => void) {
public async conditional(feature:string, featureNotAvailable?:() => void, featureAvailable?:() => void) {
await this.configuration.initialize();
if (this.showBannerFor(feature)) {
this.callMaybe(bannersVisible);
if (this.allowsTo(feature)) {
this.callMaybe(featureAvailable);
} else {
this.callMaybe(bannersNotVisible);
this.callMaybe(featureNotAvailable);
}
}
@@ -7,7 +7,7 @@ import { ConfigurationService } from 'core-app/core/config/configuration.service
@Injectable()
export class TypeBannerService extends BannersService {
showBanners = this.showBannerFor('edit_attribute_groups');
eeAvailable = this.allowsTo('edit_attribute_groups');
constructor(
@Inject(DOCUMENT) protected documentElement:Document,
@@ -10,7 +10,7 @@
</button>
</li>
<li
*ngIf="!typeBanner.showBanners"
*ngIf="typeBanner.eeAvailable"
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.showBannerFor('board_view'),
disabled: !this.bannersService.allowsTo('board_view'),
}));
}
@@ -1,6 +1,11 @@
<ng-container *ngIf="(board$ | async) as board">
<op-enterprise-banner-frame
class="boards-list--enterprise-banner"
feature="board_view"
[dismissable]="true"
></op-enterprise-banner-frame>
<div
*ngIf="!needEnterpriseEdition; else enterpriseBanner"
*ngIf="available"
class="boards-list--container"
[ngClass]="{ '-free' : board.isFree }"
#container
@@ -38,12 +43,4 @@
</div>
</div>
</div>
<ng-template #enterpriseBanner>
<op-enterprise-banner-frame
class="boards-list--enterprise-banner"
feature="board_view"
[dismissable]="true"
></op-enterprise-banner-frame>
</ng-template>
</ng-container>
@@ -88,7 +88,7 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements
showHiddenListWarning:boolean = false;
needEnterpriseEdition = this.Banner.showBannerFor('board_view');
available = this.Banner.allowsTo('board_view');
private currentQueryUpdatedMonitoring:Subscription;
@@ -126,7 +126,7 @@ readonly I18n:I18nService,
);
this.board$.subscribe((board) => {
this.needEnterpriseEdition = this.Banner.showBannerFor('board_view') && !board.isFree;
this.available = this.Banner.allowsTo('board_view') || board.isFree;
});
this.Boards.currentBoard$.next(id);
@@ -1,5 +1,5 @@
<turbo-frame
id="enterprise_banner_{{feature}}"
[id]="frameID"
*ngIf="visible"
[src]="frameURL"
>
@@ -43,6 +43,7 @@ export class EnterpriseBannerFrameComponent implements OnInit {
visible:boolean;
frameURL:string;
frameID:string;
constructor(
protected pathHelper:PathHelperService,
@@ -53,5 +54,8 @@ export class EnterpriseBannerFrameComponent implements OnInit {
ngOnInit() {
this.visible = this.banners.showBannerFor(this.feature);
this.frameURL = this.pathHelper.bannerFramePath(this.feature, this.dismissable);
const trialSuffix = this.banners.trialling(this.feature) ? '_trial' : '';
this.frameID = `enterprise_banner_${this.feature}${trialSuffix}`;
}
}
@@ -119,7 +119,14 @@ export class ProjectSelectionComponent implements OnInit {
}
private setPlaceholderOption():void {
if (this.bannersService.showBannerFor('placeholder_users')) {
if (this.bannersService.allowsTo('placeholder_users')) {
this.typeOptions.push({
value: PrincipalType.Placeholder,
title: this.I18n.t('js.invite_user_modal.type.placeholder.title'),
description: this.I18n.t('js.invite_user_modal.type.placeholder.description'),
disabled: false,
});
} else {
this.typeOptions.push({
value: PrincipalType.Placeholder,
title: this.I18n.t('js.invite_user_modal.type.placeholder.title_no_ee'),
@@ -131,13 +138,6 @@ export class ProjectSelectionComponent implements OnInit {
}),
disabled: true,
});
} else {
this.typeOptions.push({
value: PrincipalType.Placeholder,
title: this.I18n.t('js.invite_user_modal.type.placeholder.title'),
description: this.I18n.t('js.invite_user_modal.type.placeholder.description'),
disabled: false,
});
}
}
@@ -71,7 +71,7 @@
<p>{{ text.dateAlerts.description }}</p>
</div>
<div *ngIf="!eeShowBanners">
<div *ngIf="eeAvailable">
<div
class="op-reminder-settings-date-alerts--row"
formGroupName="startDate"
@@ -54,7 +54,7 @@ export class NotificationsSettingsPageComponent extends UntilDestroyedMixin impl
public availableTimesOverdue = overDueReminderTimes();
public eeShowBanners = false;
public eeAvailable = false;
public form = new UntypedFormGroup({
assignee: new UntypedFormControl(false),
@@ -137,7 +137,7 @@ export class NotificationsSettingsPageComponent extends UntilDestroyedMixin impl
ngOnInit():void {
this.form.disable();
this.eeShowBanners = this.bannersService.showBannerFor('date_alerts');
this.eeAvailable = this.bannersService.allowsTo('date_alerts');
this
.currentUserService
@@ -88,7 +88,7 @@
</ng-container>
</tr>
<tr *ngIf="!eeShowBanners">
<tr *ngIf="eeAvailable">
<th class="op-table--cell op-table--cell_soft-heading">
<h5>{{ text.dateAlerts.title }}</h5>
<p>{{ text.dateAlerts.description }}</p>
@@ -98,7 +98,7 @@
</ng-container>
</tr>
<tr *ngIf="!eeShowBanners">
<tr *ngIf="eeAvailable">
<th class="op-table--cell op-table--cell_soft-heading">
{{ text.startDate }}
</th>
@@ -122,7 +122,7 @@
</ng-container>
</tr>
<tr *ngIf="!eeShowBanners">
<tr *ngIf="eeAvailable">
<th class="op-table--cell op-table--cell_soft-heading">
{{ text.dueDate }}
</th>
@@ -146,7 +146,7 @@
</ng-container>
</tr>
<tr *ngIf="!eeShowBanners">
<tr *ngIf="eeAvailable">
<th class="op-table--cell op-table--cell_soft-heading">
{{ text.overdue }}
</th>
@@ -21,7 +21,7 @@ export class NotificationSettingsTableComponent implements OnInit {
@Input() settings:UntypedFormArray;
public eeShowBanners = false;
public eeAvailable = false;
public availableTimes = [
{
@@ -81,7 +81,7 @@ export class NotificationSettingsTableComponent implements OnInit {
) {}
ngOnInit():void {
this.eeShowBanners = this.bannersService.showBannerFor('date_alerts');
this.eeAvailable = this.bannersService.allowsTo('date_alerts');
}
projectLink(href:string) {
@@ -98,7 +98,7 @@ export class OpBaselineComponent extends UntilDestroyedMixin implements OnInit {
public tooltipPosition = SpotDropAlignmentOption.TopRight;
available = !this.Banner.showBannerFor('baseline_comparison');
available = this.Banner.allowsTo('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.showBannerFor('work_package_sharing');
showEnterpriseIcon = !this.bannersService.allowsTo('work_package_sharing');
shareCount$:Observable<number>;
@@ -48,7 +48,7 @@ export class WpCustomActionsComponent extends UntilDestroyedMixin implements OnI
actions:CustomActionResource[] = [];
available = !this.bannersService.showBannerFor('custom_actions');
available = this.bannersService.allowsTo('custom_actions');
constructor(
readonly apiV3Service:ApiV3Service,
@@ -9,7 +9,7 @@
<div class="form--field">
<label class="form--label">
<input type="radio"
[attr.disabled]="disabledValue(eeShowBanners)"
[attr.disabled]="disabledValue(eeAvailable)"
[(ngModel)]="highlightingMode"
(change)="updateMode($event.target.value)"
value="inline"
@@ -41,7 +41,7 @@
<div class="form--field">
<label class="form--label">
<input type="radio"
[attr.disabled]="disabledValue(eeShowBanners)"
[attr.disabled]="disabledValue(eeAvailable)"
[(ngModel)]="entireRowMode"
(change)="updateMode('entire-row')"
[value]="true"
@@ -54,7 +54,7 @@
<ng-select #rowHighlightNgSelect
[items]="availableRowHighlightedAttributes"
[(ngModel)]="lastEntireRowAttribute"
[disabled]="disabledValue(eeShowBanners)"
[disabled]="disabledValue(eeAvailable)"
[clearable]="false"
(open)="onOpen(rowHighlightNgSelect)"
(change)="updateMode($event.value)"
@@ -72,7 +72,7 @@
<div class="form--field">
<label class="form--label">
<input type="radio"
[attr.disabled]="disabledValue(eeShowBanners)"
[attr.disabled]="disabledValue(eeAvailable)"
[(ngModel)]="highlightingMode"
(change)="updateMode($event.target.value)"
value="none"
@@ -28,7 +28,7 @@ export class WpTableConfigurationHighlightingTabComponent implements TabComponen
public lastEntireRowAttribute:HighlightingMode = 'status';
public eeShowBanners = false;
public eeAvailable = false;
public availableInlineHighlightedAttributes:HalResource[] = [];
@@ -74,10 +74,10 @@ export class WpTableConfigurationHighlightingTabComponent implements TabComponen
this.setSelectedValues();
this.eeShowBanners = this.Banners.showBannerFor('conditional_highlighting');
this.eeAvailable = this.Banners.allowsTo('conditional_highlighting');
this.updateMode(this.wpTableHighlight.current.mode);
if (this.eeShowBanners) {
if (!this.eeAvailable) {
this.updateMode('none');
}
}
@@ -106,8 +106,8 @@ export class WpTableConfigurationHighlightingTabComponent implements TabComponen
this.selectedAttributes = model;
}
public disabledValue(value:boolean):string | null {
return value ? 'disabled' : null;
public disabledValue(allowed:boolean):string | null {
return allowed ? null : 'disabled';
}
public get availableHighlightedAttributes():HalResource[] {
@@ -32,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.showBannerFor('conditional_highlighting')) {
if (!this.isInline || !this.Banners.allowsTo('conditional_highlighting')) {
return false;
}
@@ -55,11 +55,11 @@ export default class CustomFieldsController extends Controller {
static values = {
formatConfig: Array,
enterpriseEdition: Boolean,
hierarchyEnabled: Boolean,
};
declare readonly formatConfigValue:[string, string, string[]][];
declare readonly enterpriseEditionValue:boolean;
declare readonly hierarchyEnabledValue:boolean;
declare readonly formatTarget:HTMLInputElement;
declare readonly dragContainerTarget:HTMLElement;
@@ -252,14 +252,11 @@ export default class CustomFieldsController extends Controller {
private toggleFormat(format:string) {
if (this.hasSubmitButtonTarget) {
this.submitButtonTarget.disabled = format === 'hierarchy' && !this.enterpriseEditionValue;
this.submitButtonTarget.disabled = format === 'hierarchy' && !this.hierarchyEnabledValue;
}
this.formatConfigValue.forEach(([targetsName, operator, formats]) => {
let active = operator === 'only' ? formats.includes(format) : !formats.includes(format);
if (targetsName === 'enterpriseBanner' && this.enterpriseEditionValue) {
active = false;
}
const active = operator === 'only' ? formats.includes(format) : !formats.includes(format);
const targets = this[`${targetsName}Targets` as keyof typeof this] as HTMLElement[];
if (targets) {
@@ -100,7 +100,12 @@ module API
property :available_features,
getter: ->(*) {
EnterpriseToken.current&.available_features || []
EnterpriseToken.available_features
}
property :trialling_features,
getter: ->(*) {
EnterpriseToken.trialling_features
}
property :allowed_link_protocols,
@@ -144,7 +144,7 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do
context "when banner is dismissed" do
let(:preference) { build_stubbed(:user_preference) }
let(:user) { build_stubbed(:user, preference:) }
let(:dismiss_key) { :some_enterprise_feature }
let(:dismiss_key) { "some_enterprise_feature" }
let(:component_args) { { dismissable: true } }
before do
@@ -164,7 +164,7 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do
end
context "when using a custom dismiss_key" do
let(:dismiss_key) { :foo }
let(:dismiss_key) { "foo" }
let(:component_args) { { dismiss_key:, dismissable: true } }
it_behaves_like "does not render the component"
@@ -295,4 +295,27 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do
end
end
end
context "with a trial token" do
before do
allow(EnterpriseToken).to receive(:trialling?).and_return(true)
end
it_behaves_like "renders the component"
it "renders with trial overrides" do
render_component_in_mo
component = find_test_selector(component_test_selector)
expect(component[:class]).to include("op-enterprise-banner_trial")
expect(component[:class]).not_to include("op-enterprise-banner_medium")
expect(component[:class]).not_to include("op-enterprise-banner_large")
within(component) do
expect(page).to have_css(".op-enterprise-banner--close_icon")
expect(page).to have_content("Buy now")
end
end
end
end
@@ -80,6 +80,7 @@ RSpec.describe "custom fields", :js do
label_possible_values = I18n.t("activerecord.attributes.custom_field.possible_values").upcase # Possible values, capitalized on UI
label_default_value = I18n.t("activerecord.attributes.custom_field.default_value") # Default value
label_is_required = I18n.t("activerecord.attributes.custom_field.is_required") # Required
label_ee_banner_hierarchy = I18n.t("ee.upsell.custom_field_hierarchies.description") # Hierarchy Enterprise banner
# Spent time SFs don't show "Searchable". Not tested here.
# Project CFs don't show "For all projects" and "Used as a filter". Not tested here.
# Content right to left is not shown for Project CFs Long text. Strange. Not tested.
@@ -101,7 +102,7 @@ RSpec.describe "custom fields", :js do
label_min_length, label_max_length, label_regexp, label_default_value, label_is_required
)
expect_page_not_to_have_texts(
label_multi_value, label_allow_non_open_versions, label_possible_values
label_multi_value, label_allow_non_open_versions, label_possible_values, label_ee_banner_hierarchy
)
select "Long text", from: "custom_field_field_format"
@@ -109,7 +110,7 @@ RSpec.describe "custom fields", :js do
label_min_length, label_max_length, label_regexp, label_default_value, label_is_required
)
expect_page_not_to_have_texts(
label_multi_value, label_allow_non_open_versions, label_possible_values
label_multi_value, label_allow_non_open_versions, label_possible_values, label_ee_banner_hierarchy
)
# Both Integer and Float have min/max_len and regex as well which seems strange.
@@ -118,7 +119,7 @@ RSpec.describe "custom fields", :js do
label_min_length, label_max_length, label_regexp, label_default_value, label_is_required
)
expect_page_not_to_have_texts(
label_multi_value, label_allow_non_open_versions, label_possible_values
label_multi_value, label_allow_non_open_versions, label_possible_values, label_ee_banner_hierarchy
)
select "Float", from: "custom_field_field_format"
@@ -126,7 +127,7 @@ RSpec.describe "custom fields", :js do
label_min_length, label_max_length, label_regexp, label_default_value, label_is_required
)
expect_page_not_to_have_texts(
label_multi_value, label_allow_non_open_versions, label_possible_values
label_multi_value, label_allow_non_open_versions, label_possible_values, label_ee_banner_hierarchy
)
select "List", from: "custom_field_field_format"
@@ -134,14 +135,16 @@ RSpec.describe "custom fields", :js do
label_multi_value, label_possible_values, label_is_required
)
expect_page_not_to_have_texts(
label_min_length, label_max_length, label_regexp, label_allow_non_open_versions, label_default_value
label_min_length, label_max_length, label_regexp, label_allow_non_open_versions,
label_default_value, label_ee_banner_hierarchy
)
select "Date", from: "custom_field_field_format"
expect_page_to_have_texts(label_is_required)
expect_page_not_to_have_texts(
label_min_length, label_max_length, label_regexp, label_multi_value,
label_allow_non_open_versions, label_possible_values, label_default_value
label_allow_non_open_versions, label_possible_values, label_default_value,
label_ee_banner_hierarchy
)
select "Boolean", from: "custom_field_field_format"
@@ -150,7 +153,7 @@ RSpec.describe "custom fields", :js do
)
expect_page_not_to_have_texts(
label_min_length, label_max_length, label_regexp, label_multi_value,
label_allow_non_open_versions, label_possible_values
label_allow_non_open_versions, label_possible_values, label_ee_banner_hierarchy
)
select "User", from: "custom_field_field_format"
@@ -159,7 +162,7 @@ RSpec.describe "custom fields", :js do
)
expect_page_not_to_have_texts(
label_min_length, label_max_length, label_regexp, label_allow_non_open_versions,
label_possible_values, label_default_value
label_possible_values, label_default_value, label_ee_banner_hierarchy
)
select "Version", from: "custom_field_field_format"
@@ -168,8 +171,20 @@ RSpec.describe "custom fields", :js do
)
expect_page_not_to_have_texts(
label_min_length, label_max_length, label_regexp,
label_possible_values, label_default_value
label_possible_values, label_default_value, label_ee_banner_hierarchy
)
if hierarchy_type_available
select "Hierarchy", from: "custom_field_field_format"
expect_page_to_have_texts(
label_multi_value, label_is_required, label_ee_banner_hierarchy
)
expect_page_not_to_have_texts(
label_min_length, label_max_length, label_regexp, label_allow_non_open_versions,
label_possible_values, label_default_value
)
expect(page).to have_button("Save", disabled: true)
end
end
it "shows the correct breadcrumbs" do
@@ -186,14 +201,53 @@ RSpec.describe "custom fields", :js do
end
describe "work packages" do
let(:hierarchy_type_available) { true }
it_behaves_like "creating a new custom field", "Work packages"
context "for hierarchy type" do
before do
cf_page.visit_tab "Work packages"
click_on "Create a new custom field"
wait_for_reload
cf_page.set_name "Ignored"
select "Hierarchy", from: "custom_field_field_format"
end
context "with an active enterprise token with custom_field_hierarchies feature", with_ee: [:custom_field_hierarchies] do
it "does not show the enterprise upsell banner and can save" do
expect(page).to have_no_text(I18n.t("ee.upsell.custom_field_hierarchies.description"))
expect(page).to have_button("Save", disabled: false)
end
end
context "with an active enterprise token without custom_field_hierarchies feature", with_ee: [:another_feature] do
it "shows the enterprise upsell banner and cannot save" do
expect(page).to have_text(I18n.t("ee.upsell.custom_field_hierarchies.description"))
expect(page).to have_button("Save", disabled: true)
end
end
context "with a trial enterprise token", :with_ee_trial, with_ee: [:custom_field_hierarchies] do
it "shows the enterprise upsell banner and can save" do
expect(page).to have_text(I18n.t("ee.upsell.custom_field_hierarchies.description"))
expect(page).to have_button("Save", disabled: false)
end
end
end
end
describe "time entries" do
let(:hierarchy_type_available) { false }
it_behaves_like "creating a new custom field", "Spent time"
end
describe "versions" do
let(:hierarchy_type_available) { false }
it_behaves_like "creating a new custom field", "Versions"
end
@@ -255,7 +309,7 @@ RSpec.describe "custom fields", :js do
check("custom_field_multi_value")
check("custom_field_custom_options_attributes_0_default_value")
check("custom_field_custom_options_attributes_2_default_value")
within all(".custom-option-row").first do
within first(".custom-option-row") do
click_on "Move to bottom"
end
click_on "Save"
@@ -37,7 +37,7 @@ RSpec.describe "custom fields of type hierarchy", :js do
let(:hierarchy_page) { Pages::CustomFields::HierarchyPage.new }
before do
allow(EnterpriseToken).to receive(:allows_to?).and_return(true)
allow(EnterpriseToken).to receive_messages(allows_to?: true, trialling?: false)
login_as admin
end
+2 -1
View File
@@ -47,7 +47,7 @@ end
RSpec.configure do |config|
config.before do |example|
allowed = ee_actions(example)
if allowed.present?
if allowed.present? || example.metadata[:with_ee_trial]
allowed = aggregate_parent_array(example, allowed.to_set)
token_double = instance_double(EnterpriseToken)
@@ -65,6 +65,7 @@ RSpec.configure do |config|
.to receive_messages(token_object: token_object_double,
available_features: allowed.to_a,
expired?: false,
trial?: !!example.metadata[:with_ee_trial],
restrictions: {})
end
end