data_porter 0.1.0

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.
Files changed (159) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/commands/blog-status.md +10 -0
  3. data/.claude/commands/blog.md +109 -0
  4. data/.claude/commands/task-done.md +27 -0
  5. data/.claude/commands/tm/add-dependency.md +58 -0
  6. data/.claude/commands/tm/add-subtask.md +79 -0
  7. data/.claude/commands/tm/add-task.md +81 -0
  8. data/.claude/commands/tm/analyze-complexity.md +124 -0
  9. data/.claude/commands/tm/analyze-project.md +100 -0
  10. data/.claude/commands/tm/auto-implement-tasks.md +100 -0
  11. data/.claude/commands/tm/command-pipeline.md +80 -0
  12. data/.claude/commands/tm/complexity-report.md +120 -0
  13. data/.claude/commands/tm/convert-task-to-subtask.md +74 -0
  14. data/.claude/commands/tm/expand-all-tasks.md +52 -0
  15. data/.claude/commands/tm/expand-task.md +52 -0
  16. data/.claude/commands/tm/fix-dependencies.md +82 -0
  17. data/.claude/commands/tm/help.md +101 -0
  18. data/.claude/commands/tm/init-project-quick.md +49 -0
  19. data/.claude/commands/tm/init-project.md +53 -0
  20. data/.claude/commands/tm/install-taskmaster.md +118 -0
  21. data/.claude/commands/tm/learn.md +106 -0
  22. data/.claude/commands/tm/list-tasks-by-status.md +42 -0
  23. data/.claude/commands/tm/list-tasks-with-subtasks.md +30 -0
  24. data/.claude/commands/tm/list-tasks.md +46 -0
  25. data/.claude/commands/tm/next-task.md +69 -0
  26. data/.claude/commands/tm/parse-prd-with-research.md +51 -0
  27. data/.claude/commands/tm/parse-prd.md +52 -0
  28. data/.claude/commands/tm/project-status.md +67 -0
  29. data/.claude/commands/tm/quick-install-taskmaster.md +23 -0
  30. data/.claude/commands/tm/remove-all-subtasks.md +94 -0
  31. data/.claude/commands/tm/remove-dependency.md +65 -0
  32. data/.claude/commands/tm/remove-subtask.md +87 -0
  33. data/.claude/commands/tm/remove-subtasks.md +89 -0
  34. data/.claude/commands/tm/remove-task.md +110 -0
  35. data/.claude/commands/tm/setup-models.md +52 -0
  36. data/.claude/commands/tm/show-task.md +85 -0
  37. data/.claude/commands/tm/smart-workflow.md +58 -0
  38. data/.claude/commands/tm/sync-readme.md +120 -0
  39. data/.claude/commands/tm/tm-main.md +147 -0
  40. data/.claude/commands/tm/to-cancelled.md +58 -0
  41. data/.claude/commands/tm/to-deferred.md +50 -0
  42. data/.claude/commands/tm/to-done.md +47 -0
  43. data/.claude/commands/tm/to-in-progress.md +39 -0
  44. data/.claude/commands/tm/to-pending.md +35 -0
  45. data/.claude/commands/tm/to-review.md +43 -0
  46. data/.claude/commands/tm/update-single-task.md +122 -0
  47. data/.claude/commands/tm/update-task.md +75 -0
  48. data/.claude/commands/tm/update-tasks-from-id.md +111 -0
  49. data/.claude/commands/tm/validate-dependencies.md +72 -0
  50. data/.claude/commands/tm/view-models.md +52 -0
  51. data/.env.example +12 -0
  52. data/.mcp.json +24 -0
  53. data/.taskmaster/CLAUDE.md +435 -0
  54. data/.taskmaster/config.json +44 -0
  55. data/.taskmaster/docs/prd.txt +2044 -0
  56. data/.taskmaster/state.json +6 -0
  57. data/.taskmaster/tasks/task_001.md +19 -0
  58. data/.taskmaster/tasks/task_002.md +19 -0
  59. data/.taskmaster/tasks/task_003.md +19 -0
  60. data/.taskmaster/tasks/task_004.md +19 -0
  61. data/.taskmaster/tasks/task_005.md +19 -0
  62. data/.taskmaster/tasks/task_006.md +19 -0
  63. data/.taskmaster/tasks/task_007.md +19 -0
  64. data/.taskmaster/tasks/task_008.md +19 -0
  65. data/.taskmaster/tasks/task_009.md +19 -0
  66. data/.taskmaster/tasks/task_010.md +19 -0
  67. data/.taskmaster/tasks/task_011.md +19 -0
  68. data/.taskmaster/tasks/task_012.md +19 -0
  69. data/.taskmaster/tasks/task_013.md +19 -0
  70. data/.taskmaster/tasks/task_014.md +19 -0
  71. data/.taskmaster/tasks/task_015.md +19 -0
  72. data/.taskmaster/tasks/task_016.md +19 -0
  73. data/.taskmaster/tasks/task_017.md +19 -0
  74. data/.taskmaster/tasks/task_018.md +19 -0
  75. data/.taskmaster/tasks/task_019.md +19 -0
  76. data/.taskmaster/tasks/task_020.md +19 -0
  77. data/.taskmaster/tasks/tasks.json +299 -0
  78. data/.taskmaster/templates/example_prd.txt +47 -0
  79. data/.taskmaster/templates/example_prd_rpg.txt +511 -0
  80. data/CHANGELOG.md +29 -0
  81. data/CLAUDE.md +65 -0
  82. data/CODE_OF_CONDUCT.md +10 -0
  83. data/CONTRIBUTING.md +49 -0
  84. data/LICENSE +21 -0
  85. data/README.md +463 -0
  86. data/Rakefile +12 -0
  87. data/app/assets/stylesheets/data_porter/application.css +646 -0
  88. data/app/channels/data_porter/import_channel.rb +10 -0
  89. data/app/controllers/data_porter/imports_controller.rb +68 -0
  90. data/app/javascript/data_porter/progress_controller.js +33 -0
  91. data/app/jobs/data_porter/dry_run_job.rb +12 -0
  92. data/app/jobs/data_porter/import_job.rb +12 -0
  93. data/app/jobs/data_porter/parse_job.rb +12 -0
  94. data/app/models/data_porter/data_import.rb +49 -0
  95. data/app/views/data_porter/imports/index.html.erb +142 -0
  96. data/app/views/data_porter/imports/new.html.erb +88 -0
  97. data/app/views/data_porter/imports/show.html.erb +49 -0
  98. data/config/database.yml +3 -0
  99. data/config/routes.rb +12 -0
  100. data/docs/SPEC.md +2012 -0
  101. data/docs/UI.md +32 -0
  102. data/docs/blog/001-why-build-a-data-import-engine.md +166 -0
  103. data/docs/blog/002-scaffolding-a-rails-engine.md +188 -0
  104. data/docs/blog/003-configuration-dsl.md +222 -0
  105. data/docs/blog/004-store-model-jsonb.md +237 -0
  106. data/docs/blog/005-target-dsl.md +284 -0
  107. data/docs/blog/006-parsing-csv-sources.md +300 -0
  108. data/docs/blog/007-orchestrator.md +247 -0
  109. data/docs/blog/008-actioncable-stimulus.md +376 -0
  110. data/docs/blog/009-phlex-ui-components.md +446 -0
  111. data/docs/blog/010-controllers-routing.md +374 -0
  112. data/docs/blog/011-generators.md +364 -0
  113. data/docs/blog/012-json-api-sources.md +323 -0
  114. data/docs/blog/013-testing-rails-engine.md +618 -0
  115. data/docs/blog/014-dry-run.md +307 -0
  116. data/docs/blog/015-publishing-retro.md +264 -0
  117. data/docs/blog/016-erb-view-templates.md +431 -0
  118. data/docs/blog/017-showcase-final-retro.md +220 -0
  119. data/docs/blog/BACKLOG.md +8 -0
  120. data/docs/blog/SERIES.md +154 -0
  121. data/docs/screenshots/index-with-previewing.jpg +0 -0
  122. data/docs/screenshots/index.jpg +0 -0
  123. data/docs/screenshots/modal-new-import.jpg +0 -0
  124. data/docs/screenshots/preview.jpg +0 -0
  125. data/lib/data_porter/broadcaster.rb +29 -0
  126. data/lib/data_porter/components/base.rb +10 -0
  127. data/lib/data_porter/components/failure_alert.rb +20 -0
  128. data/lib/data_porter/components/preview_table.rb +54 -0
  129. data/lib/data_porter/components/progress_bar.rb +33 -0
  130. data/lib/data_porter/components/results_summary.rb +19 -0
  131. data/lib/data_porter/components/status_badge.rb +16 -0
  132. data/lib/data_porter/components/summary_cards.rb +30 -0
  133. data/lib/data_porter/components.rb +14 -0
  134. data/lib/data_porter/configuration.rb +25 -0
  135. data/lib/data_porter/dsl/api_config.rb +25 -0
  136. data/lib/data_porter/dsl/column.rb +17 -0
  137. data/lib/data_porter/engine.rb +15 -0
  138. data/lib/data_porter/orchestrator.rb +141 -0
  139. data/lib/data_porter/record_validator.rb +32 -0
  140. data/lib/data_porter/registry.rb +33 -0
  141. data/lib/data_porter/sources/api.rb +49 -0
  142. data/lib/data_porter/sources/base.rb +35 -0
  143. data/lib/data_porter/sources/csv.rb +43 -0
  144. data/lib/data_porter/sources/json.rb +45 -0
  145. data/lib/data_porter/sources.rb +20 -0
  146. data/lib/data_porter/store_models/error.rb +13 -0
  147. data/lib/data_porter/store_models/import_record.rb +52 -0
  148. data/lib/data_porter/store_models/report.rb +21 -0
  149. data/lib/data_porter/target.rb +89 -0
  150. data/lib/data_porter/type_validator.rb +46 -0
  151. data/lib/data_porter/version.rb +5 -0
  152. data/lib/data_porter.rb +32 -0
  153. data/lib/generators/data_porter/install/install_generator.rb +33 -0
  154. data/lib/generators/data_porter/install/templates/create_data_porter_imports.rb.erb +21 -0
  155. data/lib/generators/data_porter/install/templates/initializer.rb +30 -0
  156. data/lib/generators/data_porter/target/target_generator.rb +44 -0
  157. data/lib/generators/data_porter/target/templates/target.rb.tt +20 -0
  158. data/sig/data_porter.rbs +4 -0
  159. metadata +274 -0
