dbdoc_engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +331 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/builds/dbdoc_engine/application.css +5 -0
  5. data/app/assets/images/dbdoc_engine/arrowdown.svg +3 -0
  6. data/app/assets/images/dbdoc_engine/arrowhorizontal.svg +3 -0
  7. data/app/assets/images/dbdoc_engine/arrowleft.svg +3 -0
  8. data/app/assets/images/dbdoc_engine/changelog.svg +3 -0
  9. data/app/assets/images/dbdoc_engine/column_stats_dbdocs.svg +23 -0
  10. data/app/assets/images/dbdoc_engine/diagram.svg +3 -0
  11. data/app/assets/images/dbdoc_engine/double_arrow.svg +4 -0
  12. data/app/assets/images/dbdoc_engine/group_bu.svg +3 -0
  13. data/app/assets/images/dbdoc_engine/japan_circle.png +0 -0
  14. data/app/assets/images/dbdoc_engine/log_in_image.png +0 -0
  15. data/app/assets/images/dbdoc_engine/logo.svg +12 -0
  16. data/app/assets/images/dbdoc_engine/orange_changelog.svg +3 -0
  17. data/app/assets/images/dbdoc_engine/orange_fields.svg +23 -0
  18. data/app/assets/images/dbdoc_engine/orange_logo.svg +12 -0
  19. data/app/assets/images/dbdoc_engine/orange_table.svg +21 -0
  20. data/app/assets/images/dbdoc_engine/orange_updates.svg +43 -0
  21. data/app/assets/images/dbdoc_engine/orange_wiki.svg +3 -0
  22. data/app/assets/images/dbdoc_engine/search.svg +3 -0
  23. data/app/assets/images/dbdoc_engine/setting.svg +3 -0
  24. data/app/assets/images/dbdoc_engine/table_dbdocs.svg +21 -0
  25. data/app/assets/images/dbdoc_engine/uk_circle_transparent.png +0 -0
  26. data/app/assets/images/dbdoc_engine/update_stats_dbdocs.svg +43 -0
  27. data/app/assets/images/dbdoc_engine/wiki.svg +3 -0
  28. data/app/assets/stylesheets/dbdoc_engine/admin.css +176 -0
  29. data/app/assets/stylesheets/dbdoc_engine/admin_header.css +179 -0
  30. data/app/assets/stylesheets/dbdoc_engine/application.scss +1 -0
  31. data/app/assets/stylesheets/dbdoc_engine/changelog.css +173 -0
  32. data/app/assets/stylesheets/dbdoc_engine/dashboard.css +513 -0
  33. data/app/assets/stylesheets/dbdoc_engine/dbdoc_application.css +117 -0
  34. data/app/assets/stylesheets/dbdoc_engine/ecommerce.css +253 -0
  35. data/app/assets/stylesheets/dbdoc_engine/group_details.css +178 -0
  36. data/app/assets/stylesheets/dbdoc_engine/header.css +212 -0
  37. data/app/assets/stylesheets/dbdoc_engine/loading_spinner.css +127 -0
  38. data/app/assets/stylesheets/dbdoc_engine/login.css +213 -0
  39. data/app/assets/stylesheets/dbdoc_engine/schema_diagram.css +149 -0
  40. data/app/assets/stylesheets/dbdoc_engine/sidebar.css +296 -0
  41. data/app/assets/stylesheets/dbdoc_engine/table_details.css +417 -0
  42. data/app/controllers/dbdoc_engine/admin/base_controller.rb +23 -0
  43. data/app/controllers/dbdoc_engine/admin/dashboard_controller.rb +16 -0
  44. data/app/controllers/dbdoc_engine/admin/data_transfer_controller.rb +63 -0
  45. data/app/controllers/dbdoc_engine/admin/db_design_dynamic_tables_controller.rb +198 -0
  46. data/app/controllers/dbdoc_engine/admin/db_design_table_groups_controller.rb +107 -0
  47. data/app/controllers/dbdoc_engine/application_controller.rb +65 -0
  48. data/app/controllers/dbdoc_engine/concerns/internationalization.rb +57 -0
  49. data/app/controllers/dbdoc_engine/db_doc_sessions_controller.rb +33 -0
  50. data/app/controllers/dbdoc_engine/home_controller.rb +79 -0
  51. data/app/controllers/dbdoc_engine/schema_diagram_controller.rb +293 -0
  52. data/app/helper/dbdoc_engine/application_helper.rb +35 -0
  53. data/app/helpers/dbdoc_engine/application_helper.rb +4 -0
  54. data/app/helpers/dbdoc_engine/changelogs_helper.rb +27 -0
  55. data/app/helpers/dbdoc_engine/column_helper.rb +30 -0
  56. data/app/helpers/dbdoc_engine/db_design_dynamic_tables_helper.rb +15 -0
  57. data/app/helpers/dbdoc_engine/home_helper.rb +75 -0
  58. data/app/javascript/dbdoc_engine/application.js +12 -0
  59. data/app/javascript/dbdoc_engine/controllers/application.js +29 -0
  60. data/app/javascript/dbdoc_engine/controllers/auto_submit_controller.js +17 -0
  61. data/app/javascript/dbdoc_engine/controllers/chart_controller.js +58 -0
  62. data/app/javascript/dbdoc_engine/controllers/column-type_controller.js +149 -0
  63. data/app/javascript/dbdoc_engine/controllers/column_controller.js +362 -0
  64. data/app/javascript/dbdoc_engine/controllers/column_search_controller.js +42 -0
  65. data/app/javascript/dbdoc_engine/controllers/dbdoc_accordion_controller.js +42 -0
  66. data/app/javascript/dbdoc_engine/controllers/ecommerce_controller.js +73 -0
  67. data/app/javascript/dbdoc_engine/controllers/group_details_controller.js +88 -0
  68. data/app/javascript/dbdoc_engine/controllers/import_export_controller.js +200 -0
  69. data/app/javascript/dbdoc_engine/controllers/index.js +9 -0
  70. data/app/javascript/dbdoc_engine/controllers/language_controller.js +100 -0
  71. data/app/javascript/dbdoc_engine/controllers/loading_spinner_controller.js +48 -0
  72. data/app/javascript/dbdoc_engine/controllers/login_controller.js +75 -0
  73. data/app/javascript/dbdoc_engine/controllers/notification_controller.js +15 -0
  74. data/app/javascript/dbdoc_engine/controllers/schema_diagram_controller.js +1129 -0
  75. data/app/javascript/dbdoc_engine/controllers/select2_controller.js +67 -0
  76. data/app/javascript/dbdoc_engine/controllers/sidebar_controller.js +943 -0
  77. data/app/javascript/dbdoc_engine/controllers/table_details_controller.js +245 -0
  78. data/app/javascript/dbdoc_engine/controllers/table_group_validation_controller.js +148 -0
  79. data/app/javascript/dbdoc_engine/controllers/table_validation_controller.js +423 -0
  80. data/app/jobs/dbdoc_engine/application_job.rb +4 -0
  81. data/app/mailers/dbdoc_engine/application_mailer.rb +6 -0
  82. data/app/models/dbdoc_engine/application_record.rb +6 -0
  83. data/app/models/dbdoc_engine/concerns/soft_deletable.rb +30 -0
  84. data/app/models/dbdoc_engine/db_design_changelog.rb +44 -0
  85. data/app/models/dbdoc_engine/db_design_dynamic_column.rb +211 -0
  86. data/app/models/dbdoc_engine/db_design_dynamic_table.rb +124 -0
  87. data/app/models/dbdoc_engine/db_design_table_group.rb +88 -0
  88. data/app/models/dbdoc_engine/user.rb +21 -0
  89. data/app/queries/dbdoc_engine/admin_dashboard_queries.rb +71 -0
  90. data/app/queries/dbdoc_engine/db_design_changelog_queries.rb +68 -0
  91. data/app/queries/dbdoc_engine/db_design_dynamic_column_queries.rb +37 -0
  92. data/app/queries/dbdoc_engine/db_design_dynamic_table_commands.rb +106 -0
  93. data/app/queries/dbdoc_engine/db_design_dynamic_table_queries.rb +194 -0
  94. data/app/queries/dbdoc_engine/db_design_table_group_queries.rb +154 -0
  95. data/app/services/dbdoc_engine/db_design_dynamic_table_export_service.rb +38 -0
  96. data/app/services/dbdoc_engine/db_design_dynamic_table_handler_service.rb +49 -0
  97. data/app/services/dbdoc_engine/db_design_dynamic_tables_service.rb +21 -0
  98. data/app/services/dbdoc_engine/error_handler_service.rb +43 -0
  99. data/app/services/dbdoc_engine/schema_rb_import_service.rb +194 -0
  100. data/app/services/dbdoc_engine/schema_rb_parser_service.rb +339 -0
  101. data/app/services/dbdoc_engine/table_filter_service.rb +35 -0
  102. data/app/services/dbdoc_engine/table_groups_service.rb +199 -0
  103. data/app/services/dbdoc_engine/table_management_service.rb +192 -0
  104. data/app/views/dbdoc_engine/admin/dashboard/_action_badge.html.erb +11 -0
  105. data/app/views/dbdoc_engine/admin/dashboard/_changelog_rows.html.erb +22 -0
  106. data/app/views/dbdoc_engine/admin/dashboard/_changelog_table_headers.html.erb +8 -0
  107. data/app/views/dbdoc_engine/admin/dashboard/_filter_fields.html.erb +43 -0
  108. data/app/views/dbdoc_engine/admin/dashboard/index.html.erb +159 -0
  109. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_column_fields.html.erb +225 -0
  110. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_deleted_table_index.html.erb +110 -0
  111. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_foreign_key_fields.html.erb +51 -0
  112. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_form.html.erb +75 -0
  113. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_recent_activity.html.erb +39 -0
  114. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_table_columns.html.erb +127 -0
  115. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_table_index.html.erb +109 -0
  116. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_table_information.html.erb +99 -0
  117. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/deleted_tables.html.erb +95 -0
  118. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/edit.html.erb +23 -0
  119. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/export_all_to_excel.xlsx.axlsx +240 -0
  120. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/export_to_excel.xlsx.axlsx +135 -0
  121. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/index.html.erb +109 -0
  122. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/new.html.erb +25 -0
  123. data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/show_table_info.html.erb +125 -0
  124. data/app/views/dbdoc_engine/admin/db_design_table_groups/_deleted_table_groups_list.html.erb +75 -0
  125. data/app/views/dbdoc_engine/admin/db_design_table_groups/_form.html.erb +88 -0
  126. data/app/views/dbdoc_engine/admin/db_design_table_groups/_table_groups_list.html.erb +82 -0
  127. data/app/views/dbdoc_engine/admin/db_design_table_groups/deleted_groups.html.erb +60 -0
  128. data/app/views/dbdoc_engine/admin/db_design_table_groups/edit.html.erb +25 -0
  129. data/app/views/dbdoc_engine/admin/db_design_table_groups/index.html.erb +85 -0
  130. data/app/views/dbdoc_engine/admin/db_design_table_groups/new.html.erb +26 -0
  131. data/app/views/dbdoc_engine/db_doc_sessions/new.html.erb +59 -0
  132. data/app/views/dbdoc_engine/home/changelog_details.html.erb +80 -0
  133. data/app/views/dbdoc_engine/home/changelogs.html.erb +20 -0
  134. data/app/views/dbdoc_engine/home/group_details.html.erb +94 -0
  135. data/app/views/dbdoc_engine/home/index.html.erb +11 -0
  136. data/app/views/dbdoc_engine/home/partials/_action_badge.html.erb +11 -0
  137. data/app/views/dbdoc_engine/home/partials/_breadcrumb_navigation.html.erb +30 -0
  138. data/app/views/dbdoc_engine/home/partials/_changelog_rows.html.erb +35 -0
  139. data/app/views/dbdoc_engine/home/partials/_changelog_table_headers.html.erb +16 -0
  140. data/app/views/dbdoc_engine/home/partials/_column_headers.html.erb +23 -0
  141. data/app/views/dbdoc_engine/home/partials/_column_row.html.erb +157 -0
  142. data/app/views/dbdoc_engine/home/partials/_filter_form.html.erb +47 -0
  143. data/app/views/dbdoc_engine/home/partials/_group_section.html.erb +84 -0
  144. data/app/views/dbdoc_engine/home/partials/_pagination.html.erb +5 -0
  145. data/app/views/dbdoc_engine/home/partials/_stats_container.html.erb +46 -0
  146. data/app/views/dbdoc_engine/home/partials/_table_groups.html.erb +7 -0
  147. data/app/views/dbdoc_engine/home/partials/_table_information_section.html.erb +50 -0
  148. data/app/views/dbdoc_engine/home/partials/_table_section.html.erb +48 -0
  149. data/app/views/dbdoc_engine/home/table_details.html.erb +9 -0
  150. data/app/views/dbdoc_engine/schema_diagram/index.html.erb +102 -0
  151. data/app/views/dbdoc_engine/shared/_admin_header.html.erb +78 -0
  152. data/app/views/dbdoc_engine/shared/_header.html.erb +94 -0
  153. data/app/views/dbdoc_engine/shared/_js_translations.html.erb +3 -0
  154. data/app/views/dbdoc_engine/shared/_language_button.html.erb +14 -0
  155. data/app/views/dbdoc_engine/shared/_sidebar.html.erb +128 -0
  156. data/app/views/kaminari/dbdoc_engine/_first_page.html.erb +3 -0
  157. data/app/views/kaminari/dbdoc_engine/_gap.html.erb +3 -0
  158. data/app/views/kaminari/dbdoc_engine/_last_page.html.erb +3 -0
  159. data/app/views/kaminari/dbdoc_engine/_next_page.html.erb +3 -0
  160. data/app/views/kaminari/dbdoc_engine/_page.html.erb +9 -0
  161. data/app/views/kaminari/dbdoc_engine/_paginator.html.erb +17 -0
  162. data/app/views/kaminari/dbdoc_engine/_prev_page.html.erb +3 -0
  163. data/app/views/layouts/dbdoc_engine/application.html.erb +107 -0
  164. data/app/views/layouts/dbdoc_engine/header.html.erb +108 -0
  165. data/config/importmap.rb +11 -0
  166. data/config/locales/en.yml +307 -0
  167. data/config/locales/ja.yml +306 -0
  168. data/config/routes.rb +73 -0
  169. data/db/migrate/rails7/20250227060610_create_db_design_table_groups.rb +15 -0
  170. data/db/migrate/rails7/20250227094626_create_db_design_dynamic_tables.rb +19 -0
  171. data/db/migrate/rails7/20250228022732_create_db_design_dynamic_columns.rb +34 -0
  172. data/db/migrate/rails7/20250401051453_create_db_design_changelogs.rb +26 -0
  173. data/db/migrate/rails7/20250411040822_create_users.rb +14 -0
  174. data/db/migrate/rails7/20250421080851_add_missing_indexes_to_dbdoc_tables.rb +23 -0
  175. data/db/migrate/rails8/20250227060610_create_db_design_table_groups.rb +15 -0
  176. data/db/migrate/rails8/20250227094626_create_db_design_dynamic_tables.rb +19 -0
  177. data/db/migrate/rails8/20250228022732_create_db_design_dynamic_columns.rb +34 -0
  178. data/db/migrate/rails8/20250401051453_create_db_design_changelogs.rb +26 -0
  179. data/db/migrate/rails8/20250411040822_create_users.rb +14 -0
  180. data/db/migrate/rails8/20250421080851_add_missing_indexes_to_dbdoc_tables.rb +23 -0
  181. data/db/seeds.rb +28 -0
  182. data/lib/dbdoc_engine/engine.rb +57 -0
  183. data/lib/dbdoc_engine/version.rb +3 -0
  184. data/lib/dbdoc_engine.rb +9 -0
  185. data/lib/generators/dbdoc_engine/install/install_generator.rb +245 -0
  186. data/lib/generators/dbdoc_engine/uninstall/uninstall_generator.rb +196 -0
  187. data/lib/tasks/dbdoc_engine_tasks.rake +44 -0
  188. data/public/dbdoc_engine_assets/images/camel_chess_head.png +0 -0
  189. data/public/dbdoc_engine_assets/images/dblogo.svg +4 -0
  190. data/public/dbdoc_engine_assets/images/japan_circle.png +0 -0
  191. data/public/dbdoc_engine_assets/images/king_chess_head.png +0 -0
  192. data/public/dbdoc_engine_assets/images/login-bg.svg +44 -0
  193. data/public/dbdoc_engine_assets/images/logo.png +0 -0
  194. data/public/dbdoc_engine_assets/images/logo.svg +12 -0
  195. data/public/dbdoc_engine_assets/images/queen_chess_head.png +0 -0
  196. data/public/dbdoc_engine_assets/images/soldier_chess_headd.png +0 -0
  197. data/public/dbdoc_engine_assets/images/uk_circle_transparent.png +0 -0
  198. metadata +415 -0
