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,245 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Table Details Controller
5
+ * Handles the interaction for the table details page including field toggling,
6
+ * navigation between tables and groups, and type description tooltips.
7
+ */
8
+ export default class extends Controller {
9
+ // Define controller targets
10
+ static targets = ['fieldsToggle', 'fieldsContent', 'infoIcon'];
11
+
12
+ connect() {
13
+ // Initialize tooltips if Bootstrap is available
14
+ if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
15
+ this.initTooltips();
16
+ }
17
+
18
+ // Add click event listener to document to close tooltips
19
+ document.addEventListener('click', this.handleDocumentClick.bind(this));
20
+
21
+ this.setConstraintTooltips();
22
+ if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
23
+ this.initTooltips();
24
+ this.initConstraintBadgeTooltips();
25
+ }
26
+ document.addEventListener('click', this.handleDocumentClick.bind(this));
27
+ }
28
+
29
+ initConstraintBadgeTooltips() {
30
+ document.querySelectorAll('.constraint-badge').forEach(badge => {
31
+ new bootstrap.Tooltip(badge);
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Toggles the visibility of the fields content section
37
+ * @param {Event} event - The click event from the toggle button
38
+ */
39
+ toggleFields(event) {
40
+ const viewButton = event.currentTarget;
41
+ // Find the closest .group-section (or .main-content-body if that's your outer div)
42
+ const groupSection = viewButton.closest('.main-content-body, .group-section');
43
+ if (!groupSection) return;
44
+
45
+ // Find the .group-content inside this section
46
+ const fieldsContent = groupSection.querySelector('.group-content');
47
+ if (!fieldsContent) return;
48
+
49
+ // Toggle active class on view button
50
+ viewButton.classList.toggle('active');
51
+
52
+ // Toggle visibility of fields content based on button state
53
+ if (viewButton.classList.contains('active')) {
54
+ fieldsContent.classList.add('active');
55
+ } else {
56
+ fieldsContent.classList.remove('active');
57
+ }
58
+
59
+ event.stopPropagation();
60
+ }
61
+
62
+ /**
63
+ * Toggles the fields content when the header is clicked
64
+ * @param {Event} event - The click event triggered by the user.
65
+ */
66
+ toggleFieldsHeader(event) {
67
+ // Find the header that was clicked
68
+ const header = event.currentTarget;
69
+ // Find the closest section (div with .main-content-body or .group-section)
70
+ const section = header.closest('.main-content-body, .group-section');
71
+ if (!section) return;
72
+
73
+ // Find the view button and group-content inside this section
74
+ const viewButton = section.querySelector('.view-button');
75
+ const fieldsContent = section.querySelector('.group-content');
76
+ if (!viewButton || !fieldsContent) return;
77
+
78
+ // Toggle active class on view button
79
+ viewButton.classList.toggle('active');
80
+ // Toggle content
81
+ if (viewButton.classList.contains('active')) {
82
+ fieldsContent.classList.add('active');
83
+ } else {
84
+ fieldsContent.classList.remove('active');
85
+ }
86
+ }
87
+
88
+ // Initialize tooltips for info icons
89
+ initTooltips() {
90
+ this.infoIconTargets.forEach(icon => {
91
+ const type = icon.dataset.type;
92
+ const description = this.getTypeDescription(type);
93
+
94
+ const tooltipContent = `
95
+ <div class="type-tooltip-content">
96
+ <div class="type-tooltip-heading">${type}</div>
97
+ <div class="type-tooltip-desc">${description.en}</div>
98
+ <div class="type-tooltip-desc">${description.ja}</div>
99
+ </div>
100
+ `;
101
+
102
+ icon.setAttribute('title', tooltipContent);
103
+
104
+ new bootstrap.Tooltip(icon, {
105
+ html: true,
106
+ trigger: 'manual',
107
+ placement: 'right',
108
+ customClass: 'custom-tooltip', // Bootstrap 5.2+ supports this
109
+ template: `
110
+ <div class="tooltip custom-tooltip" role="tooltip">
111
+ <div class="tooltip-arrow"></div>
112
+ <div class="tooltip-inner"></div>
113
+ </div>
114
+ `
115
+ });
116
+ });
117
+ }
118
+
119
+ // Toggle tooltip on info icon click
120
+ toggleTooltip(event) {
121
+ const icon = event.currentTarget;
122
+ const tooltip = bootstrap.Tooltip.getInstance(icon);
123
+ if (!tooltip) return;
124
+
125
+ // Hide all other tooltips
126
+ this.infoIconTargets.forEach(otherIcon => {
127
+ if (otherIcon !== icon) {
128
+ const otherTooltip = bootstrap.Tooltip.getInstance(otherIcon);
129
+ if (otherTooltip) otherTooltip.hide();
130
+ }
131
+ });
132
+
133
+ tooltip.toggle();
134
+ event.stopPropagation();
135
+ }
136
+
137
+ // Handle document clicks to close tooltips
138
+ handleDocumentClick(event) {
139
+ if (!event.target.classList.contains('info-icon')) {
140
+ this.infoIconTargets.forEach(icon => {
141
+ const tooltip = bootstrap.Tooltip.getInstance(icon);
142
+ if (tooltip) tooltip.hide();
143
+ });
144
+ }
145
+ }
146
+
147
+ // Get type description from existing data
148
+ getTypeDescription(type) {
149
+ const typeDescriptions = {
150
+ integer: { en: 'A whole number (e.g., 1, 2, 3).', ja: '整数(例:1、2、3)。' },
151
+ smallint: { en: 'A smaller range of whole numbers.', ja: '小さな範囲の整数。' },
152
+ bigint: { en: 'A larger range of whole numbers.', ja: '大きな範囲の整数。' },
153
+ numeric: { en: 'A number with precision and scale.', ja: '精度とスケールを持つ数値。' },
154
+ real: { en: 'A single-precision floating-point number.', ja: '単精度浮動小数点数。' },
155
+ double_precision: { en: 'A double-precision floating-point number.', ja: '倍精度浮動小数点数。' },
156
+ varchar: { en: 'A variable-length string of characters.', ja: '可変長の文字列。' },
157
+ character: { en: 'A fixed-length string of characters.', ja: '固定長の文字列。' },
158
+ text: { en: 'A long string of characters.', ja: '長い文字列。' },
159
+ char: { en: 'A single character.', ja: '単一の文字。' },
160
+ boolean: { en: 'A true or false value.', ja: '真または偽の値。' },
161
+ date: { en: 'A calendar date (e.g., YYYY-MM-DD).', ja: 'カレンダーの日付(例:YYYY-MM-DD)。' },
162
+ timestamp: { en: 'A precise point in time (e.g., YYYY-MM-DD HH:mm:ss).', ja: '正確な時点(例:YYYY-MM-DD HH:mm:ss)。' },
163
+ timestamptz: { en: 'A timestamp with time zone information.', ja: 'タイムゾーン情報を含むタイムスタンプ。' },
164
+ time: { en: 'A time without date information.', ja: '日付情報を含まない時間。' },
165
+ timetz: { en: 'A time with time zone information.', ja: 'タイムゾーン情報を含む時間。' },
166
+ interval: { en: 'A duration of time.', ja: '時間の期間。' },
167
+ uuid: { en: 'A universally unique identifier.', ja: 'ユニバーサル一意識別子。' }
168
+ };
169
+
170
+ return typeDescriptions[type] || { en: 'Description not available.', ja: '説明がありません。' };
171
+ }
172
+
173
+ /**
174
+ * Navigates back to the main ecommerce page
175
+ * Used for breadcrumb navigation
176
+ */
177
+ navigateToEcommerce() {
178
+ window.location.href = '/dbdoc/';
179
+ }
180
+
181
+ /**
182
+ * Navigates to a specific group details page
183
+ * @param {Event} event - The click event containing the group ID
184
+ */
185
+ navigateToGroup(event) {
186
+ const groupId = event.currentTarget.dataset.groupId;
187
+ if (groupId) {
188
+ window.location.href = '/dbdoc/group_details/' + groupId;
189
+ }
190
+ }
191
+
192
+ // Clean up when controller disconnects
193
+ disconnect() {
194
+ document.removeEventListener('click', this.handleDocumentClick.bind(this));
195
+
196
+ if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
197
+ this.infoIconTargets.forEach(icon => {
198
+ const tooltip = bootstrap.Tooltip.getInstance(icon);
199
+ if (tooltip) tooltip.dispose();
200
+ });
201
+ }
202
+ }
203
+ setConstraintTooltips() {
204
+ // Simple, short explanations (EN/JA)
205
+ const badgeTexts = {
206
+ 'NOT_NULL': {
207
+ en: "This column cannot be empty.",
208
+ ja: "このカラムは必ず値が必要です。"
209
+ },
210
+ 'FK': {
211
+ en: "Links to another table (foreign key).",
212
+ ja: "他のテーブルへの参照(外部キー)。"
213
+ },
214
+ 'PK': {
215
+ en: "Uniquely identifies each row (primary key).",
216
+ ja: "各行を一意に識別する主キー。"
217
+ },
218
+ 'CANDIDATE': {
219
+ en: "Can uniquely identify rows (candidate key).",
220
+ ja: "行を一意に識別できる候補キー。"
221
+ },
222
+ 'INDEXED': {
223
+ en: "Speeds up searches (indexed column).",
224
+ ja: "検索を高速化するインデックス付きカラム。"
225
+ }
226
+ };
227
+ // Detect current locale
228
+ let locale = document.documentElement.lang || document.body.dataset.locale || 'en';
229
+ document.querySelectorAll('.constraint-badge').forEach(badge => {
230
+ const key = badge.getAttribute('data-badge');
231
+ if (badgeTexts[key]) {
232
+ badge.setAttribute('data-tooltip',
233
+ badgeTexts[key].en + "\n" + badgeTexts[key].ja
234
+ );
235
+ }
236
+ // Remove any previous inner arrow
237
+ const prev = badge.querySelector('.tooltip-arrow-inner');
238
+ if (prev) prev.remove();
239
+ // Add inner arrow element
240
+ const arrow = document.createElement('span');
241
+ arrow.className = 'tooltip-arrow-inner';
242
+ badge.appendChild(arrow);
243
+ });
244
+ }
245
+ }
@@ -0,0 +1,148 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["groupName", "groupColor", "createdBy", "updatedBy"]
5
+
6
+ // Optional: array to store existing group names for uniqueness check
7
+ existingGroupNames = []
8
+
9
+ connect() {
10
+ // Fetch existing group names when the controller connects
11
+ // this.fetchExistingGroupNames()
12
+
13
+ // If creating a new record (both targets exist), set up the event listener to copy values
14
+ if (this.hasCreatedByTarget && this.hasUpdatedByTarget) {
15
+ this.createdByTarget.addEventListener('input', this.copyCreatedByToUpdatedBy.bind(this))
16
+ }
17
+ }
18
+
19
+ // Copy the created_by value to updated_by field
20
+ copyCreatedByToUpdatedBy() {
21
+ this.updatedByTarget.value = this.createdByTarget.value
22
+ }
23
+
24
+ // Fetch existing group names from the server
25
+ async fetchExistingGroupNames() {
26
+ try {
27
+ const tableGroupId = this.element.dataset.tableGroupId || ""; // Get the group ID (empty if creating)
28
+ const response = await fetch(`/admin/db_design_table_groups/existing_names?id=${tableGroupId}`);
29
+
30
+ if (response.ok) {
31
+ this.existingGroupNames = await response.json();
32
+ }
33
+ } catch (error) {
34
+ console.error("Error fetching existing group names:", error);
35
+ }
36
+ }
37
+
38
+ async validate(event) {
39
+ event.preventDefault(); // Prevent submission unless valid
40
+
41
+ let isValid = true;
42
+ this.clearErrors(); // Reset previous errors
43
+
44
+ // Ensure we have the latest existing group names before validation
45
+ await this.fetchExistingGroupNames();
46
+
47
+ // Validate group name
48
+ const groupName = this.groupNameTarget.value.trim();
49
+ if (!groupName) {
50
+ this.showError(this.groupNameTarget, "group_name_required");
51
+ isValid = false;
52
+ } else if (this.isGroupNameTaken(groupName)) {
53
+ this.showError(this.groupNameTarget, "group_name_unique");
54
+ isValid = false;
55
+ }
56
+
57
+ // For new records: validate created_by and automatically copy to updated_by
58
+ if (this.hasCreatedByTarget) {
59
+ const createdBy = this.createdByTarget.value.trim();
60
+ if (!createdBy) {
61
+ this.showError(this.createdByTarget, "created_by_required");
62
+ isValid = false;
63
+ } else if (this.hasUpdatedByTarget) {
64
+ // Ensure updated_by has the same value as created_by before submitting
65
+ this.updatedByTarget.value = createdBy;
66
+ }
67
+ }
68
+
69
+ // For editing existing records: validate updated_by
70
+ if (this.hasUpdatedByTarget && !this.hasCreatedByTarget) {
71
+ const updatedBy = this.updatedByTarget.value.trim();
72
+ if (!updatedBy) {
73
+ this.showError(this.updatedByTarget, "updated_by_required");
74
+ isValid = false;
75
+ }
76
+ }
77
+
78
+ // Validate group color
79
+ const groupColor = this.groupColorTarget.value.trim();
80
+ if (!groupColor) {
81
+ this.showError(this.groupColorTarget, "group_color_required");
82
+ isValid = false;
83
+ }
84
+
85
+ if (isValid) {
86
+ event.target.submit(); // Only submit if valid
87
+ }
88
+ }
89
+
90
+ // Check if group name already exists
91
+ isGroupNameTaken(name) {
92
+ // Get the current table group ID (if editing)
93
+ const currentId = this.element.dataset.tableGroupId;
94
+
95
+ // If we're editing an existing record and the name hasn't changed,
96
+ // it's not considered "taken"
97
+ if (currentId) {
98
+ const currentName = this.element.dataset.originalGroupName;
99
+ if (currentName && currentName.toLowerCase() === name.toLowerCase()) {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ return this.existingGroupNames.some(
105
+ existingName => existingName.toLowerCase() === name.toLowerCase()
106
+ );
107
+ }
108
+
109
+ showError(element, messageKey) {
110
+ if (!element) return;
111
+
112
+ // Add 'is-invalid' class to input field
113
+ element.classList.add('is-invalid');
114
+
115
+ // Get translated message from the hidden translations div
116
+ const translatedMessage = this.getTranslation(messageKey);
117
+
118
+ // Check if error message already exists
119
+ let errorElement = element.nextElementSibling;
120
+ if (!errorElement || !errorElement.classList.contains('invalid-feedback')) {
121
+ errorElement = document.createElement('div');
122
+ errorElement.classList.add('invalid-feedback', 'd-block');
123
+ element.parentNode.appendChild(errorElement);
124
+ }
125
+
126
+ // Set error message
127
+ errorElement.textContent = translatedMessage;
128
+ }
129
+
130
+ getTranslation(key) {
131
+ // Use the global Translations object
132
+ if (window.Translations && window.Translations[key]) {
133
+ return window.Translations[key];
134
+ }
135
+ return key;
136
+ }
137
+
138
+ clearErrors() {
139
+ // Remove 'is-invalid' class and error messages
140
+ this.element.querySelectorAll('.is-invalid').forEach(el => {
141
+ el.classList.remove('is-invalid');
142
+ const errorEl = el.nextElementSibling;
143
+ if (errorEl && errorEl.classList.contains('invalid-feedback')) {
144
+ errorEl.remove();
145
+ }
146
+ });
147
+ }
148
+ }