diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 65861579447..65a6f65e107 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -70,6 +70,7 @@ def breadcrumbs def add_breadcrumb(name, link = nil) breadcrumbs << [name, link] end + helper_method :add_breadcrumb protected diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb new file mode 100644 index 00000000000..5edc5e8d33c --- /dev/null +++ b/app/controllers/organizations_controller.rb @@ -0,0 +1,31 @@ +class OrganizationsController < ApplicationController + before_action :redirect_to_signin, only: :index, unless: :signed_in? + before_action :redirect_to_new_mfa, only: :index, if: :mfa_required_not_yet_enabled? + before_action :redirect_to_settings_strong_mfa_required, only: :index, if: :mfa_required_weak_level_enabled? + + before_action :find_organization, only: %i[show] + + layout "subject" + + # GET /organizations + def index + @memberships = current_user.memberships.includes(:organization) + end + + # GET /organizations/1 + def show + add_breadcrumb t("breadcrumbs.org_name", name: @organization.handle) + + @latest_events = [] # @organization.latest_events + @gems = [] # @organization.rubygems + @gems_count = @gems.size + @memberships = @organization.memberships + @memberships_count = @organization.memberships.count + end + + private + + def find_organization + @organization = Organization.find_by_handle!(params[:id]) + end +end diff --git a/app/helpers/layout_hepler.rb b/app/helpers/layout_hepler.rb new file mode 100644 index 00000000000..98ba6c527dc --- /dev/null +++ b/app/helpers/layout_hepler.rb @@ -0,0 +1,10 @@ +module LayoutHelper + # <%= layout_section "Footer Nav", class: "py-8 bg-orange-100 dark:bg-orange-950 text-neutral-800 dark:text-neutral-200 flex-col items-center"> + def layout_section(_name, **options, &) + options[:class] = "w-full px-8 #{options[:class]}" + + tag.div(**options) do + tag.div(class: "max-w-screen-xl mx-auto flex flex-col", &) + end + end +end diff --git a/app/helpers/organizations_helper.rb b/app/helpers/organizations_helper.rb new file mode 100644 index 00000000000..24cc9a80e9b --- /dev/null +++ b/app/helpers/organizations_helper.rb @@ -0,0 +1,2 @@ +module OrganizationsHelper +end diff --git a/app/javascript/controllers/auto_refresh_controller.js b/app/javascript/controllers/auto_refresh_controller.js new file mode 100644 index 00000000000..a17c9ca9e82 --- /dev/null +++ b/app/javascript/controllers/auto_refresh_controller.js @@ -0,0 +1,19 @@ +/* A controller that, when loaded, causes the page to refresh periodically */ + +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { + interval: { type: Number, default: 5000 } + } + + connect() { + this.refresh() + } + + refresh() { + setTimeout(() => { + window.location.reload() + }, this.intervalValue) + } +} diff --git a/app/models/organization.rb b/app/models/organization.rb index 9194f21afa6..31d0d57c91d 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -22,4 +22,16 @@ def unique_with_user_handle after_create do record_event!(Events::OrganizationEvent::CREATED, actor_gid: memberships.first&.to_gid) end + + def self.find_by_handle(handle) + not_deleted.find_by("lower(handle) = lower(?)", handle) + end + + def self.find_by_handle!(handle) + find_by_handle(handle) || raise(ActiveRecord::RecordNotFound) + end + + def to_param + handle + end end diff --git a/app/views/dashboards/_subject.html.erb b/app/views/dashboards/_subject.html.erb index 64981449be9..96e171408d4 100644 --- a/app/views/dashboards/_subject.html.erb +++ b/app/views/dashboards/_subject.html.erb @@ -3,40 +3,47 @@ current ||= :dashboard %> -
- <%= avatar 328, "user_gravatar", theme: :dark, class: "h-24 w-24 lg:h-40 lg:w-40 rounded-lg object-cover mr-4" %> +
+
+ <%= avatar 328, "user_gravatar", theme: :dark, class: "h-24 w-24 lg:h-40 lg:w-40 rounded-lg object-cover mr-4" %> -
-

<%= user.display_handle %>

- <% if user.full_name.present? %> -

<%= user.full_name %>

- <% end %> +
+

<%= user.display_handle %>

+ <% if user.full_name.present? %> +

<%= user.full_name %>

+ <% end %> +
-
-<% if user.public_email? || user == current_user %> -
- <%= icon_tag("mail", color: :primary, class: "h-6 w-6 text-orange mr-3") %> -

<%= - mail_to(user.email, encode: "hex") - %>

