data_porter 2.0.0 → 2.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +7 -1
- data/ROADMAP.md +64 -76
- data/app/assets/javascripts/data_porter/progress_controller.js +3 -3
- data/app/controllers/data_porter/concerns/import_validation.rb +5 -5
- data/app/jobs/data_porter/webhook_job.rb +45 -0
- data/app/views/data_porter/imports/index.html.erb +23 -23
- data/app/views/data_porter/imports/new.html.erb +11 -11
- data/app/views/data_porter/imports/show.html.erb +19 -19
- data/app/views/data_porter/mapping_templates/_form.html.erb +10 -10
- data/app/views/data_porter/mapping_templates/edit.html.erb +2 -2
- data/app/views/data_porter/mapping_templates/index.html.erb +10 -10
- data/app/views/data_porter/mapping_templates/new.html.erb +2 -2
- data/config/locales/en.yml +123 -0
- data/config/locales/fr.yml +123 -0
- data/config/routes.rb +2 -2
- data/lib/data_porter/column_transformer.rb +56 -0
- data/lib/data_porter/components/mapping/column_row.rb +1 -1
- data/lib/data_porter/components/mapping/form.rb +4 -4
- data/lib/data_porter/components/mapping/template_select.rb +1 -1
- data/lib/data_porter/components/preview/results_summary.rb +13 -5
- data/lib/data_porter/components/preview/summary_cards.rb +5 -4
- data/lib/data_porter/components/preview/table.rb +3 -3
- data/lib/data_porter/components/progress/bar.rb +9 -2
- data/lib/data_porter/components/shared/pagination.rb +9 -5
- data/lib/data_porter/components/shared/status_badge.rb +3 -1
- data/lib/data_porter/configuration.rb +3 -1
- data/lib/data_porter/dsl/column.rb +3 -2
- data/lib/data_porter/dsl/webhook.rb +31 -0
- data/lib/data_porter/engine.rb +4 -0
- data/lib/data_porter/orchestrator/importer.rb +1 -0
- data/lib/data_porter/orchestrator/record_builder.rb +2 -1
- data/lib/data_porter/orchestrator.rb +3 -0
- data/lib/data_porter/record_validator.rb +2 -2
- data/lib/data_porter/target.rb +11 -1
- data/lib/data_porter/version.rb +1 -1
- data/lib/data_porter/webhook_notifier.rb +100 -0
- data/lib/data_porter.rb +2 -0
- data/lib/generators/data_porter/install/templates/initializer.rb +5 -0
- data/lib/generators/data_porter/locale/locale_generator.rb +42 -0
- data/mkdocs.yml +98 -0
- metadata +10 -3
- data/bookmarklet.md +0 -217
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
<div class="data-porter">
|
|
2
2
|
<div class="dp-header">
|
|
3
3
|
<div class="dp-header__actions">
|
|
4
|
-
<%= link_to "
|
|
4
|
+
<%= link_to t("data_porter.imports.back_to_imports"), imports_path, class: "dp-btn dp-btn--secondary" %>
|
|
5
5
|
</div>
|
|
6
6
|
<h1 class="dp-title">
|
|
7
|
-
<%= @target._label
|
|
7
|
+
<%= t("data_porter.imports.show_title", target: @target._label, id: @import.id) %>
|
|
8
8
|
</h1>
|
|
9
9
|
<%= raw DataPorter::Components::Shared::StatusBadge.new(status: @import.status).call %>
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
12
|
<div class="dp-import-details">
|
|
13
13
|
<dl class="dp-details-grid">
|
|
14
|
-
<dt
|
|
14
|
+
<dt><%= t("data_porter.imports.details.target") %></dt>
|
|
15
15
|
<dd><%= @target._label %></dd>
|
|
16
|
-
<dt
|
|
16
|
+
<dt><%= t("data_porter.imports.details.source") %></dt>
|
|
17
17
|
<dd><%= @import.source_type.upcase %></dd>
|
|
18
18
|
<% if @import.file.attached? %>
|
|
19
|
-
<dt
|
|
19
|
+
<dt><%= t("data_porter.imports.details.file") %></dt>
|
|
20
20
|
<dd><%= @import.file.filename %></dd>
|
|
21
21
|
<% end %>
|
|
22
|
-
<dt
|
|
22
|
+
<dt><%= t("data_porter.imports.details.created") %></dt>
|
|
23
23
|
<dd><%= @import.created_at&.strftime("%Y-%m-%d %H:%M") %></dd>
|
|
24
24
|
<% if @import.report.records_count.positive? %>
|
|
25
|
-
<dt
|
|
25
|
+
<dt><%= t("data_porter.imports.details.records") %></dt>
|
|
26
26
|
<dd><%= @import.report.records_count %></dd>
|
|
27
27
|
<% end %>
|
|
28
28
|
</dl>
|
|
@@ -58,18 +58,18 @@
|
|
|
58
58
|
|
|
59
59
|
<div class="dp-actions">
|
|
60
60
|
<%= button_to confirm_import_path(@import), method: :post, class: "dp-btn dp-btn--primary", data: { dp_submit: true } do %>
|
|
61
|
-
|
|
61
|
+
<%= t("data_porter.imports.confirm_import") %>
|
|
62
62
|
<% end %>
|
|
63
63
|
<% if @target._dry_run_enabled %>
|
|
64
64
|
<%= button_to dry_run_import_path(@import), method: :post, class: "dp-btn dp-btn--secondary", data: { dp_submit: true } do %>
|
|
65
|
-
|
|
65
|
+
<%= t("data_porter.imports.dry_run") %>
|
|
66
66
|
<% end %>
|
|
67
67
|
<% end %>
|
|
68
68
|
<% if @import.file_based? %>
|
|
69
|
-
<%= button_to "
|
|
69
|
+
<%= button_to t("data_porter.imports.back_to_mapping"), back_to_mapping_import_path(@import),
|
|
70
70
|
method: :post, class: "dp-btn dp-btn--secondary" %>
|
|
71
71
|
<% end %>
|
|
72
|
-
<%= button_to "
|
|
72
|
+
<%= button_to t("data_porter.imports.cancel"), cancel_import_path(@import),
|
|
73
73
|
method: :post, class: "dp-btn dp-btn--danger" %>
|
|
74
74
|
</div>
|
|
75
75
|
<% end %>
|
|
@@ -89,25 +89,25 @@
|
|
|
89
89
|
).call %>
|
|
90
90
|
<% end %>
|
|
91
91
|
<div class="dp-actions">
|
|
92
|
-
<%= link_to "
|
|
92
|
+
<%= link_to t("data_porter.imports.back_to_imports"), imports_path, class: "dp-btn dp-btn--primary" %>
|
|
93
93
|
<% rejected = @import.report.errored_count.to_i + @import.report.missing_count.to_i + @import.report.partial_count.to_i %>
|
|
94
94
|
<% if rejected.positive? %>
|
|
95
|
-
<%= link_to "
|
|
95
|
+
<%= link_to t("data_porter.imports.download_rejects"), export_rejects_import_path(@import), class: "dp-btn dp-btn--secondary" %>
|
|
96
96
|
<% end %>
|
|
97
|
-
<%= button_to "
|
|
97
|
+
<%= button_to t("data_porter.imports.delete"), import_path(@import),
|
|
98
98
|
method: :delete, class: "dp-btn dp-btn--danger",
|
|
99
|
-
data: { turbo_confirm: "
|
|
99
|
+
data: { turbo_confirm: t("data_porter.imports.delete_confirm") } %>
|
|
100
100
|
</div>
|
|
101
101
|
<% end %>
|
|
102
102
|
|
|
103
103
|
<% if @import.failed? %>
|
|
104
104
|
<%= raw DataPorter::Components::Shared::FailureAlert.new(report: @import.report).call %>
|
|
105
105
|
<div class="dp-actions">
|
|
106
|
-
<%= button_to "
|
|
106
|
+
<%= button_to t("data_porter.imports.retry"), parse_import_path(@import),
|
|
107
107
|
method: :post, class: "dp-btn dp-btn--primary" %>
|
|
108
|
-
<%= button_to "
|
|
108
|
+
<%= button_to t("data_porter.imports.delete"), import_path(@import),
|
|
109
109
|
method: :delete, class: "dp-btn dp-btn--danger",
|
|
110
|
-
data: { turbo_confirm: "
|
|
110
|
+
data: { turbo_confirm: t("data_porter.imports.delete_confirm") } %>
|
|
111
111
|
</div>
|
|
112
112
|
<% end %>
|
|
113
113
|
</div>
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
document.querySelectorAll("[data-dp-submit]").forEach(function(btn) {
|
|
118
118
|
btn.closest("form").addEventListener("submit", function() {
|
|
119
119
|
btn.disabled = true;
|
|
120
|
-
btn.innerHTML = '<span class="dp-spinner"></span
|
|
120
|
+
btn.innerHTML = '<span class="dp-spinner"></span><%= j t("data_porter.imports.processing") %>';
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
123
|
})();
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
<%= form_with model: template, url: template.persisted? ? mapping_template_path(template) : mapping_templates_path, class: "dp-form", method: template.persisted? ? :patch : :post, data: { controller: "data-porter--template-form", "data-porter--template-form-columns-value": target_columns_map.to_json } do |f| %>
|
|
2
2
|
<div class="dp-field">
|
|
3
|
-
<%= f.label :target_key, "
|
|
3
|
+
<%= f.label :target_key, t("data_porter.mapping_templates.form.target_label"), class: "dp-label" %>
|
|
4
4
|
<%= f.select :target_key,
|
|
5
5
|
targets.map { |t| [t[:label], t[:key]] },
|
|
6
|
-
{ prompt: "
|
|
6
|
+
{ prompt: t("data_porter.mapping_templates.form.target_prompt") },
|
|
7
7
|
class: "dp-select",
|
|
8
8
|
data: { action: "change->data-porter--template-form#targetChanged" } %>
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
11
|
<div class="dp-field">
|
|
12
|
-
<%= f.label :name, "
|
|
13
|
-
<%= f.text_field :name, class: "dp-select", placeholder: "
|
|
12
|
+
<%= f.label :name, t("data_porter.mapping_templates.form.name_label"), class: "dp-label" %>
|
|
13
|
+
<%= f.text_field :name, class: "dp-select", placeholder: t("data_porter.mapping_templates.form.name_placeholder") %>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
16
|
<div class="dp-field">
|
|
17
|
-
<label class="dp-label"
|
|
17
|
+
<label class="dp-label"><%= t("data_porter.mapping_templates.form.column_mappings") %></label>
|
|
18
18
|
<div id="dp-mapping-pairs" data-data-porter--template-form-target="pairsContainer">
|
|
19
19
|
<% (template.mapping.presence || { "" => "" }).each do |header, field| %>
|
|
20
20
|
<div class="dp-mapping-pair" style="display: flex; gap: 0.5rem; margin-bottom: 0.5rem;">
|
|
21
|
-
<input type="text" name="mapping_template[mapping_keys][]" value="<%= header %>" placeholder="
|
|
21
|
+
<input type="text" name="mapping_template[mapping_keys][]" value="<%= header %>" placeholder="<%= t("data_porter.mapping_templates.form.file_header_placeholder") %>" class="dp-select" style="flex: 1;" />
|
|
22
22
|
<select name="mapping_template[mapping_values][]" class="dp-select" style="flex: 1;" data-data-porter--template-form-target="fieldSelect">
|
|
23
|
-
<option value=""
|
|
23
|
+
<option value=""><%= t("data_porter.mapping_templates.form.field_prompt") %></option>
|
|
24
24
|
<% if template.target_key.present? && target_columns_map[template.target_key.to_s].present? %>
|
|
25
25
|
<% target_columns_map[template.target_key.to_s].each do |label, name| %>
|
|
26
26
|
<option value="<%= name %>" <%= "selected" if name == field.to_s %>><%= label %></option>
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
</div>
|
|
31
31
|
<% end %>
|
|
32
32
|
</div>
|
|
33
|
-
<button type="button" class="dp-btn dp-btn--secondary" style="margin-top: 0.5rem;" data-action="data-porter--template-form#addPair"
|
|
33
|
+
<button type="button" class="dp-btn dp-btn--secondary" style="margin-top: 0.5rem;" data-action="data-porter--template-form#addPair"><%= t("data_porter.mapping_templates.form.add_mapping") %></button>
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
36
|
<div class="dp-actions">
|
|
37
|
-
<%= f.submit template.persisted? ? "
|
|
38
|
-
<%= link_to "
|
|
37
|
+
<%= f.submit template.persisted? ? t("data_porter.mapping_templates.form.update") : t("data_porter.mapping_templates.form.create"), class: "dp-btn dp-btn--primary" %>
|
|
38
|
+
<%= link_to t("data_porter.mapping_templates.form.cancel"), mapping_templates_path, class: "dp-btn dp-btn--secondary" %>
|
|
39
39
|
</div>
|
|
40
40
|
<% end %>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<div class="data-porter">
|
|
2
2
|
<div class="dp-header">
|
|
3
|
-
<h1 class="dp-title"
|
|
3
|
+
<h1 class="dp-title"><%= t("data_porter.mapping_templates.edit_title") %></h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<%= render "form", template: @template, targets: @targets, target_columns_map: @target_columns_map %>
|
|
7
7
|
|
|
8
8
|
<div class="dp-nav">
|
|
9
|
-
<%= link_to "
|
|
9
|
+
<%= link_to t("data_porter.mapping_templates.back_to_templates"), mapping_templates_path, class: "dp-link" %>
|
|
10
10
|
</div>
|
|
11
11
|
</div>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<div class="data-porter">
|
|
2
2
|
<div class="dp-header">
|
|
3
3
|
<div class="dp-header__actions">
|
|
4
|
-
<%= link_to "
|
|
4
|
+
<%= link_to t("data_porter.mapping_templates.back_to_imports"), imports_path, class: "dp-btn dp-btn--secondary" %>
|
|
5
5
|
</div>
|
|
6
|
-
<h1 class="dp-title"
|
|
7
|
-
<%= link_to "
|
|
6
|
+
<h1 class="dp-title"><%= t("data_porter.mapping_templates.title") %></h1>
|
|
7
|
+
<%= link_to t("data_porter.mapping_templates.new_template"), new_mapping_template_path, class: "dp-btn dp-btn--primary" %>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
10
|
<% if @grouped.any? %>
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
<table class="dp-table">
|
|
14
14
|
<thead>
|
|
15
15
|
<tr>
|
|
16
|
-
<th
|
|
17
|
-
<th
|
|
16
|
+
<th><%= t("data_porter.mapping_templates.name") %></th>
|
|
17
|
+
<th><%= t("data_porter.mapping_templates.mappings") %></th>
|
|
18
18
|
<th></th>
|
|
19
19
|
</tr>
|
|
20
20
|
</thead>
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
<% templates.each do |template| %>
|
|
23
23
|
<tr>
|
|
24
24
|
<td><%= template.name %></td>
|
|
25
|
-
<td><%= template.mapping.size
|
|
25
|
+
<td><%= t("data_porter.mapping_templates.columns_count", count: template.mapping.size) %></td>
|
|
26
26
|
<td style="display: flex; gap: 0.5rem; align-items: center;">
|
|
27
|
-
<%= link_to "
|
|
28
|
-
<%= button_to "
|
|
27
|
+
<%= link_to t("data_porter.mapping_templates.edit"), edit_mapping_template_path(template), class: "dp-btn dp-btn--secondary" %>
|
|
28
|
+
<%= button_to t("data_porter.mapping_templates.delete"), mapping_template_path(template), method: :delete, class: "dp-btn dp-btn--danger" %>
|
|
29
29
|
</td>
|
|
30
30
|
</tr>
|
|
31
31
|
<% end %>
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
<% else %>
|
|
36
36
|
<div class="dp-empty-state">
|
|
37
37
|
<div class="dp-empty-state__icon">📋</div>
|
|
38
|
-
<p class="dp-empty-state__text"
|
|
39
|
-
<%= link_to "
|
|
38
|
+
<p class="dp-empty-state__text"><%= t("data_porter.mapping_templates.no_templates") %></p>
|
|
39
|
+
<%= link_to t("data_porter.mapping_templates.create_first"), new_mapping_template_path, class: "dp-btn dp-btn--primary" %>
|
|
40
40
|
</div>
|
|
41
41
|
<% end %>
|
|
42
42
|
</div>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<div class="data-porter">
|
|
2
2
|
<div class="dp-header">
|
|
3
|
-
<h1 class="dp-title"
|
|
3
|
+
<h1 class="dp-title"><%= t("data_porter.mapping_templates.new_title") %></h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<%= render "form", template: @template, targets: @targets, target_columns_map: @target_columns_map %>
|
|
7
7
|
|
|
8
8
|
<div class="dp-nav">
|
|
9
|
-
<%= link_to "
|
|
9
|
+
<%= link_to t("data_porter.mapping_templates.back_to_templates"), mapping_templates_path, class: "dp-link" %>
|
|
10
10
|
</div>
|
|
11
11
|
</div>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
en:
|
|
2
|
+
data_porter:
|
|
3
|
+
imports:
|
|
4
|
+
title: "Imports"
|
|
5
|
+
new_import: "New Import"
|
|
6
|
+
new_title: "New Import"
|
|
7
|
+
show_title: "%{target} Import #%{id}"
|
|
8
|
+
back_to_imports: "Back to imports"
|
|
9
|
+
view: "View"
|
|
10
|
+
delete: "Delete"
|
|
11
|
+
delete_confirm: "Delete this import?"
|
|
12
|
+
retry: "Retry"
|
|
13
|
+
start_import: "Start Import"
|
|
14
|
+
confirm_import: "Confirm Import"
|
|
15
|
+
dry_run: "Dry Run"
|
|
16
|
+
cancel: "Cancel"
|
|
17
|
+
back_to_mapping: "Back to Mapping"
|
|
18
|
+
download_rejects: "Download rejects CSV"
|
|
19
|
+
processing: "Processing..."
|
|
20
|
+
no_imports: "No imports yet"
|
|
21
|
+
create_first: "Create your first import"
|
|
22
|
+
details:
|
|
23
|
+
target: "Target"
|
|
24
|
+
source: "Source"
|
|
25
|
+
file: "File"
|
|
26
|
+
created: "Created"
|
|
27
|
+
records: "Records"
|
|
28
|
+
table:
|
|
29
|
+
id: "ID"
|
|
30
|
+
target: "Target"
|
|
31
|
+
source: "Source"
|
|
32
|
+
status: "Status"
|
|
33
|
+
created: "Created"
|
|
34
|
+
form:
|
|
35
|
+
target_label: "Target"
|
|
36
|
+
target_prompt: "Select a target..."
|
|
37
|
+
source_label: "Source Type"
|
|
38
|
+
source_prompt: "Select source type..."
|
|
39
|
+
file_label: "File"
|
|
40
|
+
dropzone_text: "Drop your file here or <strong>browse</strong>"
|
|
41
|
+
dropzone_hint: "CSV, JSON, or XLSX files accepted"
|
|
42
|
+
select_prompt: "Select..."
|
|
43
|
+
mapping:
|
|
44
|
+
load_template: "Load Template"
|
|
45
|
+
save_as_template: "Save as template"
|
|
46
|
+
template_name_placeholder: "Template name"
|
|
47
|
+
continue: "Continue"
|
|
48
|
+
skip_column: "Skip this column"
|
|
49
|
+
select_template: "Select a template..."
|
|
50
|
+
mapping_templates:
|
|
51
|
+
title: "Mapping Templates"
|
|
52
|
+
new_template: "New Template"
|
|
53
|
+
new_title: "New Mapping Template"
|
|
54
|
+
edit_title: "Edit Mapping Template"
|
|
55
|
+
back_to_templates: "Back to templates"
|
|
56
|
+
back_to_imports: "Back to imports"
|
|
57
|
+
name: "Name"
|
|
58
|
+
mappings: "Mappings"
|
|
59
|
+
columns_count:
|
|
60
|
+
one: "%{count} column"
|
|
61
|
+
other: "%{count} columns"
|
|
62
|
+
edit: "Edit"
|
|
63
|
+
delete: "Delete"
|
|
64
|
+
no_templates: "No mapping templates yet"
|
|
65
|
+
create_first: "Create your first template"
|
|
66
|
+
form:
|
|
67
|
+
target_label: "Target"
|
|
68
|
+
target_prompt: "Select a target..."
|
|
69
|
+
name_label: "Template Name"
|
|
70
|
+
name_placeholder: "e.g. French Headers"
|
|
71
|
+
column_mappings: "Column Mappings"
|
|
72
|
+
file_header_placeholder: "File header"
|
|
73
|
+
field_prompt: "Select a field..."
|
|
74
|
+
add_mapping: "+ Add Mapping"
|
|
75
|
+
create: "Create Template"
|
|
76
|
+
update: "Update Template"
|
|
77
|
+
cancel: "Cancel"
|
|
78
|
+
components:
|
|
79
|
+
summary_cards:
|
|
80
|
+
ready: "Ready"
|
|
81
|
+
incomplete: "Incomplete"
|
|
82
|
+
missing: "Missing"
|
|
83
|
+
duplicates: "Duplicates"
|
|
84
|
+
results_summary:
|
|
85
|
+
completed: "Import completed"
|
|
86
|
+
completed_with_errors: "Import completed with errors"
|
|
87
|
+
imported: "Imported"
|
|
88
|
+
errors: "Errors"
|
|
89
|
+
skipped: "Skipped"
|
|
90
|
+
duration: "Duration: %{duration}"
|
|
91
|
+
table:
|
|
92
|
+
line_number: "#"
|
|
93
|
+
status: "Status"
|
|
94
|
+
errors: "Errors"
|
|
95
|
+
progress:
|
|
96
|
+
waiting: "Waiting..."
|
|
97
|
+
extracting_headers: "Extracting headers..."
|
|
98
|
+
parsing: "Parsing records..."
|
|
99
|
+
importing: "Importing..."
|
|
100
|
+
dry_running: "Dry run..."
|
|
101
|
+
processing: "Processing..."
|
|
102
|
+
pagination:
|
|
103
|
+
previous: "Previous"
|
|
104
|
+
next: "Next"
|
|
105
|
+
page_info: "Page %{page} of %{total}"
|
|
106
|
+
status_badge:
|
|
107
|
+
pending: "Pending"
|
|
108
|
+
extracting_headers: "Extracting headers"
|
|
109
|
+
mapping: "Mapping"
|
|
110
|
+
parsing: "Parsing"
|
|
111
|
+
previewing: "Previewing"
|
|
112
|
+
importing: "Importing"
|
|
113
|
+
completed: "Completed"
|
|
114
|
+
failed: "Failed"
|
|
115
|
+
dry_running: "Dry running"
|
|
116
|
+
errors:
|
|
117
|
+
required: "%{label} is required"
|
|
118
|
+
invalid_type: "%{label}: invalid %{type}"
|
|
119
|
+
max_records: "File contains %{count} records, exceeds maximum of %{max}"
|
|
120
|
+
source_unavailable: "%{source} is not available for this target"
|
|
121
|
+
file_required: "must be attached for %{source} imports"
|
|
122
|
+
file_too_large: "is too large (max %{max} MB)"
|
|
123
|
+
invalid_content_type: "has an invalid content type (%{type})"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
fr:
|
|
2
|
+
data_porter:
|
|
3
|
+
imports:
|
|
4
|
+
title: "Imports"
|
|
5
|
+
new_import: "Nouvel import"
|
|
6
|
+
new_title: "Nouvel import"
|
|
7
|
+
show_title: "Import %{target} #%{id}"
|
|
8
|
+
back_to_imports: "Retour aux imports"
|
|
9
|
+
view: "Voir"
|
|
10
|
+
delete: "Supprimer"
|
|
11
|
+
delete_confirm: "Supprimer cet import ?"
|
|
12
|
+
retry: "Réessayer"
|
|
13
|
+
start_import: "Lancer l'import"
|
|
14
|
+
confirm_import: "Confirmer l'import"
|
|
15
|
+
dry_run: "Essai à blanc"
|
|
16
|
+
cancel: "Annuler"
|
|
17
|
+
back_to_mapping: "Retour au mapping"
|
|
18
|
+
download_rejects: "Télécharger les rejets CSV"
|
|
19
|
+
processing: "Traitement..."
|
|
20
|
+
no_imports: "Aucun import"
|
|
21
|
+
create_first: "Créer votre premier import"
|
|
22
|
+
details:
|
|
23
|
+
target: "Cible"
|
|
24
|
+
source: "Source"
|
|
25
|
+
file: "Fichier"
|
|
26
|
+
created: "Créé le"
|
|
27
|
+
records: "Enregistrements"
|
|
28
|
+
table:
|
|
29
|
+
id: "ID"
|
|
30
|
+
target: "Cible"
|
|
31
|
+
source: "Source"
|
|
32
|
+
status: "Statut"
|
|
33
|
+
created: "Créé le"
|
|
34
|
+
form:
|
|
35
|
+
target_label: "Cible"
|
|
36
|
+
target_prompt: "Sélectionner une cible..."
|
|
37
|
+
source_label: "Type de source"
|
|
38
|
+
source_prompt: "Sélectionner un type..."
|
|
39
|
+
file_label: "Fichier"
|
|
40
|
+
dropzone_text: "Déposez votre fichier ici ou <strong>parcourir</strong>"
|
|
41
|
+
dropzone_hint: "Fichiers CSV, JSON ou XLSX acceptés"
|
|
42
|
+
select_prompt: "Sélectionner..."
|
|
43
|
+
mapping:
|
|
44
|
+
load_template: "Charger un modèle"
|
|
45
|
+
save_as_template: "Sauvegarder comme modèle"
|
|
46
|
+
template_name_placeholder: "Nom du modèle"
|
|
47
|
+
continue: "Continuer"
|
|
48
|
+
skip_column: "Ignorer cette colonne"
|
|
49
|
+
select_template: "Sélectionner un modèle..."
|
|
50
|
+
mapping_templates:
|
|
51
|
+
title: "Modèles de mapping"
|
|
52
|
+
new_template: "Nouveau modèle"
|
|
53
|
+
new_title: "Nouveau modèle de mapping"
|
|
54
|
+
edit_title: "Modifier le modèle de mapping"
|
|
55
|
+
back_to_templates: "Retour aux modèles"
|
|
56
|
+
back_to_imports: "Retour aux imports"
|
|
57
|
+
name: "Nom"
|
|
58
|
+
mappings: "Mappings"
|
|
59
|
+
columns_count:
|
|
60
|
+
one: "%{count} colonne"
|
|
61
|
+
other: "%{count} colonnes"
|
|
62
|
+
edit: "Modifier"
|
|
63
|
+
delete: "Supprimer"
|
|
64
|
+
no_templates: "Aucun modèle de mapping"
|
|
65
|
+
create_first: "Créer votre premier modèle"
|
|
66
|
+
form:
|
|
67
|
+
target_label: "Cible"
|
|
68
|
+
target_prompt: "Sélectionner une cible..."
|
|
69
|
+
name_label: "Nom du modèle"
|
|
70
|
+
name_placeholder: "ex. En-têtes français"
|
|
71
|
+
column_mappings: "Correspondances de colonnes"
|
|
72
|
+
file_header_placeholder: "En-tête du fichier"
|
|
73
|
+
field_prompt: "Sélectionner un champ..."
|
|
74
|
+
add_mapping: "+ Ajouter une correspondance"
|
|
75
|
+
create: "Créer le modèle"
|
|
76
|
+
update: "Mettre à jour le modèle"
|
|
77
|
+
cancel: "Annuler"
|
|
78
|
+
components:
|
|
79
|
+
summary_cards:
|
|
80
|
+
ready: "Prêt"
|
|
81
|
+
incomplete: "Incomplet"
|
|
82
|
+
missing: "Manquant"
|
|
83
|
+
duplicates: "Doublons"
|
|
84
|
+
results_summary:
|
|
85
|
+
completed: "Import terminé"
|
|
86
|
+
completed_with_errors: "Import terminé avec des erreurs"
|
|
87
|
+
imported: "Importés"
|
|
88
|
+
errors: "Erreurs"
|
|
89
|
+
skipped: "Ignorés"
|
|
90
|
+
duration: "Durée : %{duration}"
|
|
91
|
+
table:
|
|
92
|
+
line_number: "#"
|
|
93
|
+
status: "Statut"
|
|
94
|
+
errors: "Erreurs"
|
|
95
|
+
progress:
|
|
96
|
+
waiting: "En attente..."
|
|
97
|
+
extracting_headers: "Extraction des en-têtes..."
|
|
98
|
+
parsing: "Analyse des enregistrements..."
|
|
99
|
+
importing: "Import en cours..."
|
|
100
|
+
dry_running: "Essai à blanc..."
|
|
101
|
+
processing: "Traitement..."
|
|
102
|
+
pagination:
|
|
103
|
+
previous: "Précédent"
|
|
104
|
+
next: "Suivant"
|
|
105
|
+
page_info: "Page %{page} sur %{total}"
|
|
106
|
+
status_badge:
|
|
107
|
+
pending: "En attente"
|
|
108
|
+
extracting_headers: "Extraction"
|
|
109
|
+
mapping: "Mapping"
|
|
110
|
+
parsing: "Analyse"
|
|
111
|
+
previewing: "Aperçu"
|
|
112
|
+
importing: "Import"
|
|
113
|
+
completed: "Terminé"
|
|
114
|
+
failed: "Échoué"
|
|
115
|
+
dry_running: "Essai à blanc"
|
|
116
|
+
errors:
|
|
117
|
+
required: "%{label} est requis"
|
|
118
|
+
invalid_type: "%{label} : %{type} invalide"
|
|
119
|
+
max_records: "Le fichier contient %{count} enregistrements, dépasse le maximum de %{max}"
|
|
120
|
+
source_unavailable: "%{source} n'est pas disponible pour cette cible"
|
|
121
|
+
file_required: "doit être joint pour les imports %{source}"
|
|
122
|
+
file_too_large: "est trop volumineux (max %{max} Mo)"
|
|
123
|
+
invalid_content_type: "a un type de contenu invalide (%{type})"
|
data/config/routes.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
DataPorter::Engine.routes.draw do
|
|
4
|
+
resources :mapping_templates
|
|
5
|
+
|
|
4
6
|
resources :imports, path: "/", only: %i[index new create show destroy] do
|
|
5
7
|
member do
|
|
6
8
|
post :parse
|
|
@@ -13,6 +15,4 @@ DataPorter::Engine.routes.draw do
|
|
|
13
15
|
get :export_rejects
|
|
14
16
|
end
|
|
15
17
|
end
|
|
16
|
-
|
|
17
|
-
resources :mapping_templates
|
|
18
18
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DataPorter
|
|
4
|
+
module ColumnTransformer
|
|
5
|
+
BUILT_IN = {
|
|
6
|
+
strip: :strip.to_proc,
|
|
7
|
+
downcase: :downcase.to_proc,
|
|
8
|
+
upcase: :upcase.to_proc,
|
|
9
|
+
titleize: :titleize.to_proc,
|
|
10
|
+
normalize_phone: ->(value) { value.gsub(/[\s\-().]+/, "") },
|
|
11
|
+
parse_date: ->(value) { Date.parse(value).iso8601 rescue value }, # rubocop:disable Style/RescueModifier
|
|
12
|
+
parse_boolean: ->(value) { %w[true 1 yes oui].include?(value.downcase) ? "true" : "false" },
|
|
13
|
+
parse_integer: ->(value) { Float(value, exception: false) ? value.to_f.to_i.to_s : value },
|
|
14
|
+
parse_decimal: ->(value) { Float(value, exception: false)&.to_s || value }
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def self.apply(value, transformer_name)
|
|
18
|
+
return value if value.nil?
|
|
19
|
+
|
|
20
|
+
transformer = BUILT_IN[transformer_name] || custom_transformers[transformer_name]
|
|
21
|
+
raise Error, "Unknown transformer: #{transformer_name}" unless transformer
|
|
22
|
+
|
|
23
|
+
transformer.call(value.to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.register(name, &block)
|
|
27
|
+
custom_transformers[name.to_sym] = block
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.apply_all(record, columns)
|
|
31
|
+
columns.each do |col|
|
|
32
|
+
next if col.transform.empty?
|
|
33
|
+
|
|
34
|
+
key = resolve_key(record.data, col.name)
|
|
35
|
+
next unless key
|
|
36
|
+
|
|
37
|
+
col.transform.each do |t|
|
|
38
|
+
record.data[key] = apply(record.data[key], t)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.resolve_key(data, name)
|
|
44
|
+
return name.to_s if data.key?(name.to_s)
|
|
45
|
+
return name if data.key?(name)
|
|
46
|
+
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.custom_transformers
|
|
51
|
+
@custom_transformers ||= {}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private_class_method :custom_transformers, :resolve_key
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -36,7 +36,7 @@ module DataPorter
|
|
|
36
36
|
data_data_porter__mapping_target: "columnSelect",
|
|
37
37
|
data_action: "change->data-porter--mapping#onChange"
|
|
38
38
|
) do
|
|
39
|
-
option(value: "") { "
|
|
39
|
+
option(value: "") { I18n.t("data_porter.mapping.skip_column") }
|
|
40
40
|
@target_fields.each { |label, value, required| render_field_option(label, value, required) }
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -54,7 +54,7 @@ module DataPorter
|
|
|
54
54
|
return if @templates.empty?
|
|
55
55
|
|
|
56
56
|
div(class: "dp-field") do
|
|
57
|
-
label(class: "dp-label") { "
|
|
57
|
+
label(class: "dp-label") { I18n.t("data_porter.mapping.load_template") }
|
|
58
58
|
render TemplateSelect.new(templates: @templates)
|
|
59
59
|
end
|
|
60
60
|
end
|
|
@@ -96,14 +96,14 @@ module DataPorter
|
|
|
96
96
|
def render_template_checkbox
|
|
97
97
|
label(style: "display: flex; align-items: center; gap: 0.5rem;") do
|
|
98
98
|
input(type: "checkbox", name: "save_template", value: "1")
|
|
99
|
-
span { "
|
|
99
|
+
span { I18n.t("data_porter.mapping.save_as_template") }
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
def render_template_name_input
|
|
104
104
|
input(
|
|
105
105
|
type: "text", name: "template_name",
|
|
106
|
-
placeholder: "
|
|
106
|
+
placeholder: I18n.t("data_porter.mapping.template_name_placeholder"),
|
|
107
107
|
class: "dp-select",
|
|
108
108
|
style: "margin-top: 0.5rem;"
|
|
109
109
|
)
|
|
@@ -111,7 +111,7 @@ module DataPorter
|
|
|
111
111
|
|
|
112
112
|
def render_actions
|
|
113
113
|
div(class: "dp-actions") do
|
|
114
|
-
button(type: "submit", class: "dp-btn dp-btn--primary") { "
|
|
114
|
+
button(type: "submit", class: "dp-btn dp-btn--primary") { I18n.t("data_porter.mapping.continue") }
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
|
|
@@ -15,7 +15,7 @@ module DataPorter
|
|
|
15
15
|
class: "dp-select dp-mapping-template",
|
|
16
16
|
data_action: "change->data-porter--mapping#loadTemplate"
|
|
17
17
|
) do
|
|
18
|
-
option(value: "") { "
|
|
18
|
+
option(value: "") { I18n.t("data_porter.mapping.select_template") }
|
|
19
19
|
@templates.each { |t| render_option(t) }
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -26,19 +26,27 @@ module DataPorter
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def render_title
|
|
29
|
-
|
|
29
|
+
key = success? ? "completed" : "completed_with_errors"
|
|
30
|
+
h3(class: "dp-results__title") { I18n.t("data_porter.components.results_summary.#{key}") }
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def render_stats
|
|
33
34
|
div(class: "dp-results__cards") do
|
|
34
|
-
stat("dp-results__stat--success", @report.imported_count,
|
|
35
|
-
|
|
36
|
-
stat("dp-results__stat--
|
|
35
|
+
stat("dp-results__stat--success", @report.imported_count,
|
|
36
|
+
I18n.t("data_porter.components.results_summary.imported"))
|
|
37
|
+
stat("dp-results__stat--error", @report.errored_count,
|
|
38
|
+
I18n.t("data_porter.components.results_summary.errors"))
|
|
39
|
+
if skipped_count.positive?
|
|
40
|
+
stat("dp-results__stat--warning", skipped_count,
|
|
41
|
+
I18n.t("data_porter.components.results_summary.skipped"))
|
|
42
|
+
end
|
|
37
43
|
end
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
def render_duration
|
|
41
|
-
div(class: "dp-results__duration")
|
|
47
|
+
div(class: "dp-results__duration") do
|
|
48
|
+
I18n.t("data_porter.components.results_summary.duration", duration: @duration)
|
|
49
|
+
end
|
|
42
50
|
end
|
|
43
51
|
|
|
44
52
|
def stat(css_class, count, label)
|