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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bab71132b89e3a2b246802692149a2d662bc42bcfa54b9db4d71ab9593dc2c2a
4
- data.tar.gz: 8d2160e56a9872ad9e1dfa274e5ccbdcb16c8ff1b65c134be00b806d91841a55
3
+ metadata.gz: 54a9324f879a9f3be0d71ab93e1ead14ca185cc3513fe44c86aec45927899ff8
4
+ data.tar.gz: 139bb5d6f6bc46ef2b16e7b9c77362d3231e709cb87f79f17f2304062dd3d3a3
5
5
  SHA512:
6
- metadata.gz: c00de395e8f283afd2f6c905ae1e448438296ecfdc2167606d2216d3c39a492d27d2c60334d35234e5d460472a66730a6893fe79b81ce1f972b48b67d767b1b6
7
- data.tar.gz: 8f9a53eb14f23c5aa2bb006458dcda9de39d031aa49907ff0a017f76d96950d101f14785292a7e0a062aac91915face67bbe34f28bfffc1fdf126076375127a8
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** - 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
@@ -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
- 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>
@@ -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"
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.9.4-alpha.2"
2
+ VERSION = "0.9.4-alpha.3"
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.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