diff --git a/.github/workflows/pullpreview.yml b/.github/workflows/pullpreview.yml
index 6456db167c44..426d21db8ac0 100644
--- a/.github/workflows/pullpreview.yml
+++ b/.github/workflows/pullpreview.yml
@@ -53,3 +53,4 @@ jobs:
AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
AWS_REGION: eu-central-1
+ OPENPROJECT_FEATURE_PERSONAL_THEME_SELECTION_ACTIVE: true
diff --git a/Gemfile b/Gemfile
index 36a52408b2cc..8cce44907ae3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -279,6 +279,9 @@ group :development do
gem 'livingstyleguide', '~> 2.1.0'
gem 'sassc-rails'
+ # Lookbook
+ gem 'lookbook', '~> 2.0.3'
+
gem 'colored2'
# git hooks manager
diff --git a/Gemfile.lock b/Gemfile.lock
index fab826db1e84..b41f5355423d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -378,6 +378,8 @@ GEM
crack (0.4.5)
rexml
crass (1.0.6)
+ css_parser (1.14.0)
+ addressable
cuprite (0.14.3)
capybara (~> 3.0)
ferrum (~> 0.13.0)
@@ -534,7 +536,9 @@ GEM
html-pipeline (2.14.3)
activesupport (>= 2)
nokogiri (>= 1.4)
+ htmlbeautifier (1.4.2)
htmldiff (0.0.1)
+ htmlentities (4.3.4)
http-accept (1.7.0)
http-cookie (1.0.5)
domain_name (~> 0.5)
@@ -600,6 +604,18 @@ GEM
loofah (2.21.3)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
+ lookbook (2.0.3)
+ activemodel
+ css_parser
+ htmlbeautifier (~> 1.3)
+ htmlentities (~> 4.3.4)
+ marcel (~> 1.0)
+ railties (>= 5.0)
+ redcarpet (~> 3.5)
+ rouge (>= 3.26, < 5.0)
+ view_component (>= 2.0)
+ yard (~> 0.9.25)
+ zeitwerk (~> 2.5)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
@@ -686,6 +702,11 @@ GEM
ttfunk (~> 1.7)
prawn-table (0.2.2)
prawn (>= 1.3.0, < 3.0.0)
+ primer_view_components (0.1.9)
+ actionview (>= 5.0.0)
+ activesupport (>= 5.0.0)
+ octicons (>= 18.0.0)
+ view_component (> 2.0, < 4.0)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
@@ -983,6 +1004,7 @@ GEM
activerecord (>= 4.2)
xpath (3.2.0)
nokogiri (~> 1.8)
+ yard (0.9.34)
zeitwerk (2.6.8)
PLATFORMS
@@ -1058,6 +1080,7 @@ DEPENDENCIES
listen (~> 3.8.0)
livingstyleguide (~> 2.1.0)
lograge (~> 0.12.0)
+ lookbook (~> 2.0.3)
mail (= 2.8.1)
matrix (~> 0.4.2)
md_to_pdf!
@@ -1174,4 +1197,4 @@ RUBY VERSION
ruby 3.2.1p31
BUNDLED WITH
- 2.4.7
+ 2.4.6
diff --git a/app/assets/images/icon_logo.svg b/app/assets/images/icon_logo.svg
new file mode 100644
index 000000000000..8cfeda1fe4fb
--- /dev/null
+++ b/app/assets/images/icon_logo.svg
@@ -0,0 +1,56 @@
+
+
diff --git a/app/assets/images/icon_logo_white.svg b/app/assets/images/icon_logo_white.svg
new file mode 100644
index 000000000000..8cec02cbf056
--- /dev/null
+++ b/app/assets/images/icon_logo_white.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/components/concerns/op_turbo/streamable.rb b/app/components/concerns/op_turbo/streamable.rb
new file mode 100644
index 000000000000..4802b7e83e90
--- /dev/null
+++ b/app/components/concerns/op_turbo/streamable.rb
@@ -0,0 +1,124 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2023 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 OpTurbo
+ module Streamable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def wrapper_key
+ name.underscore.gsub("/", "-").gsub("_", "-")
+ end
+ end
+
+ included do
+ def render_as_turbo_stream(view_context:, action: :update)
+ case action
+ when :update
+ @inner_html_only = true
+ template = render_in(view_context)
+ when :replace
+ template = render_in(view_context)
+ when :remove
+ template = nil
+ else
+ raise "Unsupported action #{action}"
+ end
+
+ unless @component_wrapper_used
+ raise "You need to wrap your component in a `component_wrapper` block in order to use the turbo-stream methods"
+ end
+
+ OpTurbo::StreamWrapperComponent.new(
+ action:,
+ target: wrapper_key,
+ template:
+ ).render_in(view_context)
+ end
+
+ def insert_as_turbo_stream(component:, view_context:, action: :append)
+ template = component.render_in(view_context)
+
+ unless @component_wrapper_used
+ raise "You need to wrap your component in a `component_wrapper` block in order to use the turbo-stream methods"
+ end
+
+ OpTurbo::StreamWrapperComponent.new(
+ action:,
+ target: insert_target_modified? ? insert_target_modifier_id : wrapper_key,
+ template:
+ ).render_in(view_context)
+ end
+
+ def component_wrapper(tag: "div", class: nil, data: nil, style: nil, &block)
+ @component_wrapper_used = true
+ if inner_html_only?
+ capture(&block)
+ else
+ content_tag(tag, id: wrapper_key, class:, data:, style:, &block)
+ end
+ end
+
+ def inner_html_only?
+ @inner_html_only == true
+ end
+
+ def wrapper_key
+ if wrapper_uniq_by.nil?
+ self.class.wrapper_key
+ else
+ "#{self.class.wrapper_key}-#{wrapper_uniq_by}"
+ end
+ end
+
+ def wrapper_uniq_by
+ # optionally implemented in subclass in order to make the wrapper key unique
+ end
+
+ def insert_target_modified?
+ # optionally overriden (returning true) in subclass in order to indicate thate the insert target
+ # is modified and should not be the root inner html element
+ # insert_target_container needs to be present on component's erb template then
+ false
+ end
+
+ def insert_target_container(tag: "div", class: nil, data: nil, style: nil, &block)
+ unless insert_target_modified?
+ raise "`insert_target_modified?` needs to be implemented and return true if `insert_target_container` is " \
+ "used in this component"
+ end
+
+ content_tag(tag, id: insert_target_modifier_id, class:, data:, style:, &block)
+ end
+
+ def insert_target_modifier_id
+ "#{wrapper_key}-insert-target-modifier"
+ end
+ end
+ end
+end
diff --git a/app/components/icon_component.rb b/app/components/icon_component.rb
new file mode 100644
index 000000000000..99a2a622b607
--- /dev/null
+++ b/app/components/icon_component.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class IconComponent < ViewComponent::Base
+ def initialize(name:, classnames: '')
+ @name = name
+ @classnames = classnames
+ end
+
+ def call
+ helpers.spot_icon @name, classnames: @classnames
+ end
+end
diff --git a/app/components/op_turbo/stream_wrapper_component.html.erb b/app/components/op_turbo/stream_wrapper_component.html.erb
new file mode 100644
index 000000000000..4ef68b5bbbd0
--- /dev/null
+++ b/app/components/op_turbo/stream_wrapper_component.html.erb
@@ -0,0 +1,36 @@
+<%#-- copyright
+OpenProject is an open source project management software.
+Copyright (C) 2012-2023 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.
+
+++#%>
+
+ <% if @template %>
+
+ <%= @template %>
+
+ <% end %>
+
+
diff --git a/app/components/op_turbo/stream_wrapper_component.rb b/app/components/op_turbo/stream_wrapper_component.rb
new file mode 100644
index 000000000000..6ad310353fc3
--- /dev/null
+++ b/app/components/op_turbo/stream_wrapper_component.rb
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2023 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 OpTurbo
+ class StreamWrapperComponent < ApplicationComponent
+ def initialize(template:, action:, target:)
+ super()
+
+ @template = template
+ @action = action
+ @target = target
+ end
+ end
+end
diff --git a/app/components/switch_component.html.erb b/app/components/switch_component.html.erb
new file mode 100644
index 000000000000..0eca0c64768e
--- /dev/null
+++ b/app/components/switch_component.html.erb
@@ -0,0 +1,8 @@
+<%= helpers.angular_component_tag(
+ 'spot-switch',
+ inputs: {
+ name: @name,
+ checked: @checked,
+ disabled: @disabled
+ }.compact
+ ) -%>
diff --git a/app/components/switch_component.rb b/app/components/switch_component.rb
new file mode 100644
index 000000000000..03830955e3eb
--- /dev/null
+++ b/app/components/switch_component.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class SwitchComponent < ViewComponent::Base
+ def initialize(checked: false, disabled: false, name: nil)
+ @checked = checked
+ @disabled = disabled
+ @name = name
+ end
+end
diff --git a/app/components/tooltip_component.html.erb b/app/components/tooltip_component.html.erb
new file mode 100644
index 000000000000..e2ea57c9319a
--- /dev/null
+++ b/app/components/tooltip_component.html.erb
@@ -0,0 +1,6 @@
+
+ <%= trigger %>
+
+ <%= body %>
+
+
diff --git a/app/components/tooltip_component.rb b/app/components/tooltip_component.rb
new file mode 100644
index 000000000000..ba3d7d7f1ae0
--- /dev/null
+++ b/app/components/tooltip_component.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class TooltipComponent < ViewComponent::Base
+ renders_one :trigger
+ renders_one :body
+
+ def initialize(alignment: 'bottom-center')
+ @alignment = alignment
+ end
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 118a59e423c7..625808a14ba9 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -132,6 +132,7 @@ def handle_unverified_request
before_action :user_setup,
:set_localization,
+ :turbolinks_nonce,
:tag_request,
:check_if_login_required,
:log_requesting_user,
@@ -168,6 +169,13 @@ def set_cache_buster
end
end
+ def turbolinks_nonce
+ # use the same csp nonce for turbolinks requests
+ if request.env['HTTP_X_TURBOLINKS_REFERRER'].present?
+ request.env[::SecureHeaders::NONCE_KEY] = request.env['HTTP_X_TURBOLINKS_NONCE']
+ end
+ end
+
def tag_request
::OpenProject::Appsignal.tag_request(controller: self, request:)
end
diff --git a/app/controllers/concerns/op_turbo/component_stream.rb b/app/controllers/concerns/op_turbo/component_stream.rb
new file mode 100644
index 000000000000..7a530b97d21b
--- /dev/null
+++ b/app/controllers/concerns/op_turbo/component_stream.rb
@@ -0,0 +1,80 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2023 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 OpTurbo
+ module ComponentStream
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :initialize_streams
+ end
+
+ def initialize_streams
+ @turbo_streams = []
+ end
+
+ def respond_to_with_turbo_streams(&format_block)
+ respond_to do |format|
+ format.turbo_stream do
+ render turbo_stream: @turbo_streams
+ end
+
+ format_block.call(format) if block_given?
+ end
+ end
+ alias_method :respond_with_turbo_streams, :respond_to_with_turbo_streams
+
+ def update_via_turbo_stream(component:)
+ modify_via_turbo_stream(component:, action: :update)
+ end
+
+ def replace_via_turbo_stream(component:)
+ modify_via_turbo_stream(component:, action: :replace)
+ end
+
+ def remove_via_turbo_stream(component:)
+ modify_via_turbo_stream(component:, action: :remove)
+ end
+
+ def modify_via_turbo_stream(component:, action:)
+ @turbo_streams << component.render_as_turbo_stream(
+ view_context:,
+ action:
+ )
+ end
+
+ def append_via_turbo_stream(component:, target_component:)
+ @turbo_streams << target_component.insert_as_turbo_stream(component:, view_context:, action: :append)
+ end
+
+ def prepend_via_turbo_stream(component:, target_component:)
+ @turbo_streams << target_component.insert_as_turbo_stream(component:, view_context:,
+ action: :prepend)
+ end
+ end
+end
diff --git a/app/helpers/frontend_asset_helper.rb b/app/helpers/frontend_asset_helper.rb
index a19b69c3da73..664c7c88d29b 100644
--- a/app/helpers/frontend_asset_helper.rb
+++ b/app/helpers/frontend_asset_helper.rb
@@ -54,6 +54,12 @@ def include_frontend_assets
end
end
+ def include_spot_assets
+ capture do
+ concat stylesheet_link_tag variable_asset_path("spot.css"), media: :all, skip_pipeline: true
+ end
+ end
+
private
def angular_cli_asset(path)
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 73508be3da9a..210e175b6401 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -131,6 +131,10 @@ def time_zone
super.presence || Setting.user_default_timezone.presence
end
+ def theme
+ super.presence || Setting.user_default_theme
+ end
+
def daily_reminders
super.presence || { enabled: true, times: ["08:00:00+00:00"] }.with_indifferent_access
end
diff --git a/app/models/work_package.rb b/app/models/work_package.rb
index bb119391d0a7..de43b280d884 100644
--- a/app/models/work_package.rb
+++ b/app/models/work_package.rb
@@ -61,6 +61,9 @@ class WorkPackage < ApplicationRecord
has_many :storages, through: :project
+ has_many :issues, class_name: 'WorkPackageIssue', dependent: :destroy
+ has_many :meeting_agenda_items, through: :issues
+
has_and_belongs_to_many :changesets, -> { # rubocop:disable Rails/HasAndBelongsToMany
order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")
}
diff --git a/app/views/homescreen/index.html.erb b/app/views/homescreen/index.html.erb
index 8e273eab2fd8..839fcf06ec1c 100644
--- a/app/views/homescreen/index.html.erb
+++ b/app/views/homescreen/index.html.erb
@@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
+
<% breadcrumb_paths(nil) %>
<%= organization_icon %>
@@ -58,4 +59,6 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
+
<%= call_hook :homescreen_after_links %>
+
diff --git a/app/views/layouts/_common_head.html.erb b/app/views/layouts/_common_head.html.erb
index c86576a63727..8e14380d2905 100644
--- a/app/views/layouts/_common_head.html.erb
+++ b/app/views/layouts/_common_head.html.erb
@@ -2,6 +2,7 @@
<%= output_title_and_meta_tags %>
<%= appsignal_frontend_tag %>
+<%= javascript_include_tag("primer_view_components") %>
<% relative_url_root = OpenProject::Configuration['rails_relative_url_root'] || '' %>
@@ -18,7 +19,7 @@
data-mail="<%= User.current.mail %>"
data-id="<%= User.current.id %>" />
<% end %>
-
+
<% if Setting.demo_projects_available %><% end %>
<% if Setting.boards_demo_data_available %><% end %>
<% if Setting.demo_view_of_type_team_planner_seeded %><% end %>
diff --git a/app/views/layouts/angular/angular.html.erb b/app/views/layouts/angular/angular.html.erb
index 5ea4f152f9b6..07d221aacd82 100644
--- a/app/views/layouts/angular/angular.html.erb
+++ b/app/views/layouts/angular/angular.html.erb
@@ -37,7 +37,7 @@ See COPYRIGHT and LICENSE files for more details.
<%# Allow projection pages to show rails rendered content after initialization %>
-
+
<%= yield %>
<% end -%>
diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb
index 4da3e863b091..4ef60485f8b2 100644
--- a/app/views/layouts/base.html.erb
+++ b/app/views/layouts/base.html.erb
@@ -177,5 +177,6 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
<%= call_hook :view_layouts_base_body_bottom %>
+