databasium 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 (125) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +32 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +109 -0
  5. data/Rakefile +6 -0
  6. data/app/assets/builds/application.js +9045 -0
  7. data/app/assets/builds/application.js.map +7 -0
  8. data/app/assets/builds/databasium.css +2 -0
  9. data/app/assets/config/databasium_manifest.js +1 -0
  10. data/app/assets/javascript/databasium/application.js +2 -0
  11. data/app/assets/javascript/databasium/controllers/attribute_controller.js +27 -0
  12. data/app/assets/javascript/databasium/controllers/collapse_controller.js +18 -0
  13. data/app/assets/javascript/databasium/controllers/error_controller.js +15 -0
  14. data/app/assets/javascript/databasium/controllers/filter_controller.js +224 -0
  15. data/app/assets/javascript/databasium/controllers/flash_controller.js +18 -0
  16. data/app/assets/javascript/databasium/controllers/graph_controller.js +193 -0
  17. data/app/assets/javascript/databasium/controllers/index.js +7 -0
  18. data/app/assets/javascript/databasium/controllers/layout_controller.js +13 -0
  19. data/app/assets/javascript/databasium/controllers/model_controller.js +32 -0
  20. data/app/assets/javascript/databasium/controllers/new_migration_controller.js +107 -0
  21. data/app/assets/javascript/databasium/controllers/relation_controller.js +10 -0
  22. data/app/assets/javascript/databasium/controllers/search_controller.js +23 -0
  23. data/app/assets/javascript/databasium/controllers/table_controller.js +283 -0
  24. data/app/assets/javascript/databasium/controllers/table_select_controller.js +19 -0
  25. data/app/assets/javascript/databasium/controllers/toggle_controller.js +28 -0
  26. data/app/assets/javascript/databasium/controllers/validation_controller.js +78 -0
  27. data/app/assets/javascript/databasium/shapes/erd_table_shape.js +54 -0
  28. data/app/assets/stylesheets/databasium/application.css +15 -0
  29. data/app/assets/stylesheets/databasium/colors.css +55 -0
  30. data/app/assets/stylesheets/databasium/custom.css +36 -0
  31. data/app/assets/stylesheets/databasium/databasium_engine.css +6 -0
  32. data/app/assets/stylesheets/databasium/pagy-tailwind.css +66 -0
  33. data/app/components/base.rb +50 -0
  34. data/app/components/databasium/collapsable.rb +62 -0
  35. data/app/components/databasium/forms/model.rb +147 -0
  36. data/app/components/databasium/forms/search.rb +31 -0
  37. data/app/components/databasium/global/error.rb +60 -0
  38. data/app/components/databasium/global/flash.rb +73 -0
  39. data/app/components/databasium/global/header_actions.rb +36 -0
  40. data/app/components/databasium/global/sidebar.rb +45 -0
  41. data/app/components/databasium/global/suggestion.rb +25 -0
  42. data/app/components/databasium/migrations/action.rb +39 -0
  43. data/app/components/databasium/migrations/file.rb +58 -0
  44. data/app/components/databasium/migrations/form.rb +222 -0
  45. data/app/components/databasium/migrations/header_actions.rb +87 -0
  46. data/app/components/databasium/migrations/migration_status.rb +22 -0
  47. data/app/components/databasium/migrations/preview.rb +29 -0
  48. data/app/components/databasium/migrations/show_turbo_stream.rb +19 -0
  49. data/app/components/databasium/migrations/sidebar.rb +28 -0
  50. data/app/components/databasium/models/attributes.rb +49 -0
  51. data/app/components/databasium/models/form.rb +100 -0
  52. data/app/components/databasium/models/header_actions.rb +51 -0
  53. data/app/components/databasium/models/model_preview.rb +31 -0
  54. data/app/components/databasium/models/sidebar.rb +25 -0
  55. data/app/components/databasium/models/templates/attribute.rb +99 -0
  56. data/app/components/databasium/models/templates/base.rb +6 -0
  57. data/app/components/databasium/models/templates/relation.rb +56 -0
  58. data/app/components/databasium/models/templates/validation.rb +285 -0
  59. data/app/components/databasium/navigation/base_icon.rb +32 -0
  60. data/app/components/databasium/navigation/frontend_icon.rb +17 -0
  61. data/app/components/databasium/navigation/get_icon.rb +26 -0
  62. data/app/components/databasium/navigation/icon.rb +28 -0
  63. data/app/components/databasium/navigation/icon_panel.rb +26 -0
  64. data/app/components/databasium/navigation/post_icon.rb +25 -0
  65. data/app/components/databasium/navigation/put_icon.rb +18 -0
  66. data/app/components/databasium/records/filter.rb +73 -0
  67. data/app/components/databasium/records/foreign_records.rb +84 -0
  68. data/app/components/databasium/records/header_actions.rb +110 -0
  69. data/app/components/databasium/records/show_turbo_stream.rb +75 -0
  70. data/app/components/databasium/records/sidebar.rb +23 -0
  71. data/app/components/databasium/records/table/record_panel.rb +60 -0
  72. data/app/components/databasium/records/table/row.rb +104 -0
  73. data/app/components/databasium/records/table.rb +125 -0
  74. data/app/components/databasium/records/table_turbo_frame.rb +37 -0
  75. data/app/components/databasium/records/utilities.rb +25 -0
  76. data/app/components/databasium/schemas/header_actions.rb +99 -0
  77. data/app/components/databasium/schemas/sidebar.rb +25 -0
  78. data/app/components/databasium/search_results/migrations.rb +37 -0
  79. data/app/components/databasium/search_results/models.rb +36 -0
  80. data/app/components/databasium/search_results/schema_models.rb +37 -0
  81. data/app/components/databasium/search_results/tables.rb +31 -0
  82. data/app/components/databasium/type_select.rb +35 -0
  83. data/app/controllers/databasium/application_controller.rb +68 -0
  84. data/app/controllers/databasium/homepage_controller.rb +5 -0
  85. data/app/controllers/databasium/migrations_controller.rb +186 -0
  86. data/app/controllers/databasium/models_controller.rb +105 -0
  87. data/app/controllers/databasium/records_controller.rb +156 -0
  88. data/app/controllers/databasium/schemas_controller.rb +52 -0
  89. data/app/helpers/databasium/application_helper.rb +4 -0
  90. data/app/helpers/databasium/heroicon_helper.rb +21 -0
  91. data/app/helpers/databasium/models_helper.rb +4 -0
  92. data/app/jobs/databasium/application_job.rb +4 -0
  93. data/app/mailers/databasium/application_mailer.rb +6 -0
  94. data/app/models/databasium/application_record.rb +5 -0
  95. data/app/models/model.json +0 -0
  96. data/app/services/databasium/migration.rb +176 -0
  97. data/app/services/databasium/model.rb +182 -0
  98. data/app/services/databasium/record.rb +65 -0
  99. data/app/services/databasium/schema.rb +146 -0
  100. data/app/views/base.rb +13 -0
  101. data/app/views/databasium/errors/non_development.rb +21 -0
  102. data/app/views/databasium/homepage/index.rb +29 -0
  103. data/app/views/databasium/migrations/index.rb +33 -0
  104. data/app/views/databasium/migrations/new.rb +29 -0
  105. data/app/views/databasium/models/index.rb +31 -0
  106. data/app/views/databasium/models/new.rb +37 -0
  107. data/app/views/databasium/records/index.rb +24 -0
  108. data/app/views/databasium/schemas/index.rb +39 -0
  109. data/app/views/layouts/databasium/application.rb +56 -0
  110. data/config/importmap.rb +12 -0
  111. data/config/initializers/heroicon.rb +12 -0
  112. data/config/initializers/pagy.rb +48 -0
  113. data/config/initializers/phlex.rb +19 -0
  114. data/config/routes.rb +31 -0
  115. data/config/tailwind.config.js +10 -0
  116. data/lib/databasium/engine.rb +57 -0
  117. data/lib/databasium/engine_mount.rb +37 -0
  118. data/lib/databasium/middleware/conditional_check_pending.rb +27 -0
  119. data/lib/databasium/templates/create_table_migration.rb.tt +29 -0
  120. data/lib/databasium/templates/migration.rb.tt +48 -0
  121. data/lib/databasium/templates/model.rb.tt +23 -0
  122. data/lib/databasium/version.rb +3 -0
  123. data/lib/databasium.rb +11 -0
  124. data/lib/tasks/databasium_tasks.rake +4 -0
  125. metadata +272 -0
