querylane 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +92 -1
- data/CONTRIBUTING.md +1 -1
- data/README.md +2 -0
- data/app/assets/stylesheets/querylane.css +28 -6
- data/app/javascript/querylane/controllers/box_controller.js +1 -1
- data/app/javascript/querylane/controllers/results_controller.js +12 -1
- data/app/views/querylane/_chips.html.erb +2 -1
- data/app/views/querylane/_denied.html.erb +2 -1
- data/app/views/querylane/_empty.html.erb +10 -4
- data/app/views/querylane/_facets.html.erb +3 -2
- data/app/views/querylane/_first_load.html.erb +2 -1
- data/app/views/querylane/_form.html.erb +19 -9
- data/app/views/querylane/_group.html.erb +2 -1
- data/app/views/querylane/_help.html.erb +3 -2
- data/app/views/querylane/_options.html.erb +5 -3
- data/app/views/querylane/_options_row.html.erb +18 -7
- data/app/views/querylane/_outcome.html.erb +5 -3
- data/app/views/querylane/_pager.html.erb +3 -2
- data/app/views/querylane/_preview.html.erb +2 -1
- data/app/views/querylane/_record.html.erb +2 -1
- data/app/views/querylane/_related.html.erb +2 -1
- data/app/views/querylane/_results.html.erb +2 -2
- data/app/views/querylane/_rows.html.erb +2 -1
- data/app/views/querylane/_saved.html.erb +15 -6
- data/app/views/querylane/_suggest_box.html.erb +10 -7
- data/app/views/querylane/_suggestions.html.erb +2 -2
- data/app/views/querylane/_toolbar.html.erb +2 -1
- data/app/views/querylane/_unavailable.html.erb +2 -1
- data/app/views/querylane/_writes.html.erb +4 -3
- data/config/locales/querylane.en.yml +32 -0
- data/lib/querylane/chips.rb +24 -7
- data/lib/querylane/configuration.rb +9 -0
- data/lib/querylane/controller.rb +5 -1
- data/lib/querylane/copy.rb +28 -0
- data/lib/querylane/definition.rb +44 -10
- data/lib/querylane/parser.rb +16 -13
- data/lib/querylane/presenter.rb +19 -7
- data/lib/querylane/query.rb +59 -17
- data/lib/querylane/request.rb +32 -13
- data/lib/querylane/rewriter.rb +44 -2
- data/lib/querylane/search.rb +12 -0
- data/lib/querylane/searchers/meilisearch.rb +19 -2
- data/lib/querylane/version.rb +1 -1
- data/lib/querylane.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cae1575fd12bcaf1bd45457aebfde411632341db4dbe029019cd3c683e839497
|
|
4
|
+
data.tar.gz: 02c42e8191a53bf52c19360f3542218cd7e8b2c7c158e08cdefcaabc29030999
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44c16d7ee50b1a3a6bd8408f72f098f49c330e301f66908892c0ed5e32c4dcef3a12330e4769dc7c541b2e41d209a820ffa99a67f1bcd25557fe0def64bd9022
|
|
7
|
+
data.tar.gz: 86e14bc61a17bcefe91ecdd1775ef9333b8dc67aded55ce420f25a167ce394e83036a0217a15cd2f90488fdab6dee35ef1e5ad668b9e2f305019525d2b9f7116
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,95 @@ change public APIs; each entry says how.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-11-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Confirmed per-entry saved-search deletion through the CSRF-protected POST
|
|
15
|
+
form. Stores implement `delete(context:, id:)` with a current-user-scoped
|
|
16
|
+
lookup and a boolean result. Deletion preserves the active query, sort and
|
|
17
|
+
page, updates the Saved star, and refuses denied scopes and GET mutations.
|
|
18
|
+
The saved-search popover stays inside narrow mobile viewports. Each entry
|
|
19
|
+
shares one hover/focus highlight with its inline delete icon, while Apply
|
|
20
|
+
and Delete keep separate, non-overlapping click targets.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- The saved-search POST-only guard and request parser share normalization for
|
|
25
|
+
symbol- and string-keyed params, including false-valued host params, even
|
|
26
|
+
when GET search queries are enabled.
|
|
27
|
+
|
|
28
|
+
## [0.2.0] - 2026-11-10
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `config.logger`: querylane logs why a search was unavailable (Meilisearch
|
|
33
|
+
unreachable, timed out, or an API error such as a missing index). API
|
|
34
|
+
errors are logged by code, type and HTTP status only, never their message,
|
|
35
|
+
which can echo the filter; connection errors lose any URL credentials and
|
|
36
|
+
query string. The default logger writes to standard output; a Rails host
|
|
37
|
+
passes `Rails.logger`.
|
|
38
|
+
- `p.row ... hint:, placeholder:` on options-panel rows: a line under the row
|
|
39
|
+
(its fields' `aria-describedby`) and a placeholder for its empty inputs (a
|
|
40
|
+
masked field's "In the box: …" still wins). `placeholder:` on a flags or
|
|
41
|
+
options row raises.
|
|
42
|
+
- `d.filter ... invalid:`: a second sentence for the bad-value error.
|
|
43
|
+
- `d.filter ... as_words: true`: when nothing matches, the empty state offers
|
|
44
|
+
"Search "smiht" as part of a name" (`querylane.states.as_words`, request
|
|
45
|
+
param `as_words=<chip index>`, `Rewriter.as_words`), which replaces the
|
|
46
|
+
filter term with its typed value as plain words. Refused on a sensitive
|
|
47
|
+
filter.
|
|
48
|
+
- `d.filter ... display_value: :typed`: chips and the options panel show the
|
|
49
|
+
value as typed, and the panel writes it back as typed; the search still
|
|
50
|
+
uses the normalized value. Needs `mask: :none`.
|
|
51
|
+
- `hint:`, `placeholder:` and `invalid:` take a locale key (a Symbol or a
|
|
52
|
+
dotted String) or plain text (`Querylane::Copy`), translated when shown.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- Development and CI dependencies: rspec-rails 8.1.0, playwright 1.65.0,
|
|
57
|
+
ruby/setup-ruby 1.338.0.
|
|
58
|
+
- Development Ruby 3.3.13 (from 3.3.10): the lowest supported series' release
|
|
59
|
+
with the Onigmo regexp fixes (CVE-2026-89078, CVE-2026-93577,
|
|
60
|
+
CVE-2026-97229, CVE-2026-110264). `required_ruby_version` and the CI
|
|
61
|
+
matrix are unchanged.
|
|
62
|
+
- Every query error is a translation under `querylane.errors.*` (syntax,
|
|
63
|
+
limits, unknown and parked operators, bad filter and flag values, names in
|
|
64
|
+
OR, NOT and parentheses, one text operator). The English text is unchanged
|
|
65
|
+
and stays the default (`Querylane::Query::MESSAGES`).
|
|
66
|
+
- With no filters declared, the one-text-operator error no longer ends in
|
|
67
|
+
"or combine with ."; it reads "Search name or id one at a time."
|
|
68
|
+
- `Definition::FilterOperator` gains `invalid`, `as_words` and
|
|
69
|
+
`display_value`, `Definition::PanelRow` gains `hint` and `placeholder`, and
|
|
70
|
+
`Chips::Chip` gains `as_words`. Each new member has a default, so code that
|
|
71
|
+
builds these structs with the earlier members, by position or keyword,
|
|
72
|
+
still works.
|
|
73
|
+
|
|
74
|
+
- Every partial declares its locals on its first line (Rails strict locals,
|
|
75
|
+
`<%# locals: (...) -%>`). A host override must keep the line, and a render
|
|
76
|
+
with other locals now raises. `_help` takes `popover_id: nil` instead of
|
|
77
|
+
reading `local_assigns`.
|
|
78
|
+
- Controls follow one hierarchy (theming guide, "Bootstrap"): the box's
|
|
79
|
+
Search and the options panel's Search are outline buttons
|
|
80
|
+
(`btn-outline-primary`); Open and View are Go links (`.ql-go`, still a
|
|
81
|
+
button that posts); the empty state's actions and the panel's Cancel are
|
|
82
|
+
quiet links (`.ql-link`). `Presenter::Action#style` no longer changes the
|
|
83
|
+
look.
|
|
84
|
+
- No Bootstrap utility classes: `.ql-visually-hidden` replaces
|
|
85
|
+
`visually-hidden`, `.ql-open-form` replaces `d-inline`, and `.ql-pager`
|
|
86
|
+
sets its own margin instead of `mb-0`.
|
|
87
|
+
- The search box and the suggestion box sit in a `<search>` landmark
|
|
88
|
+
(`.ql-search`) instead of a form with `role="search"`.
|
|
89
|
+
- The facet column's accessible name comes from `querylane.facets.label`.
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
|
|
93
|
+
- Space on a record right after a new answer drew its rows opens the preview.
|
|
94
|
+
The preview state was reset on `turbo:frame-render`, which Turbo fires a few
|
|
95
|
+
repaints after the rows are in, so the preview's answer was dropped as if
|
|
96
|
+
Esc had closed it. It is now reset when the new, empty preview frame
|
|
97
|
+
connects.
|
|
98
|
+
|
|
10
99
|
## [0.1.0] - 2026-10-20
|
|
11
100
|
|
|
12
101
|
The first release.
|
|
@@ -80,5 +169,7 @@ The first release.
|
|
|
80
169
|
- `search.querylane` and `saved_search_failed.querylane` instrumentation, without
|
|
81
170
|
query text. The query parameter and `compose` are filtered from logs.
|
|
82
171
|
|
|
83
|
-
[Unreleased]: https://github.com/clearstackio/querylane/compare/v0.
|
|
172
|
+
[Unreleased]: https://github.com/clearstackio/querylane/compare/v0.3.0...HEAD
|
|
173
|
+
[0.3.0]: https://github.com/clearstackio/querylane/compare/v0.2.0...v0.3.0
|
|
174
|
+
[0.2.0]: https://github.com/clearstackio/querylane/compare/v0.1.0...v0.2.0
|
|
84
175
|
[0.1.0]: https://github.com/clearstackio/querylane/releases/tag/v0.1.0
|
data/CONTRIBUTING.md
CHANGED
|
@@ -73,7 +73,7 @@ also the example the guides quote.
|
|
|
73
73
|
request (`spec/requests/`) or browser (`spec/system/`).
|
|
74
74
|
- **Changelog:** add a line under `[Unreleased]` in [CHANGELOG.md](CHANGELOG.md)
|
|
75
75
|
for anything a host would notice.
|
|
76
|
-
- **CI:** GitHub Actions (
|
|
76
|
+
- **CI:** GitHub Actions ([design](docs/design/ci.md), [operations](docs/ops/ci.md)): `rake` on Ruby 3.3, 3.4
|
|
77
77
|
and 4.0 against Meilisearch 1.13 and headless Chromium. Actions are pinned to
|
|
78
78
|
commit SHAs. FracturedJSON is not run in CI; run `bin/lint` before pushing.
|
|
79
79
|
- **Security:** no secrets in the repo; see [SECURITY.md](SECURITY.md).
|
data/README.md
CHANGED
|
@@ -169,6 +169,8 @@ A complete, working version of this example lives in [`spec/dummy`](spec/dummy).
|
|
|
169
169
|
Rails.application.config.to_prepare do
|
|
170
170
|
Querylane.configure do |config|
|
|
171
171
|
config.searcher = -> { Querylane::Searchers::Meilisearch.new(client: Meilisearch::Client.new(ENV["MEILISEARCH_URL"], ENV["MEILISEARCH_API_KEY"])) }
|
|
172
|
+
# Why a search could not run (default: standard output).
|
|
173
|
+
config.logger = Rails.logger
|
|
172
174
|
end
|
|
173
175
|
end
|
|
174
176
|
```
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
/* The hidden attribute wins over the display rules below. */
|
|
8
8
|
:where(.querylane-form, .ql-results, .ql-suggest, .ql-palette) [hidden] { display: none !important; }
|
|
9
|
+
/* A label read by assistive technology only. */
|
|
10
|
+
.ql-visually-hidden { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
|
|
11
|
+
clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
12
|
+
/* The search landmark around a box and its form. */
|
|
13
|
+
.ql-search { display: block; }
|
|
9
14
|
|
|
10
15
|
:root {
|
|
11
16
|
--ql-accent: var(--bs-primary, #0d6efd);
|
|
@@ -69,6 +74,9 @@
|
|
|
69
74
|
border-radius: 13px; background: var(--ql-chip); color: var(--ql-text); }
|
|
70
75
|
.ql-chip-remove { border: 0; background: none; padding: 0; color: var(--ql-muted); line-height: 1; }
|
|
71
76
|
.ql-link { border: 0; background: none; padding: 0; color: var(--bs-link-color, #0865d1); font-weight: 500; }
|
|
77
|
+
/* A Go link: opens a record or a page (bold, no box), even when it is a button that posts. */
|
|
78
|
+
.ql-go { border: 0; background: none; padding: 0; color: var(--bs-link-color, #0865d1); font-weight: 700; }
|
|
79
|
+
.ql-link:hover, .ql-go:hover { text-decoration: underline; }
|
|
72
80
|
[data-querylane-stale] .ql-chips { opacity: 0.55; }
|
|
73
81
|
.ql-state { padding: 28px 0; font-size: 15px; color: var(--ql-muted); }
|
|
74
82
|
.ql-state-unavailable { padding: 20px 24px; border-left: 3px solid #977405; background: #fffbea; color: var(--ql-text); }
|
|
@@ -95,6 +103,7 @@
|
|
|
95
103
|
.ql-count-note { margin-left: 10px; font-size: 13px; font-weight: 400; color: var(--ql-muted); }
|
|
96
104
|
.ql-count { font-size: 18px; line-height: 22px; font-weight: 700; color: var(--ql-heading); }
|
|
97
105
|
.ql-toolbar-controls { display: flex; align-items: center; gap: 16px; }
|
|
106
|
+
.ql-pager { margin-bottom: 0; }
|
|
98
107
|
.ql-control { display: flex; align-items: center; gap: 6px; margin: 0; font-size: 13px; color: var(--ql-muted); }
|
|
99
108
|
.ql-control .form-select { width: auto; }
|
|
100
109
|
|
|
@@ -125,7 +134,7 @@
|
|
|
125
134
|
.ql-badge-neutral { background: var(--ql-neutral-bg); color: var(--ql-neutral-fg); }
|
|
126
135
|
.ql-flag-neutral { border: 1px solid var(--ql-border); color: var(--ql-muted); }
|
|
127
136
|
.ql-reason { font-size: 13px; line-height: 17px; color: var(--ql-muted); }
|
|
128
|
-
.ql-open {
|
|
137
|
+
.ql-open-form { display: inline; }
|
|
129
138
|
|
|
130
139
|
/* Footer ---------------------------------------------------------------------- */
|
|
131
140
|
.ql-footer { display: flex; align-items: center; justify-content: space-between; padding-top: 16px; font-size: 13px; color: var(--ql-muted); }
|
|
@@ -173,6 +182,8 @@
|
|
|
173
182
|
.ql-options-inputs { display: flex; gap: 12px; }
|
|
174
183
|
.ql-options-input { position: relative; flex: 1; min-width: 0; }
|
|
175
184
|
.ql-options-input input { width: 100%; }
|
|
185
|
+
/* A row's hint: under its fields, in the field column. */
|
|
186
|
+
.ql-options-hint { grid-column: 2; margin: -6px 0 0; color: var(--ql-muted); font-size: 12px; line-height: 16px; }
|
|
176
187
|
.ql-masked { -webkit-text-security: disc; }
|
|
177
188
|
/* Above the field even when a host lifts a focused .form-control (Bootstrap 3 themes set z-index: 2). */
|
|
178
189
|
.ql-options-reveal { position: absolute; top: 50%; right: 10px; z-index: 3; transform: translateY(-50%); font-size: 13px; }
|
|
@@ -267,11 +278,14 @@
|
|
|
267
278
|
.ql-saved-refusal { margin: 0; padding: 10px 16px; border-bottom: 1px solid var(--ql-rule); font-size: 13px; color: var(--ql-text); }
|
|
268
279
|
.ql-saved-heading { padding: 10px 16px 4px; font-size: 13px; font-weight: 600; letter-spacing: 0.06em; font-variant-caps: all-small-caps; color: var(--ql-muted); }
|
|
269
280
|
.ql-saved-none { margin: 0; padding: 4px 16px 14px; color: var(--ql-muted); font-size: 13px; }
|
|
270
|
-
.ql-saved-
|
|
271
|
-
|
|
272
|
-
.ql-saved-
|
|
273
|
-
.ql-saved-
|
|
274
|
-
.ql-saved-
|
|
281
|
+
.ql-saved-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; margin: 2px 8px; padding-right: 4px; border-radius: 4px; }
|
|
282
|
+
.ql-saved-row:hover, .ql-saved-row:focus-within { background: var(--ql-accent-soft); }
|
|
283
|
+
.ql-saved-delete { min-width: 32px; height: 32px; padding: 4px; border: 0; border-radius: 4px; background: none; color: var(--ql-muted); }
|
|
284
|
+
.ql-saved-delete:hover, .ql-saved-delete:focus-visible { color: var(--ql-error); }
|
|
285
|
+
.ql-saved-item { display: grid; grid-template-columns: 1fr auto; gap: 2px 12px; width: 100%; padding: 8px; border: 0;
|
|
286
|
+
min-width: 0; background: none; text-align: left; }
|
|
287
|
+
.ql-saved-name { overflow-wrap: anywhere; font-weight: 600; color: var(--ql-heading); }
|
|
288
|
+
.ql-saved-query { grid-column: 1; overflow-wrap: anywhere; font: 400 12px/18px var(--ql-font-mono); color: var(--ql-muted); }
|
|
275
289
|
.ql-saved-used { grid-column: 2; grid-row: 1 / span 2; align-self: center; font-size: 12px; color: var(--ql-faint); }
|
|
276
290
|
.ql-notice { margin-top: 8px; font-size: 13px; color: var(--ql-text); }
|
|
277
291
|
.ql-notice-refused, .ql-notice-store_failed { color: var(--ql-error); }
|
|
@@ -279,6 +293,14 @@
|
|
|
279
293
|
.ql-box-saved { anchor-name: --ql-saved; }
|
|
280
294
|
.ql-saved { position-anchor: --ql-saved; inset: auto; top: anchor(bottom); right: anchor(right); margin: 8px 0 0; }
|
|
281
295
|
}
|
|
296
|
+
@media (max-width: 600px) {
|
|
297
|
+
.ql-saved { left: 16px; right: 16px; width: auto; }
|
|
298
|
+
.ql-saved-head { grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
|
|
299
|
+
.ql-saved-head label { grid-column: 1 / -1; }
|
|
300
|
+
.ql-saved-head input { min-width: 0; }
|
|
301
|
+
.ql-saved-item { grid-template-columns: minmax(0, 1fr); }
|
|
302
|
+
.ql-saved-used { grid-column: 1; grid-row: auto; }
|
|
303
|
+
}
|
|
282
304
|
|
|
283
305
|
/* Searching, too short, reasons, unavailable, denied ------------------------ */
|
|
284
306
|
.ql-box-busy-label { display: none; flex-shrink: 0; font-size: 13px; color: var(--ql-muted); }
|
|
@@ -90,7 +90,7 @@ export default class extends Controller {
|
|
|
90
90
|
// new revision; the rewrite comes back at that same revision.
|
|
91
91
|
submitted(event) {
|
|
92
92
|
clearTimeout(this.timer)
|
|
93
|
-
if (["toggle", "remove", "clear", "compose_submit", "saved", "fix", "instead", "widen", "relax"].includes(event.submitter?.name)) this.bump()
|
|
93
|
+
if (["toggle", "remove", "clear", "compose_submit", "saved", "delete_saved", "fix", "instead", "widen", "relax", "as_words"].includes(event.submitter?.name)) this.bump()
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
setQuery(event) {
|
|
@@ -23,7 +23,6 @@ export default class extends Controller {
|
|
|
23
23
|
this.element.addEventListener("turbo:before-frame-render", this.guardPreview)
|
|
24
24
|
this.restore = (event) => {
|
|
25
25
|
if (event.target !== this.element) return
|
|
26
|
-
this.previewGroup = null
|
|
27
26
|
this.restoreFocus()
|
|
28
27
|
}
|
|
29
28
|
this.element.addEventListener("querylane:focus-first", this.focusFirst)
|
|
@@ -82,6 +81,10 @@ export default class extends Controller {
|
|
|
82
81
|
document.getElementById(event.target.dataset.querylaneEnter)?.click()
|
|
83
82
|
}
|
|
84
83
|
|
|
84
|
+
deleteSaved(event) {
|
|
85
|
+
if (!window.confirm(event.currentTarget.dataset.confirm)) event.preventDefault()
|
|
86
|
+
}
|
|
87
|
+
|
|
85
88
|
closePreview() {
|
|
86
89
|
if (!this.hasPreviewTarget) return
|
|
87
90
|
this.previewTarget.innerHTML = ""
|
|
@@ -95,6 +98,14 @@ export default class extends Controller {
|
|
|
95
98
|
if (event.target.matches("select[form]")) event.target.form?.requestSubmit()
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
// A new answer brings a new, empty preview frame: nothing is previewed any
|
|
102
|
+
// more. Stimulus calls this as the rows are swapped in; turbo:frame-render
|
|
103
|
+
// comes a few repaints later, after a quick Space may have started a preview.
|
|
104
|
+
previewTargetConnected() {
|
|
105
|
+
this.previewGroup = null
|
|
106
|
+
this.previewKey = null
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
// private
|
|
99
110
|
|
|
100
111
|
get records() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# locals: (result:, id:) -%>
|
|
2
|
+
<%# "Searching for" chips. × removes that term (server rewrites the query). %>
|
|
2
3
|
<% if result.chips.any? %>
|
|
3
4
|
<div class="ql-chips">
|
|
4
5
|
<span class="ql-chips-label"><%= t("querylane.chips.searching_for") %></span>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
<%# locals: (result:, id:) -%>
|
|
1
2
|
<%# The user's access could not be checked (Querylane::Scope.denied): nothing
|
|
2
|
-
is shown rather than records outside it.
|
|
3
|
+
is shown rather than records outside it. %>
|
|
3
4
|
<div class="ql-state ql-state-denied" role="alert">
|
|
4
5
|
<h2><%= t("querylane.states.denied_title") %></h2>
|
|
5
6
|
<p><%= t("querylane.states.denied") %></p>
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# locals: (result:, id:) -%>
|
|
2
|
+
<%# No hits: say what found nothing and offer one-click ways to widen. Each
|
|
3
|
+
way only rewrites the query, so it is a quiet link, never a button. %>
|
|
2
4
|
<div class="ql-empty">
|
|
3
5
|
<h2><%= t("querylane.states.empty_title", searched: result.chips.map(&:label).join(" · ")) %></h2>
|
|
4
6
|
<div class="ql-empty-actions">
|
|
5
7
|
<% result.widenable_facets.each do |facet| %>
|
|
6
|
-
<button type="submit" form="<%= id %>-form" name="widen" value="<%= facet.key %>" class="
|
|
8
|
+
<button type="submit" form="<%= id %>-form" name="widen" value="<%= facet.key %>" class="ql-link"><%= t("querylane.states.widen_#{facet.key}", default: t("querylane.states.widen", facet: facet.label.downcase)) %></button>
|
|
7
9
|
<% end %>
|
|
8
10
|
<%# Every filter can be dropped; the words are the search itself. %>
|
|
9
11
|
<% result.chips.reject(&:words).each do |chip| %>
|
|
10
|
-
<button type="submit" form="<%= id %>-form" name="remove" value="<%= chip.index %>" class="
|
|
12
|
+
<button type="submit" form="<%= id %>-form" name="remove" value="<%= chip.index %>" class="ql-link"><%= t("querylane.states.drop", label: chip.label) %></button>
|
|
13
|
+
<% end %>
|
|
14
|
+
<%# An exact filter that opts in (as_words) can search its typed value as plain words. %>
|
|
15
|
+
<% result.chips.select(&:as_words).each do |chip| %>
|
|
16
|
+
<button type="submit" form="<%= id %>-form" name="as_words" value="<%= chip.index %>" class="ql-link"><%= t("querylane.states.as_words", value: chip.as_words) %></button>
|
|
11
17
|
<% end %>
|
|
12
18
|
<% unless result.relaxed %>
|
|
13
|
-
<button type="submit" form="<%= id %>-form" name="relax" value="1" class="
|
|
19
|
+
<button type="submit" form="<%= id %>-form" name="relax" value="1" class="ql-link"><%= t("querylane.states.relax") %></button>
|
|
14
20
|
<% end %>
|
|
15
21
|
</div>
|
|
16
22
|
<p class="ql-facet-note"><%= t("querylane.states.empty_note") %></p>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
<%# locals: (result:, id:) -%>
|
|
1
2
|
<%# Facets. A value is a submit button with checkbox semantics: it asks the server
|
|
2
|
-
to tick or untick that value in the query text.
|
|
3
|
-
<aside class="ql-facets" aria-label="
|
|
3
|
+
to tick or untick that value in the query text. %>
|
|
4
|
+
<aside class="ql-facets" aria-label="<%= t("querylane.facets.label") %>">
|
|
4
5
|
<% result.facets.each do |facet| %>
|
|
5
6
|
<section class="ql-facet">
|
|
6
7
|
<h3 class="ql-facet-title"><%= facet.label %></h3>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# locals: (definition:) -%>
|
|
2
|
+
<%# Before anything is typed: what to type, with the definition's examples. %>
|
|
2
3
|
<div class="ql-first-load">
|
|
3
4
|
<div class="ql-first-load-lead">
|
|
4
5
|
<h2><%= t("querylane.states.first_load_title") %></h2>
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
<%#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
<%# locals: (result:, id:, url:, input_id:, label:, placeholder:, mask:, button_id:, options:, help:, extra:) -%>
|
|
2
|
+
<%# The search form and box, in a search landmark. Outside the results frame,
|
|
3
|
+
so typing never loses the caret. Search applies the box (an outline
|
|
4
|
+
button): the host's page keeps its one solid button for a commit. %>
|
|
5
|
+
<%
|
|
6
|
+
form_id = "#{id}-form"
|
|
7
|
+
frame_id = "#{id}-results"
|
|
8
|
+
help_id = "#{id}-help-text" if help
|
|
9
|
+
described_by = [help_id, "#{id}-error"].compact.join(" ")
|
|
10
|
+
%>
|
|
11
|
+
<search class="ql-search">
|
|
12
|
+
<%= form_with url: url, method: :post, id: form_id, class: "querylane-form",
|
|
13
|
+
data: {turbo: true, turbo_frame: frame_id, controller: "querylane--box",
|
|
5
14
|
querylane__box_debounce_value: Querylane.configuration.debounce_ms,
|
|
6
15
|
querylane__box_mask_value: mask.to_s,
|
|
7
|
-
querylane__box_frame_value:
|
|
16
|
+
querylane__box_frame_value: frame_id,
|
|
8
17
|
querylane__box_min_value: result.definition.min_text_length.to_i,
|
|
9
18
|
action: "submit->querylane--box#submitted turbo:before-cache@document->querylane--box#forget keydown@document->querylane--box#shortcut"} do %>
|
|
10
19
|
<%# The default button: Enter in the box searches (Options comes first in the box). %>
|
|
@@ -14,10 +23,10 @@
|
|
|
14
23
|
<svg class="ql-box-icon" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true"><circle cx="8.5" cy="8.5" r="6" fill="none" stroke="currentColor" stroke-width="2"/><path d="M13 13l5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
15
24
|
<div class="ql-box-field">
|
|
16
25
|
<div class="ql-box-overlay" aria-hidden="true" data-querylane--box-target="overlay"></div>
|
|
17
|
-
<label class="visually-hidden" for="<%= input_id %>"><%= label %></label>
|
|
26
|
+
<label class="ql-visually-hidden" for="<%= input_id %>"><%= label %></label>
|
|
18
27
|
<input type="text" id="<%= input_id %>" name="<%= Querylane.configuration.param_name %>" value="<%= result.query %>"
|
|
19
28
|
class="ql-box-input" placeholder="<%= placeholder %>" autocomplete="off" autocapitalize="off" spellcheck="false"
|
|
20
|
-
aria-describedby="<%=
|
|
29
|
+
aria-describedby="<%= described_by %>"
|
|
21
30
|
data-querylane--box-target="input"
|
|
22
31
|
data-action="input->querylane--box#typed keydown->querylane--box#keys scroll->querylane--box#paint select->querylane--box#paint click->querylane--box#paint compositionstart->querylane--box#compose compositionend->querylane--box#composed">
|
|
23
32
|
</div>
|
|
@@ -39,8 +48,9 @@
|
|
|
39
48
|
<%= t("querylane.box.options") %>
|
|
40
49
|
</button>
|
|
41
50
|
<% end %>
|
|
42
|
-
<button type="submit" class="btn btn-primary ql-box-submit" <%=
|
|
51
|
+
<button type="submit" class="btn btn-outline-primary ql-box-submit" <%= tag.attributes(id: button_id) %>><%= t("querylane.box.search") %></button>
|
|
43
52
|
</div>
|
|
44
|
-
<% if help %><p class="ql-box-help" id="<%=
|
|
53
|
+
<% if help %><p class="ql-box-help" id="<%= help_id %>"><%= help %></p><% end %>
|
|
45
54
|
<div class="ql-hint" role="status" hidden data-querylane--box-target="hint"><%= t("querylane.states.short", count: result.definition.min_text_length) if result.definition.min_text_length %></div>
|
|
46
55
|
<% end %>
|
|
56
|
+
</search>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
<%# locals: (presenter:, group:, columns:) -%>
|
|
1
2
|
<%# One group (e.g. a person) and its records, with "N more records" only
|
|
2
|
-
when the group has records the page does not show.
|
|
3
|
+
when the group has records the page does not show. %>
|
|
3
4
|
<% token = presenter.group_token(group) %>
|
|
4
5
|
<div class="ql-group" <%= tag.attributes(data: {querylane_group: token}) %>>
|
|
5
6
|
<div class="ql-group-head">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
<%# locals: (definition:, id:, popover_id: nil) -%>
|
|
1
2
|
<%# Syntax help card generated from the definition; a native popover (no JS).
|
|
2
|
-
|
|
3
|
-
<% popover_id
|
|
3
|
+
popover_id names it when it is shown twice on a page. %>
|
|
4
|
+
<% popover_id ||= "#{id}-help" %>
|
|
4
5
|
<button type="button" class="ql-link" popovertarget="<%= popover_id %>"><%= t("querylane.chips.syntax_help") %></button>
|
|
5
6
|
<div id="<%= popover_id %>" popover class="ql-help">
|
|
6
7
|
<div class="ql-help-head">
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
<%# locals: (result:, id:) -%>
|
|
1
2
|
<%# The options panel (M3): fields read from the box, Search writes them back.
|
|
2
3
|
Fields belong to the search form (form=…), so the live box text travels with
|
|
3
|
-
them; the server composes.
|
|
4
|
+
them; the server composes. Search applies the panel (outline), Cancel and
|
|
5
|
+
Reset are quiet links. %>
|
|
4
6
|
<section class="ql-options" id="<%= id %>-options" aria-labelledby="<%= id %>-options-title"
|
|
5
7
|
data-controller="querylane--options" data-querylane--options-debounce-value="<%= Querylane.configuration.debounce_ms %>"
|
|
6
8
|
data-action="input->querylane--options#changed change->querylane--options#changed keydown.esc->querylane--options#cancel keydown.enter->querylane--options#apply">
|
|
@@ -25,8 +27,8 @@
|
|
|
25
27
|
<%= render "querylane/help", definition: result.definition, id: id, popover_id: "#{id}-options-help" %>
|
|
26
28
|
<button type="button" class="ql-link" data-action="querylane--options#reset"><%= t("querylane.options.reset") %></button>
|
|
27
29
|
<span class="ql-options-spacer"></span>
|
|
28
|
-
<button type="submit" form="<%= id %>-form" class="
|
|
29
|
-
<button type="submit" form="<%= id %>-form" name="compose_submit" value="1" class="btn btn-primary"
|
|
30
|
+
<button type="submit" form="<%= id %>-form" class="ql-link" data-querylane--options-target="cancel"><%= t("querylane.options.cancel") %></button>
|
|
31
|
+
<button type="submit" form="<%= id %>-form" name="compose_submit" value="1" class="btn btn-outline-primary"
|
|
30
32
|
data-querylane--options-target="apply"><%= t("querylane.options.search") %></button>
|
|
31
33
|
</footer>
|
|
32
34
|
</section>
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# locals: (row:, values:, held:, definition:, id:) -%>
|
|
2
|
+
<%# One options-panel row (a Definition::PanelRow).
|
|
2
3
|
A masked field starts empty: its current terms stay in the box (shown
|
|
3
|
-
masked as the placeholder
|
|
4
|
-
|
|
4
|
+
masked as the placeholder, which wins over the row's own placeholder);
|
|
5
|
+
what is typed here is added. The row's hint sits under it and describes
|
|
6
|
+
its fields. %>
|
|
7
|
+
<%
|
|
8
|
+
field_id = ->(name) { "#{id}-compose-#{name}" }
|
|
9
|
+
hint = Querylane::Copy.text(row.hint)
|
|
10
|
+
hint_id = "#{field_id.(row.name)}-hint" if hint
|
|
11
|
+
placeholder = Querylane::Copy.text(row.placeholder)
|
|
12
|
+
%>
|
|
5
13
|
<div class="ql-options-row">
|
|
6
14
|
<% case row.kind %>
|
|
7
15
|
<% when :inputs %>
|
|
@@ -13,8 +21,8 @@
|
|
|
13
21
|
<%# Sensitive fields stay type=text (a password field would invite the browser to save it), masked by CSS. %>
|
|
14
22
|
<input type="text" id="<%= field_id.(name) %>" name="compose[<%= name %>]" form="<%= id %>-form"
|
|
15
23
|
value="<%= values[name] %>" class="form-control form-control-sm <%= "ql-masked" if row.masked %>" autocomplete="off" spellcheck="false"
|
|
16
|
-
<%= tag.attributes(placeholder: (t("querylane.options.held", text: held[name])
|
|
17
|
-
<%= tag.attributes(aria: {label: ("#{row.label} #{name.humanize}" if position.positive?)}) %>
|
|
24
|
+
<%= tag.attributes(placeholder: (held[name] ? t("querylane.options.held", text: held[name]) : placeholder)) %>
|
|
25
|
+
<%= tag.attributes(aria: {label: ("#{row.label} #{name.humanize}" if position.positive?), describedby: hint_id}) %>
|
|
18
26
|
data-querylane--options-target="field">
|
|
19
27
|
<% if row.masked %>
|
|
20
28
|
<button type="button" class="ql-link ql-options-reveal" aria-controls="<%= field_id.(name) %>" aria-pressed="false"
|
|
@@ -27,7 +35,7 @@
|
|
|
27
35
|
<% when :flags %>
|
|
28
36
|
<% flags = definition.operator(row.keys.first) %>
|
|
29
37
|
<span class="ql-options-label" id="<%= field_id.(row.name) %>-label"><%= row.label %></span>
|
|
30
|
-
<div class="ql-options-checks" role="group" aria-labelledby="<%= field_id.(row.name) %>-label"
|
|
38
|
+
<div class="ql-options-checks" role="group" aria-labelledby="<%= field_id.(row.name) %>-label" <%= tag.attributes(aria: {describedby: hint_id}) %>>
|
|
31
39
|
<% definition.flag_values(flags, row.values).each do |value, label| %>
|
|
32
40
|
<label class="ql-options-check">
|
|
33
41
|
<input type="checkbox" name="compose[<%= row.name %>][]" value="<%= value %>" form="<%= id %>-form"
|
|
@@ -42,10 +50,13 @@
|
|
|
42
50
|
<% row.keys.each do |key| %>
|
|
43
51
|
<label class="ql-options-check">
|
|
44
52
|
<input type="checkbox" name="compose[options][]" value="<%= key %>" form="<%= id %>-form"
|
|
45
|
-
<%= "checked" if values["options"].include?(key.to_s) %> data-querylane--options-target="field">
|
|
53
|
+
<%= "checked" if values["options"].include?(key.to_s) %> <%= tag.attributes(aria: {describedby: hint_id}) %> data-querylane--options-target="field">
|
|
46
54
|
<%= definition.option(key).label %>
|
|
47
55
|
</label>
|
|
48
56
|
<% end %>
|
|
49
57
|
</div>
|
|
50
58
|
<% end %>
|
|
59
|
+
<% if hint %>
|
|
60
|
+
<p class="ql-options-hint" id="<%= hint_id %>"><%= hint %></p>
|
|
61
|
+
<% end %>
|
|
51
62
|
</div>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# locals: (outcome:) -%>
|
|
2
|
+
<%# Open/View or the reason a record cannot be opened. Open/View is a Go link
|
|
3
|
+
(rows hold no buttons) drawn on a button, because it posts (a full page). %>
|
|
2
4
|
<% case outcome %>
|
|
3
5
|
<% when Querylane::Presenter::Action %>
|
|
4
6
|
<%= button_to outcome.label, outcome.url, params: outcome.params || {}, method: :post,
|
|
5
|
-
class: "
|
|
6
|
-
form: {class: "
|
|
7
|
+
class: "ql-go ql-open",
|
|
8
|
+
form: {class: "ql-open-form", data: {turbo: false}}, data: {querylane_open: true} %>
|
|
7
9
|
<% when Querylane::Presenter::Reason %>
|
|
8
10
|
<span class="ql-reason"><%= outcome.text %><% if outcome.link_url %> <%= link_to outcome.link_label, outcome.link_url, data: {turbo: false} %><% end %>
|
|
9
11
|
<% if outcome.more.any? %>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# locals: (result:, id:) -%>
|
|
2
|
+
<%# Previous, up to five pages, Next. Submit buttons of the search form. %>
|
|
2
3
|
<% pages = [result.pages, 1].max
|
|
3
4
|
start = [[result.page - 2, 1].max, [pages - 4, 1].max].min
|
|
4
5
|
window = (start..[start + 4, pages].min) %>
|
|
5
6
|
<nav aria-label="<%= t("querylane.toolbar.pages") %>">
|
|
6
|
-
<ul class="pagination pagination-sm
|
|
7
|
+
<ul class="pagination pagination-sm ql-pager">
|
|
7
8
|
<li class="page-item previous <%= "disabled" if result.page <= 1 %>">
|
|
8
9
|
<button type="submit" form="<%= id %>-form" name="page" value="<%= result.page - 1 %>" class="page-link" <%= "disabled" if result.page <= 1 %>><%= t("querylane.toolbar.previous") %></button>
|
|
9
10
|
</li>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
<%# locals: (result:, presenter:, id:) -%>
|
|
1
2
|
<%# The preview (Space on a record): every record of its group, any status.
|
|
2
|
-
|
|
3
|
+
result comes from querylane_related. %>
|
|
3
4
|
<turbo-frame id="<%= id %>-preview" class="ql-preview">
|
|
4
5
|
<section class="ql-preview-card" aria-labelledby="<%= id %>-preview-title">
|
|
5
6
|
<% if result.state == :unavailable %>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# locals: (presenter:, hit:, columns:) -%>
|
|
2
|
+
<%# One record in lanes. Focusable for ↑/↓/Enter. %>
|
|
2
3
|
<div class="ql-record" tabindex="-1" data-querylane-record="<%= presenter.record_key(hit) %>">
|
|
3
4
|
<% presenter.cells(hit).each_with_index do |cell, index| %>
|
|
4
5
|
<% width = columns[index]&.width %>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
<%# locals: (result:, presenter:, frame:) -%>
|
|
1
2
|
<%# "More records": every record of one group the page did not show.
|
|
2
|
-
|
|
3
|
+
result comes from querylane_related; frame is the requesting frame id. %>
|
|
3
4
|
<turbo-frame id="<%= frame %>" class="ql-related">
|
|
4
5
|
<% if result.state == :unavailable %>
|
|
5
6
|
<div class="ql-related-head" role="status"><%= t("querylane.group.unavailable") %></div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<%#
|
|
2
|
-
|
|
1
|
+
<%# locals: (result:, id:, info_id:, per_id:, first_load:, rows:) -%>
|
|
2
|
+
<%# The results frame: everything one search request answers. %>
|
|
3
3
|
<turbo-frame id="<%= id %>-results" class="ql-results" data-revision="<%= result.revision %>" data-controller="querylane--results"
|
|
4
4
|
data-action="change->querylane--results#submitForm keydown->querylane--results#keys">
|
|
5
5
|
<%# The answer's state, inside the frame (Turbo keeps the frame element's own
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# locals: (presenter:) -%>
|
|
2
|
+
<%# Grouped records drawn from a Querylane::Presenter. %>
|
|
2
3
|
<% columns = presenter.columns %>
|
|
3
4
|
<div class="ql-record ql-record-head" aria-hidden="true">
|
|
4
5
|
<% columns.each do |column| %>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
<%# locals: (result:, id:) -%>
|
|
1
2
|
<%# Saved searches (M12): a native popover opened by the box's Saved button.
|
|
2
3
|
Name and Save belong to the search form; choosing one sends its id and the
|
|
3
|
-
server rewrites the box.
|
|
4
|
+
server rewrites the box. %>
|
|
4
5
|
<div id="<%= id %>-saved" popover class="ql-saved">
|
|
6
|
+
<input type="hidden" name="delete_saved_page" value="<%= result.page %>" form="<%= id %>-form">
|
|
5
7
|
<div class="ql-saved-head">
|
|
6
8
|
<label for="<%= id %>-saved-name"><%= t("querylane.saved.title") %></label>
|
|
7
9
|
<input type="text" id="<%= id %>-saved-name" name="saved_name" form="<%= id %>-form" maxlength="80" autocomplete="off"
|
|
@@ -22,10 +24,17 @@
|
|
|
22
24
|
<p class="ql-saved-none"><%= t("querylane.saved.none") %></p>
|
|
23
25
|
<% end %>
|
|
24
26
|
<% result.saved_searches.each do |saved| %>
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
<div class="ql-saved-row" data-test="saved-search">
|
|
28
|
+
<button type="submit" form="<%= id %>-form" name="saved" value="<%= saved.id %>" class="ql-saved-item">
|
|
29
|
+
<span class="ql-saved-name"><%= saved.name %></span>
|
|
30
|
+
<code class="ql-saved-query"><%= saved.query %></code>
|
|
31
|
+
<% if saved.used_at %><span class="ql-saved-used"><%= t("querylane.saved.used", date: l(saved.used_at.to_date, format: :short)) %></span><% end %>
|
|
32
|
+
</button>
|
|
33
|
+
<button type="submit" form="<%= id %>-form" name="delete_saved" value="<%= saved.id %>" class="ql-saved-delete"
|
|
34
|
+
aria-label="<%= t("querylane.saved.delete_label", name: saved.name) %>" title="<%= t("querylane.saved.delete_label", name: saved.name) %>"
|
|
35
|
+
data-action="click->querylane--results#deleteSaved" data-confirm="<%= t("querylane.saved.delete_confirm") %>">
|
|
36
|
+
<%= respond_to?(:icon) ? icon("trash-2") : t("querylane.saved.delete") %>
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
30
39
|
<% end %>
|
|
31
40
|
</div>
|