-
-<% end %> + <% if user.public_email? || user == current_user %> +
+ <%= icon_tag("mail", color: :primary, class: "h-6 w-6 text-orange mr-3") %> +

<%= + mail_to(user.email, encode: "hex") + %>

+
+ <% end %> -<% if user.twitter_username.present? %> -
- <%= icon_tag("x-twitter", color: :primary, class: "w-6 text-orange mr-3") %> -

<%= - link_to( - twitter_username(user), - twitter_url(user) - ) - %>

-
-<% end %> + <% if user.twitter_username.present? %> +
+ <%= icon_tag("x-twitter", color: :primary, class: "w-6 text-orange mr-3") %> +

<%= + link_to( + twitter_username(user), + twitter_url(user) + ) + %>

+
+ <% end %> +
+ + <%= render Subject::NavComponent.new(current:) do |nav| %> <%= nav.link t("layouts.application.header.dashboard"), dashboard_path, name: :dashboard, icon: "space-dashboard" %> <%= nav.link t("dashboards.show.my_subscriptions"), subscriptions_path, name: :subscriptions, icon: "notifications" %> + <% if current_user.memberships.any? %> + <%= nav.link t("dashboards.show.organizations"), organizations_path, name: :organizations, icon: "organizations" %> + <% end %> <%= nav.link t("layouts.application.header.settings"), edit_settings_path, name: :settings, icon: "settings" %> <% end %> diff --git a/app/views/dashboards/show.html.erb b/app/views/dashboards/show.html.erb index 0d5f1a276b1..04274e6378f 100644 --- a/app/views/dashboards/show.html.erb +++ b/app/views/dashboards/show.html.erb @@ -1,7 +1,7 @@ <% @title = t('.title') %> <% content_for :subject do %> - <% render "dashboards/subject", user: current_user %> + <%= render "dashboards/subject", user: current_user, current: :dashboard %> <% end %> @@ -92,9 +92,12 @@ <% end %> <%= c.divided_list do %> <% current_user.memberships.preload(:organization).each do |membership| %> - <%= c.list_item_to("#") do %> -
-

<%= membership.organization.name %>

+ <%= c.list_item_to(organization_path(membership.organization)) do %> +
+
+

<%= membership.organization.name %>

+

<%= membership.organization.handle %>

+

<%= membership.role %>

<% end %> diff --git a/app/views/organizations/_subject.html.erb b/app/views/organizations/_subject.html.erb new file mode 100644 index 00000000000..cd3d6a9e528 --- /dev/null +++ b/app/views/organizations/_subject.html.erb @@ -0,0 +1,23 @@ +<% current ||= :dashboard %> + +
+
+

<%= organization.name %>

+

<%= organization.handle %>

+

+ + <%= icon_tag("organizations", size: 6, class: "-mt-1 -ml-1 mr-1 inline-block") -%><%= t("organizations.show.organization") %> + +

+
+
+ + + +<%= render Subject::NavComponent.new(current:) do |nav| %> + <%= nav.link t("layouts.application.header.dashboard"), dashboard_path, name: :dashboard, icon: "space-dashboard" %> + <%= nav.link t("organizations.show.history"), subscriptions_path, name: :subscriptions, icon: "notifications" %> + <%= nav.link t("organizations.show.gems"), organizations_path, name: :gems, icon: "gems" %> + <%= nav.link t("organizations.show.members"), organizations_path, name: :organizations, icon: "organizations" %> + <%= nav.link t("layouts.application.header.settings"), edit_settings_path, name: :settings, icon: "settings" %> +<% end %> diff --git a/app/views/organizations/index.html.erb b/app/views/organizations/index.html.erb new file mode 100644 index 00000000000..8621c7d0e84 --- /dev/null +++ b/app/views/organizations/index.html.erb @@ -0,0 +1,34 @@ +<% + @title = t('.title') + add_breadcrumb "Dashboard", dashboard_path + add_breadcrumb "Organizations" +%> + +<% content_for :subject do %> + <%= render "dashboards/subject", user: current_user, current: :organizations %> +<% end %> + + +

+ <%= t("dashboards.show.organizations") %> + <% unless @memberships.size.zero? %> + <%= @memberships.size %> + <% end %> +

+ + +<%= render CardComponent.new do |c| %> + <%= c.divided_list do %> + <% @memberships.each do |membership| %> + <%= c.list_item_to(organization_path(membership.organization.handle)) do %> +
+
+

<%= membership.organization.name %>

+

<%= membership.organization.handle %>

+
+

<%= membership.role %>