@@ -0,0 +1,33 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { createConsumer } from "@rails/actioncable"
3
+
4
+ export default class extends Controller {
5
+ static targets = ["bar", "text"]
6
+ static values = { id: Number }
7
+
8
+ connect() {
9
+ this.subscription = createConsumer().subscriptions.create(
10
+ { channel: "DataPorter::ImportChannel", id: this.idValue },
11
+ {
12
+ received: (data) => {
13
+ if (data.status === "processing") {
14
+ this.updateProgress(data.percentage)
15
+ } else {
16
+ window.location.reload()
17
+ }
18
+ }
19
+ }
20
+ )
21
+ }
22
+
23
+ updateProgress(percentage) {
24
+ if (this.hasBarTarget) {
25
+ this.barTarget.style.width = `${percentage}%`
26
+ this.textTarget.textContent = `${percentage}%`
27
+ }
28
+ }
29
+
30
+ disconnect() {
31
+ this.subscription?.unsubscribe()
32
+ }
33
+ }
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataPorter
4
+ class DryRunJob < ActiveJob::Base
5
+ queue_as { DataPorter.configuration.queue_name }
6
+
7
+ def perform(import_id)
8
+ data_import = DataImport.find(import_id)
9
+ Orchestrator.new(data_import).dry_run!
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataPorter
4
+ class ImportJob < ActiveJob::Base
5
+ queue_as { DataPorter.configuration.queue_name }
6
+
7
+ def perform(import_id)
8
+ data_import = DataImport.find(import_id)
9
+ Orchestrator.new(data_import).import!
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataPorter
4
+ class ParseJob < ActiveJob::Base
5
+ queue_as { DataPorter.configuration.queue_name }
6
+
7
+ def perform(import_id)
8
+ data_import = DataImport.find(import_id)
9
+ Orchestrator.new(data_import).parse!
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataPorter
4
+ class DataImport < ActiveRecord::Base
5
+ self.table_name = "data_porter_imports"
6
+
7
+ has_one_attached :file
8
+
9
+ belongs_to :user, polymorphic: true, optional: true
10
+
11
+ enum :status, {
12
+ pending: 0,
13
+ parsing: 1,
14
+ previewing: 2,
15
+ importing: 3,
16
+ completed: 4,
17
+ failed: 5,
18
+ dry_running: 6
19
+ }
20
+
21
+ attribute :records, StoreModels::ImportRecord.to_array_type, default: -> { [] }
22
+ attribute :report, StoreModels::Report.to_type, default: -> { StoreModels::Report.new }
23
+
24
+ attribute :config, :json, default: -> { {} }
25
+
26
+ validates :target_key, presence: true
27
+ validates :source_type, presence: true, inclusion: { in: %w[csv json api] }
28
+
29
+ def target_class
30
+ Registry.find(target_key)
31
+ end
32
+
33
+ def source_class
34
+ Sources.resolve(source_type)
35
+ end
36
+
37
+ def previewable?
38
+ previewing? && records.any?
39
+ end
40
+
41
+ def importable_records
42
+ records.select(&:importable?)
43
+ end
44
+
45
+ def records_summary
46
+ records.group_by(&:status).transform_values(&:count)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,142 @@
1
+ <%= stylesheet_link_tag "data_porter/application" %>
2
+
3
+ <div class="data-porter">
4
+ <div class="dp-header">
5
+ <h1 class="dp-title">Imports</h1>
6
+ <button type="button" class="dp-btn dp-btn--primary" onclick="document.getElementById('dp-modal').classList.add('dp-modal--open')">
7
+ New Import
8
+ </button>
9
+ </div>
10
+
11
+ <% if @imports.any? %>
12
+ <table class="dp-table">
13
+ <thead>
14
+ <tr>
15
+ <th>ID</th>
16
+ <th>Target</th>
17
+ <th>Source</th>
18
+ <th>Status</th>
19
+ <th>Created</th>
20
+ <th></th>
21
+ </tr>
22
+ </thead>
23
+ <tbody>
24
+ <% @imports.each do |import| %>
25
+ <tr>
26
+ <td><%= import.id %></td>
27
+ <td><%= import.target_key %></td>
28
+ <td><%= import.source_type %></td>
29
+ <td><%= raw DataPorter::Components::StatusBadge.new(status: import.status).call %></td>
30
+ <td><%= import.created_at&.strftime("%Y-%m-%d %H:%M") %></td>
31
+ <td><%= link_to "View", import_path(import), class: "dp-link" %></td>
32
+ </tr>
33
+ <% end %>
34
+ </tbody>
35
+ </table>
36
+ <% else %>
37
+ <div class="dp-empty-state">
38
+ <div class="dp-empty-state__icon">&#128230;</div>
39
+ <p class="dp-empty-state__text">No imports yet</p>
40
+ <button type="button" class="dp-btn dp-btn--primary" onclick="document.getElementById('dp-modal').classList.add('dp-modal--open')">
41
+ Create your first import
42
+ </button>
43
+ </div>
44
+ <% end %>
45
+ </div>
46
+
47
+ <div id="dp-modal" class="dp-modal">
48
+ <div class="dp-modal__backdrop" onclick="document.getElementById('dp-modal').classList.remove('dp-modal--open')"></div>
49
+ <div class="dp-modal__content">
50
+ <div class="dp-modal__header">
51
+ <h2 class="dp-modal__title">New Import</h2>
52
+ <button type="button" class="dp-modal__close" onclick="document.getElementById('dp-modal').classList.remove('dp-modal--open')">&times;</button>
53
+ </div>
54
+
55
+ <%= form_with model: DataPorter::DataImport.new, url: imports_path, class: "dp-modal__body", multipart: true do |f| %>
56
+ <div class="dp-field">
57
+ <%= f.label :target_key, "Target", class: "dp-label" %>
58
+ <%= f.select :target_key,
59
+ @targets.map { |t| [t[:label], t[:key]] },
60
+ { prompt: "Select a target..." },
61
+ class: "dp-select" %>
62
+ </div>
63
+
64
+ <div class="dp-field">
65
+ <%= f.label :source_type, "Source Type", class: "dp-label" %>
66
+ <%= f.select :source_type,
67
+ DataPorter.configuration.enabled_sources.map { |s| [s.to_s.upcase, s] },
68
+ { prompt: "Select source type..." },
69
+ id: "dp-source-select",
70
+ class: "dp-select" %>
71
+ </div>
72
+
73
+ <div id="dp-file-field" class="dp-field">
74
+ <%= f.label :file, "File", class: "dp-label" %>
75
+ <label class="dp-dropzone" id="dp-dropzone">
76
+ <input type="file" name="data_import[file]" id="dp-file-input" class="dp-dropzone__input" />
77
+ <div class="dp-dropzone__content">
78
+ <div class="dp-dropzone__icon">&#128196;</div>
79
+ <span class="dp-dropzone__text">Drop your file here or <strong>browse</strong></span>
80
+ <span class="dp-dropzone__hint">CSV or JSON files accepted</span>
81
+ </div>
82
+ <div class="dp-dropzone__selected" id="dp-file-name" style="display: none;"></div>
83
+ </label>
84
+ </div>
85
+
86
+ <div class="dp-modal__footer">
87
+ <%= f.submit "Start Import", class: "dp-btn dp-btn--primary" %>
88
+ <button type="button" class="dp-btn dp-btn--secondary" onclick="document.getElementById('dp-modal').classList.remove('dp-modal--open')">Cancel</button>
89
+ </div>
90
+ <% end %>
91
+ </div>
92
+ </div>
93
+
94
+ <script>
95
+ (function() {
96
+ var sourceSelect = document.getElementById("dp-source-select");
97
+ var fileField = document.getElementById("dp-file-field");
98
+ var fileInput = document.getElementById("dp-file-input");
99
+ var dropzone = document.getElementById("dp-dropzone");
100
+ var fileName = document.getElementById("dp-file-name");
101
+
102
+ if (sourceSelect) {
103
+ sourceSelect.addEventListener("change", function() {
104
+ fileField.style.display = this.value === "api" ? "none" : "";
105
+ });
106
+ }
107
+
108
+ if (fileInput) {
109
+ fileInput.addEventListener("change", function() {
110
+ if (this.files.length > 0) {
111
+ fileName.textContent = this.files[0].name;
112
+ fileName.style.display = "";
113
+ dropzone.classList.add("dp-dropzone--has-file");
114
+ }
115
+ });
116
+ }
117
+
118
+ if (dropzone) {
119
+ dropzone.addEventListener("dragover", function(e) {
120
+ e.preventDefault();
121
+ this.classList.add("dp-dropzone--dragover");
122
+ });
123
+ dropzone.addEventListener("dragleave", function() {
124
+ this.classList.remove("dp-dropzone--dragover");
125
+ });
126
+ dropzone.addEventListener("drop", function(e) {
127
+ e.preventDefault();
128
+ this.classList.remove("dp-dropzone--dragover");
129
+ if (e.dataTransfer.files.length > 0) {
130
+ fileInput.files = e.dataTransfer.files;
131
+ fileInput.dispatchEvent(new Event("change"));
132
+ }
133
+ });
134
+ }
135
+
136
+ document.addEventListener("keydown", function(e) {
137
+ if (e.key === "Escape") {
138
+ document.getElementById("dp-modal").classList.remove("dp-modal--open");
139
+ }
140
+ });
141
+ })();
142
+ </script>
@@ -0,0 +1,88 @@
1
+ <%= stylesheet_link_tag "data_porter/application" %>
2
+
3
+ <div class="data-porter">
4
+ <div class="dp-header">
5
+ <h1 class="dp-title">New Import</h1>
6
+ </div>
7
+
8
+ <%= form_with model: @import, url: imports_path, class: "dp-form", multipart: true do |f| %>
9
+ <div class="dp-field">
10
+ <%= f.label :target_key, "Target", class: "dp-label" %>
11
+ <%= f.select :target_key,
12
+ @targets.map { |t| [t[:label], t[:key]] },
13
+ { prompt: "Select a target..." },
14
+ class: "dp-select" %>
15
+ </div>
16
+
17
+ <div class="dp-field">
18
+ <%= f.label :source_type, "Source Type", class: "dp-label" %>
19
+ <%= f.select :source_type,
20
+ DataPorter.configuration.enabled_sources.map { |s| [s.to_s.upcase, s] },
21
+ { prompt: "Select source type..." },
22
+ id: "dp-source-select-new",
23
+ class: "dp-select" %>
24
+ </div>
25
+
26
+ <div id="dp-file-field-new" class="dp-field">
27
+ <%= f.label :file, "File", class: "dp-label" %>
28
+ <label class="dp-dropzone" id="dp-dropzone-new">
29
+ <input type="file" name="data_import[file]" id="dp-file-input-new" class="dp-dropzone__input" />
30
+ <div class="dp-dropzone__content">
31
+ <div class="dp-dropzone__icon">&#128196;</div>
32
+ <span class="dp-dropzone__text">Drop your file here or <strong>browse</strong></span>
33
+ <span class="dp-dropzone__hint">CSV or JSON files accepted</span>
34
+ </div>
35
+ <div class="dp-dropzone__selected" id="dp-file-name-new" style="display: none;"></div>
36
+ </label>
37
+ </div>
38
+
39
+ <div class="dp-actions">
40
+ <%= f.submit "Start Import", class: "dp-btn dp-btn--primary" %>
41
+ <%= link_to "Cancel", imports_path, class: "dp-btn dp-btn--secondary" %>
42
+ </div>
43
+ <% end %>
44
+ </div>
45
+
46
+ <script>
47
+ (function() {
48
+ var sourceSelect = document.getElementById("dp-source-select-new");
49
+ var fileField = document.getElementById("dp-file-field-new");
50
+ var fileInput = document.getElementById("dp-file-input-new");
51
+ var dropzone = document.getElementById("dp-dropzone-new");
52
+ var fileName = document.getElementById("dp-file-name-new");
53
+
54
+ if (sourceSelect) {
55
+ sourceSelect.addEventListener("change", function() {
56
+ fileField.style.display = this.value === "api" ? "none" : "";
57
+ });
58
+ }
59
+
60
+ if (fileInput) {
61
+ fileInput.addEventListener("change", function() {
62
+ if (this.files.length > 0) {
63
+ fileName.textContent = this.files[0].name;
64
+ fileName.style.display = "";
65
+ dropzone.classList.add("dp-dropzone--has-file");
66
+ }
67
+ });
68
+ }
69
+
70
+ if (dropzone) {
71
+ dropzone.addEventListener("dragover", function(e) {
72
+ e.preventDefault();
73
+ this.classList.add("dp-dropzone--dragover");
74
+ });
75
+ dropzone.addEventListener("dragleave", function() {
76
+ this.classList.remove("dp-dropzone--dragover");
77
+ });
78
+ dropzone.addEventListener("drop", function(e) {
79
+ e.preventDefault();
80
+ this.classList.remove("dp-dropzone--dragover");
81
+ if (e.dataTransfer.files.length > 0) {
82
+ fileInput.files = e.dataTransfer.files;
83
+ fileInput.dispatchEvent(new Event("change"));
84
+ }
85
+ });
86
+ }
87
+ })();
88
+ </script>
@@ -0,0 +1,49 @@
1
+ <%= stylesheet_link_tag "data_porter/application" %>
2
+
3
+ <div class="data-porter">
4
+ <div class="dp-header">
5
+ <h1 class="dp-title">
6
+ <%= @target._label %> Import #<%= @import.id %>
7
+ </h1>
8
+ <%= raw DataPorter::Components::StatusBadge.new(status: @import.status).call %>
9
+ </div>
10
+
11
+ <% if @import.parsing? || @import.importing? || @import.dry_running? %>
12
+ <%= raw DataPorter::Components::ProgressBar.new(import_id: @import.id).call %>
13
+ <% end %>
14
+
15
+ <% if @import.previewing? %>
16
+ <%= raw DataPorter::Components::SummaryCards.new(report: @import.report).call %>
17
+ <%= raw DataPorter::Components::PreviewTable.new(
18
+ columns: @target._columns,
19
+ records: @records
20
+ ).call %>
21
+
22
+ <div class="dp-actions">
23
+ <%= button_to "Confirm", confirm_import_path(@import),
24
+ method: :post, class: "dp-btn dp-btn--primary" %>
25
+ <% if @target._dry_run_enabled %>
26
+ <%= button_to "Dry Run", dry_run_import_path(@import),
27
+ method: :post, class: "dp-btn dp-btn--secondary" %>
28
+ <% end %>
29
+ <%= button_to "Cancel", cancel_import_path(@import),
30
+ method: :post, class: "dp-btn dp-btn--danger" %>
31
+ </div>
32
+ <% end %>
33
+
34
+ <% if @import.completed? %>
35
+ <%= raw DataPorter::Components::ResultsSummary.new(report: @import.report).call %>
36
+ <% end %>
37
+
38
+ <% if @import.failed? %>
39
+ <%= raw DataPorter::Components::FailureAlert.new(report: @import.report).call %>
40
+ <div class="dp-actions">
41
+ <%= button_to "Retry", parse_import_path(@import),
42
+ method: :post, class: "dp-btn dp-btn--primary" %>
43
+ </div>
44
+ <% end %>
45
+
46
+ <div class="dp-nav">
47
+ <%= link_to "Back to imports", imports_path, class: "dp-link" %>
48
+ </div>
49
+ </div>
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: ":memory:"
data/config/routes.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ DataPorter::Engine.routes.draw do
4
+ resources :imports, only: %i[index new create show] do
5
+ member do
6
+ post :parse
7
+ post :confirm
8
+ post :cancel
9
+ post :dry_run
10
+ end
11
+ end
12
+ end