Skip to content

Commit 5dd90bd

Browse files
committed
Refactor rows
1 parent edb958e commit 5dd90bd

File tree

11 files changed

+208
-145
lines changed

11 files changed

+208
-145
lines changed

app/controllers/hotsheet/sheets_controller.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
class Hotsheet::SheetsController < Hotsheet::ApplicationController
44
before_action :set_sheet
5+
before_action :set_row, only: :update
56

67
def index
78
@rows = @sheet.rows
8-
@cells = @sheet.model.pluck(*@rows.pluck(:name))
9+
@cells = @sheet.model.pluck(*@rows.map(&:name))
910
@cells = @rows.length == 1 ? [@cells] : @cells.transpose
1011
end
1112

12-
def update # rubocop:disable Metrics/AbcSize
13-
@sheet.update! id: params[:id], attr: params[:attr]&.to_sym, value: params[:value]
14-
respond_to { |format| format.any { render json: { error: false } } }
13+
def update
14+
@row&.update! params[:id], params[:to]
15+
json = {}
1516
rescue Hotsheet::Error => e
16-
json = { error: e.message, value: params[:prev], x: params[:x], y: params[:y] }
17+
json = { error: e.message, value: params[:from], x: params[:x], y: params[:y] }
18+
ensure
1719
respond_to { |format| format.any { render json: } }
1820
end
1921

@@ -26,4 +28,8 @@ def error
2628
def set_sheet
2729
@sheet = Hotsheet.sheets[params[:sheet_name]]
2830
end
31+
32+
def set_row
33+
@row = @sheet.rows.find { |row| row.name == params[:row_name] }
34+
end
2935
end