+
+ <% end %> + <% end %> + <% end %> +<% end %> diff --git a/app/views/organizations/show.html.erb b/app/views/organizations/show.html.erb new file mode 100644 index 00000000000..d983bdd2bb7 --- /dev/null +++ b/app/views/organizations/show.html.erb @@ -0,0 +1,88 @@ +<% content_for :subject do %> + <%= render "organizations/subject", organization: @organization, current: :dashboard %> +<% end %> + +

<%= t("dashboards.show.title") %>

+ +<%= render CardComponent.new do |c| %> + <%= c.head(divide: true) do %> + <%= c.title t(".history"), icon: :history %> + <% end %> + + <% if @latest_events.empty? %> + <%= prose do %> + <%= t('.no_history') %> + <% end %> + <% else %> + <%= c.scrollable do %> + <%= render Card::TimelineComponent.new do |t| %> + <% @latest_events.each do |version| %> + <% + pusher_link = if version.pusher.present? + link_to_user(version.pusher) + elsif version.pusher_api_key&.owner.present? + link_to_pusher(version.pusher_api_key.owner) + end + %> + <%= t.timeline_item(version.authored_at, pusher_link) do %> +
<%= link_to version.rubygem.name, rubygem_path(version.rubygem.slug) %>
+ <%= version_number(version) %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> +<% end %> + +<%= render CardComponent.new do |c| %> + <%= c.head do %> + <%= c.title t(".gems"), icon: "gems", count: @gems_count %> + <% end %> + <% if @gems.empty? %> + <%= prose do %> + <%= t('.no_gems', :creating_link => link_to(t('.creating_link_text'), "https://guides.rubygems.org/make-your-own-gem/")) %> + <% end %> + <% else %> + <%= c.divided_list do %> + <% @gems.each do |rubygem| %> + <%= c.list_item_to( + rubygem_path(rubygem.slug), + title: short_info(rubygem.most_recent_version), + ) do %> +
+
+

<%= rubygem.name %>

+ <%= version_number(rubygem.most_recent_version) %> +
+
+ <%= download_count_component(rubygem, class: "flex") %> +
<%= version_date_component(rubygem.most_recent_version) %>
+
+
+ <% end %> + <% end %> + <% end %> + <% end %> +<% end %> + +<%= render CardComponent.new do |c| %> + <%= c.head do %> + <%= c.title t(".members"), icon: "organizations", count: @memberships_count %> + <% end %> + <% if @memberships.empty? %> + <%= prose do %> + <%= t('.no_members') %> + <% end %> + <% else %> + <%= c.divided_list do %> + <% @memberships.each do |membership| %> + <%= c.list_item_to(profile_path(membership.user.handle)) do %> +
+

<%= membership.user.name %>

+

<%= membership.role %>

+
+ <% end %> + <% end %> + <% end %> + <% end %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 47e68fdab2d..5edb8120f66 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -412,6 +412,15 @@ en: popular_gems: Popular Gems popular: title: New Releases — Popular Gems + organizations: + index: + title: Organizations + show: + gems: Gems + history: History + members: Members + no_history: No events yet + no_gems: No gems yet pages: about: contributors_amount: "%{count} Rubyists" diff --git a/config/routes.rb b/config/routes.rb index 52cc5e01050..fa0c06fe479 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -158,6 +158,7 @@ end resource :dashboard, only: :show, constraints: { format: /html|atom/ } resources :subscriptions, only: :index + resources :organizations, only: %i[index show] resources :profiles, only: :show get "profile/me", to: "profiles#me", as: :my_profile resource :multifactor_auth, only: %i[update] do diff --git a/test/system/organizations_test.rb b/test/system/organizations_test.rb new file mode 100644 index 00000000000..1e51727baba --- /dev/null +++ b/test/system/organizations_test.rb @@ -0,0 +1,40 @@ +require "application_system_test_case" + +class OrganizationsTest < ApplicationSystemTestCase + setup do + @organization = organizations(:one) + end + + test "visiting the index" do + visit organizations_url + + assert_selector "h1", text: "Organizations" + end + + test "should create organization" do + visit organizations_url + click_on "New organization" + + click_on "Create Organization" + + assert_text "Organization was successfully created" + click_on "Back" + end + + test "should update Organization" do + visit organization_url(@organization) + click_on "Edit this organization", match: :first + + click_on "Update Organization" + + assert_text "Organization was successfully updated" + click_on "Back" + end + + test "should destroy Organization" do + visit organization_url(@organization) + click_on "Destroy this organization", match: :first + + assert_text "Organization was successfully destroyed" + end +end