diff --git a/app/assets/stylesheets/3-atoms/_badges.scss b/app/assets/stylesheets/3-atoms/_badges.scss index 2b7f1ba2..2e170082 100644 --- a/app/assets/stylesheets/3-atoms/_badges.scss +++ b/app/assets/stylesheets/3-atoms/_badges.scss @@ -10,7 +10,7 @@ font-size: 11px; &.archived { - background-color: rgba(29,78,216,0.2); + background-color: rgba(29, 78, 216, 0.2); border-color: #1d4ed8; } @@ -19,4 +19,7 @@ border-color: #d77e72; } + &.version-jump { + background-color: #57ce81; + } } diff --git a/app/assets/stylesheets/3-atoms/_forms.scss b/app/assets/stylesheets/3-atoms/_forms.scss index e6528415..ebd25152 100644 --- a/app/assets/stylesheets/3-atoms/_forms.scss +++ b/app/assets/stylesheets/3-atoms/_forms.scss @@ -70,6 +70,7 @@ input[type="checkbox"] { } .field, +.select, .actions { margin-bottom: 10px; } diff --git a/app/controllers/madmin/application_controller.rb b/app/controllers/madmin/application_controller.rb index 85dbf73d..12ace7d4 100644 --- a/app/controllers/madmin/application_controller.rb +++ b/app/controllers/madmin/application_controller.rb @@ -1,5 +1,6 @@ module Madmin class ApplicationController < Madmin::BaseController + include Pundit::Authorization before_action :ensure_admin def ensure_admin diff --git a/app/controllers/madmin/version_jumps_controller.rb b/app/controllers/madmin/version_jumps_controller.rb new file mode 100644 index 00000000..986de88f --- /dev/null +++ b/app/controllers/madmin/version_jumps_controller.rb @@ -0,0 +1,4 @@ +module Madmin + class VersionJumpsController < Madmin::ResourceController + end +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ec933f54..84537228 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -43,7 +43,7 @@ def toggle_locked end def new_clone - @original = Project.includes(:projects, stories: :estimates).find(params[:id]) + @original = Project.includes(:projects, :version_jump, stories: :estimates).find(params[:id]) end def clone @@ -111,11 +111,11 @@ def find_project end def projects_params - params.require(:project).permit(:title, :status, :parent_id) + params.require(:project).permit(:title, :status, :parent_id, :version_jump_id) end def clone_params - params.require(:project).permit(:title, :parent_id) + params.require(:project).permit(:title, :parent_id, :version_jump_id) end def parent_id diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index fae02738..7d60ec97 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -17,6 +17,13 @@ def link_unless_archived(project, text, url, classes: nil, method: :get, remote: end end + def version_jump_badge(project) + return "" unless project.version_jump + + jump = project.version_jump + content_tag(:span, jump.to_label, class: "status-badge version-jump") + end + private def text_content(icon, text) diff --git a/app/madmin/resources/project_resource.rb b/app/madmin/resources/project_resource.rb index 966c2b9f..750bb2ce 100644 --- a/app/madmin/resources/project_resource.rb +++ b/app/madmin/resources/project_resource.rb @@ -10,6 +10,7 @@ class ProjectResource < Madmin::Resource attribute :users attribute :parent attribute :projects + attribute :version_jump def self.display_name(record) record.title.truncate(20) diff --git a/app/madmin/resources/version_jump_resource.rb b/app/madmin/resources/version_jump_resource.rb new file mode 100644 index 00000000..6bc0f68f --- /dev/null +++ b/app/madmin/resources/version_jump_resource.rb @@ -0,0 +1,24 @@ +class VersionJumpResource < Madmin::Resource + # Attributes + attribute :id, form: false + attribute :technology + attribute :initial_version + attribute :target_version + + # Associations + attribute :projects, form: false + + # Uncomment this to customize the display name of records in the admin area. + def self.display_name(record) + record.to_label + end + + # Uncomment this to customize the default sort column and direction. + # def self.default_sort_column + # "created_at" + # end + # + # def self.default_sort_direction + # "desc" + # end +end diff --git a/app/models/project.rb b/app/models/project.rb index 68429561..f2f2293e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -4,6 +4,7 @@ class Project < ApplicationRecord has_many :stories has_many :estimates, through: :stories has_many :users, through: :estimates + belongs_to :version_jump, optional: true belongs_to :parent, class_name: "Project", required: false has_many :projects, class_name: "Project", foreign_key: :parent_id, dependent: :destroy @@ -52,6 +53,10 @@ def archived? status == "archived" end + def locked? + locked_at.present? + end + def breadcrumb parent.present? ? "#{parent.breadcrumb} ยป #{title}" : title end diff --git a/app/models/version_jump.rb b/app/models/version_jump.rb new file mode 100644 index 00000000..37f0fbac --- /dev/null +++ b/app/models/version_jump.rb @@ -0,0 +1,10 @@ +class VersionJump < ApplicationRecord + has_many :projects + + validates :technology, :initial_version, :target_version, presence: true + validates :technology, uniqueness: {scope: [:initial_version, :target_version]} + + def to_label + "#{technology} / #{initial_version} - #{target_version}" + end +end diff --git a/app/policies/estimate_policy.rb b/app/policies/estimate_policy.rb new file mode 100644 index 00000000..6c9496d3 --- /dev/null +++ b/app/policies/estimate_policy.rb @@ -0,0 +1,2 @@ +class EstimatePolicy < ApplicationPolicy +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index c9286f43..c0ac8a41 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -7,6 +7,8 @@ def initialize(user, project) end def update? + return false unless project.is_a?(Project) + return !project.locked_at? if project.parent_id.nil? !project.parent.locked_at? diff --git a/app/policies/story_policy.rb b/app/policies/story_policy.rb new file mode 100644 index 00000000..9c6c7db0 --- /dev/null +++ b/app/policies/story_policy.rb @@ -0,0 +1,2 @@ +class StoryPolicy < ApplicationPolicy +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 00000000..8b6187ab --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,2 @@ +class UserPolicy < ApplicationPolicy +end diff --git a/app/policies/version_jump_policy.rb b/app/policies/version_jump_policy.rb new file mode 100644 index 00000000..47af0fa7 --- /dev/null +++ b/app/policies/version_jump_policy.rb @@ -0,0 +1,9 @@ +class VersionJumpPolicy < ApplicationPolicy + def update? + true + end + + def create? + true + end +end diff --git a/app/views/madmin/application/index.html.erb b/app/views/madmin/application/index.html.erb index 0e17c2a7..ea987e0d 100644 --- a/app/views/madmin/application/index.html.erb +++ b/app/views/madmin/application/index.html.erb @@ -11,6 +11,12 @@ <% end %> + + <% if policy(resource).create? %> + <%= link_to resource.new_path, class: "bg-white hover:bg-gray-100 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow" do %> + New + <% end %> + <% end %> @@ -48,7 +54,9 @@ <%= link_to "View", resource.show_path(record), class: "text-indigo-500" %> - <%# = link_to "Edit", resource.edit_path(record), class: "text-indigo-500" %> + <% if policy(resource).update? %> + <%= link_to "Edit", resource.edit_path(record), class: "text-indigo-500" %> + <% end %> <% end %> diff --git a/app/views/madmin/application/show.html.erb b/app/views/madmin/application/show.html.erb index d83dd3a2..27c35bae 100644 --- a/app/views/madmin/application/show.html.erb +++ b/app/views/madmin/application/show.html.erb @@ -7,9 +7,13 @@
- <%# = link_to "Edit", resource.edit_path(@record), class: "block bg-white hover:bg-gray-100 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow" %> + <% if policy(resource).update? %> + <%= link_to "Edit", resource.edit_path(@record), class: "block bg-white hover:bg-gray-100 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow" %> + <% end %>
- <%# = button_to "Delete", resource.show_path(@record), method: :delete, data: { turbo_confirm: "Are you sure?" }, class: "bg-white hover:bg-gray-100 text-red-500 font-semibold py-2 px-4 border border-red-500 rounded shadow pointer-cursor" %> + <% if policy(resource).destroy? %> + <%= button_to "Delete", resource.show_path(@record), method: :delete, data: { turbo_confirm: "Are you sure?" }, class: "bg-white hover:bg-gray-100 text-red-500 font-semibold py-2 px-4 border border-red-500 rounded shadow pointer-cursor" %> + <% end %>
diff --git a/app/views/projects/_form.html.erb b/app/views/projects/_form.html.erb index 33fb158d..68d293c1 100644 --- a/app/views/projects/_form.html.erb +++ b/app/views/projects/_form.html.erb @@ -16,6 +16,11 @@ <%= f.text_field :title, placeholder: "Project Title", class: "project-story-title", autofocus: true %> +
+ <%= f.label :version_jump_id %> + <%= f.select :version_jump_id, options_from_collection_for_select(VersionJump.all, :id, :to_label) %> +
+
<%= f.submit yield(:button_text), class: "button green", id: "edit" %>
diff --git a/app/views/projects/_sub_project_form.html.erb b/app/views/projects/_sub_project_form.html.erb index 6bd8820c..9e544af7 100644 --- a/app/views/projects/_sub_project_form.html.erb +++ b/app/views/projects/_sub_project_form.html.erb @@ -17,6 +17,11 @@ <%= f.hidden_field :parent_id, value: parent.id %> +
+ <%= f.label :version_jump_id %> + <%= f.select :version_jump_id, options_from_collection_for_select(VersionJump.all, :id, :to_label) %> +
+
<%= f.submit yield(:button_text), class: "button green", id: "edit" %>
diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 76c6a564..afa08173 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -9,8 +9,13 @@ <%= link_to project_path(project.id) do %>

<%= project.title %>

- ">Locked - <%= project.status %> + <% if project.locked? %> + Locked + <% end %> + <% if project.status.present? %> + <%= project.status %> + <% end %> + <%= version_jump_badge(project) %>
<% end %>
diff --git a/app/views/projects/new_clone.html.erb b/app/views/projects/new_clone.html.erb index ed7e0cf3..0a5e70eb 100644 --- a/app/views/projects/new_clone.html.erb +++ b/app/views/projects/new_clone.html.erb @@ -12,6 +12,11 @@ <%= f.select :parent_id, options_from_collection_for_select(Project.active.parents, :id, :title, selected: @original.parent_id), include_blank: "None" %>
+
+ <%= f.label :version_jump_id %> + <%= f.select :version_jump_id, options_from_collection_for_select(VersionJump.all, :id, :to_label) %> +
+ <% if @original.projects.any? %>

Sub-projects to clone

diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index a569ec3d..c6555974 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -108,6 +108,7 @@ <% end %> <%= link_to 'Clone Project', new_clone_project_path(@project.id), class: "button green" %> + <%= link_to 'Edit Project', edit_project_path(@project.id), class: "button green" %> <% if is_unlocked?(@project) %> <%= link_unless_archived(@project, "Add Sub-Project", project_new_sub_project_path(@project), classes: :green) unless @project.parent_id.present? %> diff --git a/app/views/shared/_project_title.html.erb b/app/views/shared/_project_title.html.erb index 5a806332..2c3b1cfe 100644 --- a/app/views/shared/_project_title.html.erb +++ b/app/views/shared/_project_title.html.erb @@ -20,6 +20,7 @@ <% end %>
+ <%= version_jump_badge(project) %> ">Locked "><%= project.status %>
diff --git a/config/routes/madmin.rb b/config/routes/madmin.rb index 9f95e715..85982206 100644 --- a/config/routes/madmin.rb +++ b/config/routes/madmin.rb @@ -4,5 +4,6 @@ resources :stories resources :estimates, except: [:update, :edit, :create] resources :users, except: [:update, :edit, :create] + resources :version_jumps root to: "dashboard#show" end diff --git a/db/migrate/20230830143908_create_version_jumps.rb b/db/migrate/20230830143908_create_version_jumps.rb new file mode 100644 index 00000000..0861eb3b --- /dev/null +++ b/db/migrate/20230830143908_create_version_jumps.rb @@ -0,0 +1,11 @@ +class CreateVersionJumps < ActiveRecord::Migration[7.0] + def change + create_table :version_jumps do |t| + t.string :technology + t.string :initial_version + t.string :target_version + + t.timestamps + end + end +end diff --git a/db/migrate/20230831175732_add_version_jump_to_projects.rb b/db/migrate/20230831175732_add_version_jump_to_projects.rb new file mode 100644 index 00000000..18c0a8f1 --- /dev/null +++ b/db/migrate/20230831175732_add_version_jump_to_projects.rb @@ -0,0 +1,5 @@ +class AddVersionJumpToProjects < ActiveRecord::Migration[7.0] + def change + add_reference :projects, :version_jump + end +end diff --git a/db/schema.rb b/db/schema.rb index cff8cb41..22f3da62 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_06_21_141342) do +ActiveRecord::Schema[7.0].define(version: 2023_08_31_175732) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -31,6 +31,8 @@ t.integer "parent_id" t.integer "position" t.datetime "locked_at", precision: nil + t.bigint "version_jump_id" + t.index ["version_jump_id"], name: "index_projects_on_version_jump_id" end create_table "stories", force: :cascade do |t| @@ -65,4 +67,12 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + create_table "version_jumps", force: :cascade do |t| + t.string "technology" + t.string "initial_version" + t.string "target_version" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index c27828e0..701f5e6e 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -1,6 +1,7 @@ FactoryBot.define do factory :project do title { Faker::Company.name } + version_jump trait :locked do locked_at { Time.current } end diff --git a/spec/factories/version_jump.rb b/spec/factories/version_jump.rb new file mode 100644 index 00000000..2753b74a --- /dev/null +++ b/spec/factories/version_jump.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :version_jump do + sequence(:technology) { |n| ["Rails", "Ruby", "Node", "React"].sample + n.to_s } + initial_version { ["2.3", "3.0", "3.1", "3.2", "4.0", "4.1", "4.2"].sample } + target_version { ["5.0", "5.1", "5.2", "6.0", "6.1", "7.0"].sample } + end +end diff --git a/spec/features/projects_manage_spec.rb b/spec/features/projects_manage_spec.rb index fe0f2e9a..ac4bb97f 100644 --- a/spec/features/projects_manage_spec.rb +++ b/spec/features/projects_manage_spec.rb @@ -3,6 +3,7 @@ RSpec.describe "managing projects", js: true do let(:user) { FactoryBot.create(:user) } let(:project) { FactoryBot.create(:project) } + let!(:version_jump) { VersionJump.create(technology: "Rails", initial_version: "4.2", target_version: "5.0") } before do login_as(user, scope: :user) @@ -16,9 +17,16 @@ it "allows me to add a project" do visit root_path click_link "Add a Project" - fill_in "project[title]", with: "Super Project" - click_button "Create" - expect(Project.count).to eq 1 + fill_in "Title", with: "Super Project" + select "Rails / 4.2 - 5.0", from: "Version jump" + + expect { + click_button "Create" + }.to change(Project, :count).by 1 + + project = Project.last + expect(project.title).to eq "Super Project" + expect(project.version_jump.to_label).to eq "Rails / 4.2 - 5.0" end context "when the project is archived" do @@ -84,10 +92,14 @@ it "allows me to add sub projects" do visit project_path(id: project.id) click_link "Add Sub-Project" - fill_in "project[title]", with: "Super Sub Project" - click_button "Create" - expect(page).to have_content "Project created!" + fill_in "Title", with: "Super Sub Project" + expect { + click_button "Create" + }.to change(Project, :count).by 1 expect(current_path).to eq project_path(id: project.id) + + sub = project.projects.last + expect(sub.title).to eq "Super Sub Project" end it "lists available sub projects with a link" do @@ -203,7 +215,7 @@ expect(page).to have_text("Clone project #{project.title}") - fill_in :project_title, with: "Cloned Project" + fill_in "Title", with: "Cloned Project" expect { click_button "Clone" diff --git a/spec/models/version_jump_spec.rb b/spec/models/version_jump_spec.rb new file mode 100644 index 00000000..461ff6ce --- /dev/null +++ b/spec/models/version_jump_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe VersionJump, type: :model do + subject { FactoryBot.create(:version_jump) } + + it "should be uniq by technology, initial version, and target version" do + attrs = { + technology: "Rails", + initial_version: "4.2", + target_version: "5.0" + } + + jump1 = VersionJump.create(attrs) + expect(jump1).to be_persisted + + jump2 = VersionJump.new(attrs) + expect(jump2).to be_invalid + expect(jump2.errors[:technology]).to be_present + + jump2.technology = "Ruby" + expect(jump2).to be_valid + + jump2.technology = attrs[:technology] + jump2.initial_version = "4.1" + expect(jump2).to be_valid + + jump2.initial_version = attrs[:initial_version] + jump2.target_version = "5.1" + expect(jump2).to be_valid + end +end