data_porter 0.2.0 → 0.5.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 +73 -0
- data/README.md +60 -393
- data/ROADMAP.md +30 -12
- data/app/assets/javascripts/data_porter/stimulus.min.js +2 -0
- data/app/assets/javascripts/data_porter/turbo.min.js +29 -0
- data/app/assets/stylesheets/data_porter/alerts.css +83 -0
- data/app/assets/stylesheets/data_porter/application.css +12 -646
- data/app/assets/stylesheets/data_porter/badges.css +73 -0
- data/app/assets/stylesheets/data_porter/base.css +56 -0
- data/app/assets/stylesheets/data_porter/cards.css +60 -0
- data/app/assets/stylesheets/data_porter/layout.css +147 -0
- data/app/assets/stylesheets/data_porter/mapping.css +79 -0
- data/app/assets/stylesheets/data_porter/modal.css +49 -0
- data/app/assets/stylesheets/data_porter/preview.css +24 -0
- data/app/assets/stylesheets/data_porter/progress.css +62 -0
- data/app/assets/stylesheets/data_porter/table.css +51 -0
- data/app/controllers/data_porter/imports_controller.rb +96 -10
- data/app/controllers/data_porter/mapping_templates_controller.rb +85 -0
- data/app/javascript/data_porter/mapping_controller.js +86 -0
- data/app/javascript/data_porter/progress_controller.js +29 -20
- data/app/javascript/data_porter/template_form_controller.js +46 -0
- data/app/jobs/data_porter/extract_headers_job.rb +12 -0
- data/app/models/data_porter/data_import.rb +12 -1
- data/app/models/data_porter/mapping_template.rb +15 -0
- data/app/views/data_porter/imports/index.html.erb +38 -9
- data/app/views/data_porter/imports/new.html.erb +31 -4
- data/app/views/data_porter/imports/show.html.erb +74 -17
- data/app/views/data_porter/mapping_templates/_form.html.erb +40 -0
- data/app/views/data_porter/mapping_templates/edit.html.erb +11 -0
- data/app/views/data_porter/mapping_templates/index.html.erb +42 -0
- data/app/views/data_porter/mapping_templates/new.html.erb +11 -0
- data/app/views/layouts/data_porter/application.html.erb +168 -0
- data/config/routes.rb +5 -1
- data/docs/CONFIGURATION.md +81 -0
- data/docs/MAPPING.md +44 -0
- data/docs/SOURCES.md +94 -0
- data/docs/TARGETS.md +176 -0
- data/docs/screenshots/mapping.jpg +0 -0
- data/lib/data_porter/components/mapping/column_row.rb +52 -0
- data/lib/data_porter/components/mapping/form.rb +127 -0
- data/lib/data_porter/components/mapping/template_select.rb +35 -0
- data/lib/data_porter/components/preview/results_summary.rb +60 -0
- data/lib/data_porter/components/preview/summary_cards.rb +32 -0
- data/lib/data_porter/components/preview/table.rb +56 -0
- data/lib/data_porter/components/progress/bar.rb +47 -0
- data/lib/data_porter/components/shared/failure_alert.rb +22 -0
- data/lib/data_porter/components/shared/status_badge.rb +18 -0
- data/lib/data_porter/components.rb +9 -6
- data/lib/data_porter/configuration.rb +3 -1
- data/lib/data_porter/engine.rb +7 -1
- data/lib/data_porter/orchestrator.rb +35 -2
- data/lib/data_porter/registry.rb +6 -1
- data/lib/data_porter/sources/base.rb +18 -3
- data/lib/data_porter/sources/csv.rb +5 -0
- data/lib/data_porter/sources/xlsx.rb +8 -0
- data/lib/data_porter/version.rb +1 -1
- data/lib/generators/data_porter/install/install_generator.rb +4 -0
- data/lib/generators/data_porter/install/templates/create_data_porter_mapping_templates.rb.erb +16 -0
- data/lib/generators/data_porter/install/templates/initializer.rb +5 -1
- data/lib/generators/data_porter/target/target_generator.rb +5 -0
- data/lib/generators/data_porter/target/templates/target.rb.tt +1 -1
- data/lib/tasks/data_porter.rake +9 -0
- metadata +62 -39
- data/lib/data_porter/components/failure_alert.rb +0 -20
- data/lib/data_porter/components/preview_table.rb +0 -54
- data/lib/data_porter/components/progress_bar.rb +0 -33
- data/lib/data_porter/components/results_summary.rb +0 -19
- data/lib/data_porter/components/status_badge.rb +0 -16
- data/lib/data_porter/components/summary_cards.rb +0 -30
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--dp-primary: #4f46e5;
|
|
3
|
+
--dp-primary-hover: #4338ca;
|
|
4
|
+
--dp-primary-light: #eef2ff;
|
|
5
|
+
--dp-success: #059669;
|
|
6
|
+
--dp-success-light: #ecfdf5;
|
|
7
|
+
--dp-success-border: #a7f3d0;
|
|
8
|
+
--dp-warning: #d97706;
|
|
9
|
+
--dp-warning-light: #fffbeb;
|
|
10
|
+
--dp-warning-border: #fde68a;
|
|
11
|
+
--dp-danger: #dc2626;
|
|
12
|
+
--dp-danger-hover: #b91c1c;
|
|
13
|
+
--dp-danger-light: #fef2f2;
|
|
14
|
+
--dp-danger-border: #fecaca;
|
|
15
|
+
--dp-info: #2563eb;
|
|
16
|
+
--dp-info-light: #eff6ff;
|
|
17
|
+
--dp-info-border: #bfdbfe;
|
|
18
|
+
--dp-purple: #7c3aed;
|
|
19
|
+
--dp-purple-light: #f5f3ff;
|
|
20
|
+
--dp-purple-border: #ddd6fe;
|
|
21
|
+
--dp-gray-50: #f9fafb;
|
|
22
|
+
--dp-gray-100: #f3f4f6;
|
|
23
|
+
--dp-gray-200: #e5e7eb;
|
|
24
|
+
--dp-gray-300: #d1d5db;
|
|
25
|
+
--dp-gray-400: #9ca3af;
|
|
26
|
+
--dp-gray-500: #6b7280;
|
|
27
|
+
--dp-gray-600: #4b5563;
|
|
28
|
+
--dp-gray-700: #374151;
|
|
29
|
+
--dp-gray-800: #1f2937;
|
|
30
|
+
--dp-gray-900: #111827;
|
|
31
|
+
--dp-radius-sm: 0.375rem;
|
|
32
|
+
--dp-radius-md: 0.5rem;
|
|
33
|
+
--dp-radius-lg: 0.75rem;
|
|
34
|
+
--dp-radius-xl: 1rem;
|
|
35
|
+
--dp-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
36
|
+
--dp-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
|
37
|
+
--dp-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.04);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.data-porter {
|
|
41
|
+
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
|
42
|
+
color: var(--dp-gray-800);
|
|
43
|
+
max-width: 1100px;
|
|
44
|
+
margin: 2rem auto;
|
|
45
|
+
padding: 0 1.5rem;
|
|
46
|
+
line-height: 1.6;
|
|
47
|
+
-webkit-font-smoothing: antialiased;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.dp-title {
|
|
51
|
+
font-size: 1.75rem;
|
|
52
|
+
font-weight: 700;
|
|
53
|
+
margin: 0;
|
|
54
|
+
letter-spacing: -0.025em;
|
|
55
|
+
color: var(--dp-gray-900);
|
|
56
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
.dp-summary-cards {
|
|
2
|
+
display: grid;
|
|
3
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
4
|
+
gap: 1rem;
|
|
5
|
+
margin-bottom: 2rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.dp-card {
|
|
9
|
+
padding: 1.25rem;
|
|
10
|
+
border-radius: var(--dp-radius-lg);
|
|
11
|
+
border: 1px solid var(--dp-gray-200);
|
|
12
|
+
text-align: center;
|
|
13
|
+
background: white;
|
|
14
|
+
box-shadow: var(--dp-shadow-sm);
|
|
15
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.dp-card:hover {
|
|
19
|
+
transform: translateY(-1px);
|
|
20
|
+
box-shadow: var(--dp-shadow-md);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.dp-card strong {
|
|
24
|
+
display: block;
|
|
25
|
+
font-size: 2rem;
|
|
26
|
+
font-weight: 700;
|
|
27
|
+
margin-bottom: 0.25rem;
|
|
28
|
+
letter-spacing: -0.025em;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.dp-card span {
|
|
32
|
+
font-size: 0.8rem;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
text-transform: uppercase;
|
|
35
|
+
letter-spacing: 0.05em;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.dp-card--complete {
|
|
39
|
+
border-color: var(--dp-success-border);
|
|
40
|
+
background: var(--dp-success-light);
|
|
41
|
+
}
|
|
42
|
+
.dp-card--complete strong { color: var(--dp-success); }
|
|
43
|
+
|
|
44
|
+
.dp-card--partial {
|
|
45
|
+
border-color: var(--dp-warning-border);
|
|
46
|
+
background: var(--dp-warning-light);
|
|
47
|
+
}
|
|
48
|
+
.dp-card--partial strong { color: var(--dp-warning); }
|
|
49
|
+
|
|
50
|
+
.dp-card--missing {
|
|
51
|
+
border-color: var(--dp-danger-border);
|
|
52
|
+
background: var(--dp-danger-light);
|
|
53
|
+
}
|
|
54
|
+
.dp-card--missing strong { color: var(--dp-danger); }
|
|
55
|
+
|
|
56
|
+
.dp-card--duplicate {
|
|
57
|
+
border-color: var(--dp-gray-300);
|
|
58
|
+
background: var(--dp-gray-50);
|
|
59
|
+
}
|
|
60
|
+
.dp-card--duplicate strong { color: var(--dp-gray-500); }
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
.dp-header {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
gap: 1rem;
|
|
6
|
+
margin-bottom: 2rem;
|
|
7
|
+
padding-bottom: 1.5rem;
|
|
8
|
+
border-bottom: 1px solid var(--dp-gray-200);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.dp-header__actions { display: flex; align-items: center; gap: 0.75rem; }
|
|
12
|
+
|
|
13
|
+
.dp-form {
|
|
14
|
+
max-width: 520px;
|
|
15
|
+
background: white;
|
|
16
|
+
padding: 2rem;
|
|
17
|
+
border: 1px solid var(--dp-gray-200);
|
|
18
|
+
border-radius: var(--dp-radius-lg);
|
|
19
|
+
box-shadow: var(--dp-shadow-md);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.dp-field { margin-bottom: 1.5rem; }
|
|
23
|
+
|
|
24
|
+
.dp-label {
|
|
25
|
+
display: block;
|
|
26
|
+
font-weight: 600;
|
|
27
|
+
margin-bottom: 0.5rem;
|
|
28
|
+
font-size: 0.875rem;
|
|
29
|
+
color: var(--dp-gray-700);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.dp-select, .dp-file-input {
|
|
33
|
+
display: block;
|
|
34
|
+
width: 100%;
|
|
35
|
+
padding: 0.625rem 0.875rem;
|
|
36
|
+
border: 1px solid var(--dp-gray-300);
|
|
37
|
+
border-radius: var(--dp-radius-md);
|
|
38
|
+
font-size: 0.9375rem;
|
|
39
|
+
background: white;
|
|
40
|
+
color: var(--dp-gray-800);
|
|
41
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
42
|
+
appearance: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.dp-select {
|
|
46
|
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
|
47
|
+
background-position: right 0.5rem center;
|
|
48
|
+
background-repeat: no-repeat;
|
|
49
|
+
background-size: 1.5em 1.5em;
|
|
50
|
+
padding-right: 2.5rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dp-select:focus, .dp-file-input:focus {
|
|
54
|
+
outline: none;
|
|
55
|
+
border-color: var(--dp-primary);
|
|
56
|
+
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.dp-actions { display: flex; gap: 0.75rem; margin: 2rem 0; }
|
|
60
|
+
|
|
61
|
+
.dp-btn {
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: center;
|
|
65
|
+
padding: 0.625rem 1.25rem;
|
|
66
|
+
border-radius: var(--dp-radius-md);
|
|
67
|
+
font-size: 0.875rem;
|
|
68
|
+
font-weight: 600;
|
|
69
|
+
text-decoration: none;
|
|
70
|
+
border: 1px solid transparent;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
transition: all 0.15s ease;
|
|
73
|
+
line-height: 1.25;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.dp-btn:active { transform: scale(0.98); }
|
|
77
|
+
.dp-btn:disabled { opacity: 0.65; cursor: not-allowed; transform: none; }
|
|
78
|
+
|
|
79
|
+
.dp-btn .dp-spinner {
|
|
80
|
+
display: inline-block;
|
|
81
|
+
width: 1em;
|
|
82
|
+
height: 1em;
|
|
83
|
+
margin-right: 0.5em;
|
|
84
|
+
border: 2px solid currentColor;
|
|
85
|
+
border-right-color: transparent;
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
animation: dp-spin 0.6s linear infinite;
|
|
88
|
+
vertical-align: -0.125em;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@keyframes dp-spin {
|
|
92
|
+
to { transform: rotate(360deg); }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.dp-btn--primary { background: var(--dp-primary); color: white; box-shadow: var(--dp-shadow-sm); }
|
|
96
|
+
.dp-btn--primary:hover { background: var(--dp-primary-hover); box-shadow: var(--dp-shadow-md); }
|
|
97
|
+
|
|
98
|
+
.dp-btn--secondary { background: white; color: var(--dp-gray-700); border-color: var(--dp-gray-300); box-shadow: var(--dp-shadow-sm); }
|
|
99
|
+
.dp-btn--secondary:hover { background: var(--dp-gray-50); border-color: var(--dp-gray-400); }
|
|
100
|
+
|
|
101
|
+
.dp-btn--danger { background: var(--dp-danger); color: white; box-shadow: var(--dp-shadow-sm); }
|
|
102
|
+
.dp-btn--danger:hover { background: var(--dp-danger-hover); box-shadow: var(--dp-shadow-md); }
|
|
103
|
+
|
|
104
|
+
.dp-btn--sm { padding: 0.375rem 0.75rem; font-size: 0.8125rem; }
|
|
105
|
+
|
|
106
|
+
.dp-link { color: var(--dp-primary); text-decoration: none; font-weight: 500; transition: color 0.15s ease; }
|
|
107
|
+
.dp-link:hover { color: var(--dp-primary-hover); text-decoration: underline; }
|
|
108
|
+
|
|
109
|
+
.dp-nav { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--dp-gray-200); }
|
|
110
|
+
.dp-empty { text-align: center; padding: 3rem 1rem; color: var(--dp-gray-400); font-size: 0.9375rem; }
|
|
111
|
+
|
|
112
|
+
.dp-empty-state {
|
|
113
|
+
text-align: center;
|
|
114
|
+
padding: 4rem 2rem;
|
|
115
|
+
background: white;
|
|
116
|
+
border: 2px dashed var(--dp-gray-300);
|
|
117
|
+
border-radius: var(--dp-radius-xl);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.dp-empty-state__icon { font-size: 3rem; margin-bottom: 1rem; opacity: 0.6; }
|
|
121
|
+
.dp-empty-state__text { font-size: 1.125rem; color: var(--dp-gray-500); margin: 0 0 1.5rem; }
|
|
122
|
+
|
|
123
|
+
.dp-import-details {
|
|
124
|
+
background: white;
|
|
125
|
+
padding: 1.5rem;
|
|
126
|
+
border: 1px solid var(--dp-gray-200);
|
|
127
|
+
border-radius: var(--dp-radius-lg);
|
|
128
|
+
margin-bottom: 2rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.dp-details-grid {
|
|
132
|
+
display: grid;
|
|
133
|
+
grid-template-columns: auto 1fr;
|
|
134
|
+
gap: 0.5rem 1.5rem;
|
|
135
|
+
margin: 0;
|
|
136
|
+
font-size: 0.9375rem;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.dp-details-grid dt {
|
|
140
|
+
font-weight: 600;
|
|
141
|
+
color: var(--dp-gray-500);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.dp-details-grid dd {
|
|
145
|
+
margin: 0;
|
|
146
|
+
color: var(--dp-gray-800);
|
|
147
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
.dp-mapping-form {
|
|
2
|
+
max-width: 700px;
|
|
3
|
+
background: white;
|
|
4
|
+
padding: 2rem;
|
|
5
|
+
border: 1px solid var(--dp-gray-200);
|
|
6
|
+
border-radius: var(--dp-radius-lg);
|
|
7
|
+
box-shadow: var(--dp-shadow-md);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.dp-mapping-template {
|
|
11
|
+
margin-bottom: 1.5rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.dp-mapping-rows {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: 0.75rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.dp-mapping-row {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: 1rem;
|
|
24
|
+
padding: 0.75rem 1rem;
|
|
25
|
+
background: var(--dp-gray-50);
|
|
26
|
+
border: 1px solid var(--dp-gray-200);
|
|
27
|
+
border-radius: var(--dp-radius-md);
|
|
28
|
+
transition: border-color 0.15s ease;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.dp-mapping-row:hover {
|
|
32
|
+
border-color: var(--dp-gray-300);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.dp-mapping-row__header {
|
|
36
|
+
flex: 1;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
font-size: 0.875rem;
|
|
39
|
+
color: var(--dp-gray-800);
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
text-overflow: ellipsis;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.dp-mapping-row__arrow {
|
|
46
|
+
color: var(--dp-gray-400);
|
|
47
|
+
font-size: 1.125rem;
|
|
48
|
+
flex-shrink: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.dp-mapping-row__select {
|
|
52
|
+
flex: 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.dp-mapping-row--duplicate {
|
|
56
|
+
border-color: #f59e0b;
|
|
57
|
+
background: #fffbeb;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.dp-mapping-required-warning,
|
|
61
|
+
.dp-mapping-duplicate-warning {
|
|
62
|
+
padding: 0.75rem 1rem;
|
|
63
|
+
margin-bottom: 1rem;
|
|
64
|
+
border-radius: var(--dp-radius-md);
|
|
65
|
+
font-size: 0.875rem;
|
|
66
|
+
font-weight: 500;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.dp-mapping-required-warning {
|
|
70
|
+
background: #fef2f2;
|
|
71
|
+
border: 1px solid #fca5a5;
|
|
72
|
+
color: #991b1b;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.dp-mapping-duplicate-warning {
|
|
76
|
+
background: #fffbeb;
|
|
77
|
+
border: 1px solid #fcd34d;
|
|
78
|
+
color: #92400e;
|
|
79
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.dp-modal { display: none; position: fixed; inset: 0; z-index: 9999; align-items: center; justify-content: center; }
|
|
2
|
+
.dp-modal--open { display: flex; }
|
|
3
|
+
.dp-modal__backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.4); backdrop-filter: blur(4px); }
|
|
4
|
+
.dp-modal__content {
|
|
5
|
+
position: relative;
|
|
6
|
+
background: white;
|
|
7
|
+
border-radius: var(--dp-radius-xl);
|
|
8
|
+
box-shadow: var(--dp-shadow-lg), 0 25px 50px -12px rgba(0,0,0,0.15);
|
|
9
|
+
width: 100%;
|
|
10
|
+
max-width: 520px;
|
|
11
|
+
max-height: 90vh;
|
|
12
|
+
overflow-y: auto;
|
|
13
|
+
animation: dp-modal-in 0.2s ease-out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@keyframes dp-modal-in {
|
|
17
|
+
from { opacity: 0; transform: scale(0.95) translateY(10px); }
|
|
18
|
+
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.dp-modal__header { display: flex; align-items: center; justify-content: space-between; padding: 1.5rem 1.5rem 0; }
|
|
22
|
+
.dp-modal__title { font-size: 1.25rem; font-weight: 700; margin: 0; color: var(--dp-gray-900); }
|
|
23
|
+
.dp-modal__close { background: none; border: none; font-size: 1.5rem; color: var(--dp-gray-400); cursor: pointer; padding: 0.25rem; line-height: 1; border-radius: var(--dp-radius-sm); transition: color 0.15s ease, background 0.15s ease; }
|
|
24
|
+
.dp-modal__close:hover { color: var(--dp-gray-700); background: var(--dp-gray-100); }
|
|
25
|
+
.dp-modal__body { padding: 1.5rem; }
|
|
26
|
+
.dp-modal__footer { display: flex; gap: 0.75rem; justify-content: flex-end; padding-top: 0.5rem; border-top: 1px solid var(--dp-gray-100); margin-top: 0.5rem; padding-top: 1.5rem; }
|
|
27
|
+
|
|
28
|
+
.dp-dropzone {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
padding: 2rem 1.5rem;
|
|
34
|
+
border: 2px dashed var(--dp-gray-300);
|
|
35
|
+
border-radius: var(--dp-radius-lg);
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
transition: border-color 0.15s ease, background 0.15s ease;
|
|
38
|
+
text-align: center;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.dp-dropzone:hover, .dp-dropzone--dragover { border-color: var(--dp-primary); background: var(--dp-primary-light); }
|
|
42
|
+
.dp-dropzone--has-file { border-color: var(--dp-success); background: var(--dp-success-light); border-style: solid; }
|
|
43
|
+
.dp-dropzone__input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
|
|
44
|
+
.dp-dropzone__content { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
|
|
45
|
+
.dp-dropzone__icon { font-size: 2rem; opacity: 0.5; }
|
|
46
|
+
.dp-dropzone__text { font-size: 0.9375rem; color: var(--dp-gray-600); }
|
|
47
|
+
.dp-dropzone__text strong { color: var(--dp-primary); }
|
|
48
|
+
.dp-dropzone__hint { font-size: 0.8rem; color: var(--dp-gray-400); }
|
|
49
|
+
.dp-dropzone__selected { font-size: 0.875rem; font-weight: 600; color: var(--dp-success); margin-top: 0.5rem; }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.dp-preview-table {
|
|
2
|
+
overflow-x: auto;
|
|
3
|
+
margin-bottom: 2rem;
|
|
4
|
+
border-radius: var(--dp-radius-lg);
|
|
5
|
+
border: 1px solid var(--dp-gray-200);
|
|
6
|
+
box-shadow: var(--dp-shadow-sm);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.dp-preview-table .dp-table {
|
|
10
|
+
margin-bottom: 0;
|
|
11
|
+
border: none;
|
|
12
|
+
border-radius: 0;
|
|
13
|
+
box-shadow: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.dp-row--complete { background: var(--dp-success-light); }
|
|
17
|
+
.dp-row--partial { background: var(--dp-warning-light); }
|
|
18
|
+
.dp-row--missing { background: var(--dp-danger-light); }
|
|
19
|
+
|
|
20
|
+
.dp-errors {
|
|
21
|
+
color: var(--dp-danger);
|
|
22
|
+
font-size: 0.8rem;
|
|
23
|
+
font-weight: 500;
|
|
24
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
.dp-progress-container {
|
|
2
|
+
margin: 2rem 0;
|
|
3
|
+
padding: 2rem;
|
|
4
|
+
background: white;
|
|
5
|
+
border: 1px solid var(--dp-gray-200);
|
|
6
|
+
border-radius: var(--dp-radius-lg);
|
|
7
|
+
box-shadow: var(--dp-shadow-sm);
|
|
8
|
+
text-align: center;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.dp-progress-label {
|
|
12
|
+
font-size: 0.8125rem;
|
|
13
|
+
font-weight: 600;
|
|
14
|
+
color: var(--dp-gray-500);
|
|
15
|
+
margin-bottom: 1rem;
|
|
16
|
+
text-transform: uppercase;
|
|
17
|
+
letter-spacing: 0.05em;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.dp-progress {
|
|
21
|
+
background: var(--dp-gray-100);
|
|
22
|
+
border-radius: 9999px;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
height: 1.5rem;
|
|
25
|
+
border: 1px solid var(--dp-gray-200);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.dp-progress-bar {
|
|
29
|
+
height: 100%;
|
|
30
|
+
background: linear-gradient(90deg, var(--dp-primary), #818cf8, var(--dp-primary));
|
|
31
|
+
background-size: 200% 100%;
|
|
32
|
+
animation: dp-gradient-flow 3s ease infinite;
|
|
33
|
+
border-radius: 9999px;
|
|
34
|
+
transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
color: white;
|
|
39
|
+
font-size: 0.6875rem;
|
|
40
|
+
font-weight: 700;
|
|
41
|
+
min-width: 2.25rem;
|
|
42
|
+
position: relative;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.dp-progress-bar::after {
|
|
47
|
+
content: "";
|
|
48
|
+
position: absolute;
|
|
49
|
+
inset: 0;
|
|
50
|
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.25), transparent);
|
|
51
|
+
animation: dp-shimmer 2s infinite;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@keyframes dp-gradient-flow {
|
|
55
|
+
0%, 100% { background-position: 0% 50%; }
|
|
56
|
+
50% { background-position: 100% 50%; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@keyframes dp-shimmer {
|
|
60
|
+
0% { transform: translateX(-100%); }
|
|
61
|
+
100% { transform: translateX(100%); }
|
|
62
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
.dp-table {
|
|
2
|
+
width: 100%;
|
|
3
|
+
border-collapse: separate;
|
|
4
|
+
border-spacing: 0;
|
|
5
|
+
margin-bottom: 1.5rem;
|
|
6
|
+
background: white;
|
|
7
|
+
border: 1px solid var(--dp-gray-200);
|
|
8
|
+
border-radius: var(--dp-radius-lg);
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
box-shadow: var(--dp-shadow-sm);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.dp-table th,
|
|
14
|
+
.dp-table td {
|
|
15
|
+
padding: 0.75rem 1rem;
|
|
16
|
+
text-align: left;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.dp-table th {
|
|
20
|
+
font-weight: 600;
|
|
21
|
+
font-size: 0.75rem;
|
|
22
|
+
text-transform: uppercase;
|
|
23
|
+
letter-spacing: 0.05em;
|
|
24
|
+
color: var(--dp-gray-500);
|
|
25
|
+
background: var(--dp-gray-50);
|
|
26
|
+
border-bottom: 1px solid var(--dp-gray-200);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dp-table td {
|
|
30
|
+
font-size: 0.875rem;
|
|
31
|
+
color: var(--dp-gray-700);
|
|
32
|
+
border-bottom: 1px solid var(--dp-gray-100);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.dp-table tbody tr:last-child td {
|
|
36
|
+
border-bottom: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.dp-table tbody tr {
|
|
40
|
+
transition: background-color 0.15s ease;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.dp-table tbody tr:hover {
|
|
44
|
+
background: var(--dp-gray-50);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.dp-table__actions {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 0.5rem;
|
|
51
|
+
}
|
|
@@ -2,29 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module DataPorter
|
|
4
4
|
class ImportsController < DataPorter.configuration.parent_controller.constantize
|
|
5
|
-
|
|
5
|
+
layout "data_porter/application"
|
|
6
|
+
|
|
7
|
+
before_action :set_import, only: %i[show parse confirm cancel dry_run update_mapping status destroy]
|
|
8
|
+
before_action :load_targets, only: %i[index new create]
|
|
6
9
|
|
|
7
10
|
def index
|
|
8
11
|
@imports = DataPorter::DataImport.order(created_at: :desc)
|
|
9
|
-
@targets = DataPorter::Registry.available
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def new
|
|
13
15
|
@import = DataPorter::DataImport.new
|
|
14
|
-
@targets = DataPorter::Registry.available
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def create
|
|
18
|
-
|
|
19
|
-
@import.user = current_user if respond_to?(:current_user, true)
|
|
20
|
-
@import.status = :pending
|
|
19
|
+
build_import
|
|
21
20
|
|
|
22
|
-
if @import.save
|
|
23
|
-
|
|
21
|
+
if valid_source_for_target? && valid_file_presence? && @import.save
|
|
22
|
+
enqueue_after_create
|
|
24
23
|
redirect_to import_path(@import)
|
|
25
24
|
else
|
|
26
|
-
|
|
27
|
-
render :new
|
|
25
|
+
render :new, status: :unprocessable_entity
|
|
28
26
|
end
|
|
29
27
|
end
|
|
30
28
|
|
|
@@ -32,6 +30,7 @@ module DataPorter
|
|
|
32
30
|
@target = @import.target_class
|
|
33
31
|
@records = @import.records
|
|
34
32
|
@grouped = @records.group_by(&:status)
|
|
33
|
+
load_mapping_data if @import.mapping?
|
|
35
34
|
end
|
|
36
35
|
|
|
37
36
|
def parse
|
|
@@ -40,7 +39,15 @@ module DataPorter
|
|
|
40
39
|
redirect_to import_path(@import)
|
|
41
40
|
end
|
|
42
41
|
|
|
42
|
+
def update_mapping
|
|
43
|
+
save_column_mapping
|
|
44
|
+
save_template_if_requested
|
|
45
|
+
DataPorter::ParseJob.perform_later(@import.id)
|
|
46
|
+
redirect_to import_path(@import)
|
|
47
|
+
end
|
|
48
|
+
|
|
43
49
|
def confirm
|
|
50
|
+
@import.update!(status: :pending)
|
|
44
51
|
DataPorter::ImportJob.perform_later(@import.id)
|
|
45
52
|
redirect_to import_path(@import)
|
|
46
53
|
end
|
|
@@ -51,18 +58,97 @@ module DataPorter
|
|
|
51
58
|
end
|
|
52
59
|
|
|
53
60
|
def dry_run
|
|
61
|
+
@import.update!(status: :pending)
|
|
54
62
|
DataPorter::DryRunJob.perform_later(@import.id)
|
|
55
63
|
redirect_to import_path(@import)
|
|
56
64
|
end
|
|
57
65
|
|
|
66
|
+
def status
|
|
67
|
+
progress = @import.config["progress"] || {}
|
|
68
|
+
render json: { status: @import.status, progress: progress }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def destroy
|
|
72
|
+
@import.file.purge if @import.file.attached?
|
|
73
|
+
@import.destroy!
|
|
74
|
+
redirect_to imports_path
|
|
75
|
+
end
|
|
76
|
+
|
|
58
77
|
private
|
|
59
78
|
|
|
60
79
|
def set_import
|
|
61
80
|
@import = DataPorter::DataImport.find(params[:id])
|
|
62
81
|
end
|
|
63
82
|
|
|
83
|
+
def load_targets
|
|
84
|
+
@targets = DataPorter::Registry.available
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_import
|
|
88
|
+
@import = DataPorter::DataImport.new(import_params)
|
|
89
|
+
@import.user = current_user if respond_to?(:current_user, true)
|
|
90
|
+
@import.status = :pending
|
|
91
|
+
end
|
|
92
|
+
|
|
64
93
|
def import_params
|
|
65
94
|
params.require(:data_import).permit(:target_key, :source_type, :file, config: {})
|
|
66
95
|
end
|
|
96
|
+
|
|
97
|
+
def valid_source_for_target?
|
|
98
|
+
target = DataPorter::Registry.find(@import.target_key)
|
|
99
|
+
allowed = target._sources || DataPorter.configuration.enabled_sources
|
|
100
|
+
return true if allowed.map(&:to_s).include?(@import.source_type.to_s)
|
|
101
|
+
|
|
102
|
+
@import.errors.add(:source_type, "#{@import.source_type} is not available for this target")
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def valid_file_presence?
|
|
107
|
+
return true unless %w[csv json xlsx].include?(@import.source_type)
|
|
108
|
+
return true if @import.file.attached?
|
|
109
|
+
|
|
110
|
+
@import.errors.add(:file, "must be attached for #{@import.source_type.upcase} imports")
|
|
111
|
+
false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def enqueue_after_create
|
|
115
|
+
if @import.file_based?
|
|
116
|
+
DataPorter::ExtractHeadersJob.perform_later(@import.id)
|
|
117
|
+
else
|
|
118
|
+
DataPorter::ParseJob.perform_later(@import.id)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def load_mapping_data
|
|
123
|
+
target = @import.target_class
|
|
124
|
+
columns = target._columns || []
|
|
125
|
+
@file_headers = @import.config["file_headers"] || []
|
|
126
|
+
@target_columns = columns.map { |c| [c.label, c.name.to_s, c.required] }
|
|
127
|
+
@default_mapping = (target._csv_mappings || {}).transform_values(&:to_s)
|
|
128
|
+
@templates = load_templates
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def load_templates
|
|
132
|
+
return [] unless defined?(DataPorter::MappingTemplate)
|
|
133
|
+
|
|
134
|
+
DataPorter::MappingTemplate.for_target(@import.target_key)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def save_column_mapping
|
|
138
|
+
mapping = params.require(:column_mapping).permit!.to_h
|
|
139
|
+
merged = (@import.config || {}).merge("column_mapping" => mapping)
|
|
140
|
+
@import.update!(config: merged, status: :pending)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def save_template_if_requested
|
|
144
|
+
return unless params[:save_template] == "1"
|
|
145
|
+
return unless defined?(DataPorter::MappingTemplate)
|
|
146
|
+
|
|
147
|
+
mapping = params.require(:column_mapping).permit!.to_h
|
|
148
|
+
DataPorter::MappingTemplate.find_or_initialize_by(
|
|
149
|
+
target_key: @import.target_key,
|
|
150
|
+
name: params[:template_name].presence || "Default"
|
|
151
|
+
).update!(mapping: mapping)
|
|
152
|
+
end
|
|
67
153
|
end
|
|
68
154
|
end
|