app/javascripts/controllers/sheets_controller.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class SheetsController extends Controller {
1414

1515
private x = 0
1616
private y = 0
17-
private attrs: string[] = []
17+
private rowNames: string[] = []
1818
private input = document.createElement("input")
1919
private cell = this.input as HTMLElement
2020
private cells: HTMLElement[][] = []
@@ -26,9 +26,9 @@ export class SheetsController extends Controller {
2626
const sheet = window.location.pathname
2727
const id = this.cells[this.y][0].innerHTML
2828
const params = new URLSearchParams({
29-
attr: this.attrs[this.x],
30-
value: this.input.value,
31-
prev: this.value,
29+
row_name: this.rowNames[this.x],
30+
from: this.value,
31+
to: this.input.value,
3232
})
3333

3434
fetch(`${sheet}/${id}?${params}&x=${this.x}&y=${this.y}`, {
@@ -132,7 +132,7 @@ export class SheetsController extends Controller {
132132
cell.addEventListener("keydown", this.moveFocus)
133133
})
134134

135-
this.attrs = [...this.element.querySelectorAll<HTMLElement>(".header")].map(
135+
this.rowNames = [...this.element.querySelectorAll<HTMLElement>(".header")].map(
136136
(header) => header.dataset.name!,
137137
)
138138
}

app/views/hotsheet/sheets/index.html.erb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<div class="table" spellcheck="false" data-controller="sheets">
2-
<% @cells.each_with_index do |rows, x_index| %>
2+
<% @cells.each_with_index do |values, x_index| %>
33
<% row = @rows[x_index] %>
4-
<% readonly_class = row[:editable] ? "" : " readonly" %>
4+
<% readonly_class = row.editable? ? "" : " readonly" %>
55

66
<div class="row">
7-
<span class="cell header" data-name="<%= row[:name] %>"><%= row[:label] %></span>
7+
<span class="cell header" data-name="<%= row.name %>"><%= row.human_name %></span>
88

9-
<% rows.each_with_index do |value, y_index| %>
9+
<% values.each_with_index do |value, y_index| %>
1010
<span class="cell<%= readonly_class %>" tabindex="0"
1111
data-xy="<%= "#{x_index},#{y_index}" %>"><%= value %></span>
1212
<% end %>

lib/hotsheet.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require "hotsheet/config"
44
require "hotsheet/engine"
55
require "hotsheet/sheet"
6+
require "hotsheet/sheet/row"
67
require "hotsheet/version"
78

89
module Hotsheet

lib/hotsheet/config.rb

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,9 @@ def initialize
77
@sheets = {}
88
end
99

10-
def sheet(model_name, &config)
11-
sheet = Hotsheet::Sheet.new(model_name).tap do |s|
12-
if config
13-
s.instance_eval(&config)
14-
else
15-
s.model.column_names.each { |column_name| s.row column_name }
16-
end
10+
def sheet(name, config = {}, &rows)
11+
sheet = Hotsheet::Sheet.new(name, config).tap do |s|
12+
rows ? s.instance_eval(&rows) : s.use_default_configuration
1713
end
1814

1915
@sheets[sheet.model.table_name] = sheet

lib/hotsheet/sheet.rb

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,39 @@
11
# frozen_string_literal: true
22

33
class Hotsheet::Sheet
4-
DEFAULT_CONFIG = { editable: true, visible: true }.freeze
5-
64
attr_accessor :model
75

8-
def initialize(model_name)
9-
error "Unknown model '#{model_name}'" unless Object.const_defined? model_name
6+
def initialize(name, config = {})
7+
ensure_model_exists! name
108

11-
@model = model_name.to_s.constantize
9+
@config = config
10+
@model = name.to_s.constantize
1211
@rows = []
1312
end
1413

15-
def row(name, **config)
16-
@config = config
17-
18-
validate_column_name! name
19-
validate_config! name
20-
21-
@rows << @config.merge(name: name.to_sym)
14+
def human_name
15+
@model.model_name.human count: 2
2216
end
2317

24-
def rows
25-
@rows.filter_map do |row|
26-
next unless row[:visible]
27-
28-
row.merge label: @model.human_attribute_name(row[:name]), editable: row[:editable]
29-
end
18+
def row(name, config = {})
19+
row = Row.new(@model, name, config)
20+
row.validate!
21+
@rows << row
3022
end
3123

32-
def human_name
33-
@model.model_name.human count: 2
24+
def rows
25+
@rows.select(&:visible?)
3426
end
3527

36-
def update!(id:, attr:, value:)
37-
row = @rows.find { |r| r[:name] == attr }
38-
error "Forbidden" unless row && row[:visible] && row[:editable]
39-
40-
@model.find(id).update! attr => value
41-
rescue StandardError => e
42-
error e.message
28+
def use_default_configuration
29+
@model.column_names.each { |name| row name }
4330
end
4431

4532
private
4633

47-
def validate_column_name!(name)
48-
return if @model.column_names.include? name.to_s
49-
50-
error "Unknown database column '#{name}' for '#{@model.table_name}'"
51-
end
52-
53-
def validate_config!(name)
54-
@config.each do |key, value|
55-
error "Unknown config '#{key}' for row '#{name}'" unless DEFAULT_CONFIG.key? key
56-
error "Invalid config '#{key}' for row '#{name}'" unless value == false
57-
end
58-
@config = DEFAULT_CONFIG.merge @config
59-
end
34+
def ensure_model_exists!(name)
35+
return if Object.const_defined? name
6036

61-
def error(message)
62-
raise Hotsheet::Error, message
37+
raise Hotsheet::Error, "Unknown model '#{name}'"
6338
end
6439
end

lib/hotsheet/sheet/row.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# frozen_string_literal: true
2+
3+
class Hotsheet::Sheet::Row
4+
attr_accessor :name
5+
6+
DEFAULT_CONFIG = { editable: true, visible: true }.freeze
7+
8+
def initialize(model, name, config)
9+
@model = model
10+
@name = name.to_s
11+
@config = DEFAULT_CONFIG.merge config
12+
end
13+
14+
def human_name
15+
@model.human_attribute_name @name
16+
end
17+
18+
def visible?
19+
@config[:visible]
20+
end
21+
22+
def editable?
23+
@config[:editable]
24+
end
25+
26+
def update!(id, value)
27+
raise Hotsheet::Error, "Forbidden" unless editable?
28+
29+
model = @model.find_by(id:)
30+
raise Hotsheet::Error, "Not found" if model.nil?
31+
raise Hotsheet::Error, model.errors.full_messages.first unless model.update @name => value
32+
end
33+
34+
def validate!
35+
ensure_database_column_exists!
36+
37+
@config.each do |key, value|
38+
ensure_config_exists! key
39+
validate_config_value! key, value
40+
end
41+
end
42+
43+
private
44+
45+
def ensure_database_column_exists!
46+
return if @model.column_names.include? @name.to_s
47+
48+
raise Hotsheet::Error, "Unknown database column '#{@name}' for '#{@model.table_name}'"
49+
end
50+
51+
def ensure_config_exists!(key)
52+
return if DEFAULT_CONFIG.key? key
53+
54+
raise Hotsheet::Error, "Unknown config '#{key}' for row '#{@name}'"
55+
end
56+
57+
def validate_config_value!(key, value)
58+
return if [true, false].include? value
59+
60+
raise Hotsheet::Error, "Invalid config '#{key}' for row '#{@name}'"
61+
end
62+
end

spec/lib/hotsheet/config_spec.rb

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@
2121

2222
it "uses all database columns" do
2323
expect(sheet).to be_a Hotsheet::Sheet
24-
expect(sheet.model).to eq Post
25-
expect(sheet.rows.pluck(:name)).to eq %i[id title body user_id created_at updated_at]
24+
expect(sheet.rows.map(&:name)).to eq %w[id title body user_id created_at updated_at]
2625
end
2726
end
2827

@@ -37,22 +36,7 @@
3736
end
3837

3938
it "keeps the rows in the correct order" do
40-
expect(sheet.rows.pluck(:name)).to eq(%i[title body])
41-
end
42-
end
43-
44-
context "with visibility" do
45-
let(:config) do
46-
Hotsheet.configure do
47-
sheet :Post do
48-
row :title, visible: false
49-
row :body
50-
end
51-
end
52-
end
53-
54-
it "does not show the hidden rows" do
55-
expect(sheet.rows.pluck(:name)).to eq %i[body]
39+
expect(sheet.rows.map(&:name)).to eq(%w[title body])
5640
end
5741
end
5842
end

spec/lib/hotsheet/sheet/row_spec.rb

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
RSpec.describe Hotsheet::Sheet::Row do
6+
fixtures :users
7+
8+
let(:config) { {} }
9+
let(:name) { :name }
10+
let(:row) { described_class.new User, name, config }
11+
12+
describe "permissions" do
13+
it "is visible and editable by default" do
14+
expect(row.visible?).to be true
15+
expect(row.editable?).to be true
16+
end
17+
end
18+
19+
describe "#human_name" do
20+
it "reads the human name from the locales" do
21+
expect(row.human_name).to eq "Name"
22+
end
23+
end
24+
25+
describe "#update!" do
26+
let(:user) { users(:admin) }
27+
28+
context "with readonly config" do
29+
let(:config) { { editable: false } }
30+
31+
it "raises an error" do
32+
expect { row.update!(user.id, "Bob") }.to raise_error "Forbidden"
33+
end
34+
end
35+
36+
context "with invalid value" do
37+
it "raises an error" do
38+
expect { row.update!(user.id, "B") }
39+
.to raise_error "Name is too short (minimum is 2 characters)"
40+
end
41+
end
42+
43+
context "with nonexistent id" do
44+
it "raises an error" do
45+
expect { row.update!(0, "Bob") }.to raise_error "Not found"
46+
end
47+
end
48+
end
49+
50+
describe "#validate!" do
51+
context "with nonexistent database column" do
52+
let(:name) { :age }
53+
54+
it "raises an error" do
55+
expect { row.validate! }.to raise_error "Unknown database column 'age' for 'users'"
56+
end
57+
end
58+
59+
context "with nonexistent config" do
60+
let(:config) { { readonly: true } }
61+
62+
it "raises an error" do
63+
expect { row.validate! }.to raise_error "Unknown config 'readonly' for row 'name'"
64+
end
65+
end
66+
67+
context "with invalid config" do
68+
let(:config) { { editable: "no" } }
69+
70+
it "raises an error" do
71+
expect { row.validate! }.to raise_error "Invalid config 'editable' for row 'name'"
72+
end
73+
end
74+
end
75+
end

0 commit comments

Comments
 (0)