janela 0.9.0 → 0.10.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 +11 -0
- data/README.md +10 -2
- data/UPGRADING.md +27 -0
- data/app/assets/javascripts/janela/chart_controller.js +29 -3
- data/app/models/janela/frame.rb +29 -0
- data/app/models/janela/pane.rb +1 -1
- data/app/models/janela/query.rb +8 -5
- data/db/migrate/20260928000001_add_default_filter_to_janela_frames.rb +10 -0
- data/docs/composing.md +19 -0
- data/docs/decisions/043-a-frames-default-filter-names-the-model-it-narrows.md +163 -0
- data/docs/decisions/INDEX.md +4 -3
- data/docs/theming.md +1 -1
- data/lib/janela/version.rb +1 -1
- metadata +10 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eac851a1f9a4eda7cac89f9ceced6664b28b26a5c889b1939147a0f5a4ed5688
|
|
4
|
+
data.tar.gz: 5425fbb11f1585892bbc4cff9b87ddbf9f6c08a2398ec87d5cd9402e8d624d51
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d8563226d41987ac12140bc9af25a6492f1784937d02796c077e1282d999c132e0e19285d9f938a78b4d20e98958f28e2bd1bb55a14a6bad23e8267b5bf9a9b
|
|
7
|
+
data.tar.gz: 0a836386550bc34bcb4e16d284592f7a8473accdbf073385eadc88e3f6b2e1da9eb14ea61394a0d0b79baa59f3f9c6e540b7f3423d6064b23da21b24f4bdc6c7
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.10.0] - 2026-09-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `Janela::Frame` gains `default_model` and `default_where`, a permanent filter that is data rather than code: `frame.update!(default_model: "orders", default_where: { status_in: %w[paid pending] })`. Until now the only filter a frame could carry was `where:` (ADR 040), a host's own code, per record, typed into every view that rendered the frame; something true on every render, such as a queue never counting an archived row, had nowhere on the data side to live. `default_model` names the one model the condition is about, so a frame holding panes from more than one model is never narrowed by a condition that was never about the pane reading it: a pane over a different model simply does not receive it, no error. It composes ahead of `where:` and the reader's `q[...]`, applies even on a pane's own dimension, and is validated the moment you save it, against that model's declared dimensions and ADR 025's existing predicate bounds, rather than only discovered wrong at render. Set both columns together; either alone is a validation error. Needs a migration; see `UPGRADING.md` (ADR 043, #63).
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- A bar or line pane's colour now comes from `--janela-accent`, not a literal `rgba(54, 162, 235, ...)`. Three hardcoded copies of Chart.js's own default blue meant a chart disagreed with the very theme installed alongside it, vitral included: a selected table value read the theme's accent, the equivalent bar stayed Chart.js blue. Nothing to configure and nothing new in the theming contract; the property was already public (ADR 016), the chart just never read it (#62).
|
|
17
|
+
|
|
8
18
|
## [0.9.0] - 2026-09-28
|
|
9
19
|
|
|
10
20
|
### Added
|
|
@@ -205,6 +215,7 @@ First alpha, installed from GitHub for testing in a single host application.
|
|
|
205
215
|
- Only models that declare a `janela` block are addressable over HTTP.
|
|
206
216
|
- ADRs 001 to 004 in `docs/decisions/`, shipped inside the gem.
|
|
207
217
|
|
|
218
|
+
[0.10.0]: https://github.com/retail-tasker/janela/releases/tag/v0.10.0
|
|
208
219
|
[0.9.0]: https://github.com/retail-tasker/janela/releases/tag/v0.9.0
|
|
209
220
|
[0.8.0]: https://github.com/retail-tasker/janela/releases/tag/v0.8.0
|
|
210
221
|
[0.7.0]: https://github.com/retail-tasker/janela/releases/tag/v0.7.0
|
data/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Janela is an alpha on [rubygems.org](https://rubygems.org/gems/janela). It has t
|
|
|
27
27
|
|
|
28
28
|
```ruby
|
|
29
29
|
# Gemfile
|
|
30
|
-
gem "janela", "~> 0.
|
|
30
|
+
gem "janela", "~> 0.10"
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
```ruby
|
|
@@ -286,6 +286,14 @@ What an analyst writes is escaped, never rendered as markup, and a `link` must b
|
|
|
286
286
|
|
|
287
287
|
It travels in each pane's URL as `where[...]`, apart from the reader's `q[...]`, so Clear filters, Escape and a click on the same dimension cannot take it off, and the reader's selection can only narrow inside it. It is bounded like any filter: declared dimensions only, and the predicates ADR 025 allows. It is a view filter, not a permission: it is visible in the pane URL, and a reader who edits it out sees only what your `policy_scope` already allows them to. Keep a record out of reach in the scope, not here. Putting the same filter in the page URL's `q[...]` instead does not hold, because `q[...]` is the reader's to clear (ADR 040).
|
|
288
288
|
|
|
289
|
+
**A frame's own permanent filter**, for something that is true on every render, not one record: "this queue never counts an archived row." Where `where:` is code, per request, this is data, so an analyst changes it without a deploy (ADR 043):
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
frame.update!(default_model: "orders", default_where: { status_in: %w[paid pending] })
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`default_model` names the one model it narrows; a pane over any other model in the same frame is untouched by it, so a frame is free to hold panes from more than one model without the two ever fighting over what the default means. It composes ahead of `where:` and the reader's `q[...]`, applies even on a pane's own dimension, and is validated the moment you save it, against that model's declared dimensions and ADR 025's predicate bounds, the same sentence a bad request would raise, read at the point you can still fix it. Set both columns together, or neither; either alone is a validation error.
|
|
296
|
+
|
|
289
297
|
### Janela's own pages
|
|
290
298
|
|
|
291
299
|
The engine serves an index and a page per frame at the mount root, so you can install the gem and navigate the same day:
|
|
@@ -534,7 +542,7 @@ Deliberately out of scope: natural-language query, a separate data warehouse, a
|
|
|
534
542
|
|
|
535
543
|
## Status
|
|
536
544
|
|
|
537
|
-
**v0.
|
|
545
|
+
**v0.10.0 alpha.** The measures/dimensions DSL, time dimensions, cross-filtering with multi-selection, bar and line charts, pane URLs, shareable dashboard URLs, snapshots, database-backed frames found by owner and key, panes that hold words or a host partial as well as a query, a host-fixed filter no click can remove and a frame's own permanent one beside it, STI subclasses, the engine's own pages for reading and editing them and the optional vitral theme work and are covered by unit and real-browser tests, with the classes a theme may target documented in [Theming Janela](docs/theming.md). Not yet built: a visual editor, drill-down on time panes, other chart types. [Vista](docs/roadmap.md), the roadmap, says what 1.0 means and which of these are in it; open work is in [GitHub Issues](https://github.com/retail-tasker/janela/issues).
|
|
538
546
|
|
|
539
547
|
## Development
|
|
540
548
|
|
data/UPGRADING.md
CHANGED
|
@@ -12,6 +12,33 @@ bin/rails janela:doctor
|
|
|
12
12
|
|
|
13
13
|
It reads your application and lists what still needs changing.
|
|
14
14
|
|
|
15
|
+
## 0.9.0 to 0.10.0
|
|
16
|
+
|
|
17
|
+
One migration, if you use stored frames.
|
|
18
|
+
|
|
19
|
+
**Take the frame default filter migration.**
|
|
20
|
+
|
|
21
|
+
A frame can now carry a permanent filter as data (ADR 043).
|
|
22
|
+
`janela_frames` gains `default_model` and `default_where`:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bin/rails janela:install:migrations
|
|
26
|
+
bin/rails db:migrate
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Every existing frame keeps both columns nil and is unaffected. Nothing
|
|
30
|
+
changes unless you set them:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
frame.update!(default_model: "orders", default_where: { status_in: %w[paid pending] })
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Only a pane over `default_model` takes the filter; every other pane in
|
|
37
|
+
the frame is untouched by it. Set both columns together, or neither:
|
|
38
|
+
either alone is a validation error, and so is a condition your model
|
|
39
|
+
does not declare or a predicate ADR 025 does not allow for that kind of
|
|
40
|
+
dimension.
|
|
41
|
+
|
|
15
42
|
## 0.8.0 to 0.9.0
|
|
16
43
|
|
|
17
44
|
Two migrations, if you use stored frames. Both are taken by the same
|
|
@@ -20,7 +20,7 @@ export default class extends Controller {
|
|
|
20
20
|
label: this.titleValue,
|
|
21
21
|
data: this.valuesValue,
|
|
22
22
|
backgroundColor: this.colours(),
|
|
23
|
-
borderColor:
|
|
23
|
+
borderColor: this.accentColour(0.9)
|
|
24
24
|
}]
|
|
25
25
|
},
|
|
26
26
|
options: {
|
|
@@ -52,11 +52,37 @@ export default class extends Controller {
|
|
|
52
52
|
const selected = this.selectedValue.map(String)
|
|
53
53
|
return this.labelsValue.map((label) =>
|
|
54
54
|
selected.length === 0 || selected.includes(String(label))
|
|
55
|
-
?
|
|
56
|
-
:
|
|
55
|
+
? this.accentColour(0.9)
|
|
56
|
+
: this.accentColour(0.25)
|
|
57
57
|
)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// #62: this used to be a literal rgba(54, 162, 235, ...), Chart.js's own
|
|
61
|
+
// default, so a bar disagreed with --janela-accent (ADR 016) and with
|
|
62
|
+
// every other selected thing on the page. Read off this element rather
|
|
63
|
+
// than the document root, so whatever ancestor sets the property is the
|
|
64
|
+
// one honoured, the way it already inherits for everything else.
|
|
65
|
+
accentColour(alpha) {
|
|
66
|
+
const [ r, g, b ] = this.resolvedAccent().match(/\d+/g)
|
|
67
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A custom property's computed value is returned exactly as authored,
|
|
71
|
+
// "rgb(...)", "#7c3aed", a name, never resolved the way an ordinary
|
|
72
|
+
// colour property is. Setting it as one and reading that back resolves
|
|
73
|
+
// any of them the same way, rather than parsing each form by hand.
|
|
74
|
+
resolvedAccent() {
|
|
75
|
+
if (this.resolvedAccentValue) return this.resolvedAccentValue
|
|
76
|
+
|
|
77
|
+
const accent = getComputedStyle(this.element).getPropertyValue("--janela-accent").trim()
|
|
78
|
+
const probe = document.createElement("span")
|
|
79
|
+
probe.style.color = accent
|
|
80
|
+
document.body.appendChild(probe)
|
|
81
|
+
this.resolvedAccentValue = getComputedStyle(probe).color
|
|
82
|
+
probe.remove()
|
|
83
|
+
return this.resolvedAccentValue
|
|
84
|
+
}
|
|
85
|
+
|
|
60
86
|
disconnect() {
|
|
61
87
|
this.chart?.destroy()
|
|
62
88
|
this.chart = null
|
data/app/models/janela/frame.rb
CHANGED
|
@@ -18,6 +18,9 @@ module Janela
|
|
|
18
18
|
# The shape of the symbol a host passes, so nothing that reads like a
|
|
19
19
|
# name or a path gets in (ADR 041).
|
|
20
20
|
validates :key, format: { with: /\A[a-z0-9_]+\z/ }, uniqueness: { scope: %i[owner_type owner_id] }, allow_nil: true
|
|
21
|
+
# Present or absent together, and default_where is only ever as valid as
|
|
22
|
+
# default_model says it can be (ADR 043).
|
|
23
|
+
validate :default_where_matches_default_model
|
|
21
24
|
|
|
22
25
|
# The host's frame for this owner and key, created on first use (ADR 041).
|
|
23
26
|
# The block runs only when the frame is created, to set what a new frame
|
|
@@ -44,5 +47,31 @@ module Janela
|
|
|
44
47
|
pane.update_columns(position: index + 1) unless pane.position == index + 1
|
|
45
48
|
end
|
|
46
49
|
end
|
|
50
|
+
|
|
51
|
+
# This frame's permanent filter, if the pane asking is over the model it
|
|
52
|
+
# names; empty for any other model, because the condition was never
|
|
53
|
+
# claimed to be about it (ADR 043).
|
|
54
|
+
def default_for(model)
|
|
55
|
+
return {} if default_model.blank? || default_model != model.model_name.route_key
|
|
56
|
+
|
|
57
|
+
default_where.to_h
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
def default_where_matches_default_model
|
|
62
|
+
return if default_model.blank? && default_where.blank?
|
|
63
|
+
return errors.add(:default_where, "cannot be set without default_model") if default_model.blank?
|
|
64
|
+
return errors.add(:default_model, "cannot be set without default_where") if default_where.blank?
|
|
65
|
+
|
|
66
|
+
definition = Janela.definition!(default_model)
|
|
67
|
+
# A no-op relation: this checks the condition's shape against the
|
|
68
|
+
# model's declared dimensions (ADR 025), and never has to touch a row
|
|
69
|
+
# to do it.
|
|
70
|
+
definition.narrow(definition.model.none, default_where)
|
|
71
|
+
rescue Janela::NotFound
|
|
72
|
+
errors.add(:default_model, "must be a model with a janela block")
|
|
73
|
+
rescue Janela::BadRequest => e
|
|
74
|
+
errors.add(:default_where, e.message)
|
|
75
|
+
end
|
|
47
76
|
end
|
|
48
77
|
end
|
data/app/models/janela/pane.rb
CHANGED
|
@@ -73,7 +73,7 @@ module Janela
|
|
|
73
73
|
def query(filters: {}, fixed: {}, renderer: self.renderer)
|
|
74
74
|
Query.new(definition: definition, measure: measure.to_sym, dimension: dimension.presence&.to_sym,
|
|
75
75
|
renderer: renderer, granularity: granularity, limit: limit, filters: filters, fixed: fixed,
|
|
76
|
-
title: title)
|
|
76
|
+
default: frame.default_for(definition.model), title: title)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
# The row's own words, for a list or a heading. Built from the columns
|
data/app/models/janela/query.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Janela
|
|
|
7
7
|
class Query
|
|
8
8
|
RENDERERS = %w[table bar line].freeze
|
|
9
9
|
|
|
10
|
-
attr_reader :definition, :measure, :dimension, :renderer, :limit, :filters, :fixed, :snapshot
|
|
10
|
+
attr_reader :definition, :measure, :dimension, :renderer, :limit, :filters, :fixed, :default, :snapshot
|
|
11
11
|
|
|
12
12
|
# The helper renders the turbo frame and the controller renders its
|
|
13
13
|
# replacement, so both derive the id the same way from the same parameters.
|
|
@@ -17,7 +17,7 @@ module Janela
|
|
|
17
17
|
parts.compact.join("_")
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def initialize(definition:, measure:, dimension: nil, renderer: "table", granularity: nil, limit: nil, filters: {}, fixed: {}, snapshot: nil, title: nil)
|
|
20
|
+
def initialize(definition:, measure:, dimension: nil, renderer: "table", granularity: nil, limit: nil, filters: {}, fixed: {}, default: {}, snapshot: nil, title: nil)
|
|
21
21
|
@definition = definition
|
|
22
22
|
@title = title
|
|
23
23
|
@measure = measure
|
|
@@ -25,6 +25,7 @@ module Janela
|
|
|
25
25
|
@renderer = renderer.to_s
|
|
26
26
|
@filters = filters
|
|
27
27
|
@fixed = fixed
|
|
28
|
+
@default = default
|
|
28
29
|
@snapshot = snapshot
|
|
29
30
|
|
|
30
31
|
raise BadRequest, "unknown pane renderer #{renderer.inspect}" unless RENDERERS.include?(@renderer)
|
|
@@ -98,9 +99,11 @@ module Janela
|
|
|
98
99
|
def result(on: nil)
|
|
99
100
|
return snapshot.stored_result(self) if frozen?
|
|
100
101
|
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
102
|
+
# Default, then fixed, then the reader's: a frame's own permanent
|
|
103
|
+
# filter narrows first, a host's per-record filter narrows again, and
|
|
104
|
+
# both apply even on this pane's own dimension, unlike the reader's
|
|
105
|
+
# selection, which this pane shows the alternatives to (ADR 040, 043).
|
|
106
|
+
on = definition.narrow(on || model.all, default) if default.present?
|
|
104
107
|
on = definition.narrow(on || model.all, fixed) if fixed.present?
|
|
105
108
|
definition.query(measure, by: dimension, where: applicable_filters, on: on, granularity: granularity, limit: limit)
|
|
106
109
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class AddDefaultFilterToJanelaFrames < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
# A frame's own permanent filter (ADR 043): only a pane over
|
|
4
|
+
# default_model takes it, so a frame that holds panes from more than one
|
|
5
|
+
# model is never narrowed by a condition that is not about it. Present
|
|
6
|
+
# or absent together; every existing frame has neither.
|
|
7
|
+
add_column :janela_frames, :default_model, :string
|
|
8
|
+
add_column :janela_frames, :default_where, :json
|
|
9
|
+
end
|
|
10
|
+
end
|
data/docs/composing.md
CHANGED
|
@@ -93,6 +93,25 @@ pane the reader can click: any value but the fixed one returns nothing.
|
|
|
93
93
|
A figure you compute yourself for the same page, such as the progress
|
|
94
94
|
bar below, takes the same condition in its own `where:`.
|
|
95
95
|
|
|
96
|
+
## A frame's own permanent filter
|
|
97
|
+
|
|
98
|
+
Some conditions are not about which record's page this is, they are
|
|
99
|
+
true every time: "this queue never counts an archived row." `where:` is
|
|
100
|
+
code, written into a view, run again on every render. A frame's own
|
|
101
|
+
default filter is data instead, so an analyst changes it without a
|
|
102
|
+
deploy (ADR 043):
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
frame.update!(default_model: "orders", default_where: { status_in: %w[paid pending] })
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`default_model` says which model the condition is about, so a frame
|
|
109
|
+
holding panes from more than one model is never narrowed by a condition
|
|
110
|
+
that was never about the pane reading it. It is validated when you save
|
|
111
|
+
it, against that model's declared dimensions, rather than only
|
|
112
|
+
discovered wrong the next time a pane renders. Set both columns
|
|
113
|
+
together; either alone is a validation error.
|
|
114
|
+
|
|
96
115
|
## Components
|
|
97
116
|
|
|
98
117
|
Each of these is plain HTML. The class names that start `janela-` are
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-09-28
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 012, ADR 014, ADR 020, ADR 025, ADR 040
|
|
5
|
+
Triggers:
|
|
6
|
+
- a frame that should always exclude some rows, regardless of who renders it
|
|
7
|
+
- adding a column to Janela::Frame or Janela::Pane
|
|
8
|
+
- a host copying the same where: condition into every view that renders one frame
|
|
9
|
+
- validating a stored Ransack condition at save time rather than at render
|
|
10
|
+
Topics: frames, filters, cross-filtering, persistence, ransack
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ADR 043: A Frame's Default Filter Names the Model It Narrows
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
#63, found while dogfooding: ADR 040 gives a host a way to narrow a
|
|
18
|
+
frame to the current record, `where: { project_id_eq: @project.id }`,
|
|
19
|
+
code, per request, correctly not data because the value is inherently
|
|
20
|
+
per-record. It does not give a frame a way to say something that is
|
|
21
|
+
true on every render, forever: "this queue never counts an archived
|
|
22
|
+
row." That is not per-record narrowing, it is a permanent property of
|
|
23
|
+
what the dashboard means, and today it can only be expressed by typing
|
|
24
|
+
the same Ransack condition into every view that renders the frame.
|
|
25
|
+
|
|
26
|
+
ADR 012's own argument is that composition moved out of ERB because
|
|
27
|
+
the analyst, not the developer, owns a dashboard, and a decision typed
|
|
28
|
+
into a view is "a file held by the wrong owner." A permanent filter is
|
|
29
|
+
exactly that file, with nowhere on the data side to move to: `Frame`
|
|
30
|
+
and `Pane` have no column that could hold one.
|
|
31
|
+
|
|
32
|
+
### What breaks the obvious shape
|
|
33
|
+
|
|
34
|
+
The obvious answer, a `default_where` column on `Frame` applied to
|
|
35
|
+
every pane in it, runs into a fact ADR 020 already used to reject the
|
|
36
|
+
frame as the layer for a different per-pane setting: a frame can hold
|
|
37
|
+
panes from more than one model. Measured against the demo, reusing the
|
|
38
|
+
existing bound check rather than assuming it:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
Order.janela.narrow(Order.all, "expedited_eq" => "false")
|
|
42
|
+
# => Janela::BadRequest: Order does not allow filtering on expedited_eq.
|
|
43
|
+
# Declare a janela dimension, or add it to ransackable_attributes.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`expedited` is a real column on `Order`, not a declared dimension. A
|
|
47
|
+
condition applied uniformly to every pane in a frame raises for any
|
|
48
|
+
pane whose model, or whose declared dimension set, does not carry the
|
|
49
|
+
attribute the condition names. ADR 020's own case was a format string
|
|
50
|
+
being "right for the currency and wrong for the count sitting next to
|
|
51
|
+
it"; a stored Ransack condition is at least as specific to one model.
|
|
52
|
+
|
|
53
|
+
### What was considered for where the mismatch goes
|
|
54
|
+
|
|
55
|
+
**Raise for every pane whose model does not match.** Rejected: the
|
|
56
|
+
frame breaks the moment an analyst adds a pane over a second model, or
|
|
57
|
+
the moment a declared dimension is removed from the first, and the
|
|
58
|
+
failure is frame-wide rather than local to the pane that changed.
|
|
59
|
+
|
|
60
|
+
**Skip the condition quietly for a pane it does not apply to.** Rejected
|
|
61
|
+
outright. ADR 024, ADR 032 and ADR 034 all refuse the same shape of
|
|
62
|
+
quiet: a filter that means one thing for one pane and nothing for
|
|
63
|
+
another, on the same frame, with nothing on the page saying so.
|
|
64
|
+
|
|
65
|
+
**Restrict a frame with a default to panes over one model.** Considered
|
|
66
|
+
and rejected as an unnecessary restriction on `Pane`, which already
|
|
67
|
+
allows any model with a `janela` block. The mismatch is the default's
|
|
68
|
+
problem to solve, not a new constraint on what a frame may hold.
|
|
69
|
+
|
|
70
|
+
## Decision
|
|
71
|
+
|
|
72
|
+
**A frame's default filter names the one model it narrows, and only a
|
|
73
|
+
pane over that model takes it.** `Janela::Frame` gains two columns,
|
|
74
|
+
`default_model` and `default_where`, both nullable and present or
|
|
75
|
+
absent together:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
frame.update!(default_model: "orders", default_where: { "status_not_eq" => "archived" })
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**`default_model` is validated the same way `Pane#model` already is**:
|
|
82
|
+
blank, or a route key naming a model with a `janela` block, checked
|
|
83
|
+
through `Janela.definition!` and rescued the same way
|
|
84
|
+
`declared_by_a_janela_block` does. It does not have to match any pane
|
|
85
|
+
the frame currently holds. A frame can carry a default before its first
|
|
86
|
+
matching pane exists, and a pane over a different model is simply never
|
|
87
|
+
narrowed by it, no error, because the condition was never claimed to be
|
|
88
|
+
about it.
|
|
89
|
+
|
|
90
|
+
**`default_where` is validated against that one model's `Definition` at
|
|
91
|
+
save time**, not only discovered wrong at render. `Definition#narrow`
|
|
92
|
+
already runs `Ransack#ransack` and the three checks ADR 025 built
|
|
93
|
+
(dropped filters, disallowed predicates, oversized value lists); a new
|
|
94
|
+
validation calls it against `default_model`'s definition and turns a
|
|
95
|
+
raised `Janela::BadRequest` into a validation error on `default_where`,
|
|
96
|
+
the same sentence a request would have raised, read at the point an
|
|
97
|
+
analyst can still fix it. This is new ground: nothing today validates a
|
|
98
|
+
persisted, arbitrary Ransack condition at save time, only `fixed` and
|
|
99
|
+
the reader's `q[...]`, which are validated fresh on every render because
|
|
100
|
+
neither is ever stored.
|
|
101
|
+
|
|
102
|
+
**It composes as a third layer, ahead of the two ADR 040 already
|
|
103
|
+
built.** `Pane#query` asks its frame for the default that applies to its
|
|
104
|
+
own model and passes it to `Query`, which narrows with it before `fixed`
|
|
105
|
+
narrows again, before the reader's own filters:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
# Janela::Frame
|
|
109
|
+
def default_for(model)
|
|
110
|
+
default_model == model.model_name.route_key ? default_where.to_h : {}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Janela::Query#result
|
|
114
|
+
on = definition.narrow(on || model.all, default) if default.present? # frame, permanent
|
|
115
|
+
on = definition.narrow(on, fixed) if fixed.present? # host, per-record (ADR 040)
|
|
116
|
+
definition.query(measure, by: dimension, where: applicable_filters, on: on, ...) # reader
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Applied even on a pane's own dimension, the same as `fixed`: the point
|
|
120
|
+
of "this queue never counts an archived row" is that no pane on it ever
|
|
121
|
+
shows archived as one of its own bars either. Nothing the reader does
|
|
122
|
+
in `q[...]` can remove it, for the same reason nothing the reader does
|
|
123
|
+
can remove a host's fixed filter.
|
|
124
|
+
|
|
125
|
+
**Only a stored frame can have one.** A hand composed `janela_frame do
|
|
126
|
+
... end` block has no `Frame` row for a default to live on; a host
|
|
127
|
+
composing a page in ERB already writes its own permanent conditions in
|
|
128
|
+
its own code, in one place, which is what ADR 012 left unchanged for
|
|
129
|
+
that path. This is additive to the data-driven half only.
|
|
130
|
+
|
|
131
|
+
**Vocabulary, so the three layers stay distinct.** *Default* is the
|
|
132
|
+
frame's own, permanent, data. *Fixed* stays ADR 040's word for the
|
|
133
|
+
host's own, per-record, code. *Filters*, or `q[...]`, stays the
|
|
134
|
+
reader's. Three words, three owners, and none of them is allowed to
|
|
135
|
+
mean another.
|
|
136
|
+
|
|
137
|
+
## Consequences
|
|
138
|
+
|
|
139
|
+
- An analyst declares "this queue never counts an archived row" once,
|
|
140
|
+
as a row, and it holds across every page that ever renders the frame,
|
|
141
|
+
including one that does not exist yet.
|
|
142
|
+
- **A stale default is a frame-wide failure, not a pane-wide one.**
|
|
143
|
+
If `default_model`'s `janela` block later drops the dimension
|
|
144
|
+
`default_where` names, every pane over that model in the frame raises
|
|
145
|
+
at render, the same "missing pane" treatment `declared_by_a_janela_block`
|
|
146
|
+
already gives a stale measure or dimension, just wider: one condition
|
|
147
|
+
now speaks for every pane that shares its model rather than for one
|
|
148
|
+
row. Worth a line in the engine's own edit form once built.
|
|
149
|
+
- `janela_frames` gains `default_model` and `default_where`, both
|
|
150
|
+
nullable. Existing frames are unaffected either way; needs a migration
|
|
151
|
+
and an `UPGRADING.md` entry (ADR 015).
|
|
152
|
+
- **Not decided here: a frame with panes over two models, each wanting
|
|
153
|
+
its own permanent default.** One `(model, where)` pair per frame is
|
|
154
|
+
what is built. A second model needing one of its own is the trigger to
|
|
155
|
+
revisit this as a has-many rather than a second pair of columns, the
|
|
156
|
+
same way ADR 040 left a signed filter for the day a host's need for
|
|
157
|
+
one is real rather than guessed at.
|
|
158
|
+
- The engine's own frame form needs a way to set both columns together,
|
|
159
|
+
and to clear both together; not built here.
|
|
160
|
+
- What would change this decision: a host needing a permanent filter
|
|
161
|
+
that is not one model's own condition, such as one spanning an
|
|
162
|
+
association two different pane models both reach through. Nothing
|
|
163
|
+
proposes that today.
|
data/docs/decisions/INDEX.md
CHANGED
|
@@ -27,10 +27,10 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
|
|
|
27
27
|
| **Authorisation** | 002, 003, 004, 009, 017, 019, 022, 032, 033, 034, 035, 039, 040 |
|
|
28
28
|
| **Performance & storage** | 007, 017, 025 |
|
|
29
29
|
| **Ordering & formatting** | 007, 020, 038 |
|
|
30
|
-
| **Cross-filtering & Hotwire** | 003, 004, 005, 008, 024, 025, 040 |
|
|
30
|
+
| **Cross-filtering & Hotwire** | 003, 004, 005, 008, 024, 025, 040, 043 |
|
|
31
31
|
| **Layouts & views** | 011, 012, 016, 018, 020, 027, 039 |
|
|
32
32
|
| **CSS & styling** | 016, 018, 023, 026, 027, 036, 042 |
|
|
33
|
-
| **Frames, panes & persistence** | 012, 013, 014, 019, 029, 030, 033, 039, 040, 041 |
|
|
33
|
+
| **Frames, panes & persistence** | 012, 013, 014, 019, 029, 030, 033, 039, 040, 041, 043 |
|
|
34
34
|
| **Naming rule** | 014, 023, 036 |
|
|
35
35
|
| **JavaScript delivery & charts** | 004, 006, 026, 042 |
|
|
36
36
|
| **Time dimensions** | 006, 025 |
|
|
@@ -90,7 +90,8 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
|
|
|
90
90
|
| 040 | A Host Can Fix a Frame's Filter, and No Click Removes It | 2026-09-24 | Accepted |
|
|
91
91
|
| 041 | A Host Finds Its Frame by Owner and Key | 2026-09-24 | Accepted |
|
|
92
92
|
| 042 | A Chart's Title Is a Figcaption, and the Canvas Points to It | 2026-09-28 | Accepted |
|
|
93
|
+
| 043 | A Frame's Default Filter Names the Model It Narrows | 2026-09-28 | Accepted |
|
|
93
94
|
|
|
94
95
|
## Next number
|
|
95
96
|
|
|
96
|
-
Next ADR:
|
|
97
|
+
Next ADR: 044
|
data/docs/theming.md
CHANGED
|
@@ -31,7 +31,7 @@ Three, and setting them moves everything that depends on them.
|
|
|
31
31
|
| --- | --- | --- |
|
|
32
32
|
| `--janela-space` | `0.25rem` | The base unit of the whole spacing scale. Every gap and padding is a multiple of it. |
|
|
33
33
|
| `--janela-line` | `rgba(128, 128, 128, 0.3)` | Rules between rows, borders on cards and fields. |
|
|
34
|
-
| `--janela-accent` | `rgb(54, 162, 235)` | A selected value, a hovered card. |
|
|
34
|
+
| `--janela-accent` | `rgb(54, 162, 235)` | A selected value, a hovered card, a chart's bars (#62). |
|
|
35
35
|
|
|
36
36
|
```css
|
|
37
37
|
:root {
|
data/lib/janela/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: janela
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jay Killeen
|
|
@@ -141,6 +141,7 @@ files:
|
|
|
141
141
|
- db/migrate/20260921000001_add_owner_to_janela_snapshots.rb
|
|
142
142
|
- db/migrate/20260924000001_add_content_to_janela_panes.rb
|
|
143
143
|
- db/migrate/20260924000002_add_key_to_janela_frames.rb
|
|
144
|
+
- db/migrate/20260928000001_add_default_filter_to_janela_frames.rb
|
|
144
145
|
- docs/composing.md
|
|
145
146
|
- docs/decisions/001-built-to-be-forked.md
|
|
146
147
|
- docs/decisions/002-measures-and-dimensions-over-ransack.md
|
|
@@ -184,6 +185,7 @@ files:
|
|
|
184
185
|
- docs/decisions/040-a-host-can-fix-a-frames-filter.md
|
|
185
186
|
- docs/decisions/041-a-host-finds-its-frame-by-owner-and-key.md
|
|
186
187
|
- docs/decisions/042-a-charts-title-is-a-figcaption.md
|
|
188
|
+
- docs/decisions/043-a-frames-default-filter-names-the-model-it-narrows.md
|
|
187
189
|
- docs/decisions/INDEX.md
|
|
188
190
|
- docs/multi-tenancy.md
|
|
189
191
|
- docs/naming.md
|
|
@@ -209,22 +211,14 @@ metadata:
|
|
|
209
211
|
bug_tracker_uri: https://github.com/retail-tasker/janela/issues
|
|
210
212
|
rubygems_mfa_required: 'true'
|
|
211
213
|
post_install_message: |
|
|
212
|
-
Janela 0.
|
|
213
|
-
|
|
214
|
+
Janela 0.10.0: a migration if you use stored frames. Nobody else
|
|
215
|
+
has anything to do.
|
|
214
216
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Existing frames and panes are unaffected either way.
|
|
221
|
-
|
|
222
|
-
2. A chart pane's title is now a visible <figcaption> inside a
|
|
223
|
-
<figure> wrapping the canvas, not only the canvas's aria-label
|
|
224
|
-
(ADR 042). janela-pane moved from the <canvas> to the <figure>;
|
|
225
|
-
janela-chart stayed on the canvas. If you selected
|
|
226
|
-
.janela-pane.janela-chart as one element, or read a chart's
|
|
227
|
-
title from aria-label, both need updating.
|
|
217
|
+
A frame can now carry its own permanent filter as data, not only
|
|
218
|
+
the per-record one you pass in code (ADR 043):
|
|
219
|
+
bin/rails janela:install:migrations && bin/rails db:migrate
|
|
220
|
+
janela_frames gains default_model and default_where, both nil on
|
|
221
|
+
every existing frame. Nothing changes unless you set them.
|
|
228
222
|
|
|
229
223
|
Steps: UPGRADING.md in this gem, or
|
|
230
224
|
https://github.com/retail-tasker/janela/blob/main/UPGRADING.md
|