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 @@ + + + + + + image/svg+xml + + + + + + + + + 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 %> + + <% 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 %> - -<%= render_tabs [{:name => 'agenda', :action => :create_meeting_agendas, :partial => 'meeting_contents/show', :path => meeting_agenda_path(@meeting), :label => :label_meeting_agenda, :content => @meeting.agenda || @meeting.build_agenda, :content_type => "meeting_agenda"}, - {:name => 'minutes', :action => :create_meeting_minutes, :partial => 'meeting_contents/show', :path => meeting_minutes_path(@meeting), :label => :label_meeting_minutes, :content => @meeting.minutes || @meeting.build_minutes, :content_type => "meeting_minutes"}] %> - -<% if @meeting.journals.changing.present? %> -
-

<%=t(:label_history)%>

- <% @meeting.journals.each do |journal| %> - <%= render_meeting_journal @meeting, journal %> - <% end %> +
+ <%= render(MeetingAgendaItems::HeadingComponent.new(meeting: @meeting)) %> + <%= render(MeetingAgendaItems::ListComponent.new(meeting: @meeting)) %> + <%= render(MeetingAgendaItems::NewSectionComponent.new(meeting: @meeting)) %>
-<% end %> diff --git a/modules/meeting/app/views/meetings/show_in_wp_tab.html.erb b/modules/meeting/app/views/meetings/show_in_wp_tab.html.erb new file mode 100644 index 000000000000..692b41c3307c --- /dev/null +++ b/modules/meeting/app/views/meetings/show_in_wp_tab.html.erb @@ -0,0 +1,62 @@ +<%#-- 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. + +++#%> + + <%= link_to(index_in_wp_tab_project_meetings_path(project_id: @meeting.project.id, work_package_id: @active_work_package&.id)) do %> + <%= render(Primer::Beta::Button.new( + scheme: :secondary, + size: :large, + block: false, + mb: 3 + )) do |component| %> + <% component.with_leading_visual_icon(icon: "arrow-left") %> + Back to overview + <% end %> + <% end %> +
+ <%= + render(Primer::Beta::Text.new(font_weight: :bold, font_size: 4, color: :muted, mb: 1)) do + "Meeting:" + end + %> + <%= link_to(meeting_path(@meeting), target: "_blank") do %> + <%= + render(Primer::Beta::Text.new(font_weight: :bold, font_size: 4, mb: 1)) do + "#{@meeting.title}" + end + %> + <% end %> +
+ <%= + render(Primer::Beta::Text.new(font_size: :small, color: :subtle, tag: :div, mb: 3)) do + "#{format_date(@meeting.start_time)} #{format_time(@meeting.start_time, false)} - #{format_time(@meeting.end_time, false)}" + end + %> + <%= render(MeetingAgendaItems::ListComponent.new(meeting: @meeting, active_work_package: @active_work_package)) %> + <%= render(MeetingAgendaItems::NewSectionComponent.new(meeting: @meeting, state: :initial, active_work_package: @active_work_package)) %> +
\ No newline at end of file diff --git a/modules/meeting/app/views/work_package_issues/closed.html.erb b/modules/meeting/app/views/work_package_issues/closed.html.erb new file mode 100644 index 000000000000..34c92f97b9ea --- /dev/null +++ b/modules/meeting/app/views/work_package_issues/closed.html.erb @@ -0,0 +1,45 @@ +<%#-- 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. + +++#%> + + + <%= render(::Base::OpPrimer::FlexLayoutComponent.new(justify_content: :flex_end, mb: 3)) do |flex| %> + <%= flex.with_column do %> + <%= link_to(new_work_package_issue_path(@work_package)) do %> + <%= render(Primer::Beta::Button.new( + scheme: :primary, + block: false, + mb: 3 + )) do |component| %> + Create issue + <% end %> + <% end %> + <% end %> + <% end %> + <%= render(WorkPackageTab::Issues::ListComponent.new(work_package: @work_package, resolved: true)) %> + diff --git a/modules/meeting/app/views/work_package_issues/edit.html.erb b/modules/meeting/app/views/work_package_issues/edit.html.erb new file mode 100644 index 000000000000..1db6b5cb46bb --- /dev/null +++ b/modules/meeting/app/views/work_package_issues/edit.html.erb @@ -0,0 +1,42 @@ +<%#-- 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. + +++#%> + + + <%= link_to(open_work_package_issues_path(@work_package)) do %> + <%= render(Primer::Beta::Button.new( + scheme: :secondary, + block: false, + mb: 3 + )) do |component| %> + <% component.with_leading_visual_icon(icon: "arrow-left") %> + Back to issues + <% end %> + <% end %> + <%= render(WorkPackageTab::Issues::FormComponent.new(issue: @issue)) %> + diff --git a/modules/meeting/app/views/work_package_issues/edit_resolution.html.erb b/modules/meeting/app/views/work_package_issues/edit_resolution.html.erb new file mode 100644 index 000000000000..8482d47c7acd --- /dev/null +++ b/modules/meeting/app/views/work_package_issues/edit_resolution.html.erb @@ -0,0 +1,42 @@ +<%#-- 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. + +++#%> + + + <%= link_to(open_work_package_issues_path(@work_package)) do %> + <%= render(Primer::Beta::Button.new( + scheme: :secondary, + block: false, + mb: 3 + )) do |component| %> + <% component.with_leading_visual_icon(icon: "arrow-left") %> + Back to issues + <% end %> + <% end %> + <%= render(WorkPackageTab::Issues::ResolveComponent.new(issue: @issue)) %> + diff --git a/modules/meeting/app/views/work_package_issues/new.html.erb b/modules/meeting/app/views/work_package_issues/new.html.erb new file mode 100644 index 000000000000..1db6b5cb46bb --- /dev/null +++ b/modules/meeting/app/views/work_package_issues/new.html.erb @@ -0,0 +1,42 @@ +<%#-- 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. + +++#%> + + + <%= link_to(open_work_package_issues_path(@work_package)) do %> + <%= render(Primer::Beta::Button.new( + scheme: :secondary, + block: false, + mb: 3 + )) do |component| %> + <% component.with_leading_visual_icon(icon: "arrow-left") %> + Back to issues + <% end %> + <% end %> + <%= render(WorkPackageTab::Issues::FormComponent.new(issue: @issue)) %> + diff --git a/modules/meeting/app/views/work_package_issues/new_meeting.html.erb b/modules/meeting/app/views/work_package_issues/new_meeting.html.erb new file mode 100644 index 000000000000..ba519e4d1a2b --- /dev/null +++ b/modules/meeting/app/views/work_package_issues/new_meeting.html.erb @@ -0,0 +1,44 @@ +<%#-- 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. + +++#%> + + + <%= link_to(open_work_package_issues_path(@work_package)) do %> + <%= render(Primer::Beta::Button.new( + scheme: :secondary, + block: false, + mb: 3 + )) do |component| %> + <% component.with_leading_visual_icon(icon: "arrow-left") %> + Back to issues + <% end %> + <% end %> +