@@ -0,0 +1,149 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = [
5
+ "dataType",
6
+ "length",
7
+ "lengthSuggestion",
8
+ "decimalPrecision", // New target for decimal precision field
9
+ "decimalPrecisionContainer", // Container to show/hide
10
+ "columnNameInput",
11
+ "destroyInput",
12
+ "columnFields",
13
+ "removeButton"
14
+ ]
15
+
16
+ // Flag to track if length has been manually set
17
+ manualLengthSet = false
18
+
19
+ // Types that need decimal precision
20
+ decimalTypes = ['real', 'double precision', 'numeric', 'decimal']
21
+
22
+ // PostgreSQL default lengths for specific data types
23
+ dataTypeLengths = {
24
+ 'smallint': { default: 16, description: 'Suggested: 16-bit integer' },
25
+ 'integer': { default: 32, description: 'Suggested: 32-bit integer' },
26
+ 'bigint': { default: 64, description: 'Suggested: 64-bit integer' },
27
+ 'numeric': { default: 10, description: 'Suggested: Default precision' },
28
+ 'real': { default: 32, description: 'Suggested: 32-bit floating point' },
29
+ 'double precision': { default: 64, description: 'Suggested: 64-bit floating point' },
30
+ 'varchar': { default: 255, description: 'Suggested: Max length 255' },
31
+ 'character': { default: 1, description: 'Suggested: Single character' },
32
+ 'char': { default: 1, description: 'Suggested: Single character' },
33
+ 'timestamp': { default: 6, description: 'Suggested: Timestamp precision' },
34
+ 'timestamptz': { default: 6, description: 'Suggested: Timestamp with timezone precision' },
35
+ 'time': { default: 6, description: 'Suggested: Time precision' },
36
+ 'timetz': { default: 6, description: 'Suggested: Time with timezone precision' }
37
+ }
38
+
39
+ // Default decimal precisions
40
+ defaultDecimalPrecisions = {
41
+ 'real': 6,
42
+ 'double precision': 15,
43
+ 'numeric': 2,
44
+ 'decimal': 2
45
+ }
46
+
47
+ connect() {
48
+ // Only set the default length if this is a new field or the length is empty
49
+ if (!this.lengthTarget.value) {
50
+ this.updateLength()
51
+ }
52
+
53
+ // Add event listener for data type changes
54
+ this.dataTypeTarget.addEventListener('change', this.handleDataTypeChange.bind(this))
55
+
56
+ // Add event listener for manual length changes
57
+ this.lengthTarget.addEventListener('input', this.handleManualLengthChange.bind(this))
58
+
59
+ // Initialize decimal precision field visibility
60
+ this.toggleDecimalPrecisionField()
61
+ }
62
+
63
+ disconnect() {
64
+ // Remove event listeners when controller disconnects
65
+ this.dataTypeTarget.removeEventListener('change', this.handleDataTypeChange.bind(this))
66
+ this.lengthTarget.removeEventListener('input', this.handleManualLengthChange.bind(this))
67
+ }
68
+
69
+ handleDataTypeChange() {
70
+ // Only update length automatically if it hasn't been manually set
71
+ // or if the field is empty
72
+ if (!this.manualLengthSet || !this.lengthTarget.value) {
73
+ this.updateLength()
74
+ } else {
75
+ // Just update the suggestion text based on the data type
76
+ this.updateSuggestionOnly()
77
+ }
78
+
79
+ // Toggle decimal precision field visibility
80
+ this.toggleDecimalPrecisionField()
81
+ }
82
+
83
+ handleManualLengthChange() {
84
+ // If user typed something, mark as manually set
85
+ if (this.lengthTarget.value !== '') {
86
+ this.manualLengthSet = true
87
+ this.clearSuggestion()
88
+ } else {
89
+ // If field was cleared, reset the flag
90
+ this.manualLengthSet = false
91
+ }
92
+ }
93
+
94
+ updateLength() {
95
+ const selectedType = this.dataTypeTarget.value
96
+
97
+ // Reset suggested length text
98
+ this.lengthSuggestionTarget.textContent = ''
99
+
100
+ // If the selected type has a default length
101
+ if (this.dataTypeLengths.hasOwnProperty(selectedType)) {
102
+ // Set a default value and show suggested length
103
+ this.lengthTarget.value = this.dataTypeLengths[selectedType].default
104
+ this.lengthSuggestionTarget.textContent = this.dataTypeLengths[selectedType].description
105
+ } else {
106
+ // Clear the value for types without specific length
107
+ this.lengthTarget.value = ''
108
+ }
109
+
110
+ // Reset the manual flag when automatically setting length
111
+ this.manualLengthSet = false
112
+ }
113
+
114
+ updateSuggestionOnly() {
115
+ const selectedType = this.dataTypeTarget.value
116
+
117
+ // Only update the suggestion text, not the value
118
+ if (this.dataTypeLengths.hasOwnProperty(selectedType)) {
119
+ this.lengthSuggestionTarget.textContent = this.dataTypeLengths[selectedType].description
120
+ } else {
121
+ this.lengthSuggestionTarget.textContent = ''
122
+ }
123
+ }
124
+
125
+ clearSuggestion() {
126
+ this.lengthSuggestionTarget.textContent = ''
127
+ }
128
+
129
+ toggleDecimalPrecisionField() {
130
+ const selectedType = this.dataTypeTarget.value
131
+
132
+ // Check if this data type needs decimal precision
133
+ if (this.decimalTypes.includes(selectedType)) {
134
+ // Show the decimal precision field
135
+ this.decimalPrecisionContainerTarget.classList.remove('d-none')
136
+
137
+ // Set default value if empty
138
+ if (!this.decimalPrecisionTarget.value && this.defaultDecimalPrecisions[selectedType]) {
139
+ this.decimalPrecisionTarget.value = this.defaultDecimalPrecisions[selectedType]
140
+ }
141
+ } else {
142
+ // Hide the decimal precision field
143
+ this.decimalPrecisionContainerTarget.classList.add('d-none')
144
+
145
+ // Clear the value when not needed
146
+ this.decimalPrecisionTarget.value = ''
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,362 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Stimulus Controller for managing database columns in a table design interface
5
+ * Handles column addition, removal, foreign key relationships, dependency checks,
6
+ * and dynamic UI updates for column management
7
+ */
8
+ export default class extends Controller {
9
+ static targets = ["columns", "destroyField", "searchInput"]
10
+
11
+ get adminPath() {
12
+ const match = window.location.pathname.match(/(.+?)\/admin/);
13
+ return match ? `${match[1]}/admin` : "/admin";
14
+ }
15
+
16
+ /**
17
+ * Lifecycle method called when the controller connects to the DOM
18
+ * Initializes existing foreign key relationships and checks dependencies
19
+ */
20
+ connect() {
21
+ // Initialize any existing foreign key checkboxes - show foreign key fields if checked
22
+ document.querySelectorAll('.foreign-key-checkbox').forEach(checkbox => {
23
+ if (checkbox.checked) {
24
+ this.showForeignKeyFields(checkbox);
25
+ }
26
+ });
27
+
28
+ // For existing foreign tables, load their columns
29
+ this.element.querySelectorAll('.foreign-table-dropdown').forEach(select => {
30
+ const selectedTable = select.dataset.columnExistingForeignTable;
31
+ if (selectedTable) {
32
+ this.loadColumns({ target: select }, true);
33
+ }
34
+ });
35
+
36
+ // Check column dependencies on page load to disable removal of referenced columns
37
+ this.checkAllColumnDependencies();
38
+ }
39
+
40
+ /**
41
+ * Filters the column list based on search input
42
+ * Shows/hides columns based on whether they match the search text
43
+ *
44
+ * @param {Event} event - Input event from the search field
45
+ */
46
+ filterColumns(event) {
47
+ const searchText = event.target.value.toLowerCase().trim();
48
+
49
+ this.columnsTarget.querySelectorAll('.column-fields').forEach(columnField => {
50
+ const columnNameInput = columnField.querySelector('[name*="[column_name]"]');
51
+ const columnName = columnNameInput ? columnNameInput.value.toLowerCase() : '';
52
+
53
+ if (searchText === '' || columnName.includes(searchText)) {
54
+ // Show the column if it matches or search is empty
55
+ columnField.style.display = '';
56
+ } else {
57
+ // Hide the column if it doesn't match the search
58
+ columnField.style.display = 'none';
59
+ }
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Checks dependencies for all columns in the table
65
+ * Disables remove buttons for columns that are referenced as foreign keys in other tables
66
+ */
67
+ checkAllColumnDependencies() {
68
+ document.querySelectorAll('.column-fields').forEach(columnField => {
69
+ const columnInput = columnField.querySelector('[name*="[column_name]"]');
70
+ const tableInput = document.querySelector('[name="db_design_dynamic_table[id]"]');
71
+ const removeButton = columnField.querySelector('button[data-action="column#remove"]');
72
+
73
+ const columnName = columnInput ? columnInput.value.trim() : "";
74
+ const tableId = tableInput ? tableInput.value : null;
75
+
76
+ if (!columnName || !tableId) {
77
+ return;
78
+ }
79
+
80
+ this.checkColumnDependency(columnName, tableId, removeButton);
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Checks if a specific column is referenced by foreign keys in other tables
86
+ * Disables the remove button if the column is referenced
87
+ *
88
+ * @param {string} columnName - Name of the column to check
89
+ * @param {string|number} tableId - ID of the table containing the column
90
+ * @param {HTMLElement} removeButton - Button element for removing the column
91
+ */
92
+ checkColumnDependency(columnName, tableId, removeButton) {
93
+ fetch(`${this.adminPath}/db_design_dynamic_tables/check_column_dependency?column_name=${columnName}&table_id=${tableId}`)
94
+ .then(response => response.json())
95
+ .then(data => {
96
+ if (data.is_referenced) {
97
+ removeButton.disabled = true;
98
+ removeButton.classList.replace('btn-danger', 'btn-secondary');
99
+ removeButton.setAttribute('title', `Cannot delete column '${columnName}' because it is referenced as a foreign key in another table.`);
100
+ }
101
+ })
102
+ .catch(error => {
103
+ console.error("Error checking column dependencies:", error);
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Adds a new column to the table by fetching the column template HTML
109
+ * Inserts the new column at the beginning of the columns container
110
+ *
111
+ * @param {Event} event - Click event from the add column button
112
+ */
113
+ add(event) {
114
+ event.preventDefault();
115
+
116
+ // Get current locale from URL or document
117
+ const currentLocale = new URLSearchParams(window.location.search).get('locale') ||
118
+ document.documentElement.lang || 'en';
119
+
120
+ // Add locale to the fetch URL
121
+ fetch(`${this.adminPath}/db_design_dynamic_tables/render_column_fields?locale=${currentLocale}`)
122
+ .then(response => response.text())
123
+ .then(html => {
124
+ // Insert the new column at the beginning of the columns container
125
+ this.columnsTarget.insertAdjacentHTML('afterbegin', html);
126
+
127
+ // Find the newly added column
128
+ const newColumn = this.columnsTarget.firstElementChild;
129
+
130
+ // For the new column, we want to expand it on initial add
131
+ // All existing columns remain collapsed
132
+ if (typeof bootstrap !== 'undefined' && bootstrap.Collapse) {
133
+ // Find the collapse element in the new column
134
+ const collapseElement = newColumn.querySelector('.accordion-collapse');
135
+ if (collapseElement) {
136
+ // Initialize and show the new column's accordion
137
+ const bsCollapse = new bootstrap.Collapse(collapseElement, {
138
+ toggle: false
139
+ });
140
+ bsCollapse.show();
141
+
142
+ // Update the button state
143
+ const accordionButton = newColumn.querySelector('.accordion-button');
144
+ if (accordionButton) {
145
+ accordionButton.classList.remove('collapsed');
146
+ accordionButton.setAttribute('aria-expanded', 'true');
147
+ }
148
+ }
149
+ }
150
+
151
+ // Scroll to the new column
152
+ newColumn.scrollIntoView({ behavior: 'smooth' });
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Updates the accordion title with column metadata when column properties change
158
+ * Displays column name, data type, length, and badges for PK, FK, and NOT NULL constraints
159
+ *
160
+ * @param {Event} event - Change event from column property fields
161
+ */
162
+ updateAccordionTitle(event) {
163
+ const columnField = event.target.closest('.column-fields');
164
+ const accordionButton = columnField.querySelector('.accordion-button');
165
+
166
+ // Get values for the accordion header
167
+ const nameInput = columnField.querySelector('[name*="[column_name]"]');
168
+ const dataTypeSelect = columnField.querySelector('[name*="[data_type]"]');
169
+ const lengthInput = columnField.querySelector('[name*="[length]"]');
170
+ const isPrimaryKey = columnField.querySelector('[name*="[is_primary_key]"]').checked;
171
+ const isForeignKey = columnField.querySelector('[name*="[is_foreign_key]"]').checked;
172
+ const isNotNull = columnField.querySelector('[name*="[not_null]"]').checked;
173
+
174
+ // Build the header content
175
+ let headerContent = '';
176
+ if (nameInput.value.trim()) {
177
+ headerContent = `<strong>${nameInput.value.trim()}</strong>`;
178
+
179
+ if (dataTypeSelect.value) {
180
+ headerContent += `<span class="ms-2 text-muted">(${dataTypeSelect.value}`;
181
+ if (lengthInput.value && ['varchar', 'char', 'character'].includes(dataTypeSelect.value)) {
182
+ headerContent += `(${lengthInput.value})`;
183
+ }
184
+ headerContent += ')</span>';
185
+ }
186
+
187
+ // Add badges for key constraints
188
+ if (isPrimaryKey) headerContent += '<span class="badge bg-primary ms-2">PK</span>';
189
+ if (isForeignKey) headerContent += '<span class="badge bg-info ms-2">FK</span>';
190
+ if (isNotNull) headerContent += '<span class="badge bg-warning ms-2">NOT NULL</span>';
191
+ } else {
192
+ headerContent = '<em>New Column</em>';
193
+ }
194
+
195
+ accordionButton.innerHTML = headerContent;
196
+ }
197
+
198
+ /**
199
+ * Handles column removal after checking for dependencies
200
+ * Prevents removal of columns that are referenced by foreign keys in other tables
201
+ *
202
+ * @param {Event} event - Click event from the remove column button
203
+ */
204
+ remove(event) {
205
+ event.preventDefault();
206
+ const columnField = event.target.closest('.column-fields');
207
+
208
+ // Get table ID from data attribute
209
+ const tableId = columnField.dataset.tableId;
210
+
211
+ // Get column name
212
+ const columnInput = columnField.querySelector('[name*="[column_name]"]');
213
+ const columnName = columnInput ? columnInput.value.trim() : "";
214
+ if (!columnName || !tableId) {
215
+ console.error("Missing column name or table ID");
216
+ this.markForRemoval(columnField, true);
217
+ return;
218
+ }
219
+
220
+ // Check if the column can be safely removed
221
+ fetch(`${this.adminPath}/db_design_dynamic_tables/check_column_dependency?column_name=${columnName}&table_id=${tableId}`)
222
+ .then(response => response.json())
223
+ .then(data => {
224
+ if (data.is_referenced) {
225
+ Swal.fire({
226
+ icon: "error",
227
+ title: "Cannot Delete",
228
+ text: `Column '${columnName}' is referenced as a foreign key in another table.`,
229
+ confirmButtonColor: "#364380"
230
+ });
231
+ } else {
232
+ this.markForRemoval(columnField, true);
233
+ }
234
+ })
235
+ .catch(error => {
236
+ console.error("Error checking column dependencies:", error);
237
+ Swal.fire({
238
+ icon: "warning",
239
+ title: "Dependency Check Failed",
240
+ text: "Error checking column dependencies. Please try again.",
241
+ confirmButtonColor: "#364380"
242
+ });
243
+ });
244
+ }
245
+
246
+ /**
247
+ * Marks a column for removal by setting the _destroy field to 1
248
+ * For existing columns, this flags them for deletion on form submission
249
+ * For new columns, they are immediately removed from the DOM
250
+ *
251
+ * @param {HTMLElement} columnField - The column container element
252
+ * @param {boolean} allowDeletion - Whether deletion is allowed
253
+ */
254
+ markForRemoval(columnField, allowDeletion) {
255
+ if (!allowDeletion) return;
256
+
257
+ const destroyInput = columnField.querySelector('[data-column-target="destroyField"]');
258
+ if (destroyInput) {
259
+ // For existing columns, set _destroy=1 to mark for deletion and hide the column
260
+ destroyInput.value = "1";
261
+ columnField.style.display = "none";
262
+ } else {
263
+ // For new columns that haven't been saved yet, just remove from DOM
264
+ columnField.remove();
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Toggles the visibility of foreign key fields based on checkbox state
270
+ *
271
+ * @param {Event} event - Change event from the foreign key checkbox
272
+ */
273
+ toggleForeignKeyFields(event) {
274
+ const checkbox = event.target;
275
+ if (checkbox.checked) {
276
+ this.showForeignKeyFields(checkbox);
277
+ } else {
278
+ this.hideForeignKeyFields(checkbox);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Shows the foreign key fields when foreign key checkbox is checked
284
+ *
285
+ * @param {HTMLElement} checkbox - The foreign key checkbox element
286
+ */
287
+ showForeignKeyFields(checkbox) {
288
+ const columnField = checkbox.closest('.column-fields');
289
+ const foreignKeyFields = columnField.querySelector('.foreign-key-fields');
290
+ if (foreignKeyFields) {
291
+ foreignKeyFields.classList.remove('hidden'); // Remove hidden class
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Hides the foreign key fields when foreign key checkbox is unchecked
297
+ *
298
+ * @param {HTMLElement} checkbox - The foreign key checkbox element
299
+ */
300
+ hideForeignKeyFields(checkbox) {
301
+ const columnField = checkbox.closest('.column-fields');
302
+ const foreignKeyFields = columnField.querySelector('.foreign-key-fields');
303
+ if (foreignKeyFields) {
304
+ foreignKeyFields.classList.add('hidden'); // Add hidden class
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Loads available columns from a selected referenced table for foreign key selection
310
+ * Populates the column dropdown with columns from the selected table
311
+ *
312
+ * @param {Event} event - Change event from the table dropdown
313
+ * @param {boolean} isInitialLoad - Whether this is the initial load (to select existing values)
314
+ */
315
+ loadColumns(event, isInitialLoad = false) {
316
+ const tableSelect = event.target;
317
+ const columnField = tableSelect.closest('.column-fields');
318
+ const columnSelect = columnField.querySelector('.foreign-column-dropdown');
319
+ const tableName = tableSelect.value; // Using table name instead of ID
320
+ const existingColumn = tableSelect.dataset.columnExistingForeignColumn;
321
+
322
+ // Get the select-search controller for the column dropdown
323
+ const selectSearchController = window.Stimulus.getControllerForElementAndIdentifier(
324
+ columnSelect.closest('[data-controller="select-search"]'),
325
+ 'select-search'
326
+ );
327
+
328
+ if (tableName) {
329
+ // Fetch columns for the selected table
330
+ fetch(`${this.adminPath}/db_design_dynamic_tables/columns?table_name=${encodeURIComponent(tableName)}`)
331
+ .then(response => response.json())
332
+ .then(columns => {
333
+ // Reset and populate the column dropdown
334
+ columnSelect.innerHTML = '<option value="">Select Column</option>';
335
+ columns.forEach(column => {
336
+ const option = document.createElement('option');
337
+ option.value = column.column_name;
338
+ option.textContent = column.column_name;
339
+ // Select the existing column if initializing
340
+ if (isInitialLoad && column.column_name === existingColumn) {
341
+ option.selected = true;
342
+ }
343
+ columnSelect.appendChild(option);
344
+ });
345
+
346
+ // Refresh the select-search to capture the new options
347
+ if (selectSearchController) {
348
+ selectSearchController.refreshOptions();
349
+ }
350
+ })
351
+ .catch(error => console.error("Error fetching columns:", error));
352
+ } else {
353
+ // Reset column dropdown if no table selected
354
+ columnSelect.innerHTML = '<option value="">Select Column</option>';
355
+
356
+ // Refresh the select-search
357
+ if (selectSearchController) {
358
+ selectSearchController.refreshOptions();
359
+ }
360
+ }
361
+ }
362
+ }
@@ -0,0 +1,42 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ // Store all table rows for quick access
6
+ this.tableRows = document.querySelectorAll('table tbody tr');
7
+ }
8
+
9
+ filter() {
10
+ const query = this.element.value.toLowerCase().trim();
11
+
12
+ // If search is empty, show all rows
13
+ if (!query) {
14
+ this.tableRows.forEach(row => {
15
+ row.style.display = '';
16
+ });
17
+ return;
18
+ }
19
+
20
+ // Filter rows based on search query
21
+ this.tableRows.forEach(row => {
22
+ const columnName = row.querySelector('td:nth-child(2)').textContent.toLowerCase();
23
+ const physicalName = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
24
+ const dataType = row.querySelector('td:nth-child(4)').textContent.toLowerCase();
25
+ const constraints = row.querySelector('td:nth-child(5)').textContent.toLowerCase();
26
+ const defaultValue = row.querySelector('td:nth-child(6)').textContent.toLowerCase();
27
+ const description = row.querySelector('td:nth-child(7)').textContent.toLowerCase();
28
+
29
+ // Show row if any field contains the query
30
+ if (columnName.includes(query) ||
31
+ physicalName.includes(query) ||
32
+ dataType.includes(query) ||
33
+ constraints.includes(query) ||
34
+ defaultValue.includes(query) ||
35
+ description.includes(query)) {
36
+ row.style.display = '';
37
+ } else {
38
+ row.style.display = 'none';
39
+ }
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,42 @@
1
+ // app/javascript/controllers/dbdoc_accordion_controller.js
2
+ import { Controller } from "@hotwired/stimulus"
3
+
4
+ export default class extends Controller {
5
+ static targets = ["button", "content"]
6
+
7
+ connect() {
8
+ this.ensureClosedState()
9
+ }
10
+
11
+ ensureClosedState() {
12
+ // Always start closed
13
+ this.contentTarget.classList.remove("show")
14
+ this.buttonTarget.setAttribute("aria-expanded", "false")
15
+ this.buttonTarget.classList.add("collapsed")
16
+ }
17
+
18
+ toggle(event) {
19
+ event.preventDefault()
20
+ event.stopPropagation()
21
+
22
+ const isCurrentlyOpen = this.contentTarget.classList.contains("show")
23
+
24
+ if (isCurrentlyOpen) {
25
+ this.close()
26
+ } else {
27
+ this.open()
28
+ }
29
+ }
30
+
31
+ open() {
32
+ this.contentTarget.classList.add("show")
33
+ this.buttonTarget.setAttribute("aria-expanded", "true")
34
+ this.buttonTarget.classList.remove("collapsed")
35
+ }
36
+
37
+ close() {
38
+ this.contentTarget.classList.remove("show")
39
+ this.buttonTarget.setAttribute("aria-expanded", "false")
40
+ this.buttonTarget.classList.add("collapsed")
41
+ }
42
+ }
@@ -0,0 +1,73 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+ static targets = ['groupContent', 'viewButton', 'tableGroups'];
5
+
6
+ connect() {
7
+ // Initialize clickable rows when the controller connects
8
+ this.initializeClickableRows();
9
+ }
10
+
11
+ /**
12
+ * Toggles the visibility of the group content when the view button is clicked.
13
+ * @param {Event} event - The click event triggered by the user.
14
+ */
15
+ toggleGroupContent(event) {
16
+ // Get the button that was clicked
17
+ const viewButton = event.currentTarget;
18
+
19
+ // Find the closest group section to the clicked button
20
+ const groupSection = viewButton.closest('.group-section');
21
+
22
+ // Find the associated group content using the Stimulus target
23
+ const groupContent = groupSection.querySelector('[data-ecommerce-target="groupContent"]');
24
+
25
+ // Toggle the 'active' class on the view button
26
+ viewButton.classList.toggle('active');
27
+
28
+ // Toggle the visibility of the group content based on the button state
29
+ if (viewButton.classList.contains('active')) {
30
+ groupContent.classList.add('active');
31
+ } else {
32
+ groupContent.classList.remove('active');
33
+ }
34
+
35
+ // Prevent the event from bubbling up to the header
36
+ event.stopPropagation();
37
+ }
38
+
39
+ /**
40
+ * Toggles the group content when the header is clicked
41
+ * @param {Event} event - The click event triggered by the user.
42
+ */
43
+ toggleGroupContentHeader(event) {
44
+ // Find the closest group section to the clicked header
45
+ const groupSection = event.currentTarget.closest('.group-section');
46
+
47
+ // Find the view button and group content
48
+ const viewButton = groupSection.querySelector('[data-ecommerce-target="viewButton"]');
49
+ const groupContent = groupSection.querySelector('[data-ecommerce-target="groupContent"]');
50
+
51
+ // Toggle the 'active' class on the view button
52
+ viewButton.classList.toggle('active');
53
+
54
+ // Toggle the visibility of the group content based on the button state
55
+ if (viewButton.classList.contains('active')) {
56
+ groupContent.classList.add('active');
57
+ } else {
58
+ groupContent.classList.remove('active');
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Initialize clickable table rows
64
+ */
65
+ initializeClickableRows() {
66
+ document.addEventListener('click', (event) => {
67
+ const row = event.target.closest('.group-table-row');
68
+ if (row && row.dataset.tableLink && !event.target.closest('.table-link')) {
69
+ window.location.href = row.dataset.tableLink;
70
+ }
71
+ });
72
+ }
73
+ }