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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +32 -0
- data/MIT-LICENSE +20 -0
- data/README.md +109 -0
- data/Rakefile +6 -0
- data/app/assets/builds/application.js +9045 -0
- data/app/assets/builds/application.js.map +7 -0
- data/app/assets/builds/databasium.css +2 -0
- data/app/assets/config/databasium_manifest.js +1 -0
- data/app/assets/javascript/databasium/application.js +2 -0
- data/app/assets/javascript/databasium/controllers/attribute_controller.js +27 -0
- data/app/assets/javascript/databasium/controllers/collapse_controller.js +18 -0
- data/app/assets/javascript/databasium/controllers/error_controller.js +15 -0
- data/app/assets/javascript/databasium/controllers/filter_controller.js +224 -0
- data/app/assets/javascript/databasium/controllers/flash_controller.js +18 -0
- data/app/assets/javascript/databasium/controllers/graph_controller.js +193 -0
- data/app/assets/javascript/databasium/controllers/index.js +7 -0
- data/app/assets/javascript/databasium/controllers/layout_controller.js +13 -0
- data/app/assets/javascript/databasium/controllers/model_controller.js +32 -0
- data/app/assets/javascript/databasium/controllers/new_migration_controller.js +107 -0
- data/app/assets/javascript/databasium/controllers/relation_controller.js +10 -0
- data/app/assets/javascript/databasium/controllers/search_controller.js +23 -0
- data/app/assets/javascript/databasium/controllers/table_controller.js +283 -0
- data/app/assets/javascript/databasium/controllers/table_select_controller.js +19 -0
- data/app/assets/javascript/databasium/controllers/toggle_controller.js +28 -0
- data/app/assets/javascript/databasium/controllers/validation_controller.js +78 -0
- data/app/assets/javascript/databasium/shapes/erd_table_shape.js +54 -0
- data/app/assets/stylesheets/databasium/application.css +15 -0
- data/app/assets/stylesheets/databasium/colors.css +55 -0
- data/app/assets/stylesheets/databasium/custom.css +36 -0
- data/app/assets/stylesheets/databasium/databasium_engine.css +6 -0
- data/app/assets/stylesheets/databasium/pagy-tailwind.css +66 -0
- data/app/components/base.rb +50 -0
- data/app/components/databasium/collapsable.rb +62 -0
- data/app/components/databasium/forms/model.rb +147 -0
- data/app/components/databasium/forms/search.rb +31 -0
- data/app/components/databasium/global/error.rb +60 -0
- data/app/components/databasium/global/flash.rb +73 -0
- data/app/components/databasium/global/header_actions.rb +36 -0
- data/app/components/databasium/global/sidebar.rb +45 -0
- data/app/components/databasium/global/suggestion.rb +25 -0
- data/app/components/databasium/migrations/action.rb +39 -0
- data/app/components/databasium/migrations/file.rb +58 -0
- data/app/components/databasium/migrations/form.rb +222 -0
- data/app/components/databasium/migrations/header_actions.rb +87 -0
- data/app/components/databasium/migrations/migration_status.rb +22 -0
- data/app/components/databasium/migrations/preview.rb +29 -0
- data/app/components/databasium/migrations/show_turbo_stream.rb +19 -0
- data/app/components/databasium/migrations/sidebar.rb +28 -0
- data/app/components/databasium/models/attributes.rb +49 -0
- data/app/components/databasium/models/form.rb +100 -0
- data/app/components/databasium/models/header_actions.rb +51 -0
- data/app/components/databasium/models/model_preview.rb +31 -0
- data/app/components/databasium/models/sidebar.rb +25 -0
- data/app/components/databasium/models/templates/attribute.rb +99 -0
- data/app/components/databasium/models/templates/base.rb +6 -0
- data/app/components/databasium/models/templates/relation.rb +56 -0
- data/app/components/databasium/models/templates/validation.rb +285 -0
- data/app/components/databasium/navigation/base_icon.rb +32 -0
- data/app/components/databasium/navigation/frontend_icon.rb +17 -0
- data/app/components/databasium/navigation/get_icon.rb +26 -0
- data/app/components/databasium/navigation/icon.rb +28 -0
- data/app/components/databasium/navigation/icon_panel.rb +26 -0
- data/app/components/databasium/navigation/post_icon.rb +25 -0
- data/app/components/databasium/navigation/put_icon.rb +18 -0
- data/app/components/databasium/records/filter.rb +73 -0
- data/app/components/databasium/records/foreign_records.rb +84 -0
- data/app/components/databasium/records/header_actions.rb +110 -0
- data/app/components/databasium/records/show_turbo_stream.rb +75 -0
- data/app/components/databasium/records/sidebar.rb +23 -0
- data/app/components/databasium/records/table/record_panel.rb +60 -0
- data/app/components/databasium/records/table/row.rb +104 -0
- data/app/components/databasium/records/table.rb +125 -0
- data/app/components/databasium/records/table_turbo_frame.rb +37 -0
- data/app/components/databasium/records/utilities.rb +25 -0
- data/app/components/databasium/schemas/header_actions.rb +99 -0
- data/app/components/databasium/schemas/sidebar.rb +25 -0
- data/app/components/databasium/search_results/migrations.rb +37 -0
- data/app/components/databasium/search_results/models.rb +36 -0
- data/app/components/databasium/search_results/schema_models.rb +37 -0
- data/app/components/databasium/search_results/tables.rb +31 -0
- data/app/components/databasium/type_select.rb +35 -0
- data/app/controllers/databasium/application_controller.rb +68 -0
- data/app/controllers/databasium/homepage_controller.rb +5 -0
- data/app/controllers/databasium/migrations_controller.rb +186 -0
- data/app/controllers/databasium/models_controller.rb +105 -0
- data/app/controllers/databasium/records_controller.rb +156 -0
- data/app/controllers/databasium/schemas_controller.rb +52 -0
- data/app/helpers/databasium/application_helper.rb +4 -0
- data/app/helpers/databasium/heroicon_helper.rb +21 -0
- data/app/helpers/databasium/models_helper.rb +4 -0
- data/app/jobs/databasium/application_job.rb +4 -0
- data/app/mailers/databasium/application_mailer.rb +6 -0
- data/app/models/databasium/application_record.rb +5 -0
- data/app/models/model.json +0 -0
- data/app/services/databasium/migration.rb +176 -0
- data/app/services/databasium/model.rb +182 -0
- data/app/services/databasium/record.rb +65 -0
- data/app/services/databasium/schema.rb +146 -0
- data/app/views/base.rb +13 -0
- data/app/views/databasium/errors/non_development.rb +21 -0
- data/app/views/databasium/homepage/index.rb +29 -0
- data/app/views/databasium/migrations/index.rb +33 -0
- data/app/views/databasium/migrations/new.rb +29 -0
- data/app/views/databasium/models/index.rb +31 -0
- data/app/views/databasium/models/new.rb +37 -0
- data/app/views/databasium/records/index.rb +24 -0
- data/app/views/databasium/schemas/index.rb +39 -0
- data/app/views/layouts/databasium/application.rb +56 -0
- data/config/importmap.rb +12 -0
- data/config/initializers/heroicon.rb +12 -0
- data/config/initializers/pagy.rb +48 -0
- data/config/initializers/phlex.rb +19 -0
- data/config/routes.rb +31 -0
- data/config/tailwind.config.js +10 -0
- data/lib/databasium/engine.rb +57 -0
- data/lib/databasium/engine_mount.rb +37 -0
- data/lib/databasium/middleware/conditional_check_pending.rb +27 -0
- data/lib/databasium/templates/create_table_migration.rb.tt +29 -0
- data/lib/databasium/templates/migration.rb.tt +48 -0
- data/lib/databasium/templates/model.rb.tt +23 -0
- data/lib/databasium/version.rb +3 -0
- data/lib/databasium.rb +11 -0
- data/lib/tasks/databasium_tasks.rake +4 -0
- 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,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
|
+
}
|