+ To be implemented! +

+
diff --git a/modules/meeting/app/views/work_package_issues/open.html.erb b/modules/meeting/app/views/work_package_issues/open.html.erb new file mode 100644 index 000000000000..eaa729ee7c53 --- /dev/null +++ b/modules/meeting/app/views/work_package_issues/open.html.erb @@ -0,0 +1,45 @@ +<%#-- 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. + +++#%> + + + <%= render(::Base::OpPrimer::FlexLayoutComponent.new(justify_content: :flex_end, mb: 3)) do |flex| %> + <%= flex.with_column do %> + <%= link_to(new_work_package_issue_path(@work_package)) do %> + <%= render(Primer::Beta::Button.new( + scheme: :primary, + block: false, + mb: 3 + )) do |component| %> + Create issue + <% end %> + <% end %> + <% end %> + <% end %> + <%= render(WorkPackageTab::Issues::ListComponent.new(work_package: @work_package, resolved: false)) %> + diff --git a/modules/meeting/config/routes.rb b/modules/meeting/config/routes.rb index 0c743ee9e843..4b3aeaad057e 100644 --- a/modules/meeting/config/routes.rb +++ b/modules/meeting/config/routes.rb @@ -31,7 +31,44 @@ resources :meetings, only: %i[index new create] end + resources :work_packages, only: %i[] do + resources :issues, only: %i[new create edit update destroy], controller: 'work_package_issues' do + collection do + get :open + get :closed + end + member do + get :edit_resolution + patch :resolve + patch :reopen + get :new_meeting + patch :save_meeting + end + end + end + resources :meetings do + resources :agenda_items, controller: 'meeting_agenda_items' do + collection do + get 'new(/:work_package_id)', action: :new, as: :new + get :cancel_new + put :lock + put :unlock + put :close + put :open + end + member do + get :cancel_edit + put :drop + get :edit_issue_resolution + get :cancel_edit_issue_resolution + patch :resolve_issue + get :edit_notes + get :cancel_edit_notes + patch :save_notes + end + end + resource :agenda, controller: 'meeting_agendas', only: [:update] do member do get :history diff --git a/modules/meeting/db/migrate/20230621142734_create_meeting_agenda_items.rb b/modules/meeting/db/migrate/20230621142734_create_meeting_agenda_items.rb new file mode 100644 index 000000000000..d42908e586bc --- /dev/null +++ b/modules/meeting/db/migrate/20230621142734_create_meeting_agenda_items.rb @@ -0,0 +1,16 @@ +class CreateMeetingAgendaItems < ActiveRecord::Migration[5.1] + def change + create_table :meeting_agenda_items do |t| + t.references :meeting, foreign_key: true + t.references :user, foreign_key: true + t.string :title + t.text :description + t.integer :position + t.integer :duration_in_minutes + t.datetime :start_time + t.datetime :end_time + + t.timestamps + end + end +end \ No newline at end of file diff --git a/modules/meeting/db/migrate/20230629193312_add_optional_work_package_reference_to_meeting_agenda_items.rb b/modules/meeting/db/migrate/20230629193312_add_optional_work_package_reference_to_meeting_agenda_items.rb new file mode 100644 index 000000000000..b9b184cac724 --- /dev/null +++ b/modules/meeting/db/migrate/20230629193312_add_optional_work_package_reference_to_meeting_agenda_items.rb @@ -0,0 +1,5 @@ +class AddOptionalWorkPackageReferenceToMeetingAgendaItems < ActiveRecord::Migration[5.1] + def change + add_reference :meeting_agenda_items, :work_package, foreign_key: true + end +end \ No newline at end of file diff --git a/modules/meeting/db/migrate/20230630162748_add_input_output_to_meeting_agenda_items.rb b/modules/meeting/db/migrate/20230630162748_add_input_output_to_meeting_agenda_items.rb new file mode 100644 index 000000000000..b3156483a4cc --- /dev/null +++ b/modules/meeting/db/migrate/20230630162748_add_input_output_to_meeting_agenda_items.rb @@ -0,0 +1,6 @@ +class AddInputOutputToMeetingAgendaItems < ActiveRecord::Migration[5.1] + def change + add_column :meeting_agenda_items, :input, :text + add_column :meeting_agenda_items, :output, :text + end +end \ No newline at end of file diff --git a/modules/meeting/db/migrate/20230720142115_add_details_to_meeting_agenda_items.rb b/modules/meeting/db/migrate/20230720142115_add_details_to_meeting_agenda_items.rb new file mode 100644 index 000000000000..6086870a7470 --- /dev/null +++ b/modules/meeting/db/migrate/20230720142115_add_details_to_meeting_agenda_items.rb @@ -0,0 +1,5 @@ +class AddDetailsToMeetingAgendaItems < ActiveRecord::Migration[5.1] + def change + add_column :meeting_agenda_items, :details, :text + end +end \ No newline at end of file diff --git a/modules/meeting/db/migrate/20230726182337_add_lock_to_meetings.rb b/modules/meeting/db/migrate/20230726182337_add_lock_to_meetings.rb new file mode 100644 index 000000000000..a87c65b05468 --- /dev/null +++ b/modules/meeting/db/migrate/20230726182337_add_lock_to_meetings.rb @@ -0,0 +1,5 @@ +class AddLockToMeetings < ActiveRecord::Migration[5.1] + def change + add_column :meetings, :agenda_items_locked, :boolean, default: false + end +end diff --git a/modules/meeting/db/migrate/20230727153643_add_agenda_items_state_to_meetings.rb b/modules/meeting/db/migrate/20230727153643_add_agenda_items_state_to_meetings.rb new file mode 100644 index 000000000000..00ff12f0af15 --- /dev/null +++ b/modules/meeting/db/migrate/20230727153643_add_agenda_items_state_to_meetings.rb @@ -0,0 +1,6 @@ +class AddAgendaItemsStateToMeetings < ActiveRecord::Migration[5.1] + def change + remove_column :meetings, :agenda_items_locked + add_column :meetings, :agenda_items_state, :integer, default: 0 + end +end diff --git a/modules/meeting/db/migrate/20230728115607_create_work_package_issues.rb b/modules/meeting/db/migrate/20230728115607_create_work_package_issues.rb new file mode 100644 index 000000000000..4a2286aba0a4 --- /dev/null +++ b/modules/meeting/db/migrate/20230728115607_create_work_package_issues.rb @@ -0,0 +1,15 @@ +class CreateWorkPackageIssues < ActiveRecord::Migration[5.1] + def change + create_table :work_package_issues do |t| + t.references :work_package + t.references :author, foreign_key: { to_table: :users } + t.references :resolved_by, foreign_key: { to_table: :users }, null: true + t.integer :issue_type, default: 0, null: false + t.text :description + t.text :resolution + t.datetime :resolved_at + + t.timestamps + end + end +end diff --git a/modules/meeting/db/migrate/20230801102556_add_optional_issue_reference_to_meeting_agenda_items.rb b/modules/meeting/db/migrate/20230801102556_add_optional_issue_reference_to_meeting_agenda_items.rb new file mode 100644 index 000000000000..c7a6fda2ee66 --- /dev/null +++ b/modules/meeting/db/migrate/20230801102556_add_optional_issue_reference_to_meeting_agenda_items.rb @@ -0,0 +1,8 @@ +class AddOptionalIssueReferenceToMeetingAgendaItems < ActiveRecord::Migration[5.1] + def change + remove_reference :meeting_agenda_items, :work_package + remove_column :meeting_agenda_items, :input, :text + remove_column :meeting_agenda_items, :output, :text + add_reference :meeting_agenda_items, :work_package_issue, foreign_key: true + end +end diff --git a/modules/meeting/frontend/module/issues-tab/issues-tab.component.ts b/modules/meeting/frontend/module/issues-tab/issues-tab.component.ts new file mode 100644 index 000000000000..a28d0943f7aa --- /dev/null +++ b/modules/meeting/frontend/module/issues-tab/issues-tab.component.ts @@ -0,0 +1,65 @@ +//-- 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. +//++ + +import { Component, Input, OnInit, AfterViewInit, ElementRef } from '@angular/core'; +import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource"; +import { TabComponent } from "core-app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/tab"; +import { I18nService } from "core-app/core/i18n/i18n.service"; +import { PathHelperService } from "core-app/core/path-helper/path-helper.service"; + +@Component({ + selector: 'issues-tab', + templateUrl: './issues-tab.template.html' +}) +export class IssuesTabComponent implements OnInit, TabComponent { + @Input() public workPackage: WorkPackageResource; + turboFrameSrc: string; + + constructor( + private elementRef: ElementRef, + readonly PathHelper: PathHelperService, + readonly I18n: I18nService + ) {} + + ngOnInit(): void { + const storedSrc = localStorage.getItem(`turboFrameSrcIssuesTabForWorkPackage${this.workPackage.id}`); + this.turboFrameSrc = storedSrc ? storedSrc : `/work_packages/${this.workPackage.id}/issues/open`; + } + + ngAfterViewInit(): void { + const turboFrame = this.elementRef.nativeElement.querySelector('#work-package-issues-tab-content'); + if (turboFrame) { + turboFrame.addEventListener('turbo:frame-load', (event: Event) => { + console.log(event) + const target = event.target as HTMLElement; + const newSrc = target.getAttribute('src'); + localStorage.setItem(`turboFrameSrcIssuesTabForWorkPackage${this.workPackage.id}`, newSrc||''); + }); + } + } +} diff --git a/modules/meeting/frontend/module/issues-tab/issues-tab.template.html b/modules/meeting/frontend/module/issues-tab/issues-tab.template.html new file mode 100644 index 000000000000..de104cdeb336 --- /dev/null +++ b/modules/meeting/frontend/module/issues-tab/issues-tab.template.html @@ -0,0 +1 @@ + diff --git a/modules/meeting/frontend/module/main.ts b/modules/meeting/frontend/module/main.ts new file mode 100644 index 000000000000..6c059109e3ce --- /dev/null +++ b/modules/meeting/frontend/module/main.ts @@ -0,0 +1,64 @@ +// -- 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, +// 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. + +import { + Injector, + NgModule, + CUSTOM_ELEMENTS_SCHEMA +} from '@angular/core'; +import { OpSharedModule } from 'core-app/shared/shared.module'; +import { OpenprojectTabsModule } from 'core-app/shared/components/tabs/openproject-tabs.module'; +import { WorkPackageTabsService } from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service'; +import { IssuesTabComponent } from './issues-tab/issues-tab.component'; + +export function initializeMeetingPlugin(injector:Injector) { + const wpTabService = injector.get(WorkPackageTabsService); + wpTabService.register({ + component: IssuesTabComponent, + name: "Issues", + id: 'issues', + displayable: (workPackage) => true, + }); +} + +@NgModule({ + imports: [ + OpSharedModule, + OpenprojectTabsModule, + ], + declarations: [ + IssuesTabComponent, + ], + exports: [ + IssuesTabComponent, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class PluginModule { + constructor(injector:Injector) { + initializeMeetingPlugin(injector); + } +} diff --git a/modules/meeting/lib/open_project/meeting/engine.rb b/modules/meeting/lib/open_project/meeting/engine.rb index 59680de3d489..3e10d320a020 100644 --- a/modules/meeting/lib/open_project/meeting/engine.rb +++ b/modules/meeting/lib/open_project/meeting/engine.rb @@ -38,7 +38,7 @@ class Engine < ::Rails::Engine author_url: 'https://www.openproject.org', bundled: true do project_module :meetings do - permission :view_meetings, meetings: %i[index show], meeting_agendas: %i[history show diff], + permission :view_meetings, meetings: %i[index index_in_wp_tab show show_in_wp_tab], meeting_agendas: %i[history show diff], meeting_minutes: %i[history show diff] permission :create_meetings, { meetings: %i[new create copy] }, diff --git a/modules/meeting/openproject-meeting.gemspec b/modules/meeting/openproject-meeting.gemspec index a1de2f76ff67..e927dfd8e837 100644 --- a/modules/meeting/openproject-meeting.gemspec +++ b/modules/meeting/openproject-meeting.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |s| and invitees." s.license = 'GPLv3' - s.files = Dir['{app,config,db,lib,doc}/**/*', 'README.md'] + s.files = Dir['{app,config,db,lib,frontend,doc}/**/*', 'README.md'] s.add_dependency 'icalendar', '~> 2.8.0' s.metadata['rubygems_mfa_required'] = 'true' diff --git a/spec/components/docs/01_design-system.md.erb b/spec/components/docs/01_design-system.md.erb new file mode 100644 index 000000000000..cdb3402f81ce --- /dev/null +++ b/spec/components/docs/01_design-system.md.erb @@ -0,0 +1,62 @@ +--- +title: Design System +--- + +At OpenProject, we use a design system to help ensure that our design delivers a consistent experience to users. The system describes the styles, components and patterns that come together to build the foundation for a consistent user experience. To do this, we provide documentation explaining how each element should be used, the different states, variations and options it offers, along with code examples. + +Our goal is to reach a point where every view in OpenProject is built with our design system principles and patterns in mind. However, we are aware that reaching this goal will take time for a tool as complex and layered as OpenProject. + +As such, the design system is still in its infancy and will grow with each release. + +## Purpose (what it is and what it's not) + +This Storybook-based design system documentation only seeks to describe the actual reusable components; it does not seek to document features where these components are used. + +**The purpose of the design system is to:** + +- Ensure that OpenProject has a beautiful, usable and delightful design +- Improve consistency and predictability by defining standards that are reused or extended when there is a need for deviations or alternate versions +- Improve quality (a component that is reused is likely to have gone through more testing and adjustment with each reuse and testing cycle) +- Make design assets easier to maintain (change once to change all) +- Improve accessibility by taking accessibility requirements into account when designing components and patterns + +**The purpose of this document is to:** + +- Maintain a singular reference on what reusable components and patterns look like, how they behave and what options they provide +- Allow product designers and front-end developers to have a shared understanding of how these common elements are designed and how/when they should be used +- Provide quality assurance personnel a common base of reference when testing features that use these reusable components and patterns (and there is a doubt about an unnecessary deviation) +- Give developers important context about available components and how to use them. We want to enable both team and community members to compose and develop complex interfaces easily, preventing fragmentation and technical debt where possible. + +**This document does not:** + +- Define every single component in OpenProject +- Define specific features or implementation details +- Define how things *should* look like; the stories contained here will only describe things as they are currently implemented +- Define possible exceptions or variations made within a feature (if necessary) +- Seek to serve as a central documentation for QA testing of features +- Seek to be exhaustive at any point; it will remain a living document that require maintenance as changes are made + + +## SPOT + +Our design system is referred to simply as "OpenProject Design System". + +You will often see the `spot-` prefix used in code; this is primarily to distinguish newer refactored components from older elements that have the `op-` prefix. + +## Approach + +OpenProject is a complex, powerful tool. One of its key strengths is its customisability and its ability to adapt to a range of different needs. This includes complex filtering options, custom types and statuses, custom fields and a wide range of options to configure views and work package forms. + +Nevertheless, it is very important that OpenProject be intuitive for new users who might not necessarily need that complexity, or indeed be overwhelmed by it. + +Our design approach aims to strike the right balance between powerful and accessible with a two-tiered approach: apply sane defaults and present the most common options, and allow advanced users the option (via an additional click) to customise and fine-tune. + +## The UX of Open Source + +As an open source project with a considerably long history and a large number of contributors, different parts of OpenProject have evolved at different paces, sometimes with completely different technology. Similar components are sometimes implemented somewhat differently in different parts of the software, and there are even multiple implementations of the same basic design. + +This is quite normal for a large open-source project that has not had a dedicated design team for most of its conception. + +One of the goals of the design system is to introduce more coherence and introduce a more modern design language. Whilst we would naturally prefer to be able to update everything at the same time and push the new design system to the entire software, we recognise the need for a more pragmatic approach. The design system will be rolled out in phases, with a careful study of the consequences of updating each component or pattern, and the potential dependencies that will be affected. + +_We recognise that UI/UX has not always been the highest priority for open-source projects. This is somewhat understandable given how open source projects have relatively fewer design resources dedicated to it than commercial products. Our goal is to do our part to improve that situation as much as we can and document our process._ diff --git a/spec/components/docs/02_devices-and-accessibility.md.erb b/spec/components/docs/02_devices-and-accessibility.md.erb new file mode 100644 index 000000000000..b06b989b518e --- /dev/null +++ b/spec/components/docs/02_devices-and-accessibility.md.erb @@ -0,0 +1,25 @@ +--- +title: Devices and Accessbility +--- + +## Desktop-first + +OpenProject is not primarily designed for mobile use or with a mobile-first approach, despite most parts of the software adapting fairly well to smaller screens. The majority of features and views are designed to take advantage of larger screens and more complex interactions, including keyboard shortcuts. + +Nevertheless,  certain features are particularly useful in a mobile context. These include accessing notifications on the go, viewing work packages, reading and responding to comments and viewing attachments and linked files. These features will be given particular attention and optimised for mobile use. + +We do not consider project planning, complex scheduling and team planning to be priority use cases on mobile. + +## Accessibility + +We recognise the importance of accessibility and are aware that OpenProject still has a fair bit of progress to make in this regard. We intend to initially focus on improving contrast, ensuring UI elements have meaningful alt-text and descriptions and expanding the support for keyboard shortcuts. + +We will incrementally evaluate ways to improve our design to better serve users with visual, auditory or motor impairment. + +## Localisation and internationalisation + +OpenProject supports several languages. Our designs must take internalisation into account, notably in terms of string length and spacing. + +The primary design language is English. Our Figma prototypes are always designed first with strings in English and, when relevant, tested with German, French and Spanish translations. This covers our largest user base. + +For other languages, we rely on our translators and our community on \[Crowdin\](https://crowdin.com/translate/openproject/) for their help. If there are design-specific issues that are present in certain languages, we encourage the community to file bug reports so we may fix them. diff --git a/spec/components/docs/03-how-to-use.md.erb b/spec/components/docs/03-how-to-use.md.erb new file mode 100644 index 000000000000..bd5aecfe4964 --- /dev/null +++ b/spec/components/docs/03-how-to-use.md.erb @@ -0,0 +1,65 @@ +--- +title: Using this resource +--- + +We use this lookbook to document our design system. You will find defined styles, components and patterns in the relevant sections. + +You will not find _all_ elements in Storybook yet. The design system at OpenProject is a work in progress; many parts of the software still use older elements that have not been standardised and defined. + +As we work through defining and coumenting each new component, we will find them here. + +## Other resources + +All of the components described here also exist in our Figma libraries: + +- [Foundation styles](https://www.figma.com/file/vOw6PEVIyzaQOIgf02VZFW/Foundations-Library?node-id=228%3A2) +- [Components](https://www.figma.com/file/XhCsrvs6rePifqbBpKYRWD/Components-Library?node-id=386%3A3606) + +_You might find examples of some older elements in the older [Living Style Guide](http://spike.openproject-stage.com:4200/assets/styleguide.html#forms). This will eventually be replaced by Storybook, but this will require time._ + + +## Semantics + +When describing components, we use certain words in very particular ways: + +### Disabled + +_Disabled_ is when no interaction is possible. A disabled element can itself have multiple states: a checkbox can be checked and disabled, a switch can be ON and disabled, a dropdown might have a value but but disabled. + +The opposite of disabled is _enabled_. All elements are described in their enabled state unless explicitly mentioned. + +### Focused and active + +Focus simply refers to when an element has the current object in DOM that has the input focus. In OpenProject, elements that are focused generally have a blue outline. + +The opposite of _in focus_ (or focused) is _not in focus_ (or focused). + +_Active_ is a similar state as focused, but not entirely the same. For example, in the date picker, the start date can be "active" (meaning that clicking on a date in the mini-calendar will change the start date) but not in focus (because the mouse has clicked outside of the field). + +The opposite of active is _inactive_. + +### Checked and on + +Checkboxes can be _checked_ (true) or _unchecked_ (false) + +Radio options can also be "checked" (as in, in a set of three options, only one can be checked) despite this not being the technically correct word. This isi to to distinguish it from "selected" (which refers to selecting an element or an area with the cursor). + +Switches can be _ON_ (true) or _OFF_ (false). + +## Browsing through the docs + +This Storybook is divided in three sections: + +**Styles** include foundational elements like typography, colour, spacing and shadows. All other elements are defined using these base styles. + +**Components** are individual elements (like checkboxes, buttons or text fields), each of which has its own states, variants and behaviour. + +**Patterns** are larger, recurring UI elements made up of a set of different components. Each component used in a pattern still retains its own specificities (options, variatns), but can also have certiain behaviour at the level of the pattern itself. (For example, an action bar can have two or three buttons, which can either be on the left side or the right side). + +## To be added: + +- What the left-side menu includes (and does not include) +- How to find linked/dependent elements +- Contributing: + - As a developer + - As a designer diff --git a/spec/components/docs/styles/01-typography.md.erb b/spec/components/docs/styles/01-typography.md.erb new file mode 100644 index 000000000000..f6fade140af8 --- /dev/null +++ b/spec/components/docs/styles/01-typography.md.erb @@ -0,0 +1,195 @@ +--- +title: Typography +--- + +<%= include_spot_assets %> + +OpenProject uses the "[Lato Sans](https://github.com/latofonts/lato-source)" typeface created by Adam Twardoch, Botio Nikoltchev, and Łukasz Dziedzic (released with the [SIL Open Font license](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)). + +There are 7 sizes, each with between three and eight variations. + +## Considerations + +A few things to keep in mind: + +### Black text + +“Black” text in OpenProject does not use the standard black HEX code (#000000); instead, black is defined as Grey-1 (#333333). + +### Line height + +The default line height is 16px, which corresponds to 1 REM with our Spacing baseline. + +For each style, line height is specified with a slash. For example, for Header Small Bold, 24/32 represents 1.5 REM font size and 2 REM line height. + +## Header Big + +This is a larger header than the default, used in rare occasions where a header is needed for a containing element to a page. To be used only if absolutely necessary. Prefer "Header small" as much as possible. + +So far, zero recorded use. Might get removed if this style is not used in until mid-2022. + +
+