@@ -0,0 +1,107 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ const MIGRATION_ACTION_UI = {
4
+ create: {
5
+ show: ["table_name", "add_model_container", "validations"],
6
+ hide: ["table_name_from", "table_name_to"],
7
+ addModelDisabled: false
8
+ },
9
+ remove: {
10
+ show: ["table_name_from"],
11
+ hide: ["table_name_to", "table_name", "add_model_container", "validations"],
12
+ addModelDisabled: true
13
+ },
14
+ add: {
15
+ show: ["table_name_to"],
16
+ hide: ["table_name_from", "table_name", "add_model_container", "validations"],
17
+ addModelDisabled: true
18
+ }
19
+ };
20
+
21
+ // Connects to data-controller="new-migration"
22
+ export default class extends Controller {
23
+ static targets = [
24
+ "column",
25
+ "table_name_from",
26
+ "table_name_to",
27
+ "table_name",
28
+ "add_model_container",
29
+ "add_model",
30
+ "validation",
31
+ "validations",
32
+ "validation_column_name"
33
+ ];
34
+
35
+ connect() {
36
+ this.addedColumns = [];
37
+ this.columnsNames = [];
38
+ }
39
+
40
+ addColumn(e) {
41
+ const column = this.columnTarget.cloneNode(true);
42
+ this.addedColumns.push(column);
43
+ column.classList.remove("hidden");
44
+ e.currentTarget.before(column);
45
+ }
46
+
47
+ updateColumnNames() {
48
+ this.columnsNames = this.addedColumns.map((column) => column.querySelector("input").value);
49
+
50
+ const options = [new Option("Select a column", "")];
51
+
52
+ this.columnsNames.forEach((name) => {
53
+ options.push(new Option(name, name));
54
+ });
55
+
56
+ if (!this.hasValidationTarget) return;
57
+
58
+ this.validation_column_nameTargets.forEach((target) => {
59
+ const selected = target.options[target.selectedIndex].value;
60
+ target.innerHTML = "";
61
+ options.forEach((opt) => {
62
+ const clone = opt.cloneNode(true);
63
+ clone.selected = selected === opt.value;
64
+ target.add(clone);
65
+ });
66
+ });
67
+ }
68
+
69
+ removeColumn(e) {
70
+ if (this.columnTargets.length <= 1) {
71
+ alert("You need at least one column");
72
+ return;
73
+ }
74
+ this.addedColumns = this.addedColumns.filter(
75
+ (column) => column !== e.currentTarget.parentElement
76
+ );
77
+ e.currentTarget.parentElement.remove();
78
+ this.updateColumnNames();
79
+ }
80
+
81
+ addValidation(e) {
82
+ const validation = this.validationTarget.cloneNode(true);
83
+ validation.classList.remove("hidden");
84
+ e.currentTarget.before(validation);
85
+ }
86
+
87
+ getColumnNames() {
88
+ this.columnsNames = this.addedColumns.map((column) => column.querySelector("input").value);
89
+ }
90
+
91
+ removeValidation(e) {
92
+ e.currentTarget.parentElement.remove();
93
+ }
94
+
95
+ set_action(e) {
96
+ const config = MIGRATION_ACTION_UI[e.currentTarget.value];
97
+ if (!config) return;
98
+
99
+ for (const name of config.show) {
100
+ this[`${name}Target`].classList.remove("hidden");
101
+ }
102
+ for (const name of config.hide) {
103
+ this[`${name}Target`].classList.add("hidden");
104
+ }
105
+ this.add_modelTarget.disabled = config.addModelDisabled;
106
+ }
107
+ }
@@ -0,0 +1,10 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // Connects to data-controller="relation"
4
+ export default class extends Controller {
5
+ connect() {}
6
+
7
+ removeRelation(e) {
8
+ e.currentTarget.parentElement.remove();
9
+ }
10
+ }
@@ -0,0 +1,23 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // Connects to data-controller="search"
4
+ export default class extends Controller {
5
+ static targets = [];
6
+
7
+ connect() {
8
+ this.debounceTimer = null;
9
+ this.lastSubmittedValue = null;
10
+ }
11
+
12
+ update(event) {
13
+ const currentValue = event.target.value;
14
+ clearTimeout(this.debounceTimer);
15
+ this.debounceTimer = setTimeout(() => {
16
+ if (this.lastSubmittedValue === currentValue) return;
17
+
18
+ this.lastSubmittedValue = currentValue;
19
+
20
+ if (event.target.form) event.target.form.requestSubmit();
21
+ }, 100);
22
+ }
23
+ }
@@ -0,0 +1,283 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ const SPACES_UP_TO_OPEN_FORM = 135;
4
+ const CLICK_DELAY = 200;
5
+
6
+ // Connects to data-controller="table"
7
+ export default class extends Controller {
8
+ static targets = [
9
+ "deleteButton",
10
+ "recordsPanel",
11
+ "recordTab",
12
+ "recordTabs",
13
+ "addRecordForm",
14
+ "recordTabsContent",
15
+ "toggleAllRecordsButton",
16
+ "checkbox"
17
+ ];
18
+
19
+ connect() {
20
+ this.selectedRecords = 0;
21
+ this.enableUpdate = false;
22
+ this.opened_tabs = new Map();
23
+ this.opened_form = null;
24
+ this.opened_tab = null;
25
+ this.opened_record_id = null;
26
+ this.allRecordsSelected = false;
27
+ this.clickTimer = null;
28
+ }
29
+
30
+ handleClick(e) {
31
+ if (this.clickTimer === null) {
32
+ this.clickTimer = setTimeout(() => {
33
+ this.clickTimer = null;
34
+ this.selectRecord(e);
35
+ }, CLICK_DELAY);
36
+ } else {
37
+ clearTimeout(this.clickTimer);
38
+ this.clickTimer = null;
39
+ this.appendRecordCard(e);
40
+ }
41
+ }
42
+
43
+ selectRecord(e) {
44
+ const tr = e.target.closest("tr");
45
+ if (!tr) return;
46
+ const checkbox = tr.querySelector("input[type='checkbox']");
47
+ if (!checkbox) return;
48
+ if (!e.target.closest("input, label, a, button")) {
49
+ checkbox.checked = !checkbox.checked;
50
+ }
51
+ this.updateDeleteButton(checkbox.checked);
52
+ this.allRecordsSelected = this.checkboxTargets.length === this.selectedRecords;
53
+ this.updateStyleOfToggleAllRecordsButton();
54
+ }
55
+
56
+ toggleAllRecords() {
57
+ // this.allRecordsSelected = !this.allRecordsSelected;
58
+ this.checkboxTargets.forEach((checkbox) => {
59
+ checkbox.checked = !this.allRecordsSelected;
60
+ });
61
+ this.allRecordsSelected = !this.allRecordsSelected;
62
+ this.updateStyleOfToggleAllRecordsButton();
63
+ this.updateDeleteButtonAllRecordsSelected();
64
+ this.updateDeleteButtonText();
65
+ }
66
+
67
+ updateStyleOfToggleAllRecordsButton() {
68
+ if (this.allRecordsSelected) {
69
+ this.toggleAllRecordsButtonTarget.classList.add("text-selected");
70
+ } else {
71
+ this.toggleAllRecordsButtonTarget.classList.remove("text-selected");
72
+ }
73
+ }
74
+
75
+ updateDeleteButtonAllRecordsSelected() {
76
+ if (this.allRecordsSelected) {
77
+ this.selectedRecords = this.checkboxTargets.length;
78
+ } else if (this.allRecordsSelected === false) {
79
+ this.selectedRecords = 0;
80
+ }
81
+ this.updateDeleteButtonText();
82
+ }
83
+
84
+ updateDeleteButton(checked) {
85
+ if (!checked) {
86
+ this.selectedRecords--;
87
+ } else {
88
+ this.selectedRecords++;
89
+ }
90
+ this.updateDeleteButtonText();
91
+ }
92
+
93
+ updateDeleteButtonText() {
94
+ if (this.selectedRecords > 0) {
95
+ this.deleteButtonTarget.parentElement.classList.remove("hidden");
96
+ } else {
97
+ this.deleteButtonTarget.parentElement.classList.add("hidden");
98
+ }
99
+ this.deleteButtonTarget.innerHTML = `Delete records (${this.selectedRecords})`;
100
+ }
101
+
102
+ resetDeleteButton() {
103
+ this.selectedRecords = 0;
104
+ this.deleteButtonTarget.parentElement.classList.add("hidden");
105
+ }
106
+
107
+ openRecordsPanel() {
108
+ this.recordsPanelTarget.classList.remove("hidden");
109
+ }
110
+
111
+ closeRecordsPanel() {
112
+ this.recordsPanelTarget.classList.add("hidden");
113
+ }
114
+
115
+ appendRecordCard(e) {
116
+ this.openRecordsPanel();
117
+ const row = e.currentTarget.closest("tr");
118
+ if (this.opened_tabs.has(row.id)) {
119
+ this.openTab(e, row.id);
120
+ this.scrollToOpenedTab();
121
+ return;
122
+ }
123
+ this.opened_record_id = row.id;
124
+ if (this.opened_form === null) {
125
+ this.recordTabsContentTarget.innerHTML = "";
126
+ }
127
+
128
+ if (this.opened_tab) {
129
+ this.opened_tab.classList.remove("bg-accent");
130
+ }
131
+
132
+ const copy = this.recordTabTarget.content.cloneNode(true);
133
+ copy.firstElementChild.querySelector("[data-table-target='recordTabTitle']").innerHTML = row.id;
134
+ copy.firstElementChild.dataset.recordId = row.id;
135
+ copy.firstElementChild.classList.add("bg-accent");
136
+ copy.firstElementChild.id = `record-tab-${row.id}`;
137
+
138
+ this.opened_tab = copy.firstElementChild;
139
+ this.recordTabsTarget.appendChild(copy);
140
+ this.scrollToOpenedTab();
141
+
142
+ const form = this.createAddRecordForm(row);
143
+ this.opened_tabs.set(row.id, form);
144
+
145
+ if (this.opened_form) {
146
+ this.opened_form.classList.add("hidden");
147
+ }
148
+ this.opened_form = form;
149
+ this.recordTabsContentTarget.appendChild(form);
150
+ }
151
+
152
+ scrollToOpenedTab() {
153
+ this.opened_tab.scrollIntoView({
154
+ behavior: "smooth",
155
+ block: "nearest",
156
+ inline: "end"
157
+ });
158
+ }
159
+
160
+ createAddRecordForm(row) {
161
+ const form = this.element.querySelector("#addRecord").cloneNode(true);
162
+ form.id = `record-form-${row.id}`;
163
+ form.classList.remove("hidden", "max-h-100");
164
+ // 135px is the space up to the open form cant define as constants because of tailwind dynamic classes
165
+ form.classList.add(`max-h-[calc(100dvh-135px)]`, "pb-6");
166
+ const addRecordButton = form.querySelector("#add_record_button");
167
+ addRecordButton.value = `Update record ${row.id}`;
168
+ const inputs = {};
169
+ form.method = "patch";
170
+ form.action = `/databasium/records/${row.dataset.recordId}`;
171
+
172
+ form.querySelectorAll("input, select, textarea").forEach((i) => {
173
+ const match = i.name.match(/\[(\w+)\]/);
174
+ if (match) inputs[match[1]] = i;
175
+ });
176
+
177
+ [...row.children].forEach((child) => {
178
+ const attr = child.dataset.attributeName;
179
+ const input = inputs[attr];
180
+ if (!input) return;
181
+ const value = child.textContent.trim();
182
+ switch (input.type) {
183
+ case "file":
184
+ // this.showExistingFile(input, value);
185
+ break;
186
+ case "checkbox":
187
+ input.checked = value === "true" || value === "1";
188
+ break;
189
+ case "datetime-local":
190
+ input.value = this.toDatetimeLocal(value);
191
+ break;
192
+ case "date":
193
+ input.value = this.toDate(value);
194
+ break;
195
+ case "time":
196
+ input.value = this.toTime(value);
197
+ break;
198
+ default:
199
+ input.value = value;
200
+ }
201
+ });
202
+
203
+ return form;
204
+ }
205
+
206
+ openTab(e, givenRecordId = null) {
207
+ const recordId = givenRecordId || e.currentTarget.dataset.recordId;
208
+ if (!this.opened_tabs.has(recordId)) return;
209
+ this.opened_record_id = recordId;
210
+ if (this.opened_tab) {
211
+ this.opened_tab.classList.remove("bg-accent");
212
+ }
213
+
214
+ if (this.opened_form) {
215
+ this.opened_form.classList.add("hidden");
216
+ }
217
+ const form = this.opened_tabs.get(recordId);
218
+
219
+ const recordTab = this.recordTabsTarget.querySelector(`[data-record-id="${recordId}"]`);
220
+ recordTab.classList.add("bg-accent");
221
+ this.opened_tab = recordTab;
222
+
223
+ form.classList.remove("hidden");
224
+ this.opened_form = form;
225
+ }
226
+
227
+ closeTab(e) {
228
+ const recordTab = e.currentTarget.parentElement;
229
+ const recordId = recordTab.dataset.recordId;
230
+
231
+ if (!this.opened_tabs.has(recordId)) return;
232
+
233
+ recordTab.remove();
234
+ this.opened_tabs.get(recordId).remove();
235
+
236
+ this.opened_tabs.delete(recordId);
237
+
238
+ if (this.opened_record_id === recordId) {
239
+ this.opened_tab.remove();
240
+ this.opened_form.remove();
241
+ this.opened_form = null;
242
+ this.opened_tab = null;
243
+ this.opened_record_id = null;
244
+ }
245
+ }
246
+
247
+ showExistingFile(input, filename) {
248
+ if (!filename) return;
249
+ let label = input.parentElement.querySelector("[data-existing-file]");
250
+ if (!label) {
251
+ label = document.createElement("span");
252
+ label.dataset.existingFile = "true";
253
+ label.className = "text-sm text-muted ml-2";
254
+ input.insertAdjacentElement("afterend", label);
255
+ }
256
+ label.textContent = `Current: ${filename}`;
257
+ }
258
+
259
+ toDatetimeLocal(v) {
260
+ if (!v) return "";
261
+ const d = new Date(v.includes("T") ? v : v.replace(" ", "T"));
262
+ if (isNaN(d)) return "";
263
+ const pad = (n) => String(n).padStart(2, "0");
264
+ return (
265
+ `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
266
+ `T${pad(d.getHours())}:${pad(d.getMinutes())}`
267
+ );
268
+ }
269
+
270
+ toDate(v) {
271
+ if (!v) return "";
272
+ const d = new Date(v.includes("T") ? v : v.replace(" ", "T"));
273
+ if (isNaN(d)) return "";
274
+ const pad = (n) => String(n).padStart(2, "0");
275
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
276
+ }
277
+
278
+ toTime(v) {
279
+ if (!v) return "";
280
+ const match = v.match(/(\d{2}):(\d{2})(?::(\d{2}))?/);
281
+ return match ? match[0] : "";
282
+ }
283
+ }
@@ -0,0 +1,19 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // Connects to data-controller="table-select"
4
+ export default class extends Controller {
5
+ static targets = ["foreignKeyInput", "table", "selectedRecord"];
6
+
7
+ connect() {}
8
+
9
+ toggleVisibility() {
10
+ this.tableTarget.remove();
11
+ }
12
+
13
+ selectRecord(e) {
14
+ e.preventDefault();
15
+ const record = e.currentTarget;
16
+ this.foreignKeyInputTarget.value = record.dataset.recordId;
17
+ this.selectedRecordTarget.textContent = record.dataset.recordId;
18
+ }
19
+ }
@@ -0,0 +1,28 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // Connects to data-controller="toggle"
4
+ export default class extends Controller {
5
+ connect() {
6
+ this.openEl = null;
7
+ }
8
+
9
+ toggle(e) {
10
+ const el = document.getElementById(e.currentTarget.dataset.toggle);
11
+ if (!el) return;
12
+ const isOpen = !el.classList.contains("hidden");
13
+
14
+ if (this.openEl && this.openEl !== el) {
15
+ this.openEl.classList.add("hidden");
16
+ }
17
+
18
+ el.classList.toggle("hidden", isOpen);
19
+ this.openEl = isOpen ? (this.openEl === el ? null : this.openEl) : el;
20
+ }
21
+
22
+ toggleSticky(e) {
23
+ const el = document.getElementById(e.currentTarget.dataset.toggle);
24
+ if (!el) return;
25
+ const isOpen = !el.classList.contains("hidden");
26
+ el.classList.toggle("hidden", isOpen);
27
+ }
28
+ }
@@ -0,0 +1,78 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ const BASIC_INFO = {
4
+ presence:
5
+ 'Ensures the attribute is not blank — i.e. not nil and not an empty or whitespace-only string. Default error: "can\'t be blank".',
6
+ absence:
7
+ 'Ensures the attribute is blank — i.e. nil or an empty/whitespace-only string. Often used with conditional validations (e.g. `if: :invited?`). Default error: "must be blank".',
8
+ uniqueness:
9
+ 'Ensures the attribute\'s value is unique across rows by running a SELECT before save. Supports `:scope`, `:case_sensitive`, and `:conditions`. Note: this is not a DB constraint — add a unique index too. Default error: "has already been taken".',
10
+ inclusion:
11
+ "Ensures the attribute's value is in a given set passed via `:in` (array, range, proc, or lambda). Uses `Range#cover?` for ranges, otherwise `include?`.",
12
+ exclusion:
13
+ "Ensures the attribute's value is NOT in a given set passed via `:in` (array, range, proc, or lambda). Mirror of `inclusion`.",
14
+ validates_associated:
15
+ 'Calls `valid?` on each associated object when this record is validated. Use only on one side of an association to avoid infinite loops. Default error: "is invalid".',
16
+ numericality:
17
+ "Ensures the attribute is a number. Use `only_integer: true` to require integers, plus comparison options (`:greater_than`, `:less_than`, `:in`, `:odd`, `:even`, etc.). Doesn't allow nil unless `allow_nil: true`.",
18
+ length:
19
+ "Validates the length of the attribute. Use `:minimum`, `:maximum`, `:in` (range), or `:is`. Customize messages with `:too_short`, `:too_long`, `:wrong_length`.",
20
+ acceptance:
21
+ "Validates that a checkbox was checked (e.g. terms of service). Creates a virtual attribute if no DB column exists. `:accept` defaults to `['1', true]`. Default error: \"must be accepted\".",
22
+ confirmation:
23
+ 'Validates that two fields match — e.g. `email` and `email_confirmation`. Adds a virtual `_confirmation` attribute. Use `case_sensitive: false` to ignore case. Default error: "doesn\'t match confirmation".',
24
+ comparison:
25
+ "Validates a comparison between two comparable values via `:greater_than`, `:greater_than_or_equal_to`, `:equal_to`, `:less_than`, `:less_than_or_equal_to`, or `:other_than`. Each option accepts a value, proc, or symbol.",
26
+ format:
27
+ 'Validates the attribute against a regular expression via `:with` (must match) or `:without` (must not match). Prefer `\\A` and `\\z` over `^`/`$`. Default error: "is invalid".'
28
+ };
29
+ const PRESELECTED_TYPE = "presence";
30
+ // Connects to data-controller="validation"
31
+ export default class extends Controller {
32
+ static targets = ["valueInput", "basicInfo", "basicInfoText"];
33
+
34
+ static values = {
35
+ selectedType: String
36
+ };
37
+ connect() {
38
+ this.selectedType = this.selectedTypeValue || PRESELECTED_TYPE;
39
+ this.valueInputTarget.setAttribute("list", `${this.selectedType}-options`);
40
+ }
41
+
42
+ updateType(event) {
43
+ this.selectedType = event.target.value;
44
+ this.valueInputTarget.setAttribute("list", `${this.selectedType}-options`);
45
+ }
46
+
47
+ allowNil() {
48
+ const value = this.valueInputTarget.value;
49
+ const isEmpty = value.trim() === "";
50
+ this.valueInputTarget.value = `${value} ${isEmpty ? "" : ","} allow_nil: true`;
51
+ }
52
+
53
+ allowBlank() {
54
+ const value = this.valueInputTarget.value;
55
+ const isEmpty = value.trim() === "";
56
+
57
+ this.valueInputTarget.value = `${value} ${isEmpty ? "" : ","} allow_blank: true`;
58
+ }
59
+
60
+ onAction() {
61
+ const value = this.valueInputTarget.value;
62
+ const isEmpty = value.trim() === "";
63
+ this.valueInputTarget.value = `${value} ${isEmpty ? "" : ","} on: :action`;
64
+ }
65
+
66
+ remove() {
67
+ this.element.remove();
68
+ }
69
+
70
+ showBasicInfo() {
71
+ this.basicInfoTarget.classList.remove("hidden");
72
+ this.basicInfoTextTarget.textContent = BASIC_INFO[this.selectedType] || "";
73
+ }
74
+
75
+ hideBasicInfo() {
76
+ this.basicInfoTarget.classList.add("hidden");
77
+ }
78
+ }
@@ -0,0 +1,54 @@
1
+ import { Shape, ShapeRegistry } from "@maxgraph/core";
2
+
3
+ export default class ErdTableShape extends Shape {
4
+ paintVertexShape(c, x, y, w, h) {
5
+ const { name, fields } = this.state.cell.value;
6
+ const headerHeight = 32;
7
+
8
+ c.setFillColor("var(--color-panel)");
9
+ c.setStrokeColor("var(--color-border)");
10
+ c.setStrokeWidth(2);
11
+ c.rect(x, y, w, fields.length * 32 + headerHeight);
12
+ c.fillAndStroke();
13
+
14
+ c.setFillColor("var(--color-panel)");
15
+ c.rect(x, y, w, headerHeight);
16
+ c.fillAndStroke();
17
+
18
+ c.setFontStyle(1);
19
+ c.setFontSize(16);
20
+ c.setFontColor("var(--color-main-text)");
21
+ c.text(x + 4, y + 4, 0, 0, name, "left", "top", false, false);
22
+
23
+ c.setFontStyle(0);
24
+ c.setFontSize(16);
25
+
26
+ let rowY = y + headerHeight;
27
+ const rowHeight = 32;
28
+ fields?.forEach((field) => {
29
+ c.setStrokeWidth(1);
30
+ c.setStrokeColor("var(--color-border)");
31
+ c.stroke();
32
+ c.begin();
33
+ c.moveTo(x, rowY);
34
+ c.lineTo(x + w, rowY);
35
+ c.stroke();
36
+ c.setFontColor("var(--color-main-text)");
37
+ c.text(
38
+ x + 6,
39
+ rowY + 4,
40
+ 0,
41
+ 0,
42
+ `${field.name} ${" - " + field.sql_type}`,
43
+ "left",
44
+ "top",
45
+ false,
46
+ false
47
+ );
48
+
49
+ rowY += rowHeight;
50
+ });
51
+ }
52
+ }
53
+
54
+ ShapeRegistry.add("erdTable", ErdTableShape);
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,55 @@
1
+ :root {
2
+ --databasium-primary: var(--color-blue-500);
3
+ --databasium-secondary: var(--color-blue-500);
4
+
5
+ --databasium-accent: var(--color-blue-500);
6
+ --databasium-background: var(--color-slate-900);
7
+ --databasium-text: var(--color-slate-100);
8
+ --databasium-border: var(--color-slate-600);
9
+ --databasium-panel: var(--color-slate-800);
10
+ --databasium-hover: var(--color-yellow-400);
11
+ --databasium-selected: var(--color-green-500);
12
+ --databasium-shadow: 0 0 6px rgba(96, 165, 250, 0.5);
13
+
14
+ --databasium-active: var(--color-blue-500);
15
+ --databasium-focus: var(--color-blue-500);
16
+ --databasium-disabled: var(--color-blue-500);
17
+ --databasium-error: var(--color-blue-500);
18
+ --databasium-success: var(--color-blue-500);
19
+ --databasium-warning: var(--color-blue-500);
20
+ }
21
+
22
+ [data-theme="white"] {
23
+ --app-primary: #ffffff;
24
+ --app-secondary: yellow;
25
+ --app-accent: #888888;
26
+ --app-background: #111111;
27
+ --app-text: #ffffff;
28
+ }
29
+
30
+ [data-theme="ocean"] {
31
+ --app-primary: #0f172a;
32
+ --app-secondary: #38bdf8;
33
+ --app-accent: #06b6d4;
34
+ --app-background: #ecfeff;
35
+ --app-text: #082f49;
36
+ }
37
+
38
+ @theme inline {
39
+ --color-primary: var(--databasium-primary);
40
+ --color-secondary: var(--databasium-secondary);
41
+ --color-accent: var(--databasium-accent);
42
+
43
+ --color-background: var(--databasium-background);
44
+ --color-main-text: var(--databasium-text);
45
+ --color-border: var(--databasium-border);
46
+ --color-panel: var(--databasium-panel);
47
+
48
+ --shadow-accent: var(--databasium-shadow);
49
+ --color-hover: var(--databasium-hover);
50
+ --color-active: var(--databasium-active);
51
+ --color-focus: var(--databasium-focus);
52
+ --color-disabled: var(--databasium-disabled);
53
+ --color-error: var(--databasium-error);
54
+ --color-selected: var(--databasium-selected);
55
+ }