Skip to content

Commit f69c68b

Browse files
committed
Ingredients CRUD v1
1 parent a7d22b1 commit f69c68b

25 files changed

+785
-16
lines changed

.yarn/install-state.gz

2.21 KB
Binary file not shown.

Gemfile

+5-1
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@ group :development do
3737
gem "better_html", require: false
3838
gem "erb_lint", require: false
3939
gem "overcommit", require: false
40+
gem "rails-erd", "~> 1.7.2"
41+
end
42+
43+
group :test do
4044
gem "capybara"
45+
gem "capybara-lockstep", "~> 2.2.2"
4146
gem "database_cleaner-active_record"
4247
gem "selenium-webdriver", "~> 4.27.0"
4348
gem "mocha", "~> 2.7.1"
44-
gem "rails-erd", "~> 1.7.2"
4549
end
4650

4751
group :development, :test do

Gemfile.lock

+6
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ GEM
123123
rack-test (>= 0.6.3)
124124
regexp_parser (>= 1.5, < 3.0)
125125
xpath (~> 3.2)
126+
capybara-lockstep (2.2.2)
127+
activesupport (>= 4.2)
128+
capybara (>= 3.0)
129+
ruby2_keywords
130+
selenium-webdriver (>= 4.0)
126131
childprocess (5.1.0)
127132
logger (~> 1.5)
128133
choice (0.2.0)
@@ -561,6 +566,7 @@ DEPENDENCIES
561566
bootsnap
562567
brakeman (~> 7.0.0)
563568
capybara
569+
capybara-lockstep (~> 2.2.2)
564570
database_cleaner-active_record
565571
debug (~> 1.10.0)
566572
dotenv-rails (~> 3.1.7)
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
class IngredientsController < ApplicationController
2+
before_action :authenticate_user!
3+
before_action :set_ingredient, only: [ :show, :edit, :update, :destroy ]
4+
5+
def index
6+
@ingredients = Ingredient.all
7+
end
8+
9+
def show
10+
end
11+
12+
def new
13+
@ingredient = current_user.ingredients.build
14+
end
15+
16+
def edit
17+
end
18+
19+
def create
20+
@ingredient = current_user.ingredients.build(ingredient_params)
21+
22+
if @ingredient.save
23+
redirect_to @ingredient, notice: "Ingredient was successfully created."
24+
else
25+
render :new, status: :unprocessable_entity
26+
end
27+
end
28+
29+
def update
30+
if @ingredient.update(ingredient_params)
31+
redirect_to @ingredient, notice: "Ingredient was successfully updated."
32+
else
33+
render :edit, status: :unprocessable_entity
34+
end
35+
end
36+
37+
def destroy
38+
@ingredient.destroy
39+
redirect_to ingredients_url, notice: "Ingredient was successfully deleted."
40+
end
41+
42+
private
43+
44+
def set_ingredient
45+
@ingredient = current_user.ingredients.find(params[:id])
46+
end
47+
48+
def ingredient_params
49+
params.require(:ingredient).permit(
50+
:name,
51+
:description,
52+
:template_content,
53+
:category,
54+
:conflicts_with,
55+
:requires,
56+
:configures_with
57+
)
58+
end
59+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
export default class extends Controller {
4+
static targets = ["form", "field"]
5+
static values = {
6+
delay: { type: Number, default: 2000 }
7+
}
8+
9+
connect() {
10+
this.timeout = null
11+
}
12+
13+
fieldChanged() {
14+
if (this.timeout) {
15+
clearTimeout(this.timeout)
16+
}
17+
18+
this.timeout = setTimeout(() => {
19+
this.save()
20+
}, this.delayValue)
21+
}
22+
23+
save() {
24+
const form = this.formTarget
25+
const formData = new FormData(form)
26+
27+
fetch(form.action, {
28+
method: form.method,
29+
body: formData,
30+
headers: {
31+
"Accept": "application/json",
32+
"X-Requested-With": "XMLHttpRequest"
33+
}
34+
})
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
import CodeMirror from "codemirror"
3+
import "codemirror/mode/ruby/ruby"
4+
import "codemirror/mode/yaml/yaml"
5+
import "codemirror/lib/codemirror.css"
6+
import "codemirror/theme/monokai.css"
7+
8+
export default class extends Controller {
9+
static values = {
10+
mode: String,
11+
readonly: { type: Boolean, default: false }
12+
}
13+
14+
connect() {
15+
const config = {
16+
mode: this.modeValue || "ruby",
17+
theme: "monokai",
18+
lineNumbers: true,
19+
lineWrapping: true,
20+
readOnly: this.readonlyValue,
21+
tabSize: 2,
22+
indentWithTabs: false,
23+
viewportMargin: Infinity,
24+
scrollbarStyle: null,
25+
fixedGutter: true,
26+
gutters: ["CodeMirror-linenumbers"]
27+
}
28+
29+
if (!this.readonlyValue) {
30+
config.autofocus = true
31+
config.extraKeys = {
32+
"Tab": (cm) => {
33+
if (cm.somethingSelected()) {
34+
cm.indentSelection("add")
35+
} else {
36+
cm.replaceSelection(" ", "end")
37+
}
38+
}
39+
}
40+
}
41+
42+
// For read-only mode, we need to create a new div and set its content
43+
if (this.readonlyValue) {
44+
const content = this.element.textContent.trim()
45+
this.element.textContent = ""
46+
this.editor = CodeMirror(this.element, {
47+
...config,
48+
value: content
49+
})
50+
} else {
51+
this.editor = CodeMirror.fromTextArea(this.element, config)
52+
}
53+
54+
if (!this.readonlyValue) {
55+
this.editor.on("change", () => {
56+
this.element.value = this.editor.getValue()
57+
this.element.dispatchEvent(new Event("input"))
58+
})
59+
}
60+
61+
// Refresh after initialization to ensure proper rendering
62+
setTimeout(() => this.editor.refresh(), 1)
63+
}
64+
65+
disconnect() {
66+
if (this.editor) {
67+
if (!this.readonlyValue) {
68+
this.editor.toTextArea()
69+
}
70+
this.editor = null
71+
}
72+
}
73+
}

app/javascript/stylesheets/application.css

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
@import "tailwindcss/components";
33
@import "tailwindcss/utilities";
44
@import "./custom/basics.css";
5+
@import "./custom/codemirror.css";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.CodeMirror {
2+
height: auto !important;
3+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
4+
font-size: 14px;
5+
line-height: 1.5;
6+
border: 1px solid #e5e7eb;
7+
border-radius: 0.375rem;
8+
}
9+
10+
.CodeMirror-focused {
11+
border-color: #6366f1;
12+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
13+
}
14+
15+
.CodeMirror-gutters {
16+
border-right: 1px solid #e5e7eb;
17+
background-color: #f9fafb;
18+
}
19+
20+
.CodeMirror-linenumber {
21+
color: #6b7280;
22+
}
23+
24+
.cm-s-monokai.CodeMirror {
25+
background-color: #272822;
26+
}
27+
28+
/* Fix extra space issue */
29+
.CodeMirror-scroll {
30+
overflow: hidden !important;
31+
margin-bottom: -50px;
32+
padding-bottom: 50px;
33+
height: 100%;
34+
}

app/models/ingredient.rb

+5-5
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
#
1717
# Indexes
1818
#
19-
# index_ingredients_on_created_by_id (created_by_id)
20-
# index_ingredients_on_name (name) UNIQUE
19+
# index_ingredients_on_created_by_id (created_by_id)
20+
# index_ingredients_on_name_and_created_by_id (name,created_by_id) UNIQUE
2121
#
2222
# Foreign Keys
2323
#
@@ -28,11 +28,11 @@ class InvalidConfigurationError < StandardError; end
2828
include GitBackedModel
2929

3030
belongs_to :created_by, class_name: "User"
31-
has_many :recipe_ingredients, dependent: :destroy
31+
has_many :recipe_ingredients, dependent: :delete_all
3232
has_many :recipes, through: :recipe_ingredients
33-
has_many :app_changes, dependent: :destroy
33+
has_many :recipe_changes, dependent: :delete_all
3434

35-
validates :name, presence: true, uniqueness: true
35+
validates :name, presence: true, uniqueness: { scope: :created_by_id }
3636
validates :template_content, presence: true
3737

3838
serialize :conflicts_with, coder: YAML

app/models/user.rb

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class User < ApplicationRecord
2929
has_many :notifications, as: :recipient,
3030
dependent: :destroy,
3131
class_name: "Noticed::Notification"
32+
has_many :ingredients, foreign_key: :created_by_id, dependent: :destroy
3233

3334

3435
validates :provider, presence: true

0 commit comments

Comments
 (0)