+ Aa +
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ +
+ abcdefghijklmnopqrstuvwxyz +
+ 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

+
+ + + +## Header Small + +This is the default type used for page headers. "Bold" is default and is almost always preferred, unless there is a need to use regular to distinguish additional or supporting information. + +
+

+ Aa +
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ +
+ abcdefghijklmnopqrstuvwxyz +
+ 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

+
+ + + +## Subheader Big + +Very rarely used. Used if we need a header small than the regular header but still distinct from body text. + +So far, zero recorded use. Might get removed if this style is not used in until mid-2022. + +
+

+ Aa +
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ +
+ abcdefghijklmnopqrstuvwxyz +
+ 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

+
+ + + +## Subheader Small + +Very rarely used. Used if we need a header small than the regular header but still distinct from body text. + +
+

+ Aa +
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ +
+ abcdefghijklmnopqrstuvwxyz +
+ 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

+
+ + + +## Subheader Extra Small + +This is really an alias of _Body Small/Bold_ when this style is used as a header (in some modals) instead of body text. + +
+

+ Aa +
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ +
+ abcdefghijklmnopqrstuvwxyz +
+ 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

+
+ + + +## Body Big + +Used occasionally (where?) + +
+

+ Aa +
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ +
+ abcdefghijklmnopqrstuvwxyz +
+ 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

