slash_migrate 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/app/controllers/slash_migrate/application_controller.rb +17 -0
- data/app/controllers/slash_migrate/columns_controller.rb +4 -4
- data/app/controllers/slash_migrate/indexes_controller.rb +2 -2
- data/app/controllers/slash_migrate/migrations_controller.rb +25 -18
- data/app/controllers/slash_migrate/models_controller.rb +2 -2
- data/app/helpers/slash_migrate/application_helper.rb +23 -0
- data/app/services/slash_migrate/column.rb +7 -13
- data/app/services/slash_migrate/migration_runner.rb +7 -1
- data/app/views/layouts/slash_migrate/application.html.erb +2 -2
- data/app/views/slash_migrate/columns/edit.html.erb +3 -2
- data/app/views/slash_migrate/columns/new.html.erb +2 -1
- data/app/views/slash_migrate/indexes/new.html.erb +2 -1
- data/app/views/slash_migrate/migrations/_migrations.html.erb +79 -0
- data/app/views/slash_migrate/migrations/index.html.erb +5 -70
- data/app/views/slash_migrate/migrations/stream.html.erb +6 -0
- data/app/views/slash_migrate/models/new.html.erb +2 -1
- data/app/views/slash_migrate/shared/_flow_arrow.html.erb +10 -5
- data/app/views/slash_migrate/tables/index.html.erb +3 -1
- data/app/views/slash_migrate/tables/show.html.erb +4 -3
- data/lib/slash_migrate/assets/slash_migrate.css +7 -2
- data/lib/slash_migrate/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc38b5dc8b5c3ad04547645969779d7a6b744363a01967d974874c603ade8444
|
|
4
|
+
data.tar.gz: 4863978dd450993f5bef0a2be7695bdf8a64a05fb278a7e4a5822b374624fb99
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b2081b038e753378cacd065a390b0b789e3b200a069268ff06b973ba84e99806f0b0ad331dda23fb7ab0580a92befcde7841c6496fe8cf03902e40806db1fb9
|
|
7
|
+
data.tar.gz: ad18527b2b39c34c637f3e35ec56ac1e5ef37c5c2f308c531106c4dfe736b610beacdcba7c3d98f652ed86429299867e186179d22a7fc95de5241baed7ebaaf9
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.1.1] - 2026-05-27
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Running a migration or rollback from the GUI no longer crashes the host app
|
|
12
|
+
with `ActionDispatch::Cookies::CookieOverflow`. The migrations page now updates
|
|
13
|
+
in place via a Turbo Stream rather than carrying the command output through the
|
|
14
|
+
4 KB session cookie.
|
|
15
|
+
- Live previews and migration actions no longer return 500
|
|
16
|
+
(`ActionView::MissingTemplate`) in host apps that have `turbo-rails` installed;
|
|
17
|
+
the engine's hand-written stream templates are pinned to the HTML format.
|
|
18
|
+
- `db:migrate` / `db:rollback` no longer fail with "… is not yet checked out"
|
|
19
|
+
when the host app uses a custom `BUNDLE_PATH` (e.g. GitHub Codespaces): the task
|
|
20
|
+
now shells out within the host's own bundle environment.
|
|
21
|
+
- Restored two margins — above the "Drop this column" panel and below the
|
|
22
|
+
migration output — that an internal spacing-token rename had silently dropped,
|
|
23
|
+
and moved those values into CSS so a future rename can't break them again.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Migrations run / rollback / delete now update the page in place via Turbo
|
|
28
|
+
Stream instead of reloading after a redirect.
|
|
29
|
+
- The nav brand and page titles follow the configured mount path, and the table
|
|
30
|
+
stats line pluralizes correctly.
|
|
31
|
+
|
|
7
32
|
## [0.1.0] - 2026-05-27
|
|
8
33
|
|
|
9
34
|
### Added
|
|
@@ -18,4 +43,5 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
18
43
|
- A live preview of the migration and model code each action will generate.
|
|
19
44
|
- `SlashMigrate.configure` for `mount_path` and `enabled_environments`.
|
|
20
45
|
|
|
46
|
+
[0.1.1]: https://github.com/firstdraft/slash_migrate/compare/v0.1.0...v0.1.1
|
|
21
47
|
[0.1.0]: https://github.com/firstdraft/slash_migrate/releases/tag/v0.1.0
|
|
@@ -11,5 +11,22 @@ module SlashMigrate
|
|
|
11
11
|
rescue_from MigrationFileWriter::DuplicateError do |error|
|
|
12
12
|
redirect_to migrations_path, alert: error.message
|
|
13
13
|
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Render a hand-written <turbo-stream> template (the engine ships no
|
|
18
|
+
# turbo-rails). The html format is forced deliberately: when the host app
|
|
19
|
+
# *does* have turbo-rails, it registers the text/vnd.turbo-stream.html MIME,
|
|
20
|
+
# so a request asking for a Turbo Stream negotiates to the :turbo_stream
|
|
21
|
+
# format and a bare `render :preview` would look for a nonexistent
|
|
22
|
+
# preview.turbo_stream.erb and raise MissingTemplate (500). Pass content_type
|
|
23
|
+
# when the response is applied by native Turbo form handling (it keys off the
|
|
24
|
+
# header); the live previews are applied client-side by
|
|
25
|
+
# Turbo.renderStreamMessage and don't need it.
|
|
26
|
+
def render_stream(template, content_type: nil)
|
|
27
|
+
options = {layout: false, formats: [:html]}
|
|
28
|
+
options[:content_type] = content_type if content_type
|
|
29
|
+
render(template, **options)
|
|
30
|
+
end
|
|
14
31
|
end
|
|
15
32
|
end
|
|
@@ -10,10 +10,10 @@ module SlashMigrate
|
|
|
10
10
|
def preview
|
|
11
11
|
@migration = AddColumnsMigration.from_params(table: @table, rows: params[:attributes])
|
|
12
12
|
@hint = "Add a column to see the migration it will generate." unless @migration.any?
|
|
13
|
-
|
|
13
|
+
render_stream :preview
|
|
14
14
|
rescue => e
|
|
15
15
|
@error = e.message
|
|
16
|
-
|
|
16
|
+
render_stream :preview
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def create
|
|
@@ -50,10 +50,10 @@ module SlashMigrate
|
|
|
50
50
|
original = find_column
|
|
51
51
|
@migration = original && EditColumnMigration.new(table: @table, original: original, desired: desired_column)
|
|
52
52
|
@hint = "Change a value to see the migration it will generate." unless @migration&.changed?
|
|
53
|
-
|
|
53
|
+
render_stream :update_preview
|
|
54
54
|
rescue => e
|
|
55
55
|
@error = e.message
|
|
56
|
-
|
|
56
|
+
render_stream :update_preview
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def update
|
|
@@ -10,10 +10,10 @@ module SlashMigrate
|
|
|
10
10
|
def preview
|
|
11
11
|
@migration = build_migration
|
|
12
12
|
@hint = "Pick a column to index to see the migration it will generate." unless @migration.any?
|
|
13
|
-
|
|
13
|
+
render_stream :preview
|
|
14
14
|
rescue => e
|
|
15
15
|
@error = e.message
|
|
16
|
-
|
|
16
|
+
render_stream :preview
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def create
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
module SlashMigrate
|
|
2
2
|
class MigrationsController < ApplicationController
|
|
3
3
|
def index
|
|
4
|
-
|
|
5
|
-
@pending = runner.pending?
|
|
4
|
+
load_migrations
|
|
6
5
|
end
|
|
7
6
|
|
|
8
7
|
def run
|
|
9
|
-
|
|
8
|
+
@result = runner.migrate
|
|
9
|
+
@command = "rails db:migrate"
|
|
10
|
+
stream_result
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def rollback
|
|
13
|
-
|
|
14
|
+
@result = runner.rollback
|
|
15
|
+
@command = "rails db:rollback"
|
|
16
|
+
stream_result
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def destroy
|
|
17
|
-
result = runner.delete(params[:version])
|
|
18
|
-
|
|
19
|
-
redirect_to migrations_path
|
|
20
|
+
@result = runner.delete(params[:version])
|
|
21
|
+
stream_result
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
private
|
|
@@ -25,17 +27,22 @@ module SlashMigrate
|
|
|
25
27
|
@runner ||= MigrationRunner.new
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
def load_migrations
|
|
31
|
+
@migrations = runner.status
|
|
32
|
+
@pending = runner.pending?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Update the page in place with a Turbo Stream instead of redirecting and
|
|
36
|
+
# carrying the command output in the flash: that output can be large, and
|
|
37
|
+
# CookieStore (the Rails default) caps the session at 4 KB, so a redirect
|
|
38
|
+
# could overflow the cookie and crash the host app. The engine ships no
|
|
39
|
+
# turbo-rails, so the <turbo-stream> is written by hand (see stream.html.erb)
|
|
40
|
+
# and rendered via render_stream, which forces the html template and sets the
|
|
41
|
+
# turbo-stream MIME; native Turbo form handling applies it. A plain refresh
|
|
42
|
+
# re-issues the GET, so the task never re-runs.
|
|
43
|
+
def stream_result
|
|
44
|
+
load_migrations
|
|
45
|
+
render_stream :stream, content_type: "text/vnd.turbo-stream.html"
|
|
39
46
|
end
|
|
40
47
|
end
|
|
41
48
|
end
|
|
@@ -16,11 +16,11 @@ module SlashMigrate
|
|
|
16
16
|
@hint = "Enter a model name to see the migration it will generate."
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
render_stream :preview
|
|
20
20
|
rescue => e
|
|
21
21
|
# Partial/invalid input shouldn't 500 the live preview; show the problem.
|
|
22
22
|
@error = e.message
|
|
23
|
-
|
|
23
|
+
render_stream :preview
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def create
|
|
@@ -43,6 +43,29 @@ module SlashMigrate
|
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# Where the engine is actually mounted, as the browser sees it (e.g.
|
|
47
|
+
# "/rails/migrate"). Read from the request's script_name so a host that
|
|
48
|
+
# mounts us somewhere non-standard is reflected verbatim, rather than
|
|
49
|
+
# trusting the configured default. Falls back to that default when there's
|
|
50
|
+
# no request to read (e.g. rendering outside the engine).
|
|
51
|
+
def mount_path
|
|
52
|
+
request&.script_name.presence || SlashMigrate.config.mount_path
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The mount path drawn as the nav brand: each "/" muted via <em> (see
|
|
56
|
+
# `.nav-brand em`) with the path segments between them in ink.
|
|
57
|
+
def nav_brand
|
|
58
|
+
safe_join(mount_path.split("/", -1), tag.em("/"))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The document <title>: the name the current page set via `content_for
|
|
62
|
+
# :title`, so browser tabs, history, and bookmarks stay distinguishable.
|
|
63
|
+
# Falls back to the mount path (the plain-text form of the nav brand) for
|
|
64
|
+
# any page that doesn't set one.
|
|
65
|
+
def page_title
|
|
66
|
+
content_for(:title).presence || mount_path
|
|
67
|
+
end
|
|
68
|
+
|
|
46
69
|
# The column types worth showing first, in teaching order. Anything else the
|
|
47
70
|
# database supports follows, alphabetically.
|
|
48
71
|
COMMON_COLUMN_TYPES = %w[string text integer boolean references datetime date decimal float].freeze
|
|
@@ -34,9 +34,8 @@ module SlashMigrate
|
|
|
34
34
|
# Normalize to a String, matching the form-input path. Active Record
|
|
35
35
|
# casts defaults differently across versions and adapters (Rails 8.0
|
|
36
36
|
# hands back "0", 8.1 casts it to 0; a boolean default arrives as
|
|
37
|
-
# false), and the rest of this class — presence, downcase,
|
|
38
|
-
# interpolation — assumes a string.
|
|
39
|
-
# would also silently drop a boolean's `default: false`.
|
|
37
|
+
# false), and the rest of this class — to_s.strip.presence, downcase,
|
|
38
|
+
# literal interpolation — assumes a string.
|
|
40
39
|
default: ar_column.default&.to_s,
|
|
41
40
|
limit: meta&.limit,
|
|
42
41
|
precision: meta&.precision,
|
|
@@ -51,12 +50,12 @@ module SlashMigrate
|
|
|
51
50
|
@name = name.to_s.strip
|
|
52
51
|
@type = type.to_s
|
|
53
52
|
@null = null
|
|
54
|
-
@default = presence
|
|
55
|
-
@limit = presence
|
|
56
|
-
@precision = presence
|
|
57
|
-
@scale = presence
|
|
53
|
+
@default = default.to_s.strip.presence
|
|
54
|
+
@limit = limit.to_s.strip.presence
|
|
55
|
+
@precision = precision.to_s.strip.presence
|
|
56
|
+
@scale = scale.to_s.strip.presence
|
|
58
57
|
@index = index.to_s
|
|
59
|
-
@to_table = presence
|
|
58
|
+
@to_table = to_table.to_s.strip.presence
|
|
60
59
|
end
|
|
61
60
|
|
|
62
61
|
def blank?
|
|
@@ -189,10 +188,5 @@ module SlashMigrate
|
|
|
189
188
|
@default.inspect
|
|
190
189
|
end
|
|
191
190
|
end
|
|
192
|
-
|
|
193
|
-
def presence(value)
|
|
194
|
-
string = value.to_s.strip
|
|
195
|
-
string.empty? ? nil : string
|
|
196
|
-
end
|
|
197
191
|
end
|
|
198
192
|
end
|
|
@@ -62,7 +62,13 @@ module SlashMigrate
|
|
|
62
62
|
private
|
|
63
63
|
|
|
64
64
|
def run(task)
|
|
65
|
-
|
|
65
|
+
# db:migrate is part of the host app's OWN bundle, so reset to the env
|
|
66
|
+
# from before this process loaded Bundler with with_original_env — NOT
|
|
67
|
+
# with_unbundled_env. The latter strips the host's bundler config,
|
|
68
|
+
# including a custom BUNDLE_PATH (e.g. Codespaces installs the bundle under
|
|
69
|
+
# /home/student/.bundle); the child bin/rails then can't find gems there —
|
|
70
|
+
# notably a git-sourced gem — and dies with "is not yet checked out".
|
|
71
|
+
output, process = Bundler.with_original_env do
|
|
66
72
|
Open3.capture2e({"RAILS_ENV" => Rails.env.to_s}, rails_bin, task, chdir: Rails.root.to_s)
|
|
67
73
|
end
|
|
68
74
|
Result.new(output: output, success: process.success?)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
|
-
<title
|
|
5
|
+
<title><%= page_title %></title>
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
7
|
<%= csrf_meta_tags %>
|
|
8
8
|
<%= csp_meta_tag %>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
20
|
<header class="nav">
|
|
21
|
-
<%= link_to root_path, class: "nav-brand" do
|
|
21
|
+
<%= link_to root_path, class: "nav-brand" do %><%= nav_brand %><% end %>
|
|
22
22
|
<span class="nav-spacer"></span>
|
|
23
23
|
<nav class="nav-links">
|
|
24
24
|
<%= nav_link "Tables", root_path, :database, active: nav_section == :tables %>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<% content_for :page_class, "page-form" %>
|
|
2
|
+
<% content_for :title, "Edit #{@column.name} on #{@table}" %>
|
|
2
3
|
|
|
3
4
|
<div class="page-head">
|
|
4
5
|
<div>
|
|
5
6
|
<%= render "slash_migrate/shared/breadcrumbs", trail: [{label: "Tables", url: root_path}, {label: @table, url: table_path(@table)}, {label: "Edit #{@column.name}"}] %>
|
|
6
7
|
<h1 class="page-title">Edit <span class="mono"><%= @column.name %></span> on <span class="mono"><%= @table %></span></h1>
|
|
7
|
-
<p class="page-sub">Change the column's name, type, nullability, or default. The migration below
|
|
8
|
+
<p class="page-sub">Change the column's name, type, nullability, or default. The proposed migration will appear below in real-time as you edit.</p>
|
|
8
9
|
</div>
|
|
9
10
|
</div>
|
|
10
11
|
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
</div>
|
|
60
61
|
<% end %>
|
|
61
62
|
|
|
62
|
-
<div class="panel danger-zone"
|
|
63
|
+
<div class="panel danger-zone">
|
|
63
64
|
<div class="panel-head">
|
|
64
65
|
<div>
|
|
65
66
|
<h2>Drop this column</h2>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<% content_for :page_class, "page-form" %>
|
|
2
|
+
<% content_for :title, "Add columns to #{@table}" %>
|
|
2
3
|
|
|
3
4
|
<div class="page-head">
|
|
4
5
|
<div>
|
|
5
6
|
<%= render "slash_migrate/shared/breadcrumbs", trail: [{label: "Tables", url: root_path}, {label: @table, url: table_path(@table)}, {label: "Add columns"}] %>
|
|
6
7
|
<h1 class="page-title">Add columns to <span class="mono"><%= @table %></span></h1>
|
|
7
|
-
<p class="page-sub">
|
|
8
|
+
<p class="page-sub">Add some columns to your existing table. The proposed <span class="mono">add_column</span> migration will appear below in real-time as you type.</p>
|
|
8
9
|
</div>
|
|
9
10
|
</div>
|
|
10
11
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<% content_for :page_class, "page-form" %>
|
|
2
|
+
<% content_for :title, "Add index to #{@table}" %>
|
|
2
3
|
|
|
3
4
|
<div class="page-head">
|
|
4
5
|
<div>
|
|
5
6
|
<%= render "slash_migrate/shared/breadcrumbs", trail: [{label: "Tables", url: root_path}, {label: @table, url: table_path(@table)}, {label: "Add index"}] %>
|
|
6
7
|
<h1 class="page-title">Add index to <span class="mono"><%= @table %></span></h1>
|
|
7
|
-
<p class="page-sub">Pick
|
|
8
|
+
<p class="page-sub">Pick the columns you'd like to index — handy for speeding up lookups on foreign keys, or columns you filter or sort by often. The proposed migration will appear below in real-time as you type.</p>
|
|
8
9
|
</div>
|
|
9
10
|
</div>
|
|
10
11
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<%
|
|
2
|
+
applied = @migrations.count(&:applied?)
|
|
3
|
+
pending = @migrations.size - applied
|
|
4
|
+
%>
|
|
5
|
+
|
|
6
|
+
<div class="page-head">
|
|
7
|
+
<div>
|
|
8
|
+
<h1 class="page-title">Migrations</h1>
|
|
9
|
+
<p class="page-sub">
|
|
10
|
+
<%= applied %> applied · <%= pending %> pending. This list mirrors the files in your <span class="mono">db/migrate</span> folder.
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="row">
|
|
14
|
+
<% if @migrations.any?(&:applied?) %>
|
|
15
|
+
<%= button_to rollback_migrations_path, class: "btn btn-ghost",
|
|
16
|
+
data: {turbo_confirm: "Roll back the most recent migration? This reverses its changes."} do %>
|
|
17
|
+
<%= sm_icon(:rotate) %> Roll back last
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% if @pending %>
|
|
21
|
+
<%= button_to run_migrations_path, class: "btn btn-accent" do %><%= sm_icon(:play) %> Run pending<% end %>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<%# Feedback from the last run / rollback / delete, shown in place. Carried in
|
|
27
|
+
instance variables rather than the flash, so large command output never
|
|
28
|
+
rides through the session cookie. %>
|
|
29
|
+
<% if @command %>
|
|
30
|
+
<div class="migrate-output">
|
|
31
|
+
<%= render "slash_migrate/shared/terminal",
|
|
32
|
+
command: @command,
|
|
33
|
+
output: @result.output,
|
|
34
|
+
meta: (@result.success? ? "output" : "exited 1"),
|
|
35
|
+
meta_style: (@result.success? ? nil : "color:#E8B07F") %>
|
|
36
|
+
</div>
|
|
37
|
+
<% elsif @result %>
|
|
38
|
+
<div class="migrate-output">
|
|
39
|
+
<%= render layout: "slash_migrate/shared/callout", locals: {kind: @result.success? ? "ok" : "warn"} do %>
|
|
40
|
+
<%= @result.output %>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
45
|
+
<div class="panel">
|
|
46
|
+
<% if @migrations.empty? %>
|
|
47
|
+
<div class="panel-pad muted">No migration files yet. Create a model or add columns, and the migration shows up here.</div>
|
|
48
|
+
<% else %>
|
|
49
|
+
<table class="table">
|
|
50
|
+
<thead>
|
|
51
|
+
<tr><th>Status</th><th>Version</th><th>Migration</th><th></th></tr>
|
|
52
|
+
</thead>
|
|
53
|
+
<tbody>
|
|
54
|
+
<% @migrations.each do |migration| %>
|
|
55
|
+
<tr>
|
|
56
|
+
<td>
|
|
57
|
+
<span class="badge <%= migration.applied? ? "is-up" : "is-pending" %>"><%= migration.applied? ? "up" : "pending" %></span>
|
|
58
|
+
</td>
|
|
59
|
+
<td><span class="mono muted"><%= migration.version %></span></td>
|
|
60
|
+
<td><span class="mono"><%= migration.name %></span></td>
|
|
61
|
+
<td class="col-actions">
|
|
62
|
+
<% if migration.applied? %>
|
|
63
|
+
<span class="has-tip" tabindex="0">
|
|
64
|
+
<button type="button" class="btn btn-sm btn-disabled" disabled><%= sm_icon(:trash) %> Delete file</button>
|
|
65
|
+
<span class="tip">This migration has been run. Roll it back with <kbd>db:rollback</kbd> first — deleting its file would orphan the <kbd>schema_migrations</kbd> record and leave the change unreversible.</span>
|
|
66
|
+
</span>
|
|
67
|
+
<% else %>
|
|
68
|
+
<%= button_to migration_path(migration.version), method: :delete, class: "btn btn-sm btn-ghost",
|
|
69
|
+
data: {turbo_confirm: "Delete the “#{migration.name}” migration file? It hasn't been run."} do %>
|
|
70
|
+
<%= sm_icon(:trash) %> Delete file
|
|
71
|
+
<% end %>
|
|
72
|
+
<% end %>
|
|
73
|
+
</td>
|
|
74
|
+
</tr>
|
|
75
|
+
<% end %>
|
|
76
|
+
</tbody>
|
|
77
|
+
</table>
|
|
78
|
+
<% end %>
|
|
79
|
+
</div>
|
|
@@ -1,71 +1,6 @@
|
|
|
1
|
-
<%
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
<div class="page-head">
|
|
7
|
-
<div>
|
|
8
|
-
<h1 class="page-title">Migrations</h1>
|
|
9
|
-
<p class="page-sub">
|
|
10
|
-
<%= applied %> applied · <%= pending %> pending. This list mirrors the files in <span class="mono">db/migrate</span>.
|
|
11
|
-
</p>
|
|
12
|
-
</div>
|
|
13
|
-
<div class="row">
|
|
14
|
-
<% if @migrations.any?(&:applied?) %>
|
|
15
|
-
<%= button_to rollback_migrations_path, class: "btn btn-ghost",
|
|
16
|
-
data: {turbo_confirm: "Roll back the most recent migration? This reverses its changes."} do %>
|
|
17
|
-
<%= sm_icon(:rotate) %> Roll back last
|
|
18
|
-
<% end %>
|
|
19
|
-
<% end %>
|
|
20
|
-
<% if @pending %>
|
|
21
|
-
<%= button_to run_migrations_path, class: "btn btn-accent" do %><%= sm_icon(:play) %> Run pending<% end %>
|
|
22
|
-
<% end %>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
|
|
26
|
-
<% if flash[:migrate_output].present? %>
|
|
27
|
-
<% failed = flash[:alert].present? %>
|
|
28
|
-
<div style="margin-bottom:var(--gap-lg)">
|
|
29
|
-
<%= render "slash_migrate/shared/terminal",
|
|
30
|
-
command: (flash[:migrate_command] || "rails db:migrate"),
|
|
31
|
-
output: flash[:migrate_output],
|
|
32
|
-
meta: (failed ? "exited 1" : "output"),
|
|
33
|
-
meta_style: (failed ? "color:#E8B07F" : nil) %>
|
|
34
|
-
</div>
|
|
35
|
-
<% end %>
|
|
36
|
-
|
|
37
|
-
<div class="panel">
|
|
38
|
-
<% if @migrations.empty? %>
|
|
39
|
-
<div class="panel-pad muted">No migration files yet. Create a model or add columns, and the migration shows up here.</div>
|
|
40
|
-
<% else %>
|
|
41
|
-
<table class="table">
|
|
42
|
-
<thead>
|
|
43
|
-
<tr><th>Status</th><th>Version</th><th>Migration</th><th></th></tr>
|
|
44
|
-
</thead>
|
|
45
|
-
<tbody>
|
|
46
|
-
<% @migrations.each do |migration| %>
|
|
47
|
-
<tr>
|
|
48
|
-
<td>
|
|
49
|
-
<span class="badge <%= migration.applied? ? "is-up" : "is-pending" %>"><%= migration.applied? ? "up" : "pending" %></span>
|
|
50
|
-
</td>
|
|
51
|
-
<td><span class="mono muted"><%= migration.version %></span></td>
|
|
52
|
-
<td><span class="mono"><%= migration.name %></span></td>
|
|
53
|
-
<td class="col-actions">
|
|
54
|
-
<% if migration.applied? %>
|
|
55
|
-
<span class="has-tip" tabindex="0">
|
|
56
|
-
<button type="button" class="btn btn-sm btn-disabled" disabled><%= sm_icon(:trash) %> Delete file</button>
|
|
57
|
-
<span class="tip">This migration has been run. Roll it back with <kbd>db:rollback</kbd> first — deleting its file would orphan the <kbd>schema_migrations</kbd> record and leave the change unreversible.</span>
|
|
58
|
-
</span>
|
|
59
|
-
<% else %>
|
|
60
|
-
<%= button_to migration_path(migration.version), method: :delete, class: "btn btn-sm btn-ghost",
|
|
61
|
-
data: {turbo_confirm: "Delete the “#{migration.name}” migration file? It hasn't been run."} do %>
|
|
62
|
-
<%= sm_icon(:trash) %> Delete file
|
|
63
|
-
<% end %>
|
|
64
|
-
<% end %>
|
|
65
|
-
</td>
|
|
66
|
-
</tr>
|
|
67
|
-
<% end %>
|
|
68
|
-
</tbody>
|
|
69
|
-
</table>
|
|
70
|
-
<% end %>
|
|
1
|
+
<% content_for :title, "Migrations" %>
|
|
2
|
+
<%# The whole page lives in one Turbo Stream target so run / rollback / delete
|
|
3
|
+
can update it in place (see stream.html.erb) without a redirect. %>
|
|
4
|
+
<div id="sm-migrations">
|
|
5
|
+
<%= render "migrations" %>
|
|
71
6
|
</div>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<%# Rendered for run / rollback / delete. The controller sends this with the
|
|
2
|
+
text/vnd.turbo-stream.html MIME so native Turbo form handling applies it —
|
|
3
|
+
no turbo-rails gem, no redirect, no flash. %>
|
|
4
|
+
<turbo-stream action="update" target="sm-migrations">
|
|
5
|
+
<template><%= render "migrations" %></template>
|
|
6
|
+
</turbo-stream>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<% content_for :page_class, "page-form" %>
|
|
2
|
+
<% content_for :title, "New table" %>
|
|
2
3
|
|
|
3
4
|
<div class="page-head">
|
|
4
5
|
<div>
|
|
5
6
|
<h1 class="page-title">New table</h1>
|
|
6
|
-
<p class="page-sub">Pick a name and add columns. The migration and model files appear
|
|
7
|
+
<p class="page-sub">Pick a name for your new table and add some columns. The proposed migration and model files will appear below in real-time as you type.</p>
|
|
7
8
|
</div>
|
|
8
9
|
</div>
|
|
9
10
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
<%# Animated three-chevron
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
<%# Animated three-chevron indicators pointing from the form down to the live
|
|
2
|
+
preview. Three identical stacks, spread across the width (see .flow-arrows). %>
|
|
3
|
+
<div class="flow-arrows" aria-hidden="true">
|
|
4
|
+
<% 3.times do %>
|
|
5
|
+
<span class="stack-link">
|
|
6
|
+
<span class="chev"></span>
|
|
7
|
+
<span class="chev"></span>
|
|
8
|
+
<span class="chev"></span>
|
|
9
|
+
</span>
|
|
10
|
+
<% end %>
|
|
6
11
|
</div>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
<% content_for :title, "Tables" %>
|
|
2
|
+
|
|
1
3
|
<div class="page-head">
|
|
2
4
|
<div>
|
|
3
5
|
<h1 class="page-title">Tables</h1>
|
|
4
|
-
<p class="page-sub">The tables in
|
|
6
|
+
<p class="page-sub">The tables in your app's database. Open any one to inspect its columns, indexes, and foreign keys.</p>
|
|
5
7
|
</div>
|
|
6
8
|
<%= link_to new_model_path, class: "btn btn-accent" do %><%= sm_icon(:plus) %> New table<% end %>
|
|
7
9
|
</div>
|
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
primary_key = @table.primary_key
|
|
6
6
|
fk_targets = foreign_keys.to_h { |fk| [fk.column, fk.to_table] }
|
|
7
7
|
%>
|
|
8
|
+
<% content_for :title, @table.name %>
|
|
8
9
|
|
|
9
10
|
<div class="page-head">
|
|
10
11
|
<div>
|
|
11
12
|
<%= render "slash_migrate/shared/breadcrumbs", trail: [{label: "Tables", url: root_path}, {label: @table.name}] %>
|
|
12
13
|
<h1 class="page-title"><span class="mono"><%= @table.name %></span></h1>
|
|
13
14
|
<p class="page-sub">
|
|
14
|
-
<%= columns.size
|
|
15
|
-
<%= indexes.size
|
|
16
|
-
<%= foreign_keys.size
|
|
15
|
+
<%= pluralize(columns.size, "column") %> ·
|
|
16
|
+
<%= pluralize(indexes.size, "index", "indexes") %> ·
|
|
17
|
+
<%= pluralize(foreign_keys.size, "foreign key") %>
|
|
17
18
|
</p>
|
|
18
19
|
</div>
|
|
19
20
|
<%= link_to new_table_column_path(@table.name), class: "btn btn-ghost" do %><%= sm_icon(:plus) %> Add columns<% end %>
|
|
@@ -348,7 +348,9 @@ form.button_to { display:inline; margin:0; }
|
|
|
348
348
|
.stack { display:flex; flex-direction:column; gap:var(--space-md); }
|
|
349
349
|
.stack > .callout { margin:0; }
|
|
350
350
|
/* Three downward chevrons that shimmer toward the live preview — an affordance,
|
|
351
|
-
not a button (no fill, no circle).
|
|
351
|
+
not a button (no fill, no circle). Repeated across the width so the eye is
|
|
352
|
+
drawn down to the preview from wherever it's resting. */
|
|
353
|
+
.flow-arrows { display:flex; justify-content:space-around; }
|
|
352
354
|
.stack-link { display:flex; flex-direction:column; align-items:center; gap:1px; padding:4px 0; }
|
|
353
355
|
.stack-link .chev {
|
|
354
356
|
width:11px; height:11px;
|
|
@@ -376,6 +378,9 @@ form.button_to { display:inline; margin:0; }
|
|
|
376
378
|
/* ── Misc ────────────────────────────────────────────── */
|
|
377
379
|
.chip-soft { font-family:var(--font-mono); font-size:0.92em; background:var(--surface-2); padding:1px 6px; border-radius:4px; color:var(--ink-2); }
|
|
378
380
|
.row { display:flex; align-items:center; gap:var(--space-sm); }
|
|
379
|
-
.danger-zone { border-color:var(--err-border); background:linear-gradient(180deg, #F9E7E1 0%, var(--surface) 60%); }
|
|
381
|
+
.danger-zone { margin-top:var(--space-xl); border-color:var(--err-border); background:linear-gradient(180deg, #F9E7E1 0%, var(--surface) 60%); }
|
|
380
382
|
.danger-zone .panel-head { border-bottom-color:#ECD0C5; }
|
|
381
383
|
.danger-zone .panel-head h2 { color:var(--err); }
|
|
384
|
+
/* Spacing tokens belong in the stylesheet, not in view inline styles, so a
|
|
385
|
+
future token rename can't silently drop them (see the --gap-* history). */
|
|
386
|
+
.migrate-output { margin-bottom:var(--space-xl); }
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: slash_migrate
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Raghu Betina
|
|
@@ -68,7 +68,9 @@ files:
|
|
|
68
68
|
- app/views/slash_migrate/indexes/_preview.html.erb
|
|
69
69
|
- app/views/slash_migrate/indexes/new.html.erb
|
|
70
70
|
- app/views/slash_migrate/indexes/preview.html.erb
|
|
71
|
+
- app/views/slash_migrate/migrations/_migrations.html.erb
|
|
71
72
|
- app/views/slash_migrate/migrations/index.html.erb
|
|
73
|
+
- app/views/slash_migrate/migrations/stream.html.erb
|
|
72
74
|
- app/views/slash_migrate/models/_name_help.html.erb
|
|
73
75
|
- app/views/slash_migrate/models/_preview.html.erb
|
|
74
76
|
- app/views/slash_migrate/models/_row.html.erb
|