dbviewer 0.9.4.pre.alpha.2 → 0.9.4.pre.alpha.3
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_editing.js +208 -0
- data/app/controllers/dbviewer/tables_controller.rb +40 -1
- data/app/helpers/dbviewer/datatable_ui_table_helper.rb +19 -1
- 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 +20 -0
- data/config/routes.rb +2 -0
- data/lib/dbviewer/configuration.rb +4 -0
- data/lib/dbviewer/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54a9324f879a9f3be0d71ab93e1ead14ca185cc3513fe44c86aec45927899ff8
|
4
|
+
data.tar.gz: 139bb5d6f6bc46ef2b16e7b9c77362d3231e709cb87f79f17f2304062dd3d3a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '06767791b1d8df0bad7f1a7e98e6df81c063a50ca0bbba1f8a91186a89e093c56db8dd400e78f3fca9ae99700d1d85bc2a2351f76d9f6f6fa6692123b16b953f'
|
7
|
+
data.tar.gz: 8688791ec007fedf0e4b56ad2d65b067c5f86b77b918b322007364b2500073154a641105c3c369ff25ed35633d3a5cb055080922ebf50e380521a2eaf671ab7d
|
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
|
@@ -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,7 @@ 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
7
|
before_action :set_query_filters, only: [ :show, :export_csv ]
|
8
8
|
before_action :set_global_filters, only: [ :show, :export_csv ]
|
9
9
|
|
@@ -111,6 +111,45 @@ module Dbviewer
|
|
111
111
|
end
|
112
112
|
end
|
113
113
|
|
114
|
+
def edit_record
|
115
|
+
model_class = database_manager.get_model_for(@table_name)
|
116
|
+
primary_key = database_manager.primary_key(@table_name) || "id"
|
117
|
+
@record = model_class.find_by(primary_key => params[:record_id])
|
118
|
+
|
119
|
+
if @record.nil?
|
120
|
+
render json: { error: "Record not found" }, status: :not_found
|
121
|
+
return
|
122
|
+
end
|
123
|
+
|
124
|
+
@table_columns = filter_accessible_columns(@table_name, database_manager.table_columns(@table_name))
|
125
|
+
@metadata = database_manager.table_metadata(@table_name)
|
126
|
+
@foreign_key_options = load_foreign_key_options(@metadata)
|
127
|
+
|
128
|
+
render layout: false
|
129
|
+
end
|
130
|
+
|
131
|
+
def update_record
|
132
|
+
model_class = database_manager.get_model_for(@table_name)
|
133
|
+
primary_key = database_manager.primary_key(@table_name) || "id"
|
134
|
+
record = model_class.find_by(primary_key => params[:record_id])
|
135
|
+
|
136
|
+
if record.nil?
|
137
|
+
render json: { error: "Record not found" }, status: :not_found
|
138
|
+
return
|
139
|
+
end
|
140
|
+
|
141
|
+
begin
|
142
|
+
if record.update(record_params)
|
143
|
+
render json: { message: "Record updated successfully" }
|
144
|
+
else
|
145
|
+
render json: { errors: record.errors, messages: record.errors.full_messages }, status: :unprocessable_entity
|
146
|
+
end
|
147
|
+
rescue => e
|
148
|
+
Rails.logger.error("Error updating record in #{@table_name}: #{e.message}")
|
149
|
+
render json: { error: "Failed to update record: #{e.message}" }, status: :internal_server_error
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
114
153
|
private
|
115
154
|
|
116
155
|
def record_params
|
@@ -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(
|
@@ -157,7 +175,7 @@ module Dbviewer
|
|
157
175
|
end
|
158
176
|
|
159
177
|
# Concatenate all buttons
|
160
|
-
view_button + copy_factory_button + delete_button
|
178
|
+
view_button + copy_factory_button + edit_button + delete_button
|
161
179
|
end
|
162
180
|
end
|
163
181
|
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 %>
|
62
|
+
<!-- Date time picker -->
|
63
|
+
<div class="input-group flatpickr">
|
64
|
+
<%= form.text_field "record[#{column_name}]",
|
65
|
+
value: current_value,
|
66
|
+
class: "form-control datetime-picker",
|
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 %>
|
75
|
+
<!-- Date picker -->
|
76
|
+
<div class="input-group flatpickr">
|
77
|
+
<%= form.text_field "record[#{column_name}]",
|
78
|
+
value: current_value,
|
79
|
+
class: "form-control date-picker",
|
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>
|
@@ -15,6 +15,9 @@
|
|
15
15
|
<%= javascript_include_tag "dbviewer/table", "data-turbo-track": "reload" %>
|
16
16
|
<%= javascript_include_tag "dbviewer/record_creation", "data-turbo-track": "reload" %>
|
17
17
|
<%= javascript_include_tag "dbviewer/record_deletion", "data-turbo-track": "reload" %>
|
18
|
+
<% if Dbviewer.configuration.enable_record_editing %>
|
19
|
+
<%= javascript_include_tag "dbviewer/record_editing", "data-turbo-track": "reload" %>
|
20
|
+
<% end %>
|
18
21
|
<% end %>
|
19
22
|
|
20
23
|
<% content_for :sidebar_active do %>active<% end %>
|
@@ -193,6 +196,11 @@
|
|
193
196
|
</div>
|
194
197
|
<div class="modal-footer">
|
195
198
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
199
|
+
<% if Dbviewer.configuration.enable_record_editing %>
|
200
|
+
<button type="button" class="btn btn-primary" id="recordDetailEditBtn" data-record-id="" data-primary-key="<%= @metadata[:primary_key] || 'id' %>">
|
201
|
+
<i class="bi bi-pencil me-1"></i>Edit Record
|
202
|
+
</button>
|
203
|
+
<% end %>
|
196
204
|
<% if Dbviewer.configuration.enable_record_deletion %>
|
197
205
|
<button type="button" class="btn btn-danger" id="recordDetailDeleteBtn" data-record-id="">
|
198
206
|
<i class="bi bi-trash me-1"></i>Delete Record
|
@@ -425,8 +433,20 @@
|
|
425
433
|
</div>
|
426
434
|
<% end %>
|
427
435
|
|
436
|
+
<!-- Edit Record Modal -->
|
437
|
+
<% if Dbviewer.configuration.enable_record_editing %>
|
438
|
+
<div id="editRecordModal" class="modal fade" tabindex="-1" aria-labelledby="editRecordModalLabel" aria-hidden="true">
|
439
|
+
<div class="modal-dialog modal-lg">
|
440
|
+
<div class="modal-content">
|
441
|
+
<!-- Content will be loaded via AJAX -->
|
442
|
+
</div>
|
443
|
+
</div>
|
444
|
+
</div>
|
445
|
+
<% end %>
|
446
|
+
|
428
447
|
<!-- Toast container for notifications -->
|
429
448
|
<div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
|
430
449
|
|
431
450
|
<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
451
|
<input type="hidden" id="table_name" name="table_name" value="<%= @table_name %>">
|
452
|
+
<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,9 @@ 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
|
+
|
123
126
|
def initialize
|
124
127
|
@per_page_options = [ 10, 20, 50, 100 ]
|
125
128
|
@default_per_page = 20
|
@@ -128,6 +131,7 @@ module Dbviewer
|
|
128
131
|
@cache_expiry = 300
|
129
132
|
@enable_data_export = false
|
130
133
|
@enable_record_deletion = true
|
134
|
+
@enable_record_editing = true
|
131
135
|
@query_timeout = 30
|
132
136
|
@query_logging_mode = :memory
|
133
137
|
@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.pre.alpha.
|
4
|
+
version: 0.9.4.pre.alpha.3
|
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
|