+
+ +## Body Small + +This is the default style for most text on OpenProject. This goes for labels but also input text. By far the most used style style in the application. + +The regular version is used, among other places, on: +- Button text +- Drop down select +- Text fields +- Table text +- Card: work package title +- Sidebar element + +The bold version is used in: + +- Table headers +- Calendar header +- Sidebar header + +**Example** + +
+

+ Aa +
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ +
+ abcdefghijklmnopqrstuvwxyz +
+ 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

+
+ +## Caption + +The caption is used for indications and auxiliary information that usually adds context to a view, but are not primary elements. They are also used on elements where space is limited (like the date information on Team planner or Board cards). + +The regular version is used, among other places, in: + +- Chips +- Toast text +- Tooltips +- Card: project name and ID + +The bold version is used in: + +- Sidebar tabs + +**Example** + +
+

+ Aa +
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ +
+ abcdefghijklmnopqrstuvwxyz +
+ 0123456789 ! ? & / ( ) € $ £ ¥ ¢ = @ ; : , . +

+
+ diff --git a/spec/components/docs/styles/02-colors.md.erb b/spec/components/docs/styles/02-colors.md.erb new file mode 100644 index 000000000000..3c1e2692b261 --- /dev/null +++ b/spec/components/docs/styles/02-colors.md.erb @@ -0,0 +1,237 @@ +--- +title: Colors +--- + +**Note**: This section is a work-in-progress. If you are unsure about which colour to use, please get in touch with the UX team. + +Because OpenProject can be customised with custom colour schemes, our foundation library only describe the colour palette of the default OpenProject theme. + +Colours are organised in this format: **Category/_name_**. + +There there are six categories: + +- **Basic** +- **Main** +- **Accent** +- **Danger** +- **Indication** +- **Feedback** + +## Basic + +The basic colour set include eight shades of gray used mostly for basic text and backgrounds. The shades do not have additional semantic significance are are simply a continuum between **Basic/_Black_** and **Basic/_White_**. + +The choice of which gray to use depends on the colourspace. + +Note: **Basic/_Gray-1_** is the default colour for "black" text. Pure black (_Basic/Black_) is generally not used in OpenProject. + +> Code example here (Black, Gray-1, Gray-2, Gray-3, Gray-4, Gray-5, Gray-6, White) + +## Main + +The **Main/** colourset is a set of three shades of the OpenProject blue. + +**Main/_Main_** + +> Example + +This is the primary blue used for all main interactions and action buttons, including text links. + +It is also used in the background of the header. + +**Main/_Main Dark_** + +> Example + +This darker version of the main blue is mainly used: + +- to communicate state information (a hover or a pressed state of a button, for example) +- when the use of **Main/_Main_** does not provide enough contrast (for a border colour, for example) + +**Main/_Main Light_** + +> Example + +This lighter version of the main blue is used: + +- to communicate state information (hover on dop-down list or indicating selected toggle element, for example) +- as a background colour of interactive elements (tooltip, toast or information banners) + +## Accent + +> Example + +The **Accent/** colourset is a set of three shades of green that is used to accent certain special functions. This colour should be used sparingly, so that when it is used, it draws the user’s eye. + +**Accent/_Accent_** + +> Example + +This is the primary accent green, used for example in the default state of an accent button. + +**Accent/_Accent Dark_** + +> Example + +This darker version is mainly used: + +- to communicate state information (a hover or a pressed state of a button, for example) +- when the use of **Accent/Main** does not provide enough contrast (for a border colour, for example) + + +**Accent/_Accent Light_** + +> Example + +This lighter version of the main green is used: + +- to communicate state information (hover on dop-down list or indicating selected toggle element, for example) +- as a background colour of interactive elements (tooltip, toast or information banners) + +## Danger + +The **Danger/** colourset is a set of three shades of red used to communicate potential danger to the user, or to warn them of a problem. + +It should also be used to indicate potential destructive actions (actions like delete account or delete user) and exceptionally also used to indicate information (like in a notification badge) when the Indication/ colours cannot be used. + +**Danger/_Danger_** + +> Example + +This is the primary danger red, used for example in the default state of an accent button. + +**Danger/_Danger Dark_** + +> Example + +This darker version is mainly used: + +- to communicate state information (a hover or a pressed state of a button, for example) +- when the use of **Danger/Danger** does not provide enough contrast (for a border colour, for example) + +**Danger/_Danger Light_** + +> Example + +This darker version of the main danger red is used: + +- to communicate state information (hover on dop-down list or indicating selected toggle element, for example) +- as a background colour of interactive elements (tooltip, toast or information banners) + +## Indication + +Indication colours are used to indicate certain special states. + +**Indication/_Flagged_** + +> Example + +This colour is used when “flagging” work packages. It is a teal colour meant to be distinct from **Indication/_Attention_**. + +**Indication/_Current date_** + +> Example + +This colour is used to indicate the current date (today) in a calendar. + +**Indication/_Enterprise_** + +> Example + +This colour is used to indicate that a certain feature or module requires the Enterprise plan to use. + +**Indication/Focus** + +> Example + +This is the colour used to indicate which element has keyboard focus. + +## Feedback + +The **Feedback/**- colors are used to communicate for user feedback: + +- Error +- Warning +- Info +- Success + +Each of these has two versions (Dark and Light), the dark one usually used for the foreground text and icons and the light one for the background. + +These are typically used in toast messages. + +**Feedback/_Error_** + +> Example, Error Light and Dark + +This colour indicates that a response that is different from what the user would have expected. The user flow is usually interrupted when they see an error of this type. + +**Feedback/_Warning_** + +> Example, Warning Light and Dark + +The warning is information that suggests something requires attention (and that could cause a problem), but that no error has been caused, and that the expected user flow can continue. + +**Feedback/_Info_** + +> Example, Info Light and Dark + +This colour is used to indicate additional information that could be of interest to the user. (Like a toast that says “Loading...”, indicating something is happening in the background) + +**Feedback/_Success_** + +> Example, Success Light and Dark + +This colour indicates that the requested user action was successful. It should be used sparingly and only when such a feedback is absolutely required. + +
+ +<% +tokens = JSON.parse File.read(Rails.root.join('frontend/src/app/spot/styles/tokens/dist/tokens.json')) +tokens.select { |k, v| k.start_with?('spot-color-') }.each do |name, value| + %> + +
+
+
<%= name %>
+
<%= value %>
+
+<% end %> + +
+ + diff --git a/spec/components/docs/styles/03-spacings.md.erb b/spec/components/docs/styles/03-spacings.md.erb new file mode 100644 index 000000000000..048cbadd5949 --- /dev/null +++ b/spec/components/docs/styles/03-spacings.md.erb @@ -0,0 +1,58 @@ +--- +title: Spacings +--- + +If I write some explanatory text around these spacings. + +
+<% +tokens = JSON.parse File.read(Rails.root.join('frontend/src/app/spot/styles/tokens/dist/tokens.json')) +tokens + .select { |k, v| k.start_with?('spot-spacing-') } + .sort_by { |k, | k.split('-')[2].gsub('_', '.').to_f } + .each do |name, value| + %> +
+
<%= name %>
+
<%= value %>
+
+
+<% end %> +
+ + diff --git a/spec/components/docs/styles/04-shadows.md.erb b/spec/components/docs/styles/04-shadows.md.erb new file mode 100644 index 000000000000..816ed4bed19f --- /dev/null +++ b/spec/components/docs/styles/04-shadows.md.erb @@ -0,0 +1,95 @@ +--- +title: Shadows +--- + +# Shadows + +Shadows are important when certain components are displayed on top of other components. This is usually the case with contextual menus, drop-downs or dialogues that supplement or expand an existing view. + +Although it is best to avoid layering beyond two levels (a base screen + an overlay), it is sometimes necessary and indeed unavoidable. + +We use different shadows to communicate depth and allow the user to intuitively understand what is "on top". + +Our shadows definitions divided between Light and Hard and three levels of elevation. The shadow is always based on a black #000000 transparency level, a X and Y px value and a spread px value. + +<% +cols = ['Low', 'Mid', 'High'] +rows = ['Light', 'Hard'] +tokens = JSON.parse File.read(Rails.root.join('frontend/src/app/spot/styles/tokens/dist/tokens.json')) +tokens = tokens + .select { |k, v| k.start_with?('spot-shadow-') } + .sort_by { |k, | k.split('-')[2].gsub('_', '.').to_f } + .to_h + %> + + + + + + <% cols.each do |col| %> + + <% end %> + + <% rows.each do |row| %> + + + <% cols.each do |col| %> + <% key = "spot-shadow-#{row.downcase}-#{col.downcase}" %> + + <% end %> + +<% end %> + +
<%= col %>
<%= row %> +
+
<%= key %>
+
<%= tokens[key] %>
+
+
+ + + diff --git a/spec/components/docs/styles/05-focus.md.erb b/spec/components/docs/styles/05-focus.md.erb new file mode 100644 index 000000000000..6d0139ca20a6 --- /dev/null +++ b/spec/components/docs/styles/05-focus.md.erb @@ -0,0 +1,9 @@ +--- +title: Focus state +--- + +Focus state in OpenProject is represented by a 2-pixel outline with colour _Indication/Focus_ around the focused element. + +> Example of some elements with the focus state simulated + +**Note**: _Active_ is a similar state as focused, but not entirely the same. For example, in the date picker, the start date can be "active" (meaning that clicking on a date in the mini-calendar will change the start date) but not in focus (because the mouse pointer has clicked outside of the field and the focus is technically outside the field). diff --git a/spec/components/docs/styles/06-icons.md.erb b/spec/components/docs/styles/06-icons.md.erb new file mode 100644 index 000000000000..607dd3212af7 --- /dev/null +++ b/spec/components/docs/styles/06-icons.md.erb @@ -0,0 +1,100 @@ +--- +title: Icons +--- + +<%= include_spot_assets %> + +# Icons + +Icons are used as visual indicators or small illustrations. They provide additional context when necessary. + + + + + + + +## Design notes + +Icons should generally not be used on its own without a label, especially on interactive elements, where their function might be ambiguous. + +There are notable examples: if the meaning of the button is abundantly clear without a label and space is limited (or repetitive elements), an icon-only interactive element is possible (for example, the "Mark as read" button on each notification element). + +However, icon-only buttons or links should never be used for critical primary functions like Save, Confirm, Remove and Delete. + +## Usage + +Icons are a CSS class only. To use them, apply the `spot-icon` BEM block on a generic inline element like `span`. + +## Options + +There are four pre-defined dimensions for icons: 12×12, 16×16, 20×20 and 24×24. + +The default size is 16×16. + + + + + This is a spot-link + + + +## Available icons + +<% +icons = JSON.parse File.read(Rails.root.join('frontend/src/app/spot/icon-font/icons.json')) +%> + +
+<% icons.each do |name| %> +
+
+ +
+
<%= name %>
+
+<% end %> +
; + + diff --git a/spec/components/icon_component_spec.rb b/spec/components/icon_component_spec.rb new file mode 100644 index 000000000000..eac713d91160 --- /dev/null +++ b/spec/components/icon_component_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe IconComponent, type: :component do + pending "add some examples to (or delete) #{__FILE__}" + + # it "renders something useful" do + # expect( + # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html + # ).to include( + # "Hello, components!" + # ) + # end +end diff --git a/spec/components/previews/advanced_filters_preview.rb b/spec/components/previews/advanced_filters_preview.rb new file mode 100644 index 000000000000..f5232c64c68d --- /dev/null +++ b/spec/components/previews/advanced_filters_preview.rb @@ -0,0 +1,4 @@ +class AdvancedFiltersPreview < Lookbook::Preview + def default + end +end diff --git a/frontend/src/global_styles/content/_advanced_filters.lsg b/spec/components/previews/advanced_filters_preview/default.html.erb similarity index 97% rename from frontend/src/global_styles/content/_advanced_filters.lsg rename to spec/components/previews/advanced_filters_preview/default.html.erb index d44da69818ec..081891fa21e0 100644 --- a/frontend/src/global_styles/content/_advanced_filters.lsg +++ b/spec/components/previews/advanced_filters_preview/default.html.erb @@ -1,8 +1,3 @@ -# Advanced filters - -``` -@full-width -
Selected filters
    @@ -121,7 +116,7 @@
