dbviewer 0.9.4.pre.alpha.2 → 0.9.4
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/README.md +23 -1
- data/app/assets/javascripts/dbviewer/record_creation.js +14 -0
- data/app/assets/javascripts/dbviewer/record_deletion.js +4 -6
- data/app/assets/javascripts/dbviewer/record_editing.js +208 -0
- data/app/controllers/dbviewer/tables_controller.rb +79 -1
- data/app/helpers/dbviewer/datatable_ui_table_helper.rb +21 -2
- data/app/views/dbviewer/tables/_record_form_fields.html.erb +105 -0
- data/app/views/dbviewer/tables/edit_record.html.erb +42 -0
- data/app/views/dbviewer/tables/new_record.html.erb +9 -40
- data/app/views/dbviewer/tables/show.html.erb +28 -3
- data/config/routes.rb +2 -0
- data/lib/dbviewer/configuration.rb +8 -0
- data/lib/dbviewer/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60a92d1cfd091ad3a1688fcf9d2e532dc651ac69384cf4540b56bdb65b31416a
|
4
|
+
data.tar.gz: 1f467d83cfefb95aebc1701f18ddca6a7ad5710495b3bf711857ee29eb0c35b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 95eaf3a6eb2e0f134c92dc4a90ea87a139febfd1fe3977afc58ec17a29243975d53564b021893ecc10cfe826610a6886dfc8d32fb2752f794a6b817168583c4b
|
7
|
+
data.tar.gz: 20fb72442a5f1cb113cbce9cb04792850da47aa1401747a307253d12dd8221f3d224b2485a86086cb9e8a56f77406fde5f7ebac5508afbcc811708f3e79b4798
|
data/README.md
CHANGED
@@ -14,7 +14,7 @@ It's designed for development, debugging, and database analysis, offering a clea
|
|
14
14
|
- **Detailed Schema Information** - Column details, indexes, and constraints
|
15
15
|
- **Entity Relationship Diagram (ERD)** - Interactive database schema visualization
|
16
16
|
- **Data Browsing** - Paginated record viewing with search and filtering
|
17
|
-
- **Data Management** -
|
17
|
+
- **Data Management** - Full CRUD operations (Create, Read, Update, Delete) for database records
|
18
18
|
- **SQL Queries** - Safe SQL query execution with validation
|
19
19
|
- **Multiple Database Connections** - Support for multiple database sources
|
20
20
|
- **PII Data Masking** - Configurable masking for sensitive data
|
@@ -80,6 +80,27 @@ This is necessary because API-only Rails applications don't include the Flash mi
|
|
80
80
|
- **ERD View** (`/dbviewer/entity_relationship_diagrams`): Interactive Entity Relationship Diagram of your database
|
81
81
|
- **SQL Query Logs** (`/dbviewer/logs`): View and analyze logged SQL queries with performance metrics
|
82
82
|
|
83
|
+
### Data Management Features
|
84
|
+
|
85
|
+
DBViewer provides a complete set of CRUD (Create, Read, Update, Delete) operations for your database records:
|
86
|
+
|
87
|
+
- **Create**: Add new records via a user-friendly modal form with field validation
|
88
|
+
- **Read**: Browse and view detailed record information with relationship navigation
|
89
|
+
- **Update**: Edit existing records through an intuitive form interface
|
90
|
+
- **Delete**: Remove records with confirmation dialogs to prevent accidental deletion
|
91
|
+
|
92
|
+
All data management features can be individually enabled or disabled through configuration options:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
# config/initializers/dbviewer.rb
|
96
|
+
Dbviewer.configure do |config|
|
97
|
+
# Data management options
|
98
|
+
config.enable_data_export = false # Enable CSV export feature
|
99
|
+
config.enable_record_deletion = true # Enable record deletion feature
|
100
|
+
config.enable_record_editing = true # Enable record editing feature
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
83
104
|
## ⚙️ Configuration Options
|
84
105
|
|
85
106
|
You can configure DBViewer by using our generator to create an initializer in your application:
|
@@ -100,6 +121,7 @@ Dbviewer.configure do |config|
|
|
100
121
|
config.max_records = 10000 # Maximum records to return in any query
|
101
122
|
config.enable_data_export = false # Whether to allow data exporting
|
102
123
|
config.enable_record_deletion = true # Whether to allow record deletion
|
124
|
+
config.enable_record_editing = true # Whether to allow record editing
|
103
125
|
config.query_timeout = 30 # SQL query timeout in seconds
|
104
126
|
|
105
127
|
# Query logging options
|
@@ -57,6 +57,20 @@ function initializeFormElements() {
|
|
57
57
|
width: "100%",
|
58
58
|
});
|
59
59
|
}
|
60
|
+
|
61
|
+
if (typeof flatpickr !== "undefined") {
|
62
|
+
flatpickr(".datetime-picker", {
|
63
|
+
enableTime: true,
|
64
|
+
dateFormat: "Y-m-d H:i:S",
|
65
|
+
time_24hr: true,
|
66
|
+
wrap: true,
|
67
|
+
});
|
68
|
+
|
69
|
+
flatpickr(".date-picker", {
|
70
|
+
dateFormat: "Y-m-d",
|
71
|
+
wrap: true,
|
72
|
+
});
|
73
|
+
}
|
60
74
|
}
|
61
75
|
|
62
76
|
async function handleNewRecordSubmit(event) {
|
@@ -87,9 +87,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
87
87
|
}
|
88
88
|
});
|
89
89
|
|
90
|
-
|
91
|
-
|
92
|
-
Object.keys(recordData)[0];
|
90
|
+
// Get primary key from button's data attribute or from hidden field
|
91
|
+
const pkName = detailDeleteBtn.getAttribute("data-primary-key") || "id";
|
93
92
|
const pkValue = recordData[pkName];
|
94
93
|
|
95
94
|
setupDeleteConfirmModal(recordData, pkName, pkValue);
|
@@ -105,9 +104,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
105
104
|
document.querySelectorAll(".delete-record-btn").forEach((button) => {
|
106
105
|
button.addEventListener("click", () => {
|
107
106
|
const recordData = JSON.parse(button.dataset.recordData || "{}");
|
108
|
-
|
109
|
-
|
110
|
-
Object.keys(recordData)[0];
|
107
|
+
// Get primary key from button's data attribute or from hidden field
|
108
|
+
const pkName = button.dataset.primaryKey || "id";
|
111
109
|
const pkValue = recordData[pkName];
|
112
110
|
setupDeleteConfirmModal(recordData, pkName, pkValue);
|
113
111
|
});
|
@@ -0,0 +1,208 @@
|
|
1
|
+
document.addEventListener("DOMContentLoaded", function () {
|
2
|
+
const tableName = document.getElementById("table_name")?.value;
|
3
|
+
if (!tableName) return;
|
4
|
+
|
5
|
+
// Handle edit button in record detail modal
|
6
|
+
const recordDetailEditBtn = document.getElementById("recordDetailEditBtn");
|
7
|
+
if (recordDetailEditBtn) {
|
8
|
+
recordDetailEditBtn.addEventListener("click", () => {
|
9
|
+
const recordData = extractRecordDataFromDetailModal();
|
10
|
+
// Use the primary key from the button's data attribute
|
11
|
+
const primaryKey =
|
12
|
+
recordDetailEditBtn.getAttribute("data-primary-key") || "id";
|
13
|
+
const primaryKeyValue = recordData[primaryKey];
|
14
|
+
loadEditForm(primaryKeyValue);
|
15
|
+
});
|
16
|
+
}
|
17
|
+
|
18
|
+
// Handle edit buttons in table rows
|
19
|
+
document.querySelectorAll(".edit-record-btn").forEach((button) => {
|
20
|
+
button.addEventListener("click", () => {
|
21
|
+
const recordData = JSON.parse(button.dataset.recordData || "{}");
|
22
|
+
const primaryKeyValue = extractPrimaryKeyValue(recordData);
|
23
|
+
loadEditForm(primaryKeyValue);
|
24
|
+
});
|
25
|
+
});
|
26
|
+
|
27
|
+
function extractRecordDataFromDetailModal() {
|
28
|
+
const tableBody = document.getElementById("recordDetailTableBody");
|
29
|
+
const rows = tableBody?.querySelectorAll("tr") || [];
|
30
|
+
const recordData = {};
|
31
|
+
|
32
|
+
rows.forEach((row) => {
|
33
|
+
const cells = row.querySelectorAll("td");
|
34
|
+
if (cells.length >= 2) {
|
35
|
+
const columnName = cells[0].textContent.trim();
|
36
|
+
const cellValue =
|
37
|
+
cells[1].textContent.trim() === "NULL"
|
38
|
+
? ""
|
39
|
+
: cells[1].textContent.trim();
|
40
|
+
recordData[columnName] = cellValue;
|
41
|
+
}
|
42
|
+
});
|
43
|
+
return recordData;
|
44
|
+
}
|
45
|
+
|
46
|
+
function extractPrimaryKeyValue(recordData) {
|
47
|
+
// First try to get the primary key from the button data attribute
|
48
|
+
const clickedButton = document.activeElement;
|
49
|
+
if (
|
50
|
+
clickedButton &&
|
51
|
+
clickedButton.classList.contains("edit-record-btn") &&
|
52
|
+
clickedButton.dataset.primaryKey
|
53
|
+
) {
|
54
|
+
const primaryKey = clickedButton.dataset.primaryKey;
|
55
|
+
return recordData[primaryKey];
|
56
|
+
}
|
57
|
+
|
58
|
+
// If not available, use the table-level metadata primary key from the hidden field
|
59
|
+
const primaryKeyMetaElement = document.getElementById("table_primary_key");
|
60
|
+
if (primaryKeyMetaElement && primaryKeyMetaElement.value) {
|
61
|
+
return recordData[primaryKeyMetaElement.value];
|
62
|
+
}
|
63
|
+
|
64
|
+
// Fallback: try to find 'id' or use the first key
|
65
|
+
const primaryKey =
|
66
|
+
Object.keys(recordData).find((key) => key.toLowerCase() === "id") ||
|
67
|
+
Object.keys(recordData)[0];
|
68
|
+
return recordData[primaryKey];
|
69
|
+
}
|
70
|
+
|
71
|
+
async function loadEditForm(recordId) {
|
72
|
+
const modal = document.getElementById("editRecordModal");
|
73
|
+
const modalBody = modal.querySelector(".modal-content");
|
74
|
+
const bsModal = new bootstrap.Modal(modal);
|
75
|
+
|
76
|
+
modalBody.innerHTML = `
|
77
|
+
<div class="modal-body text-center py-5">
|
78
|
+
<div class="spinner-border text-primary" role="status"></div>
|
79
|
+
<p class="mt-3">Loading edit form...</p>
|
80
|
+
</div>
|
81
|
+
`;
|
82
|
+
|
83
|
+
bsModal.show();
|
84
|
+
|
85
|
+
try {
|
86
|
+
const response = await fetch(
|
87
|
+
`${window.location.pathname}/records/${encodeURIComponent(
|
88
|
+
recordId
|
89
|
+
)}/edit`
|
90
|
+
);
|
91
|
+
if (!response.ok) throw new Error("Failed to load form");
|
92
|
+
|
93
|
+
const html = await response.text();
|
94
|
+
modalBody.innerHTML = html;
|
95
|
+
|
96
|
+
initializeEditFormElements();
|
97
|
+
document
|
98
|
+
.getElementById("editRecordForm")
|
99
|
+
?.addEventListener("submit", handleEditFormSubmit);
|
100
|
+
} catch (error) {
|
101
|
+
modalBody.innerHTML = `
|
102
|
+
<div class="modal-header">
|
103
|
+
<h5 class="modal-title">Error</h5>
|
104
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
105
|
+
</div>
|
106
|
+
<div class="modal-body">
|
107
|
+
<div class="alert alert-danger">${error.message}</div>
|
108
|
+
</div>
|
109
|
+
<div class="modal-footer">
|
110
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
111
|
+
</div>
|
112
|
+
`;
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
function initializeEditFormElements() {
|
117
|
+
if (typeof $.fn.select2 !== "undefined") {
|
118
|
+
$(".select2-dropdown").select2({
|
119
|
+
dropdownParent: $("#editRecordModal"),
|
120
|
+
theme: "bootstrap-5",
|
121
|
+
width: "100%",
|
122
|
+
});
|
123
|
+
}
|
124
|
+
|
125
|
+
if (typeof flatpickr !== "undefined") {
|
126
|
+
flatpickr(".datetime-picker", {
|
127
|
+
enableTime: true,
|
128
|
+
dateFormat: "Y-m-d H:i:S",
|
129
|
+
time_24hr: true,
|
130
|
+
wrap: true,
|
131
|
+
});
|
132
|
+
|
133
|
+
flatpickr(".date-picker", {
|
134
|
+
dateFormat: "Y-m-d",
|
135
|
+
wrap: true,
|
136
|
+
});
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
async function handleEditFormSubmit(event) {
|
141
|
+
event.preventDefault();
|
142
|
+
|
143
|
+
const form = event.target;
|
144
|
+
const submitButton = document.getElementById("updateRecordButton");
|
145
|
+
const originalText = submitButton?.innerHTML;
|
146
|
+
const formData = new FormData(form);
|
147
|
+
const csrfToken = document.querySelector(
|
148
|
+
'meta[name="csrf-token"]'
|
149
|
+
)?.content;
|
150
|
+
|
151
|
+
disableSubmitButton(submitButton, "Updating...");
|
152
|
+
|
153
|
+
try {
|
154
|
+
const response = await fetch(form.action, {
|
155
|
+
method: form.method,
|
156
|
+
body: formData,
|
157
|
+
headers: { "X-CSRF-Token": csrfToken },
|
158
|
+
credentials: "same-origin",
|
159
|
+
});
|
160
|
+
|
161
|
+
const result = await response.json();
|
162
|
+
|
163
|
+
if (!response.ok)
|
164
|
+
throw new Error(
|
165
|
+
result?.messages?.join(", ") || "Failed to update record"
|
166
|
+
);
|
167
|
+
|
168
|
+
bootstrap.Modal.getInstance(
|
169
|
+
document.getElementById("editRecordModal")
|
170
|
+
)?.hide();
|
171
|
+
showToast(result.message || "Record updated successfully");
|
172
|
+
setTimeout(() => window.location.reload(), 1000);
|
173
|
+
} catch (error) {
|
174
|
+
showToast(error.message, "danger");
|
175
|
+
} finally {
|
176
|
+
enableSubmitButton(submitButton, originalText);
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
function disableSubmitButton(button, loadingText) {
|
181
|
+
if (!button) return;
|
182
|
+
button.disabled = true;
|
183
|
+
button.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${loadingText}`;
|
184
|
+
}
|
185
|
+
|
186
|
+
function enableSubmitButton(button, originalText) {
|
187
|
+
if (!button) return;
|
188
|
+
button.disabled = false;
|
189
|
+
button.innerHTML = originalText;
|
190
|
+
}
|
191
|
+
|
192
|
+
function showToast(message, type = "success") {
|
193
|
+
Toastify({
|
194
|
+
text: `<span class="toast-icon"><i class="bi bi-${
|
195
|
+
type === "success" ? "clipboard-check" : "exclamation-triangle"
|
196
|
+
}"></i></span> ${message}`,
|
197
|
+
className: "toast-factory-bot",
|
198
|
+
duration: 3000,
|
199
|
+
gravity: "bottom",
|
200
|
+
position: "right",
|
201
|
+
escapeMarkup: false,
|
202
|
+
style: {
|
203
|
+
animation: `slideInRight 0.3s ease-out, slideOutRight 0.3s ease-out 2.7s`,
|
204
|
+
background: type === "danger" ? "#dc3545" : undefined,
|
205
|
+
},
|
206
|
+
}).showToast();
|
207
|
+
}
|
208
|
+
});
|
@@ -3,7 +3,10 @@ module Dbviewer
|
|
3
3
|
include Dbviewer::AccessControlValidation
|
4
4
|
|
5
5
|
before_action :set_table_name, except: [ :index ]
|
6
|
-
before_action :validate_table, only: [ :show, :query, :export_csv, :new_record, :create_record, :destroy_record ]
|
6
|
+
before_action :validate_table, only: [ :show, :query, :export_csv, :new_record, :create_record, :destroy_record, :edit_record, :update_record ]
|
7
|
+
before_action :check_record_creation_enabled, only: [ :new_record, :create_record ]
|
8
|
+
before_action :check_record_editing_enabled, only: [ :edit_record, :update_record ]
|
9
|
+
before_action :check_record_deletion_enabled, only: [ :destroy_record ]
|
7
10
|
before_action :set_query_filters, only: [ :show, :export_csv ]
|
8
11
|
before_action :set_global_filters, only: [ :show, :export_csv ]
|
9
12
|
|
@@ -111,6 +114,45 @@ module Dbviewer
|
|
111
114
|
end
|
112
115
|
end
|
113
116
|
|
117
|
+
def edit_record
|
118
|
+
model_class = database_manager.get_model_for(@table_name)
|
119
|
+
primary_key = database_manager.primary_key(@table_name) || "id"
|
120
|
+
@record = model_class.find_by(primary_key => params[:record_id])
|
121
|
+
|
122
|
+
if @record.nil?
|
123
|
+
render json: { error: "Record not found" }, status: :not_found
|
124
|
+
return
|
125
|
+
end
|
126
|
+
|
127
|
+
@table_columns = filter_accessible_columns(@table_name, database_manager.table_columns(@table_name))
|
128
|
+
@metadata = database_manager.table_metadata(@table_name)
|
129
|
+
@foreign_key_options = load_foreign_key_options(@metadata)
|
130
|
+
|
131
|
+
render layout: false
|
132
|
+
end
|
133
|
+
|
134
|
+
def update_record
|
135
|
+
model_class = database_manager.get_model_for(@table_name)
|
136
|
+
primary_key = database_manager.primary_key(@table_name) || "id"
|
137
|
+
record = model_class.find_by(primary_key => params[:record_id])
|
138
|
+
|
139
|
+
if record.nil?
|
140
|
+
render json: { error: "Record not found" }, status: :not_found
|
141
|
+
return
|
142
|
+
end
|
143
|
+
|
144
|
+
begin
|
145
|
+
if record.update(record_params)
|
146
|
+
render json: { message: "Record updated successfully" }
|
147
|
+
else
|
148
|
+
render json: { errors: record.errors, messages: record.errors.full_messages }, status: :unprocessable_entity
|
149
|
+
end
|
150
|
+
rescue => e
|
151
|
+
Rails.logger.error("Error updating record in #{@table_name}: #{e.message}")
|
152
|
+
render json: { error: "Failed to update record: #{e.message}" }, status: :internal_server_error
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
114
156
|
private
|
115
157
|
|
116
158
|
def record_params
|
@@ -210,5 +252,41 @@ module Dbviewer
|
|
210
252
|
end
|
211
253
|
options
|
212
254
|
end
|
255
|
+
|
256
|
+
def check_record_creation_enabled
|
257
|
+
unless Dbviewer.configuration.enable_record_creation
|
258
|
+
respond_to do |format|
|
259
|
+
format.html {
|
260
|
+
flash[:alert] = "Record creation is disabled in the configuration"
|
261
|
+
redirect_to table_path(@table_name)
|
262
|
+
}
|
263
|
+
format.json { render json: { error: "Record creation is disabled" }, status: :forbidden }
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def check_record_editing_enabled
|
269
|
+
unless Dbviewer.configuration.enable_record_editing
|
270
|
+
respond_to do |format|
|
271
|
+
format.html {
|
272
|
+
flash[:alert] = "Record editing is disabled in the configuration"
|
273
|
+
redirect_to table_path(@table_name)
|
274
|
+
}
|
275
|
+
format.json { render json: { error: "Record editing is disabled" }, status: :forbidden }
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def check_record_deletion_enabled
|
281
|
+
unless Dbviewer.configuration.enable_record_deletion
|
282
|
+
respond_to do |format|
|
283
|
+
format.html {
|
284
|
+
flash[:alert] = "Record deletion is disabled in the configuration"
|
285
|
+
redirect_to table_path(@table_name)
|
286
|
+
}
|
287
|
+
format.json { render json: { error: "Record deletion is disabled" }, status: :forbidden }
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
213
291
|
end
|
214
292
|
end
|
@@ -137,6 +137,24 @@ module Dbviewer
|
|
137
137
|
content_tag(:i, "", class: "bi bi-clipboard")
|
138
138
|
end
|
139
139
|
|
140
|
+
# Edit Record button (only if enabled in configuration)
|
141
|
+
edit_button = if Dbviewer.configuration.enable_record_editing
|
142
|
+
button_tag(
|
143
|
+
type: "button",
|
144
|
+
class: "btn btn-sm btn-outline-primary edit-record-btn",
|
145
|
+
title: "Edit Record",
|
146
|
+
data: {
|
147
|
+
record_data: data_attributes.to_json,
|
148
|
+
table_name: table_name,
|
149
|
+
primary_key: metadata && metadata[:primary_key] ? metadata[:primary_key] : "id"
|
150
|
+
}
|
151
|
+
) do
|
152
|
+
content_tag(:i, "", class: "bi bi-pencil")
|
153
|
+
end
|
154
|
+
else
|
155
|
+
"".html_safe
|
156
|
+
end
|
157
|
+
|
140
158
|
# Delete Record button (only if enabled in configuration)
|
141
159
|
delete_button = if Dbviewer.configuration.enable_record_deletion
|
142
160
|
button_tag(
|
@@ -147,7 +165,8 @@ module Dbviewer
|
|
147
165
|
bs_toggle: "modal",
|
148
166
|
bs_target: "#deleteConfirmModal",
|
149
167
|
record_data: data_attributes.to_json,
|
150
|
-
table_name: table_name
|
168
|
+
table_name: table_name,
|
169
|
+
primary_key: metadata && metadata[:primary_key] ? metadata[:primary_key] : "id"
|
151
170
|
}
|
152
171
|
) do
|
153
172
|
content_tag(:i, "", class: "bi bi-trash")
|
@@ -157,7 +176,7 @@ module Dbviewer
|
|
157
176
|
end
|
158
177
|
|
159
178
|
# Concatenate all buttons
|
160
|
-
view_button + copy_factory_button + delete_button
|
179
|
+
view_button + copy_factory_button + edit_button + delete_button
|
161
180
|
end
|
162
181
|
end
|
163
182
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
<%
|
2
|
+
# Common variables:
|
3
|
+
# - form: The form builder
|
4
|
+
# - column: Current column hash with :name, :type, etc.
|
5
|
+
# - metadata: Table metadata with :foreign_keys etc.
|
6
|
+
# - foreign_key_options: Options for foreign key dropdowns
|
7
|
+
# - record: The record being edited (nil for new records)
|
8
|
+
# - is_edit_mode: Boolean flag indicating if this is an edit (vs create) operation
|
9
|
+
|
10
|
+
column_name = column[:name]
|
11
|
+
is_primary_key = metadata[:primary_key] == column_name
|
12
|
+
|
13
|
+
# For new records, skip auto-increment primary keys
|
14
|
+
skip_column = !is_edit_mode && is_primary_key # Skip primary key in creation mode
|
15
|
+
|
16
|
+
# Skip timestamps for both new and edit
|
17
|
+
return if skip_column || %w[created_at updated_at].include?(column_name)
|
18
|
+
|
19
|
+
# Get current value for edit mode
|
20
|
+
current_value = record ? record[column_name] : nil
|
21
|
+
|
22
|
+
# Common field properties
|
23
|
+
field_type = determine_field_type(column[:type])
|
24
|
+
foreign_key = metadata[:foreign_keys].find { |fk| fk[:column] == column_name }
|
25
|
+
field_id = "record_#{column_name}"
|
26
|
+
required = !column[:null]
|
27
|
+
disabled = is_edit_mode && is_primary_key # Disable editing of primary key in edit mode
|
28
|
+
%>
|
29
|
+
|
30
|
+
<div class="mb-3">
|
31
|
+
<%= form.label "record[#{column_name}]", column_name.humanize, class: "form-label" %>
|
32
|
+
|
33
|
+
<% if foreign_key && foreign_key_options[column_name].present? %>
|
34
|
+
<!-- Foreign key dropdown -->
|
35
|
+
<%= form.select "record[#{column_name}]",
|
36
|
+
options_for_select(foreign_key_options[column_name], current_value),
|
37
|
+
{ include_blank: column[:null] ? "-- Select --" : false },
|
38
|
+
{ class: "form-control select2-dropdown", id: field_id, disabled: disabled } %>
|
39
|
+
|
40
|
+
<% elsif field_type == :text || field_type == :text_area %>
|
41
|
+
<!-- Text area for long text fields -->
|
42
|
+
<%= form.text_area "record[#{column_name}]",
|
43
|
+
value: current_value,
|
44
|
+
class: "form-control",
|
45
|
+
id: field_id,
|
46
|
+
rows: 3,
|
47
|
+
required: required,
|
48
|
+
disabled: disabled %>
|
49
|
+
|
50
|
+
<% elsif field_type == :boolean || field_type == :check_box %>
|
51
|
+
<!-- Boolean field -->
|
52
|
+
<div class="form-check form-switch">
|
53
|
+
<%= form.check_box "record[#{column_name}]",
|
54
|
+
{ class: "form-check-input",
|
55
|
+
id: field_id,
|
56
|
+
checked: current_value,
|
57
|
+
disabled: disabled },
|
58
|
+
"true", "false" %>
|
59
|
+
</div>
|
60
|
+
|
61
|
+
<% elsif field_type == :datetime_local_field %>
|
62
|
+
<!-- Date time picker -->
|
63
|
+
<div class="input-group datetime-picker">
|
64
|
+
<%= form.text_field "record[#{column_name}]",
|
65
|
+
value: current_value,
|
66
|
+
class: "form-control",
|
67
|
+
id: field_id,
|
68
|
+
required: required,
|
69
|
+
data: { input: "" },
|
70
|
+
disabled: disabled %>
|
71
|
+
<button type="button" class="input-group-text" data-toggle><i class="bi bi-calendar-event"></i></button>
|
72
|
+
</div>
|
73
|
+
|
74
|
+
<% elsif field_type == :date_field %>
|
75
|
+
<!-- Date picker -->
|
76
|
+
<div class="input-group date-picker">
|
77
|
+
<%= form.text_field "record[#{column_name}]",
|
78
|
+
value: current_value,
|
79
|
+
class: "form-control",
|
80
|
+
id: field_id,
|
81
|
+
required: required,
|
82
|
+
data: { input: "" },
|
83
|
+
disabled: disabled %>
|
84
|
+
<button type="button" class="input-group-text" data-toggle><i class="bi bi-calendar-event"></i></button>
|
85
|
+
</div>
|
86
|
+
|
87
|
+
<% else %>
|
88
|
+
<!-- Default text input -->
|
89
|
+
<%= form.text_field "record[#{column_name}]",
|
90
|
+
value: current_value,
|
91
|
+
class: "form-control",
|
92
|
+
id: field_id,
|
93
|
+
required: required,
|
94
|
+
disabled: disabled %>
|
95
|
+
<% end %>
|
96
|
+
|
97
|
+
<% if disabled %>
|
98
|
+
<!-- Add a hidden field to preserve the primary key value -->
|
99
|
+
<%= form.hidden_field "record[#{column_name}]", value: current_value %>
|
100
|
+
<% end %>
|
101
|
+
|
102
|
+
<% if !is_edit_mode && column[:default].present? && column[:default] != "NULL" %>
|
103
|
+
<div class="form-text text-muted">Default: <%= column[:default] %></div>
|
104
|
+
<% end %>
|
105
|
+
</div>
|
@@ -0,0 +1,42 @@
|
|
1
|
+
<% if @errors.present? %>
|
2
|
+
<div class="alert alert-danger">
|
3
|
+
<ul class="mb-0">
|
4
|
+
<% @errors.full_messages.each do |message| %>
|
5
|
+
<li><%= message %></li>
|
6
|
+
<% end %>
|
7
|
+
</ul>
|
8
|
+
</div>
|
9
|
+
<% end %>
|
10
|
+
|
11
|
+
<div class="modal-header">
|
12
|
+
<h5 class="modal-title" id="editRecordModalLabel">
|
13
|
+
<i class="bi bi-pencil-square me-1"></i> Edit <%= @table_name.humanize.titleize %> Record
|
14
|
+
</h5>
|
15
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
<div class="modal-body">
|
19
|
+
<% primary_key = @metadata[:primary_key] || "id" %>
|
20
|
+
<% primary_key_value = @record[primary_key] %>
|
21
|
+
|
22
|
+
<%= form_with url: update_record_table_path(@table_name, record_id: primary_key_value), method: :patch, id: "editRecordForm" do |form| %>
|
23
|
+
<% @table_columns.each do |column| %>
|
24
|
+
<%= render partial: 'dbviewer/tables/record_form_fields',
|
25
|
+
locals: {
|
26
|
+
form: form,
|
27
|
+
column: column,
|
28
|
+
metadata: @metadata,
|
29
|
+
foreign_key_options: @foreign_key_options,
|
30
|
+
record: @record,
|
31
|
+
is_edit_mode: true
|
32
|
+
} %>
|
33
|
+
<% end %>
|
34
|
+
<% end %>
|
35
|
+
</div>
|
36
|
+
|
37
|
+
<div class="modal-footer">
|
38
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
39
|
+
<button type="submit" form="editRecordForm" class="btn btn-primary" id="updateRecordButton">
|
40
|
+
<i class="bi bi-check-lg me-1"></i>Update Record
|
41
|
+
</button>
|
42
|
+
</div>
|
@@ -18,46 +18,15 @@
|
|
18
18
|
<div class="modal-body">
|
19
19
|
<%= form_with url: create_record_table_path(@table_name), method: :post, id: "newRecordForm" do |form| %>
|
20
20
|
<% @table_columns.each do |column| %>
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
<div class="mb-3">
|
31
|
-
<%
|
32
|
-
# Handle different field types
|
33
|
-
field_type = determine_field_type(column[:type])
|
34
|
-
foreign_key = @metadata[:foreign_keys].find { |fk| fk[:column] == column_name }
|
35
|
-
field_id = "record_#{column_name}"
|
36
|
-
required = !column[:null]
|
37
|
-
%>
|
38
|
-
|
39
|
-
<%= form.label "record[#{column_name}]", column_name.humanize, class: "form-label" %>
|
40
|
-
|
41
|
-
<% if foreign_key && @foreign_key_options[column_name].present? %>
|
42
|
-
<%= form.select "record[#{column_name}]",
|
43
|
-
options_for_select(@foreign_key_options[column_name]),
|
44
|
-
{ include_blank: column[:null] ? "-- Select --" : false },
|
45
|
-
{ class: "form-select form-control select2-dropdown" }
|
46
|
-
%>
|
47
|
-
<% elsif field_type == :check_box %>
|
48
|
-
<div class="form-check">
|
49
|
-
<%= form.check_box "record[#{column_name}]", class: "form-check-input", id: field_id %>
|
50
|
-
</div>
|
51
|
-
<% elsif field_type == :text_area %>
|
52
|
-
<%= form.text_area "record[#{column_name}]", class: "form-control", id: field_id, rows: 3, required: required %>
|
53
|
-
<% else %>
|
54
|
-
<%= form.send(field_type, "record[#{column_name}]", class: "form-control", id: field_id, required: required) %>
|
55
|
-
<% end %>
|
56
|
-
|
57
|
-
<% if column[:default].present? && column[:default] != "NULL" %>
|
58
|
-
<div class="form-text text-muted">Default: <%= column[:default] %></div>
|
59
|
-
<% end %>
|
60
|
-
</div>
|
21
|
+
<%= render partial: 'dbviewer/tables/record_form_fields',
|
22
|
+
locals: {
|
23
|
+
form: form,
|
24
|
+
column: column,
|
25
|
+
metadata: @metadata,
|
26
|
+
foreign_key_options: @foreign_key_options,
|
27
|
+
record: nil,
|
28
|
+
is_edit_mode: false
|
29
|
+
} %>
|
61
30
|
<% end %>
|
62
31
|
<% end %>
|
63
32
|
</div>
|
@@ -13,8 +13,15 @@
|
|
13
13
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
14
14
|
<%= stylesheet_link_tag "dbviewer/table", "data-turbo-track": "reload" %>
|
15
15
|
<%= javascript_include_tag "dbviewer/table", "data-turbo-track": "reload" %>
|
16
|
+
<% if Dbviewer.configuration.enable_record_creation %>
|
16
17
|
<%= javascript_include_tag "dbviewer/record_creation", "data-turbo-track": "reload" %>
|
18
|
+
<% end %>
|
19
|
+
<% if Dbviewer.configuration.enable_record_deletion %>
|
17
20
|
<%= javascript_include_tag "dbviewer/record_deletion", "data-turbo-track": "reload" %>
|
21
|
+
<% end %>
|
22
|
+
<% if Dbviewer.configuration.enable_record_editing %>
|
23
|
+
<%= javascript_include_tag "dbviewer/record_editing", "data-turbo-track": "reload" %>
|
24
|
+
<% end %>
|
18
25
|
<% end %>
|
19
26
|
|
20
27
|
<% content_for :sidebar_active do %>active<% end %>
|
@@ -193,8 +200,13 @@
|
|
193
200
|
</div>
|
194
201
|
<div class="modal-footer">
|
195
202
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
203
|
+
<% if Dbviewer.configuration.enable_record_editing %>
|
204
|
+
<button type="button" class="btn btn-primary" id="recordDetailEditBtn" data-record-id="" data-primary-key="<%= @metadata[:primary_key] || 'id' %>">
|
205
|
+
<i class="bi bi-pencil me-1"></i>Edit Record
|
206
|
+
</button>
|
207
|
+
<% end %>
|
196
208
|
<% if Dbviewer.configuration.enable_record_deletion %>
|
197
|
-
<button type="button" class="btn btn-danger" id="recordDetailDeleteBtn" data-record-id="">
|
209
|
+
<button type="button" class="btn btn-danger" id="recordDetailDeleteBtn" data-record-id="" data-primary-key="<%= @metadata[:primary_key] || 'id' %>">
|
198
210
|
<i class="bi bi-trash me-1"></i>Delete Record
|
199
211
|
</button>
|
200
212
|
<% end %>
|
@@ -367,8 +379,7 @@
|
|
367
379
|
<% end %>
|
368
380
|
|
369
381
|
<!-- Floating Add Record Button -->
|
370
|
-
|
371
|
-
<% if @table_name != 'schema_migrations'%>
|
382
|
+
<% if Dbviewer.configuration.enable_record_creation && @table_name != 'schema_migrations' %>
|
372
383
|
<div class="floating-add-record d-none d-lg-block">
|
373
384
|
<button id="floatingAddRecordBtn"
|
374
385
|
class="btn btn-success btn-lg shadow-lg rounded-circle"
|
@@ -380,6 +391,7 @@
|
|
380
391
|
<% end %>
|
381
392
|
|
382
393
|
<!-- New Record Modal -->
|
394
|
+
<% if Dbviewer.configuration.enable_record_creation %>
|
383
395
|
<div id="newRecordModal" class="modal fade" tabindex="-1" aria-labelledby="newRecordModalLabel" aria-hidden="true">
|
384
396
|
<div class="modal-dialog modal-lg">
|
385
397
|
<div class="modal-content">
|
@@ -387,6 +399,7 @@
|
|
387
399
|
</div>
|
388
400
|
</div>
|
389
401
|
</div>
|
402
|
+
<% end %>
|
390
403
|
|
391
404
|
<% if Dbviewer.configuration.enable_record_deletion %>
|
392
405
|
<!-- Delete Confirmation Modal -->
|
@@ -425,8 +438,20 @@
|
|
425
438
|
</div>
|
426
439
|
<% end %>
|
427
440
|
|
441
|
+
<!-- Edit Record Modal -->
|
442
|
+
<% if Dbviewer.configuration.enable_record_editing %>
|
443
|
+
<div id="editRecordModal" class="modal fade" tabindex="-1" aria-labelledby="editRecordModalLabel" aria-hidden="true">
|
444
|
+
<div class="modal-dialog modal-lg">
|
445
|
+
<div class="modal-content">
|
446
|
+
<!-- Content will be loaded via AJAX -->
|
447
|
+
</div>
|
448
|
+
</div>
|
449
|
+
</div>
|
450
|
+
<% end %>
|
451
|
+
|
428
452
|
<!-- Toast container for notifications -->
|
429
453
|
<div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
|
430
454
|
|
431
455
|
<input type="hidden" id="mini_erd_table_path" name="mini_erd_table_path" value="<%= dbviewer.mini_erd_api_table_path(@table_name, format: :json) %>">
|
432
456
|
<input type="hidden" id="table_name" name="table_name" value="<%= @table_name %>">
|
457
|
+
<input type="hidden" id="table_primary_key" name="table_primary_key" value="<%= @metadata[:primary_key] || 'id' %>">
|
data/config/routes.rb
CHANGED
@@ -7,6 +7,8 @@ Dbviewer::Engine.routes.draw do
|
|
7
7
|
get "new_record"
|
8
8
|
post "create_record"
|
9
9
|
delete "records/:record_id", to: "tables#destroy_record", as: :destroy_record
|
10
|
+
get "records/:record_id/edit", to: "tables#edit_record", as: :edit_record
|
11
|
+
patch "records/:record_id", to: "tables#update_record", as: :update_record
|
10
12
|
end
|
11
13
|
end
|
12
14
|
|
@@ -120,6 +120,12 @@ module Dbviewer
|
|
120
120
|
# Enable or disable record deletion functionality
|
121
121
|
attr_accessor :enable_record_deletion
|
122
122
|
|
123
|
+
# Enable or disable record editing functionality
|
124
|
+
attr_accessor :enable_record_editing
|
125
|
+
|
126
|
+
# Enable or disable record creation functionality
|
127
|
+
attr_accessor :enable_record_creation
|
128
|
+
|
123
129
|
def initialize
|
124
130
|
@per_page_options = [ 10, 20, 50, 100 ]
|
125
131
|
@default_per_page = 20
|
@@ -128,6 +134,8 @@ module Dbviewer
|
|
128
134
|
@cache_expiry = 300
|
129
135
|
@enable_data_export = false
|
130
136
|
@enable_record_deletion = true
|
137
|
+
@enable_record_editing = true
|
138
|
+
@enable_record_creation = true
|
131
139
|
@query_timeout = 30
|
132
140
|
@query_logging_mode = :memory
|
133
141
|
@query_log_path = "log/dbviewer.log"
|
data/lib/dbviewer/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dbviewer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.4
|
4
|
+
version: 0.9.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wailan Tirajoh
|
@@ -88,6 +88,7 @@ files:
|
|
88
88
|
- app/assets/javascripts/dbviewer/query.js
|
89
89
|
- app/assets/javascripts/dbviewer/record_creation.js
|
90
90
|
- app/assets/javascripts/dbviewer/record_deletion.js
|
91
|
+
- app/assets/javascripts/dbviewer/record_editing.js
|
91
92
|
- app/assets/javascripts/dbviewer/sidebar.js
|
92
93
|
- app/assets/javascripts/dbviewer/table.js
|
93
94
|
- app/assets/javascripts/dbviewer/utility.js
|
@@ -137,7 +138,9 @@ files:
|
|
137
138
|
- app/views/dbviewer/home/index.html.erb
|
138
139
|
- app/views/dbviewer/logs/index.html.erb
|
139
140
|
- app/views/dbviewer/shared/_tables_sidebar.html.erb
|
141
|
+
- app/views/dbviewer/tables/_record_form_fields.html.erb
|
140
142
|
- app/views/dbviewer/tables/_table_structure.html.erb
|
143
|
+
- app/views/dbviewer/tables/edit_record.html.erb
|
141
144
|
- app/views/dbviewer/tables/index.html.erb
|
142
145
|
- app/views/dbviewer/tables/new_record.html.erb
|
143
146
|
- app/views/dbviewer/tables/query.html.erb
|
@@ -195,9 +198,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
195
198
|
version: '0'
|
196
199
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
197
200
|
requirements:
|
198
|
-
- - "
|
201
|
+
- - ">="
|
199
202
|
- !ruby/object:Gem::Version
|
200
|
-
version:
|
203
|
+
version: '0'
|
201
204
|
requirements: []
|
202
205
|
rubygems_version: 3.4.10
|
203
206
|
signing_key:
|