Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/milestone 4/116 generate button implementation #160

Merged
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/controllers/generated_apps_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,27 @@ class GeneratedAppsController < ApplicationController
def show
@generated_app = GeneratedApp.find(params[:id])
end

def create
cli_flags = [
params[:api_flag],
params[:database_choice],
params[:rails_flags]
].compact.join(" ")

recipe = Recipe.find_or_create_by_cli_flags!(cli_flags, current_user)

@generated_app = current_user.generated_apps.create!(
name: params[:app_name],
recipe: recipe,
ruby_version: recipe.ruby_version,
rails_version: recipe.rails_version,
selected_gems: [], # We'll handle this later with ingredients
configuration_options: {} # We'll handle this later with ingredients
)

AppGeneration::Orchestrator.new(@generated_app).call

redirect_to generated_app_log_entries_path(@generated_app)
end
end
20 changes: 20 additions & 0 deletions app/controllers/github_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class GithubController < ApplicationController
before_action :authenticate_user!

def check_name
validator = GithubRepositoryNameValidator.new(
params[:name],
current_user.github_username
)
begin
exists = validator.repo_exists?
render json: { available: !exists }
rescue Octokit::Unauthorized, Octokit::Forbidden => e
Rails.logger.error("GitHub authentication error: #{e.message}")
render json: { error: "GitHub authentication failed" }, status: :unauthorized
rescue => e
Rails.logger.error("GitHub validation error: #{e.message}")
render json: { error: "Could not validate repository name" }, status: :unprocessable_entity
end
end
end
47 changes: 0 additions & 47 deletions app/controllers/repositories_controller.rb

This file was deleted.

2 changes: 0 additions & 2 deletions app/helpers/repostitories_helper.rb

This file was deleted.

17 changes: 17 additions & 0 deletions app/javascript/controllers/app_name_preview_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["input"]
static outlets = ["generated-output"]

connect() {
this.update()
}

update(event) {
const value = event?.target?.value?.trim() || ""
this.generatedOutputOutlet.updateText(value)
this.dispatch("valueChanged", { detail: { value } })
this.dispatch("appNameChanged", { detail: { value } })
}
}
26 changes: 26 additions & 0 deletions app/javascript/controllers/form_values_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["appName", "apiFlag", "databaseChoice", "railsFlags"]

connect() {
// Initialize values from the command display
this.updateFromDisplay()
}

updateFromDisplay() {
const appNameOutput = document.getElementById("app-name-output")
const apiFlag = document.getElementById("api-flag")
const databaseChoice = document.getElementById("database-choice")
const railsFlags = document.getElementById("rails-flags")

if (appNameOutput) {
this.appNameTarget.value = appNameOutput.textContent.trim()
const event = new Event('input', { bubbles: true })
this.appNameTarget.dispatchEvent(event)
}
if (apiFlag) this.apiFlagTarget.value = apiFlag.textContent.trim()
if (databaseChoice) this.databaseChoiceTarget.value = databaseChoice.textContent.trim()
if (railsFlags) this.railsFlagsTarget.value = railsFlags.textContent.trim()
}
}
1 change: 1 addition & 0 deletions app/javascript/controllers/generated_output_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
updateText(text) {
this.element.innerText = text
this.dispatch("valueChanged")
}
}
102 changes: 102 additions & 0 deletions app/javascript/controllers/github_name_validator_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["input", "message", "spinner", "submitButton"]
static values = {
checkUrl: String,
debounce: { type: Number, default: 500 },
errorClass: { type: String, default: "text-red-600" },
successClass: { type: String, default: "text-green-600" }
}

initialize() {
this.validate = this.debounce(this.validate.bind(this), this.debounceValue)
this.disableSubmit()
this.boundValidate = this.validate.bind(this)
}

connect() {
document.addEventListener("app-name-preview:appNameChanged", this.boundValidate)
}

disconnect() {
document.removeEventListener("app-name-preview:appNameChanged", this.boundValidate)
}