- +
@@ -150,7 +145,7 @@
- +
@@ -340,4 +335,3 @@ -``` diff --git a/spec/components/previews/icon_component_preview.rb b/spec/components/previews/icon_component_preview.rb new file mode 100644 index 000000000000..b418b6083c14 --- /dev/null +++ b/spec/components/previews/icon_component_preview.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class IconComponentPreview < ViewComponent::Preview + + # Icon component + # ------------ + # Wrap an icon with the given name + # See [Icon page](/lookbook/pages/styles/icons) for available icons + # + # @param name + # @param classnames + def default(name: 'help1', classnames: '') + render(IconComponent.new(name:, classnames:)) + end +end diff --git a/spec/components/previews/switch_component_preview.rb b/spec/components/previews/switch_component_preview.rb new file mode 100644 index 000000000000..36a43aea8650 --- /dev/null +++ b/spec/components/previews/switch_component_preview.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class SwitchComponentPreview < ViewComponent::Preview + + # This component describes only the actual switch, without the label. + # For the full component, please refer to Selector field component, which provides a label. + # @param checked toggle + # @param disabled toggle + def default(checked: false, disabled: false) + render(SwitchComponent.new(checked:, disabled:)) + end + + # @param checked toggle + def disabled(checked: true) + render(SwitchComponent.new(checked:, disabled: true)) + end +end diff --git a/spec/components/previews/toolbar_component_preview.rb b/spec/components/previews/toolbar_component_preview.rb new file mode 100644 index 000000000000..2d1938ceded0 --- /dev/null +++ b/spec/components/previews/toolbar_component_preview.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ToolbarComponentPreview < Lookbook::Preview + + # A toolbar that can and should be used for actions on the current view. + # Initially designed for the Work package list, this can be reused throughout the application. + + def default + end + + def with_form_elements + end + + def with_labelled_form_elements + end +end diff --git a/spec/components/previews/toolbar_component_preview/default.html.erb b/spec/components/previews/toolbar_component_preview/default.html.erb new file mode 100644 index 000000000000..635b2c7cb56f --- /dev/null +++ b/spec/components/previews/toolbar_component_preview/default.html.erb @@ -0,0 +1,21 @@ +
+
+
+

Title of the page

+
+ +
+
diff --git a/spec/components/previews/toolbar_component_preview/with_form_elements.html.erb b/spec/components/previews/toolbar_component_preview/with_form_elements.html.erb new file mode 100644 index 000000000000..d206baf26000 --- /dev/null +++ b/spec/components/previews/toolbar_component_preview/with_form_elements.html.erb @@ -0,0 +1,38 @@ +
+
+
+

Dragonball Z characters

+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + + + Add + +
  • +
+
+

now with extremely long subtitle: Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste consequatur doloribus suscipit nemo temporibus deserunt alias incidunt doloremque officia rerum, nobis fuga, recusandae voluptatibus voluptatem tenetur repellendus itaque et. Eum.

+
diff --git a/spec/components/previews/toolbar_component_preview/with_labelled_form_elements.html.erb b/spec/components/previews/toolbar_component_preview/with_labelled_form_elements.html.erb new file mode 100644 index 000000000000..982dd29c8c12 --- /dev/null +++ b/spec/components/previews/toolbar_component_preview/with_labelled_form_elements.html.erb @@ -0,0 +1,36 @@ +
+
+
+

Dragonball Z characters

+
+
    +
  • +
    + +
    + +
  • +
  • +
    + +
    + +
  • +
+
+

now with extremely long subtitle: Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste consequatur doloribus suscipit nemo temporibus deserunt alias incidunt doloremque officia rerum, nobis fuga, recusandae voluptatibus voluptatem tenetur repellendus itaque et. Eum.

+
diff --git a/spec/components/previews/tooltip_component_preview.rb b/spec/components/previews/tooltip_component_preview.rb new file mode 100644 index 000000000000..e69a220cba5c --- /dev/null +++ b/spec/components/previews/tooltip_component_preview.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class TooltipComponentPreview < ViewComponent::Preview + def default + end +end diff --git a/spec/components/previews/tooltip_component_preview/default.html.erb b/spec/components/previews/tooltip_component_preview/default.html.erb new file mode 100644 index 000000000000..493fa633b419 --- /dev/null +++ b/spec/components/previews/tooltip_component_preview/default.html.erb @@ -0,0 +1,11 @@ +<%= render TooltipComponent.new do |component| %> + <% component.with_trigger do %> + Hover me to see the tooltip + <% end %> + + <% component.with_body do %> + + Content of the tooltip + + <% end %> +<% end %> diff --git a/spec/components/previews/widget_component_preview.rb b/spec/components/previews/widget_component_preview.rb new file mode 100644 index 000000000000..28638b3f2212 --- /dev/null +++ b/spec/components/previews/widget_component_preview.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# @label Widget boxes +class WidgetComponentPreview < Lookbook::Preview + def default + end +end diff --git a/spec/components/previews/widget_component_preview/default.html.erb b/spec/components/previews/widget_component_preview/default.html.erb new file mode 100644 index 000000000000..9970042f8751 --- /dev/null +++ b/spec/components/previews/widget_component_preview/default.html.erb @@ -0,0 +1,100 @@ +
+ + +
+

+
+

Widget Box

+
+

+

This widget box can be used to display content belonging to one subject.

+ +
+ + +
+

+
+

Widget Box 2

+
+

+ +
+ + +
+

+
+

Widget Box 3

+
+

+
+

+ Lorem ipsum dolor sit amet, his ei propriae suscipit. + Sit in atqui accumsan ponderum, eum ut luptatum lobortis, has ei tota illud detraxit. +

+
+ +
+ + +
+

+
+

Widget Box 4

+
+

+
    +
  • Enum1
  • +
  • Enum2
  • +
  • Enum3
  • +
+ +
+ +
diff --git a/spec/components/switch_component_spec.rb b/spec/components/switch_component_spec.rb new file mode 100644 index 000000000000..6f7fbbd2ab74 --- /dev/null +++ b/spec/components/switch_component_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe SwitchComponent, type: :component do + pending "add some examples to (or delete) #{__FILE__}" + + # it "renders something useful" do + # expect( + # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html + # ).to include( + # "Hello, components!" + # ) + # end +end diff --git a/spec/components/tooltip_component_spec.rb b/spec/components/tooltip_component_spec.rb new file mode 100644 index 000000000000..dfa1e7586b2b --- /dev/null +++ b/spec/components/tooltip_component_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe TooltipComponent, type: :component do + pending "add some examples to (or delete) #{__FILE__}" + + # it "renders something useful" do + # expect( + # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html + # ).to include( + # "Hello, components!" + # ) + # end +end diff --git a/spec/contracts/user_preferences/update_contract_spec.rb b/spec/contracts/user_preferences/update_contract_spec.rb index b50c10041cac..7b4a8f36e615 100644 --- a/spec/contracts/user_preferences/update_contract_spec.rb +++ b/spec/contracts/user_preferences/update_contract_spec.rb @@ -373,4 +373,32 @@ it_behaves_like 'contract is invalid', pause_reminders: :blank end end + + context "when theme is present" do + let(:settings) do + { + theme: + } + end + + context 'with allowed value' do + let(:theme) { 'light' } + + it_behaves_like 'contract is valid' + end + + context 'without allowed value' do + let(:theme) { 'not a theme' } + + it_behaves_like 'contract is invalid' + end + end + + context "when theme is not present" do + let(:settings) do + {} + end + + it_behaves_like 'contract is valid' + end end