airview 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 +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +65 -0
- data/app/assets/javascripts/airview/application.js +425 -0
- data/app/assets/stylesheets/airview/application.css +1239 -0
- data/app/controllers/airview/application_controller.rb +7 -0
- data/app/controllers/airview/resources_controller.rb +261 -0
- data/app/controllers/airview/setup_controller.rb +90 -0
- data/app/controllers/airview/views_controller.rb +76 -0
- data/app/helpers/airview/application_helper.rb +479 -0
- data/app/javascript/airview/controllers/grid_controller.js +7 -0
- data/app/models/airview/application_record.rb +7 -0
- data/app/models/airview/field_definition.rb +22 -0
- data/app/models/airview/resource_definition.rb +48 -0
- data/app/models/airview/view.rb +27 -0
- data/app/views/airview/resources/_delete_modal.html.erb +49 -0
- data/app/views/airview/resources/_record_modal.html.erb +62 -0
- data/app/views/airview/resources/empty.html.erb +5 -0
- data/app/views/airview/resources/show.html.erb +258 -0
- data/app/views/airview/setup/_form.html.erb +73 -0
- data/app/views/airview/setup/edit.html.erb +16 -0
- data/app/views/airview/setup/index.html.erb +52 -0
- data/app/views/airview/setup/new.html.erb +14 -0
- data/app/views/airview/shared/_sidebar.html.erb +76 -0
- data/app/views/layouts/airview/application.html.erb +21 -0
- data/config/routes.rb +23 -0
- data/db/migrate/20260806000000_create_airview_views.rb +57 -0
- data/lib/airview/configuration.rb +6 -0
- data/lib/airview/engine.rb +15 -0
- data/lib/airview/field.rb +81 -0
- data/lib/airview/model_discovery.rb +38 -0
- data/lib/airview/query.rb +194 -0
- data/lib/airview/resource.rb +51 -0
- data/lib/airview/resource_builder.rb +62 -0
- data/lib/airview/schema_inference.rb +196 -0
- data/lib/airview/version.rb +5 -0
- data/lib/airview.rb +93 -0
- data/lib/generators/airview/install/install_generator.rb +38 -0
- data/lib/generators/airview/install/templates/add_folders_to_airview_views.rb +8 -0
- data/lib/generators/airview/install/templates/airview.rb +12 -0
- data/lib/generators/airview/install/templates/create_airview_views.rb +57 -0
- data/sig/airview.rbs +54 -0
- metadata +110 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 28182d9bbb704849b5ede094038a3d6b031826c119b704f40691578639d592fd
|
|
4
|
+
data.tar.gz: ae015890868c3cf07e46e46b947baa597998937f7a39f505c17944c7561789c7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 44327f49faf4823b0439094b990d4ea007b76f39dc7c1d00f05550551cc662f96af4e5ce693e716ca51ade47c4a1058ea6970247153fca22549c92e5f82ebf8c
|
|
7
|
+
data.tar.gz: 107620ed78ff8f7477d3b435ff98696b40b0925003446834f497866aa6d21753d29fa5077a9734a07e30dc0a19b302b6a8ad0fb096ba7e97a6dfe72fae8181f2
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"airview" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["risojackson@gmail.com"](mailto:"risojackson@gmail.com").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jackson Riso
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Airview
|
|
2
|
+
|
|
3
|
+
Airview is a mountable Rails engine for building an Airtable-like internal admin UI on top of a Rails application's own database.
|
|
4
|
+
|
|
5
|
+
It lets trusted internal users enable Rails models through the Airview setup UI, then browse records, filter and sort tables, save views, create records, edit cells, delete rows, and work with simple `belongs_to` references.
|
|
6
|
+
|
|
7
|
+
Airview does not authenticate users. Protect the mounted route in your host Rails app.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add the gem to a Rails 7.1+ application:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle add airview
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run the installer and migrate:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bin/rails generate airview:install
|
|
21
|
+
bin/rails db:migrate
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Mount the engine behind your app's authentication:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# config/routes.rb
|
|
30
|
+
authenticate :user, ->(user) { user.admin? } do
|
|
31
|
+
mount Airview::Engine => "/airview"
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Visit `/airview/setup` to configure resources. Airview discovers concrete host-app ActiveRecord models, pre-fills fields from the schema cache, hides sensitive-looking fields by default, and saves enabled resources in Airview-owned database tables.
|
|
36
|
+
|
|
37
|
+
Resource configuration is DB-managed. The initializer is reserved for future global settings.
|
|
38
|
+
|
|
39
|
+
Supported field types:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
:string, :text, :integer, :float, :decimal, :boolean, :date, :datetime, :select, :belongs_to, :json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Only enabled resources and visible fields are shown. Creates, updates, and deletes go through ActiveRecord, so model validations and callbacks still apply.
|
|
46
|
+
|
|
47
|
+
## Development
|
|
48
|
+
|
|
49
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt.
|
|
50
|
+
|
|
51
|
+
Run tests and linting:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
bundle exec rake
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
58
|
+
|
|
59
|
+
## Contributing
|
|
60
|
+
|
|
61
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jacksonriso/airview. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
|
|
62
|
+
|
|
63
|
+
## Code of Conduct
|
|
64
|
+
|
|
65
|
+
Everyone interacting in the Airview project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
function storageKey(table) {
|
|
3
|
+
return "airview:columns:" + window.location.pathname + ":" + table.dataset.airviewGridKey;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function cellAt(table, rowIndex, cellIndex) {
|
|
7
|
+
var row = table.tBodies[0] && table.tBodies[0].rows[rowIndex];
|
|
8
|
+
return row && row.cells[cellIndex];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function loadColumnWidths(table) {
|
|
12
|
+
var raw = window.localStorage.getItem(storageKey(table));
|
|
13
|
+
if (!raw) return;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
var widths = JSON.parse(raw);
|
|
17
|
+
Array.prototype.forEach.call(table.querySelectorAll("th[data-airview-column]"), function (header) {
|
|
18
|
+
var width = widths[header.dataset.airviewColumn];
|
|
19
|
+
if (width) setColumnWidth(table, header, width);
|
|
20
|
+
});
|
|
21
|
+
} catch (error) {
|
|
22
|
+
window.localStorage.removeItem(storageKey(table));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function columnForHeader(table, header) {
|
|
27
|
+
return table.querySelector('col[data-airview-column-width="' + header.dataset.airviewColumn + '"]');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setColumnWidth(table, header, width) {
|
|
31
|
+
var column = columnForHeader(table, header);
|
|
32
|
+
var value = Math.max(64, width) + "px";
|
|
33
|
+
|
|
34
|
+
if (column) column.style.width = value;
|
|
35
|
+
header.style.width = value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function saveColumnWidths(table) {
|
|
39
|
+
var widths = {};
|
|
40
|
+
Array.prototype.forEach.call(table.querySelectorAll("th[data-airview-column]"), function (header) {
|
|
41
|
+
widths[header.dataset.airviewColumn] = header.offsetWidth;
|
|
42
|
+
});
|
|
43
|
+
window.localStorage.setItem(storageKey(table), JSON.stringify(widths));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var referenceSearchTimers = new WeakMap();
|
|
47
|
+
|
|
48
|
+
function escapeHtml(value) {
|
|
49
|
+
return String(value || "").replace(/[&<>"']/g, function (character) {
|
|
50
|
+
return {
|
|
51
|
+
"&": "&",
|
|
52
|
+
"<": "<",
|
|
53
|
+
">": ">",
|
|
54
|
+
'"': """,
|
|
55
|
+
"'": "'"
|
|
56
|
+
}[character];
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function referenceElements(element) {
|
|
61
|
+
var wrapper = element.closest(".airview-reference-picker");
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
wrapper: wrapper,
|
|
65
|
+
hidden: wrapper && wrapper.parentElement.querySelector("[data-airview-reference-target]"),
|
|
66
|
+
input: wrapper && wrapper.querySelector("[data-airview-reference-picker]"),
|
|
67
|
+
results: wrapper && wrapper.querySelector("[data-airview-reference-results]"),
|
|
68
|
+
preview: wrapper && wrapper.parentElement.querySelector("[data-airview-reference-preview]")
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function setReferencePreview(preview, record) {
|
|
73
|
+
if (!preview) return;
|
|
74
|
+
|
|
75
|
+
if (!record) {
|
|
76
|
+
preview.classList.add("is-empty");
|
|
77
|
+
preview.innerHTML = "+ Link record";
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
var detailText = (record.preview || []).map(function (item) {
|
|
82
|
+
return item.label + ": " + item.value;
|
|
83
|
+
}).join(" · ");
|
|
84
|
+
|
|
85
|
+
preview.classList.remove("is-empty");
|
|
86
|
+
preview.innerHTML = [
|
|
87
|
+
'<div class="airview-reference-preview-title">' + escapeHtml(record.label) + '</div>',
|
|
88
|
+
detailText ? '<div class="airview-reference-preview-details">' + escapeHtml(detailText) + '</div>' : "",
|
|
89
|
+
record.open_url ? '<a class="airview-reference-open" href="' + escapeHtml(record.open_url) + '">Open record</a>' : ""
|
|
90
|
+
].join("");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderReferenceResults(elements, records) {
|
|
94
|
+
if (!elements.results) return;
|
|
95
|
+
|
|
96
|
+
if (!records.length) {
|
|
97
|
+
elements.results.innerHTML = '<div class="airview-reference-no-results">No matching records</div>';
|
|
98
|
+
elements.results.classList.add("is-open");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
elements.results.innerHTML = records.map(function (record) {
|
|
103
|
+
var details = (record.preview || []).slice(0, 3).map(function (item) {
|
|
104
|
+
return item.label + ": " + item.value;
|
|
105
|
+
}).join(" · ");
|
|
106
|
+
|
|
107
|
+
return [
|
|
108
|
+
'<button type="button" class="airview-reference-result" data-airview-reference-result',
|
|
109
|
+
' data-record-id="' + escapeHtml(record.id) + '"',
|
|
110
|
+
' data-record-label="' + escapeHtml(record.label) + '"',
|
|
111
|
+
' data-record-payload="' + escapeHtml(JSON.stringify(record)) + '">',
|
|
112
|
+
'<span class="airview-reference-result-title">' + escapeHtml(record.label) + '</span>',
|
|
113
|
+
'<span class="airview-reference-result-meta">Id: ' + escapeHtml(record.id) + (details ? " · " + escapeHtml(details) : "") + '</span>',
|
|
114
|
+
'</button>'
|
|
115
|
+
].join("");
|
|
116
|
+
}).join("");
|
|
117
|
+
elements.results.classList.add("is-open");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function searchReferencePicker(input, clearValue) {
|
|
121
|
+
var elements = referenceElements(input);
|
|
122
|
+
if (!elements.wrapper || !elements.results) return;
|
|
123
|
+
|
|
124
|
+
window.clearTimeout(referenceSearchTimers.get(input));
|
|
125
|
+
referenceSearchTimers.set(input, window.setTimeout(function () {
|
|
126
|
+
var url = new URL(elements.wrapper.dataset.airviewReferenceUrl, window.location.origin);
|
|
127
|
+
var query = input.value.trim();
|
|
128
|
+
if (clearValue && elements.hidden) elements.hidden.value = "";
|
|
129
|
+
if (query) url.searchParams.set("q", query);
|
|
130
|
+
|
|
131
|
+
elements.results.innerHTML = '<div class="airview-reference-no-results">Searching...</div>';
|
|
132
|
+
elements.results.classList.add("is-open");
|
|
133
|
+
|
|
134
|
+
window.fetch(url.toString(), { headers: { Accept: "application/json" } })
|
|
135
|
+
.then(function (response) { return response.json(); })
|
|
136
|
+
.then(function (records) { renderReferenceResults(elements, records); })
|
|
137
|
+
.catch(function () {
|
|
138
|
+
elements.results.innerHTML = '<div class="airview-reference-no-results">Unable to load records</div>';
|
|
139
|
+
elements.results.classList.add("is-open");
|
|
140
|
+
});
|
|
141
|
+
}, 180));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function selectReferenceResult(button) {
|
|
145
|
+
var elements = referenceElements(button);
|
|
146
|
+
if (!elements.hidden || !elements.input) return;
|
|
147
|
+
|
|
148
|
+
var record = JSON.parse(button.dataset.recordPayload);
|
|
149
|
+
elements.hidden.value = button.dataset.recordId;
|
|
150
|
+
elements.input.value = button.dataset.recordLabel;
|
|
151
|
+
if (elements.results) elements.results.classList.remove("is-open");
|
|
152
|
+
setReferencePreview(elements.preview, record);
|
|
153
|
+
|
|
154
|
+
var form = button.closest(".airview-cell-form");
|
|
155
|
+
if (form) form.requestSubmit();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function filterFieldList(input) {
|
|
159
|
+
var query = input.value.toLowerCase().trim();
|
|
160
|
+
var panel = input.closest(".airview-popover-panel");
|
|
161
|
+
if (!panel) return;
|
|
162
|
+
|
|
163
|
+
Array.prototype.forEach.call(panel.querySelectorAll("[data-airview-field-list-item]"), function (item) {
|
|
164
|
+
item.classList.toggle("is-hidden", !item.dataset.airviewFieldListItem.includes(query));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function filterFieldPickerElements(element) {
|
|
169
|
+
var picker = element.closest("[data-airview-filter-field-picker]");
|
|
170
|
+
var row = picker && picker.closest("[data-airview-filter-condition]");
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
picker: picker,
|
|
174
|
+
row: row,
|
|
175
|
+
input: picker && picker.querySelector("[data-airview-filter-field-search]"),
|
|
176
|
+
results: picker && picker.querySelector("[data-airview-filter-field-results]"),
|
|
177
|
+
hidden: row && row.querySelector("[data-airview-field-target]")
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function searchFilterFieldPicker(input) {
|
|
182
|
+
var elements = filterFieldPickerElements(input);
|
|
183
|
+
var query = input.value.toLowerCase().trim();
|
|
184
|
+
if (!elements.results) return;
|
|
185
|
+
|
|
186
|
+
Array.prototype.forEach.call(elements.results.querySelectorAll("[data-airview-filter-field-option]"), function (option) {
|
|
187
|
+
var text = [option.dataset.fieldLabel, option.dataset.fieldName].join(" ").toLowerCase();
|
|
188
|
+
option.classList.toggle("is-hidden", !text.includes(query));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
elements.results.classList.add("is-open");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function operatorOptionsForFieldType(fieldType) {
|
|
195
|
+
if (fieldType === "boolean") return [["is checked", "is_true"], ["is unchecked", "is_false"]];
|
|
196
|
+
if (["integer", "float", "decimal"].includes(fieldType)) {
|
|
197
|
+
return [["=", "equals"], [">", "gt"], ["<", "lt"], [">=", "gte"], ["<=", "lte"], ["is empty", "is_empty"]];
|
|
198
|
+
}
|
|
199
|
+
if (["date", "datetime"].includes(fieldType)) return [["is", "equals"], ["before", "before"], ["after", "after"], ["is empty", "is_empty"]];
|
|
200
|
+
|
|
201
|
+
return [["contains", "contains"], ["equals", "equals"], ["starts with", "starts_with"], ["is empty", "is_empty"]];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function updateFilterOperator(row, fieldType) {
|
|
205
|
+
var select = row.querySelector('select[name$="[operator]"]');
|
|
206
|
+
if (!select) return;
|
|
207
|
+
|
|
208
|
+
select.innerHTML = operatorOptionsForFieldType(fieldType).map(function (option) {
|
|
209
|
+
return '<option value="' + escapeHtml(option[1]) + '">' + escapeHtml(option[0]) + '</option>';
|
|
210
|
+
}).join("");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function updateFilterValue(row, fieldType) {
|
|
214
|
+
var current = row.querySelector("[data-airview-filter-value]");
|
|
215
|
+
if (!current) return;
|
|
216
|
+
|
|
217
|
+
var name = current.name;
|
|
218
|
+
var replacement;
|
|
219
|
+
|
|
220
|
+
if (fieldType === "boolean") {
|
|
221
|
+
replacement = document.createElement("select");
|
|
222
|
+
replacement.innerHTML = '<option value="true">checked</option><option value="false">unchecked</option>';
|
|
223
|
+
} else {
|
|
224
|
+
replacement = document.createElement("input");
|
|
225
|
+
replacement.placeholder = fieldType === "date" || fieldType === "datetime" ? "" : "Value";
|
|
226
|
+
replacement.type = ["date", "datetime"].includes(fieldType) ? "date" : ["integer", "float", "decimal"].includes(fieldType) ? "number" : "text";
|
|
227
|
+
if (replacement.type === "number") replacement.step = "any";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
replacement.name = name;
|
|
231
|
+
replacement.dataset.airviewFilterValue = "true";
|
|
232
|
+
current.replaceWith(replacement);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function selectFilterField(button) {
|
|
236
|
+
var elements = filterFieldPickerElements(button);
|
|
237
|
+
if (!elements.input || !elements.hidden || !elements.results) return;
|
|
238
|
+
|
|
239
|
+
elements.input.value = button.dataset.fieldLabel;
|
|
240
|
+
elements.hidden.value = button.dataset.fieldName;
|
|
241
|
+
elements.results.classList.remove("is-open");
|
|
242
|
+
updateFilterOperator(elements.row, button.dataset.fieldType);
|
|
243
|
+
updateFilterValue(elements.row, button.dataset.fieldType);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function toggleModalHiddenField(checkbox) {
|
|
247
|
+
var modal = checkbox.closest("[data-airview-modal]");
|
|
248
|
+
if (!modal) return;
|
|
249
|
+
|
|
250
|
+
var selector = '[data-airview-modal-hidden-field="' + checkbox.dataset.airviewModalFieldToggle + '"]';
|
|
251
|
+
var row = modal.querySelector(selector);
|
|
252
|
+
if (row) row.classList.toggle("is-hidden", !checkbox.checked);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
256
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-airview-grid] table"), function (table, index) {
|
|
257
|
+
table.dataset.airviewGridKey = String(index);
|
|
258
|
+
loadColumnWidths(table);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
document.addEventListener("mousedown", function (event) {
|
|
263
|
+
if (!event.target.matches("[data-airview-resizer]")) return;
|
|
264
|
+
|
|
265
|
+
var header = event.target.closest("th");
|
|
266
|
+
var table = header.closest("table");
|
|
267
|
+
var startX = event.clientX;
|
|
268
|
+
var startWidth = header.offsetWidth;
|
|
269
|
+
|
|
270
|
+
function move(moveEvent) {
|
|
271
|
+
var width = Math.max(64, startWidth + moveEvent.clientX - startX);
|
|
272
|
+
setColumnWidth(table, header, width);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function stop() {
|
|
276
|
+
document.removeEventListener("mousemove", move);
|
|
277
|
+
document.removeEventListener("mouseup", stop);
|
|
278
|
+
saveColumnWidths(table);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
document.addEventListener("mousemove", move);
|
|
283
|
+
document.addEventListener("mouseup", stop);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
document.addEventListener("keydown", function (event) {
|
|
287
|
+
var cell = event.target.closest && event.target.closest(".airview-grid td");
|
|
288
|
+
if (!cell) return;
|
|
289
|
+
|
|
290
|
+
var table = cell.closest("table");
|
|
291
|
+
var row = cell.parentElement;
|
|
292
|
+
var rowIndex = row.rowIndex - 1;
|
|
293
|
+
var cellIndex = cell.cellIndex;
|
|
294
|
+
var next = null;
|
|
295
|
+
|
|
296
|
+
if (event.key === "ArrowRight") next = cellAt(table, rowIndex, cellIndex + 1);
|
|
297
|
+
if (event.key === "ArrowLeft") next = cellAt(table, rowIndex, cellIndex - 1);
|
|
298
|
+
if (event.key === "ArrowDown") next = cellAt(table, rowIndex + 1, cellIndex);
|
|
299
|
+
if (event.key === "ArrowUp") next = cellAt(table, rowIndex - 1, cellIndex);
|
|
300
|
+
|
|
301
|
+
if (next) {
|
|
302
|
+
event.preventDefault();
|
|
303
|
+
next.focus();
|
|
304
|
+
var input = next.querySelector("input:not([type=hidden]), textarea, select");
|
|
305
|
+
if (input) input.focus();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
document.addEventListener("click", function (event) {
|
|
310
|
+
if (event.target.matches("[data-airview-add-filter]")) {
|
|
311
|
+
var form = event.target.closest("form");
|
|
312
|
+
var template = form.querySelector("[data-airview-filter-template]");
|
|
313
|
+
var count = form.querySelectorAll("[data-airview-filter-condition]").length;
|
|
314
|
+
var html = template.innerHTML.replace(/__INDEX__/g, String(count));
|
|
315
|
+
|
|
316
|
+
event.target.closest(".airview-filter-actions").insertAdjacentHTML("beforebegin", html);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (event.target.matches("[data-airview-remove-filter]")) {
|
|
320
|
+
var filterForm = event.target.closest("form");
|
|
321
|
+
event.target.closest("[data-airview-filter-condition]").remove();
|
|
322
|
+
if (filterForm) filterForm.requestSubmit();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
var filterFieldOption = event.target.closest && event.target.closest("[data-airview-filter-field-option]");
|
|
326
|
+
if (filterFieldOption) {
|
|
327
|
+
selectFilterField(filterFieldOption);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
var referenceResult = event.target.closest && event.target.closest("[data-airview-reference-result]");
|
|
331
|
+
if (referenceResult) {
|
|
332
|
+
selectReferenceResult(referenceResult);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (event.target.matches("[data-airview-reference-clear]")) {
|
|
336
|
+
var wrapper = event.target.closest(".airview-reference-picker");
|
|
337
|
+
var hidden = wrapper && wrapper.parentElement.querySelector("[data-airview-reference-target]");
|
|
338
|
+
var input = wrapper && wrapper.querySelector("[data-airview-reference-picker]");
|
|
339
|
+
var results = wrapper && wrapper.querySelector("[data-airview-reference-results]");
|
|
340
|
+
var preview = wrapper && wrapper.parentElement.querySelector("[data-airview-reference-preview]");
|
|
341
|
+
|
|
342
|
+
if (hidden) hidden.value = "";
|
|
343
|
+
if (input) input.value = "";
|
|
344
|
+
if (results) results.classList.remove("is-open");
|
|
345
|
+
setReferencePreview(preview, null);
|
|
346
|
+
|
|
347
|
+
var clearForm = event.target.closest(".airview-cell-form");
|
|
348
|
+
if (clearForm) clearForm.requestSubmit();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (event.target.matches("[data-airview-fields-hide-all], [data-airview-fields-show-all]")) {
|
|
352
|
+
var checked = event.target.matches("[data-airview-fields-show-all]");
|
|
353
|
+
var panel = event.target.closest(".airview-popover-panel");
|
|
354
|
+
|
|
355
|
+
Array.prototype.forEach.call(panel.querySelectorAll('input[name="columns[]"]'), function (checkbox) {
|
|
356
|
+
checkbox.checked = checked;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
Array.prototype.forEach.call(document.querySelectorAll(".airview-popover[open]"), function (details) {
|
|
361
|
+
if (!details.contains(event.target)) details.removeAttribute("open");
|
|
362
|
+
});
|
|
363
|
+
Array.prototype.forEach.call(document.querySelectorAll(".airview-column-menu[open]"), function (details) {
|
|
364
|
+
if (!details.contains(event.target)) details.removeAttribute("open");
|
|
365
|
+
});
|
|
366
|
+
Array.prototype.forEach.call(document.querySelectorAll(".airview-view-menu[open]"), function (details) {
|
|
367
|
+
if (!details.contains(event.target)) details.removeAttribute("open");
|
|
368
|
+
});
|
|
369
|
+
Array.prototype.forEach.call(document.querySelectorAll(".airview-reference-results.is-open"), function (results) {
|
|
370
|
+
if (!results.parentElement.contains(event.target)) results.classList.remove("is-open");
|
|
371
|
+
});
|
|
372
|
+
Array.prototype.forEach.call(document.querySelectorAll(".airview-filter-field-results.is-open"), function (results) {
|
|
373
|
+
if (!results.parentElement.contains(event.target)) results.classList.remove("is-open");
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
document.addEventListener("change", function (event) {
|
|
378
|
+
if (event.target.matches("[data-airview-modal-field-toggle]")) {
|
|
379
|
+
toggleModalHiddenField(event.target);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
var form = event.target.closest && event.target.closest(".airview-cell-form");
|
|
384
|
+
if (form) form.requestSubmit();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
document.addEventListener("input", function (event) {
|
|
388
|
+
if (event.target.matches("[data-airview-field-list-search]")) {
|
|
389
|
+
filterFieldList(event.target);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (event.target.matches("[data-airview-reference-picker]")) {
|
|
393
|
+
searchReferencePicker(event.target, true);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (event.target.matches("[data-airview-filter-field-search]")) {
|
|
397
|
+
searchFilterFieldPicker(event.target);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
document.addEventListener("focusin", function (event) {
|
|
402
|
+
if (event.target.matches("[data-airview-reference-picker]")) {
|
|
403
|
+
searchReferencePicker(event.target, false);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (event.target.matches("[data-airview-filter-field-search]")) {
|
|
407
|
+
searchFilterFieldPicker(event.target);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
document.addEventListener("keydown", function (event) {
|
|
412
|
+
if (event.key === "Escape") {
|
|
413
|
+
var close = document.querySelector("[data-airview-modal-close]");
|
|
414
|
+
if (close) close.click();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
418
|
+
var form = event.target.closest && event.target.closest(".airview-cell-form");
|
|
419
|
+
if (form) {
|
|
420
|
+
event.preventDefault();
|
|
421
|
+
form.requestSubmit();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
})();
|