Replace UpsellPageCompomponent with banners rendered as part of the normal flow

This commit is contained in:
Oliver Günther
2025-05-28 09:51:49 +02:00
parent db38d4133f
commit 5ad8a063ae
22 changed files with 126 additions and 348 deletions
-1
View File
@@ -1,5 +1,4 @@
@import "enterprise_edition/banner_component"
@import "enterprise_edition/upsell_page_component"
@import "filter/filters_component"
@import "op_primer/border_box_table_component"
@import "op_primer/form_helpers"
@@ -1,41 +0,0 @@
<div class="op-enterprise-upsell-page" data-test-selector="op-enterprise-upsell-page">
<%=
render(Primer::OpenProject::FlexLayout.new(justify_content: :center, align_items: :center)) do |flex|
flex.with_column do
render(Primer::Beta::Octicon.new(icon: :"op-enterprise-addons",
size: :medium,
classes: "upsell-colored"))
end
flex.with_column(ml: 2) do
render(Primer::Beta::Heading.new(tag: :h2)) { title }
end
end
%>
<p class="upsell--feature-reference">
<%= render(Primer::Beta::Text.new) { description } %>
<%= render(Primer::Beta::Text.new) { plan_text } %>
</p>
<% if more_info_text.present? %>
<%= render(Primer::Beta::Text.new(tag: :p, color: :muted)) { more_info_text } %>
<% end %>
<% if features.present? %>
<ul>
<% features.each do |text| %>
<li><%= text %></li>
<% end %>
</ul>
<% end %>
<% if @video.present? %>
<%= video_tag @video, controls: false, class: "widget-box--teaser-video", autoplay: true, loop: true, muted: true %>
<% elsif @image.present? %>
<%= image_tag @image, class: "widget-box--teaser-image widget-box--teaser-image_default" %>
<% else %>
<%= image_tag "enterprise-add-on.svg", class: "widget-box--teaser-image widget-box--teaser-image_default" %>
<% end %>
<%= render EnterpriseEdition::UpsellButtonsComponent.new(feature_key, justify_content: :center) %>
</div>
@@ -1,61 +0,0 @@
# 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.
# ++
module EnterpriseEdition
# A full page banner for the enterprise edition
class UpsellPageComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include PlanForFeature
# @param feature_key [Symbol, NilClass] The key of the feature to show the upsell page for.
# @param i18n_scope [String] Provide the i18n scope to look for title, description, and features.
# Defaults to "ee.upsell.{feature_key}"
# @param image [String, NilClass] Path to the image to show on the upsell page, or nil.
# @param video [String, NilClass] Path to the video to show on the upsell page, or nil.
def initialize(feature_key, i18n_scope: "ee.upsell.#{feature_key}", image: nil, video: nil)
super
self.feature_key = feature_key
self.i18n_scope = i18n_scope
@image = image
@video = video
end
def more_info_text
I18n.t(:more_info, scope: i18n_scope, default: nil)
end
private
def render?
!EnterpriseToken.hide_banners?
end
end
end
@@ -1,56 +0,0 @@
/*!
/ -- 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.
/ ++
/
.op-enterprise-upsell-page
max-width: 50vw
margin: auto
padding-top: 20px
text-align: center
.widget-box--teaser-image,
.widget-box--teaser-video
width: 100%
height: auto
margin-bottom: 20px
box-shadow: var(--shadow-floating-small)
border-radius: 25px
border: 1px solid var(--borderColor-default)
.widget-box--teaser-image_default
width: 30%
margin-bottom: 20px
box-shadow: 4px 4px 10px rgb(0 0 0 / 15%)
@media screen and (max-width: $breakpoint-md)
max-width: none
.widget-box--teaser-image,
.widget-box--teaser-video
width: 90%
border-radius: 15px
+2 -6
View File
@@ -28,7 +28,7 @@
class CustomActionsController < ApplicationController
before_action :require_admin
before_action :require_enterprise_token
before_action :require_enterprise_token, except: %i[index]
self._model_object = CustomAction
before_action :find_model_object, only: %i(edit update destroy)
@@ -84,11 +84,7 @@ class CustomActionsController < ApplicationController
def require_enterprise_token
return if EnterpriseToken.allows_to?(:custom_actions)
if request.get?
render template: "custom_actions/upsell"
else
render_403
end
render_403
end
# If no action/condition is set in the view, the
+2 -8
View File
@@ -40,12 +40,12 @@ class CustomStylesController < ApplicationController
before_action :require_admin,
except: UNGUARDED_ACTIONS
before_action :require_ee_token,
except: UNGUARDED_ACTIONS + %i[upsell]
skip_before_action :check_if_login_required,
only: UNGUARDED_ACTIONS
no_authorization_required! *UNGUARDED_ACTIONS
guard_enterprise_feature(:define_custom_style, except: UNGUARDED_ACTIONS + %i[show])
def default_url_options
super.merge(tab: params[:tab])
end
@@ -180,12 +180,6 @@ class CustomStylesController < ApplicationController
CustomStyle.current || CustomStyle.create!
end
def require_ee_token
unless EnterpriseToken.allows_to?(:define_custom_style)
redirect_to custom_style_upsell_path
end
end
def custom_style_params
params.require(:custom_style).permit(:logo, :remove_logo,
:export_logo, :remove_export_logo,
+10
View File
@@ -1,4 +1,5 @@
<% html_title t(:label_administration), t("custom_actions.plural") -%>
<% available = EnterpriseToken.allows_to?(:custom_actions) %>
<%=
render Primer::OpenProject::PageHeader.new do |header|
@@ -19,6 +20,7 @@
label: t("custom_actions.new"),
test_selector: "op-admin-custom-actions--button-new",
tag: :a,
disabled: !available,
href: new_custom_action_path
) do
CustomAction.model_name.human
@@ -26,4 +28,12 @@
end
%>
<%=
render EnterpriseEdition::BannerComponent.new(
:custom_actions,
variant: :large,
video: "enterprise/custom-actions.mp4"
)
%>
<%= render(::CustomActions::TableComponent.new(rows: @custom_actions)) %>
+2 -1
View File
@@ -30,8 +30,9 @@ See COPYRIGHT and LICENSE files for more details.
<% html_title t(:label_administration), t("ee.upsell.custom_actions.title") %>
<%=
render EnterpriseEdition::UpsellPageComponent.new(
render EnterpriseEdition::BannerComponent.new(
:custom_actions,
variant: :large,
video: "enterprise/custom-actions.mp4"
)
%>
+28 -18
View File
@@ -27,25 +27,35 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%= render(Admin::DesignHeaderComponent.new(tabs: design_tabs)) %>
<% allowed = EnterpriseToken.allows_to?(:define_custom_style) %>
<% tabs = allowed ? design_tabs : [] %>
<%= render(Admin::DesignHeaderComponent.new(tabs:)) %>
<%= error_messages_for "custom_style" %>
<%= form_tag update_design_themes_path, method: :post, class: "form" do %>
<section class="form--section">
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= I18n.t("admin.custom_styles.color_theme") %></legend>
<div class="form--field">
<div class="form--field-container">
<%= styled_select_tag "theme",
options_for_select(@theme_options, @current_theme),
container_class: "-slim" %>
<%= render EnterpriseEdition::BannerComponent.new(
:define_custom_style,
variant: :large,
video: "enterprise/custom-design.mp4"
) %>
<% if allowed %>
<%= error_messages_for "custom_style" %>
<%= form_tag update_design_themes_path, method: :post, class: "form" do %>
<section class="form--section">
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= I18n.t("admin.custom_styles.color_theme") %></legend>
<div class="form--field">
<div class="form--field-container">
<%= styled_select_tag "theme",
options_for_select(@theme_options, @current_theme),
container_class: "-slim" %>
</div>
</div>
</div>
<%= styled_button_tag t(:button_save),
data: { test_selector: "color-theme-button", confirm: (t("admin.custom_styles.theme_warning") if @current_theme.blank?) } %>
</fieldset>
</section>
<%= styled_button_tag t(:button_save),
data: { test_selector: "color-theme-button", confirm: (t("admin.custom_styles.theme_warning") if @current_theme.blank?) } %>
</fieldset>
</section>
<% end %>
<%= render_tabs design_tabs %>
<% end %>
<%= render_tabs design_tabs %>
-35
View File
@@ -1,35 +0,0 @@
<%#-- 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.
++#%>
<% html_title t(:label_administration), t(:label_custom_style) %>
<%= render EnterpriseEdition::UpsellPageComponent.new(
:define_custom_style,
video: "enterprise/custom-design.mp4"
) %>
+2 -1
View File
@@ -1,6 +1,7 @@
<% html_title t("js.notifications.title") %>
<%= render EnterpriseEdition::UpsellPageComponent.new(
<%= render EnterpriseEdition::BannerComponent.new(
:date_alerts,
variant: :large,
video: "enterprise/date-alert-notifications.mp4"
) %>
@@ -1,6 +1,7 @@
<% html_title t("js.notifications.title") %>
<%= render EnterpriseEdition::UpsellPageComponent.new(
<%= render EnterpriseEdition::BannerComponent.new(
:work_package_sharing,
variant: :large,
video: "enterprise/share-work-package.mp4"
) %>
+3 -2
View File
@@ -61,6 +61,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= render PlaceholderUsers::TableComponent.new(rows: @placeholder_users) %>
<% else %>
<%= render EnterpriseEdition::UpsellPageComponent.new(:placeholder_users,
video: "enterprise/placeholder_users.mp4") %>
<%= render EnterpriseEdition::BannerComponent.new(:placeholder_users,
variant: :large,
video: "enterprise/placeholder_users.mp4") %>
<% end %>
@@ -40,5 +40,9 @@ See COPYRIGHT and LICENSE files for more details.
<%= render(Projects::Settings::WorkPackages::Activities::Form.new(f)) %>
<% end %>
<% else %>
<%= render(EnterpriseEdition::UpsellPageComponent.new(:internal_comments, video: "enterprise/internal_comments.mp4")) %>
<%= render(EnterpriseEdition::BannerComponent.new(
:internal_comments,
variant: :large,
video: "enterprise/internal_comments.mp4"
)) %>
<% end %>
@@ -1,6 +1,7 @@
<% html_title t(:label_work_package_plural) %>
<%= render EnterpriseEdition::UpsellPageComponent.new(
<%= render EnterpriseEdition::BannerComponent.new(
:work_package_sharing,
variant: :large,
video: "enterprise/share-work-package.mp4"
) %>
-1
View File
@@ -494,7 +494,6 @@ Rails.application.routes.draw do
delete "design/export_cover" => "custom_styles#export_cover_delete", as: "custom_style_export_cover_delete"
delete "design/favicon" => "custom_styles#favicon_delete", as: "custom_style_favicon_delete"
delete "design/touch_icon" => "custom_styles#touch_icon_delete", as: "custom_style_touch_icon_delete"
get "design/upsell" => "custom_styles#upsell", as: "custom_style_upsell"
post "design/colors" => "custom_styles#update_colors", as: "update_design_colors"
post "design/themes" => "custom_styles#update_themes", as: "update_design_themes"
post "design/export_cover_text_color" => "custom_styles#update_export_cover_text_color",
@@ -1,83 +0,0 @@
# 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.
# ++
module OpenProject
module EnterpriseEdition
# @logical_path OpenProject/EnterpriseEdition
class UpsellPageComponentPreview < Lookbook::Preview
# Render a full-screen upsell page, with optional video or image features.
# The easiest way to render the banner component is to provide a feature key and
# have the assorted data structures match the expectations.
# The text will be fetched from the i18n files:
# ```
# en:
# ee:
# # Title used unless it is overwritten for the specific feature
# title: "Enterprise add-on"
# upsell:
# [feature_key]:
# # Title used for this feature only. If this is missing, the default title is used.
# title: "A splendid feature"
# # Could also be description_html if necessary
# description: "This is a splendid feature that you should use. It just might transform your life."
# # An unordered list of features
# features:
# some_key: "Some feature"
# some_other_key: "Some other feature"
# ```
# You can also provide a custom i18n_scope to change the place where the component looks for
# title, description, and features.
#
# To provide a video or image, use the respective `video:` or `image:` tags.
# If none or provided, a default image will be used.
#
# The href is inferred from `OpenProject::Static::Links.enterprise_features[feature_key][:href]`.
# @display min_height 450px
def default
render ::EnterpriseEdition::UpsellPageComponent.new(:customize_life_cycle)
end
def video
render(
::EnterpriseEdition::UpsellPageComponent
.new(:date_alerts,
video: "enterprise/date-alert-notifications.mp4")
)
end
def image
render(
::EnterpriseEdition::UpsellPageComponent
.new(:ldap_groups, image: "enterprise/ldap-groups.jpg")
)
end
end
end
end
@@ -1,3 +1,6 @@
<% html_title(t(:label_administration), t("ldap_groups.synchronized_groups.plural")) -%>
<%= render EnterpriseEdition::UpsellPageComponent.new(:ldap_groups, image: "enterprise/ldap-groups.jpg") %>
<%= render EnterpriseEdition::BannerComponent.new(
:ldap_groups,
variant: :inline, # TODO
) %>
@@ -1,5 +1,8 @@
<% html_title(t(:label_administration), t("storages.upsell.title")) -%>
<%= render EnterpriseEdition::UpsellPageComponent.new(:one_drive_sharepoint_file_storage,
video: "enterprise/one_drive_sharepoint_integration.mp4",
i18n_scope: "storages.upsell") %>
<%= render EnterpriseEdition::BannerComponent.new(
:one_drive_sharepoint_file_storage,
variant: :large,
video: "enterprise/one_drive_sharepoint_integration.mp4",
i18n_scope: "storages.upsell"
) %>
@@ -1,8 +1,9 @@
<% html_title(t(:label_administration), t("ee.upsell.team_planner_view.title")) -%>
<%=
render EnterpriseEdition::UpsellPageComponent.new(
render EnterpriseEdition::BannerComponent.new(
:team_planner_view,
variant: :large,
video: "enterprise/team-planner-animation.mp4"
)
%>
@@ -39,7 +39,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
actions: { assigned_to: 1 } } }
end
shared_examples_for "read requires enterprise token" do
shared_examples_for "requires enterprise token" do
context "without an enterprise token", with_ee: false do
before do
login_as(admin)
@@ -47,23 +47,8 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
call
end
it "renders enterprise_token" do
expect(response).to render_template "custom_actions/upsell"
end
end
end
shared_examples_for "write requires enterprise token" do
context "without an enterprise token", with_ee: false do
before do
login_as(admin)
call
end
it "renders enterprise_token" do
expect(response.response_code)
.to be 403
it "redirects to index" do
expect(response).to redirect_to action: :index
end
end
end
@@ -115,8 +100,19 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
end
end
context "without an enterprise token", with_ee: false do
before do
login_as(admin)
call
end
it "renders ok" do
expect(response.response_code).to be 200
end
end
it_behaves_like "403 for non admins"
it_behaves_like "read requires enterprise token"
end
describe "#new" do
@@ -150,7 +146,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
end
it_behaves_like "403 for non admins"
it_behaves_like "read requires enterprise token"
it_behaves_like "requires enterprise token"
end
describe "#create" do
@@ -214,7 +210,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
end
it_behaves_like "403 for non admins"
it_behaves_like "write requires enterprise token"
it_behaves_like "requires enterprise token"
end
describe "#edit" do
@@ -274,7 +270,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
end
it_behaves_like "403 for non admins"
it_behaves_like "read requires enterprise token"
it_behaves_like "requires enterprise token"
end
describe "#update" do
@@ -368,7 +364,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
end
it_behaves_like "403 for non admins"
it_behaves_like "write requires enterprise token"
it_behaves_like "requires enterprise token"
end
describe "#destroy" do
@@ -420,7 +416,23 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
end
end
context "for admins without an enterprise token", with_ee: false do
before do
allow(action)
.to receive(:destroy)
.and_return(true)
login_as(admin)
call
end
it "redirects to index" do
expect(response).to redirect_to action: :index
expect(action).to have_received(:destroy)
end
end
it_behaves_like "403 for non admins"
it_behaves_like "write requires enterprise token"
end
end
@@ -53,8 +53,8 @@ RSpec.describe CustomStylesController do
allow(EnterpriseToken).to receive(:active_tokens).and_return([])
end
it "redirects to #upsell" do
expect(subject).to redirect_to action: :upsell
it "renders show" do
expect(subject).to redirect_to action: :show, tab: "interface"
end
end
end
@@ -95,6 +95,24 @@ RSpec.describe CustomStylesController do
end
end
describe "#create", with_ee: false do
let(:custom_style) { CustomStyle.new }
let(:params) do
{
custom_style: { logo: "foo", favicon: "bar", icon_touch: "yay" }
}
end
before do
post :create, params:
end
it "renders a 403" do
expect(response).to have_http_status(:forbidden)
expect(flash[:error][:message]).to match /You need the basic enterprise plan to perform this action/
end
end
describe "#update", with_ee: %i[define_custom_style] do
let(:custom_style) { build(:custom_style_with_logo) }
let(:params) do