async validate(event) {
const name = event?.detail?.value || ''
if (!name) {
this.hideMessage()
this.disableSubmit()
return
}

this.showSpinner()

try {
const response = await fetch(`${this.checkUrlValue}?name=${encodeURIComponent(name)}`)

if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to validate repository name')
}

const data = await response.json()

this.hideSpinner()

if (data.available) {
this.enableSubmit()
this.showMessage("✓ Name is available", this.successClassValue)
} else {
this.disableSubmit()
this.showMessage("✗ Name is already taken", this.errorClassValue)
}
} catch (error) {
this.hideSpinner()
this.disableSubmit()
this.showMessage("Error checking name availability", this.errorClassValue)
}
}

showMessage(text, className) {
this.messageTarget.textContent = text
this.messageTarget.className = `${className} text-sm mt-1`
this.messageTarget.classList.remove("hidden")
}

hideMessage() {
this.messageTarget.classList.add("hidden")
}

showSpinner() {
this.spinnerTarget.classList.remove("hidden")
this.hideMessage()
}

hideSpinner() {
this.spinnerTarget.classList.add("hidden")
}

debounce(func, wait) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}

disableSubmit() {
this.submitButtonTarget.disabled = true
this.submitButtonTarget.classList.add('opacity-50', 'cursor-not-allowed')
}

enableSubmit() {
this.submitButtonTarget.disabled = false
this.submitButtonTarget.classList.remove('opacity-50', 'cursor-not-allowed')
}
}
18 changes: 0 additions & 18 deletions app/javascript/controllers/text_field_change_controller.js

This file was deleted.

6 changes: 1 addition & 5 deletions app/jobs/app_generation_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def create_github_repository

def generate_rails_app
@generated_app.generate!
command = build_rails_command
command = "rails new #{@generated_app.name} #{@generated_app.recipe.cli_flags}"
CommandExecutionService.new(@generated_app, command).execute
end

Expand All @@ -49,8 +49,4 @@ def start_ci
def complete_generation
@generated_app.mark_as_completed!
end

def build_rails_command
"rails new #{@generated_app.name} --skip-action-mailbox --skip-jbuilder --asset-pipeline=propshaft --javascript=esbuild --css=tailwind --skip-spring"
end
end
3 changes: 1 addition & 2 deletions app/models/concerns/git_backed_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ def sync_to_git
end

def repo
@repo ||= GitRepo.new(
@repo ||= DataRepository.new(
user: commit_author,
repo_name: repo_name
)
end

Expand Down
4 changes: 0 additions & 4 deletions app/models/generated_app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,4 @@ def broadcast_clone_box
locals: { generated_app: self }
)
end

def repo_name
name # Use the app's name as the repo name
end
end
17 changes: 17 additions & 0 deletions app/models/recipe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,23 @@ def reorder_ingredients!(new_order)
end
end

# Class method to find or create a recipe with given CLI flags
def self.find_or_create_by_cli_flags!(cli_flags, user)
transaction do
recipe = where(cli_flags: cli_flags, status: "published").first
return recipe if recipe

create!(
name: "Rails App with #{cli_flags}",
cli_flags: cli_flags,
status: "published",
created_by: user,
ruby_version: RailsNewConfig.ruby_version_for_new_apps,
rails_version: RailsNewConfig.rails_version_for_new_apps
)
end
end

private

def ingredient_compatible?(new_ingredient)
Expand Down
24 changes: 0 additions & 24 deletions app/models/repository.rb

This file was deleted.

1 change: 0 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class User < ApplicationRecord

encrypts :github_token

has_many :repositories, dependent: :destroy
has_many :generated_apps, dependent: :nullify
has_many :notifications, as: :recipient,
dependent: :destroy,
Expand Down
3 changes: 3 additions & 0 deletions app/services/app_generation/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module AppGeneration
class InvalidStateError < StandardError; end
end
8 changes: 4 additions & 4 deletions app/services/app_generation/orchestrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ def call
AppGenerationJob.perform_later(@generated_app.id)

true
rescue AppGeneration::InvalidStateError
raise
rescue StandardError => e
@logger.error("Failed to start app generation", { error: e.message })
@generated_app.fail!(e.message)
@generated_app.mark_as_failed!(e.message)
false
end

private

def validate_initial_state!
unless @generated_app.pending?
raise InvalidStateError, "App must be in pending state to start generation"
end
raise AppGeneration::InvalidStateError, "App must be in pending state to start generation" unless @generated_app.pending?
end
end
end
Loading
Loading