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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bab71132b89e3a2b246802692149a2d662bc42bcfa54b9db4d71ab9593dc2c2a
4
- data.tar.gz: 8d2160e56a9872ad9e1dfa274e5ccbdcb16c8ff1b65c134be00b806d91841a55
3
+ metadata.gz: 60a92d1cfd091ad3a1688fcf9d2e532dc651ac69384cf4540b56bdb65b31416a
4
+ data.tar.gz: 1f467d83cfefb95aebc1701f18ddca6a7ad5710495b3bf711857ee29eb0c35b1
5
5
  SHA512:
6
- metadata.gz: c00de395e8f283afd2f6c905ae1e448438296ecfdc2167606d2216d3c39a492d27d2c60334d35234e5d460472a66730a6893fe79b81ce1f972b48b67d767b1b6
7
- data.tar.gz: 8f9a53eb14f23c5aa2bb006458dcda9de39d031aa49907ff0a017f76d96950d101f14785292a7e0a062aac91915face67bbe34f28bfffc1fdf126076375127a8
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** - Create and delete database records directly from the interface
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
- const pkName =
91
- Object.keys(recordData).find((k) => k.toLowerCase() === "id") ||
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
- const pkName =
109
- Object.keys(recordData).find((k) => k.toLowerCase() === "id") ||
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
- column_name = column[:name]
23
- is_primary_key = @metadata[:primary_key] == column_name
24
- skip_column = is_primary_key && %w[id].include?(column_name.downcase)
25
-
26
- # Skip auto-increment primary keys and timestamps
27
- next if skip_column || %w[created_at updated_at].include?(column_name)
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
- <%# TODO: move this to helpers so that we can have centralized creation check %>
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"
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.9.4-alpha.2"
2
+ VERSION = "0.9.4"
3
3
  end
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.2
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: 1.3.1
203
+ version: '0'
201
204
  requirements: []
202
205
  rubygems_version: 3.4.10
203
206
  signing_key: