mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge pull request #19045 from opf/implementation/64263-add-trial-enterprise-banner
[#64263] Add trial enterprise banner
This commit is contained in:
@@ -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
|
||||
|
||||
+8
-6
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export class BoardActionsRegistryService {
|
||||
icon: '',
|
||||
description: '',
|
||||
image: '',
|
||||
disabled: this.bannersService.showBannerFor('board_view'),
|
||||
disabled: !this.bannersService.allowsTo('board_view'),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
+6
-9
@@ -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>
|
||||
|
||||
+2
-2
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+4
-4
@@ -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>
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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'),
|
||||
|
||||
+1
-1
@@ -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>;
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+4
-4
@@ -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"
|
||||
|
||||
+5
-5
@@ -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[] {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user