settings_hub 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/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +10 -0
- data/app/assets/stylesheets/settings_hub/application.css +15 -0
- data/app/controllers/settings_hub/api/sections_controller.rb +41 -0
- data/app/controllers/settings_hub/application_controller.rb +23 -0
- data/app/controllers/settings_hub/sections_controller.rb +78 -0
- data/app/controllers/settings_hub/settings_controller.rb +7 -0
- data/app/helpers/settings_hub/app_routes_helper.rb +12 -0
- data/app/helpers/settings_hub/application_helper.rb +4 -0
- data/app/jobs/settings_hub/application_job.rb +4 -0
- data/app/mailers/settings_hub/application_mailer.rb +6 -0
- data/app/models/settings_hub/application_record.rb +5 -0
- data/app/models/settings_hub/change_name.rb +15 -0
- data/app/views/settings_hub/sections/_profile.html.erb +6 -0
- data/app/views/settings_hub/settings/show.html.erb +26 -0
- data/config/routes.rb +11 -0
- data/lib/generators/settings_hub/section/section_generator.rb +43 -0
- data/lib/generators/settings_hub/section/templates/action.rb.tt +11 -0
- data/lib/generators/settings_hub/section/templates/action_test.rb.tt +9 -0
- data/lib/generators/settings_hub/section/templates/partial.html.erb.tt +6 -0
- data/lib/settings_hub/bad_registration.rb +4 -0
- data/lib/settings_hub/engine.rb +9 -0
- data/lib/settings_hub/reference/guide.md +216 -0
- data/lib/settings_hub/registry.rb +58 -0
- data/lib/settings_hub/result.rb +22 -0
- data/lib/settings_hub/section.rb +30 -0
- data/lib/settings_hub/settings_account.rb +10 -0
- data/lib/settings_hub/version.rb +3 -0
- data/lib/settings_hub.rb +36 -0
- data/lib/tasks/settings_hub_tasks.rake +4 -0
- data/the_local/agents/settings_hub-develop.md +281 -0
- data/the_local/agents/settings_hub-info.md +92 -0
- data/the_local/agents/settings_hub-install.md +145 -0
- data/the_local/interface.yml +57 -0
- metadata +110 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module SettingsHub
|
|
2
|
+
class Registry
|
|
3
|
+
def initialize(capabilities: nil)
|
|
4
|
+
@capabilities = capabilities
|
|
5
|
+
@sections = []
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def add(section)
|
|
9
|
+
raise BadRegistration, "#{section.key} is already a section in the #{section.area} area" if taken_by_another(section)
|
|
10
|
+
|
|
11
|
+
replace(section)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def replace(section)
|
|
15
|
+
refuse_objects_the_app_cannot_find(section)
|
|
16
|
+
refuse_capabilities_the_app_does_not_recognise(section)
|
|
17
|
+
@sections.delete(taken_by_another(section))
|
|
18
|
+
@sections << section
|
|
19
|
+
section
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def in_area(area)
|
|
23
|
+
@sections.select { |section| section.area == area.to_sym }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def find(key)
|
|
27
|
+
@sections.find { |section| section.key == key.to_sym }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def areas
|
|
31
|
+
@sections.group_by(&:area)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def refuse_objects_the_app_cannot_find(section)
|
|
37
|
+
section.actions
|
|
38
|
+
rescue NameError => missing
|
|
39
|
+
raise BadRegistration, missing.message
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def refuse_capabilities_the_app_does_not_recognise(section)
|
|
43
|
+
return if section.capability.nil? || recognised_capabilities.nil?
|
|
44
|
+
return if recognised_capabilities.map(&:to_sym).include?(section.capability.to_sym)
|
|
45
|
+
|
|
46
|
+
raise BadRegistration, "#{section.capability} is not a capability this app recognises"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def recognised_capabilities
|
|
50
|
+
declared = @capabilities || SettingsHub.capabilities
|
|
51
|
+
declared.respond_to?(:call) ? declared.call : declared
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def taken_by_another(section)
|
|
55
|
+
@sections.find { |existing| existing.key == section.key && existing.area == section.area }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module SettingsHub
|
|
2
|
+
class Result
|
|
3
|
+
attr_reader :message
|
|
4
|
+
|
|
5
|
+
def self.ok
|
|
6
|
+
new(ok: true)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.refused(message)
|
|
10
|
+
new(ok: false, message: message)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(ok:, message: nil)
|
|
14
|
+
@ok = ok
|
|
15
|
+
@message = message
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ok?
|
|
19
|
+
@ok
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module SettingsHub
|
|
2
|
+
class Section
|
|
3
|
+
attr_reader :key, :area, :title, :renders, :capability, :runs, :at
|
|
4
|
+
|
|
5
|
+
def initialize(key:, area:, title:, renders: nil, capability: nil, runs: nil, at: nil)
|
|
6
|
+
@key = key.to_sym
|
|
7
|
+
@area = area.to_sym
|
|
8
|
+
@title = title
|
|
9
|
+
@renders = renders
|
|
10
|
+
@capability = capability
|
|
11
|
+
@runs = runs
|
|
12
|
+
@at = at
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def action
|
|
16
|
+
actions[key]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def actions
|
|
20
|
+
return {} if runs.nil?
|
|
21
|
+
return { key => runs.constantize } unless runs.is_a?(Hash)
|
|
22
|
+
|
|
23
|
+
runs.transform_values(&:constantize)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def named_actions?
|
|
27
|
+
runs.is_a?(Hash)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module SettingsHub
|
|
2
|
+
module SettingsAccount
|
|
3
|
+
def self.of(controller)
|
|
4
|
+
return controller.send(:settings_account) if controller.respond_to?(:settings_account, true)
|
|
5
|
+
return controller.send(:current_account) if controller.respond_to?(:current_account, true)
|
|
6
|
+
|
|
7
|
+
nil
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
data/lib/settings_hub.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "keystone_ui"
|
|
2
|
+
|
|
3
|
+
require "settings_hub/version"
|
|
4
|
+
require "settings_hub/engine"
|
|
5
|
+
require "settings_hub/bad_registration"
|
|
6
|
+
require "settings_hub/registry"
|
|
7
|
+
require "settings_hub/section"
|
|
8
|
+
require "settings_hub/result"
|
|
9
|
+
require "settings_hub/settings_account"
|
|
10
|
+
|
|
11
|
+
module SettingsHub
|
|
12
|
+
class << self
|
|
13
|
+
attr_accessor :capabilities
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.registry
|
|
17
|
+
@registry ||= Registry.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.section(key, **details)
|
|
21
|
+
registry.add(Section.new(key: key, **details))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.replace_section(key, **details)
|
|
25
|
+
registry.replace(Section.new(key: key, **details))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.prepare!
|
|
29
|
+
@registry = nil
|
|
30
|
+
register_own_sections
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.register_own_sections
|
|
34
|
+
section :profile, area: :user, title: "Profile", renders: "settings_hub/sections/profile", runs: "SettingsHub::ChangeName"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: settings_hub-develop
|
|
3
|
+
description: Use PROACTIVELY for generating a settings section, editing a generated one, registering one by hand, replacing one another gem registered, writing the object a submitted change runs, writing the partial a section draws, and reading or changing settings from a JSON caller — MUST BE USED instead of hand-building a settings page, route or controller.
|
|
4
|
+
tools: Bash, Read, Write, Edit, Grep
|
|
5
|
+
scope: settings — one settings page whose sections are registered by the app and by other gems
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local follows the steps below exactly and invents none. Where a step names a
|
|
9
|
+
decision, put it to the developer and wait for an answer rather than picking one.
|
|
10
|
+
|
|
11
|
+
## What settings_hub is
|
|
12
|
+
|
|
13
|
+
SettingsHub is a mountable Rails engine serving one settings page whose sections the
|
|
14
|
+
app and other gems register. A section is a registration, not a page: it names
|
|
15
|
+
what it is called, which list it belongs in, what is drawn for it, and what runs
|
|
16
|
+
when a person submits it. SettingsHub owns the page, each section's address and the
|
|
17
|
+
check deciding who may see one, so adding a section changes no route, no
|
|
18
|
+
navigation and no controller. The same registrations answer a caller that speaks
|
|
19
|
+
JSON rather than asking for the page. Fire this local whenever settings are being
|
|
20
|
+
added to or changed in an app that already has settings_hub mounted.
|
|
21
|
+
|
|
22
|
+
## Interface
|
|
23
|
+
|
|
24
|
+
- `bin/rails generate settings_hub:section <name>` — registers a section and writes the
|
|
25
|
+
object it runs, the partial it draws and a test for that object.
|
|
26
|
+
- `GET /settings/api/sections` — the sections the signed-in person may see, each
|
|
27
|
+
naming the actions it offers, as JSON.
|
|
28
|
+
- `PATCH /settings/api/sections/:key/:action_name` — runs one named action of one
|
|
29
|
+
section for a JSON caller and answers whether it worked.
|
|
30
|
+
- `SettingsHub.section` — registers a section, refusing a key already taken in that
|
|
31
|
+
area.
|
|
32
|
+
- `SettingsHub.replace_section` — registers a section over one already registered
|
|
33
|
+
under the same key and area, instead of refusing it.
|
|
34
|
+
- `SettingsHub.registry` — the sections registered so far, answering `find(key)`,
|
|
35
|
+
`in_area(area)` and `areas`.
|
|
36
|
+
- `SettingsHub::Result.ok` — what an object answers when the change was made.
|
|
37
|
+
- `SettingsHub::Result.refused` — what an object answers when it was not, carrying the
|
|
38
|
+
message shown to the person or given back to the caller.
|
|
39
|
+
- `SettingsHub::BadRegistration` — raised while registering when the key is taken,
|
|
40
|
+
when an object named in `runs:` cannot be found, or when a capability the app
|
|
41
|
+
does not recognise is named.
|
|
42
|
+
- `section.key` — the section's own name as a symbol, and the last part of its
|
|
43
|
+
address.
|
|
44
|
+
- `section.area` — the list it appears in, as a symbol.
|
|
45
|
+
- `section.title` — what that list calls it.
|
|
46
|
+
- `section.renders` — the partial drawn for it, or `nil`.
|
|
47
|
+
- `section.capability` — what a person must hold to see it, or `nil` when
|
|
48
|
+
everyone signed in may.
|
|
49
|
+
- `section.runs` — the class name, or hash of names, a submitted change is handed
|
|
50
|
+
to.
|
|
51
|
+
- `section.at` — the address a person is sent to instead of being drawn a
|
|
52
|
+
partial.
|
|
53
|
+
- `section.action` — the class held under the section's own key, which is the one
|
|
54
|
+
object a section naming a single object runs.
|
|
55
|
+
- `section.actions` — every class behind the section, keyed by the names given in
|
|
56
|
+
`runs:` and by the section's own key when a single object was named, and empty
|
|
57
|
+
for a section that runs nothing.
|
|
58
|
+
- `section.named_actions?` — whether the section named several objects rather
|
|
59
|
+
than one.
|
|
60
|
+
- `person` — the partial's local for who is signed in.
|
|
61
|
+
- `account` — the partial's local for the account settings act on, `nil` when the
|
|
62
|
+
host names none.
|
|
63
|
+
- `selection` — the partial's local for the query string, as a hash with symbol
|
|
64
|
+
keys.
|
|
65
|
+
- `submit_url` — the partial's local for where to submit, given to every section
|
|
66
|
+
that did not name several objects.
|
|
67
|
+
- `submit_urls` — the partial's local for where to submit each named object, as a
|
|
68
|
+
hash keyed by the names given in `runs:`.
|
|
69
|
+
|
|
70
|
+
## How to use it
|
|
71
|
+
|
|
72
|
+
1. **Settle what the section is.** Ask the developer three things and do not pick
|
|
73
|
+
any of them yourself: which area it belongs in, whether a capability is needed
|
|
74
|
+
to see it, and whether it offers one thing a person can do or several. The
|
|
75
|
+
area is any symbol and is commonly `:user`, `:team` or `:account`; sections
|
|
76
|
+
sharing one are listed together under it.
|
|
77
|
+
|
|
78
|
+
2. **Read what is already there before writing anything.** A section may arrive
|
|
79
|
+
already registered, with an object, a partial and a test written for it, in
|
|
80
|
+
which case every step below is an edit to a file that exists rather than a new
|
|
81
|
+
file. Find the registration in `config/initializers/settings_hub.rb` or in the
|
|
82
|
+
registering gem's engine, and follow `renders:` and `runs:` to the partial and
|
|
83
|
+
the object they name.
|
|
84
|
+
|
|
85
|
+
3. **Generate a new section rather than writing its files by hand.** The
|
|
86
|
+
generator takes the section's name and writes every file a working section
|
|
87
|
+
needs:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
bin/rails generate settings_hub:section reminders
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
It adds the registration to `config/initializers/settings_hub.rb`, creating that
|
|
94
|
+
file with the reload hook already in it when it is missing. It writes
|
|
95
|
+
`app/models/save_reminders.rb`, `app/views/settings/_reminders.html.erb` and
|
|
96
|
+
`test/models/save_reminders_test.rb`. The generated test passes as written and
|
|
97
|
+
the section appears on the settings page with nothing further to wire. The
|
|
98
|
+
registration it writes always uses `area: :user` and names no capability, so
|
|
99
|
+
edit it to match the answers from step 1.
|
|
100
|
+
|
|
101
|
+
4. **Register it in a reload hook.** An app not using the generator registers in
|
|
102
|
+
`config/initializers/settings_hub.rb`, a gem in its own engine:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
Rails.application.config.to_prepare do
|
|
106
|
+
SettingsHub.section :password, area: :user, title: "Password",
|
|
107
|
+
renders: "settings/password", runs: "ChangePassword"
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The first argument is the key and appears in the section's address. `area:`
|
|
112
|
+
and `title:` are required; `renders:`, `runs:`, `capability:` and `at:` are
|
|
113
|
+
not. SettingsHub clears every registration on each code reload and the hook runs
|
|
114
|
+
again, so a registration made anywhere but `to_prepare` is made once and then
|
|
115
|
+
lost.
|
|
116
|
+
|
|
117
|
+
5. **Write the partial `renders:` names.** The string is a partial path in the
|
|
118
|
+
host, so `"settings/password"` is `app/views/settings/_password.html.erb`. It
|
|
119
|
+
is drawn inside the settings page, so it writes no page heading, no frame and
|
|
120
|
+
no layout, and it uses keystone_ui helpers so it matches every other section:
|
|
121
|
+
|
|
122
|
+
```erb
|
|
123
|
+
<%= ui_panel do %>
|
|
124
|
+
<%= ui_form(action: submit_url, method: :patch) do %>
|
|
125
|
+
<%= ui_form_field(attribute: "name", label: "Name", value: person.name, required: true) %>
|
|
126
|
+
<%= ui_button(label: "Save") %>
|
|
127
|
+
<% end %>
|
|
128
|
+
<% end %>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
It is handed `person`, `account`, `selection` and either `submit_url` or
|
|
132
|
+
`submit_urls`, and nothing else — no request, no params, no controller. Submit
|
|
133
|
+
with `method: :patch`.
|
|
134
|
+
|
|
135
|
+
6. **Write the object `runs:` names.** Name the class as a string. It takes three
|
|
136
|
+
keywords and answers `call` with a result:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
class ChangePassword
|
|
140
|
+
def initialize(person:, account:, values:)
|
|
141
|
+
@person = person
|
|
142
|
+
@account = account
|
|
143
|
+
@values = values
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def call
|
|
147
|
+
return SettingsHub::Result.refused("That password is too short") unless @person.update(password: @values[:password])
|
|
148
|
+
|
|
149
|
+
SettingsHub::Result.ok
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`values` is what was submitted, as a hash with symbol keys. Reading a request,
|
|
155
|
+
a session or a params object here is what stops a caller other than the page
|
|
156
|
+
running the same object, so do neither.
|
|
157
|
+
|
|
158
|
+
7. **Answer with a result, never a boolean or an exception.**
|
|
159
|
+
`SettingsHub::Result.ok` means the change was made and the person is sent back to
|
|
160
|
+
the section. `SettingsHub::Result.refused("why not")` draws the section again with
|
|
161
|
+
that message above it, saves nothing, and does not tell the host a change was
|
|
162
|
+
made.
|
|
163
|
+
|
|
164
|
+
8. **Give each thing its own name when the section does several.** `runs:` takes
|
|
165
|
+
a hash, and each name gets its own address in `submit_urls`:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
SettingsHub.section :team, area: :team, title: "Team", capability: :manage_team,
|
|
169
|
+
renders: "citizen/members/team",
|
|
170
|
+
runs: {
|
|
171
|
+
invite: "Citizen::Invite",
|
|
172
|
+
remove: "Citizen::RemoveMember"
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```erb
|
|
177
|
+
<%= ui_form(action: submit_urls[:invite], method: :patch) do %>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Every object in the hash takes the same three keywords and answers the same
|
|
181
|
+
way as a single one. A section naming several is handed `submit_urls` and not
|
|
182
|
+
`submit_url`, so a partial written against one does not work for the other.
|
|
183
|
+
|
|
184
|
+
9. **Call the same sections from JSON when the caller is not a browser.** Two
|
|
185
|
+
addresses under wherever the engine is mounted answer a caller signed in as a
|
|
186
|
+
person the same way the page is:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
GET /settings/api/sections
|
|
190
|
+
PATCH /settings/api/sections/:key/:action_name
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The listing holds only the sections that person may see, each naming the
|
|
194
|
+
actions it offers. A section running one object names that action after the
|
|
195
|
+
section itself:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
[{"key": "profile", "actions": ["profile"]},
|
|
199
|
+
{"key": "team", "actions": ["invite", "remove"]}]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
A change names the section and one of those actions, and is answered whether
|
|
203
|
+
it happened and why not:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{"ok": false, "message": "That name is spoken for"}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The same objects run for both callers, so a section needs nothing added to it
|
|
210
|
+
to answer here. Take an action name from the listing rather than building one.
|
|
211
|
+
|
|
212
|
+
10. **Hold a choice across a request with `selection`.** A section with no
|
|
213
|
+
controller of its own reads the query string from it — a section listing
|
|
214
|
+
people links each to `?member_id=1` and draws the one `selection[:member_id]`
|
|
215
|
+
names. Keys are symbols and the hash is empty when nothing was asked for.
|
|
216
|
+
|
|
217
|
+
11. **Name a capability when not everyone may see the section.** `capability:`
|
|
218
|
+
takes the product's own word for it, the section is left out of the page and
|
|
219
|
+
out of the JSON listing for anyone who does not hold it, and both of its
|
|
220
|
+
addresses answer forbidden. A section naming none is shown to everyone the
|
|
221
|
+
app let in.
|
|
222
|
+
|
|
223
|
+
12. **Send the person elsewhere with `at:` when settings_hub cannot draw the page.** A
|
|
224
|
+
section with `at:` draws no partial and redirects to that address, which is
|
|
225
|
+
how a page another engine owns is listed beside the rest. Give it no
|
|
226
|
+
`renders:` and no `runs:`.
|
|
227
|
+
|
|
228
|
+
13. **Replace a registration rather than registering over it.** `SettingsHub.section`
|
|
229
|
+
refuses a key already taken in that area, so code meaning to override a
|
|
230
|
+
section another gem registered says so:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
SettingsHub.replace_section :profile, area: :user, title: "Profile",
|
|
234
|
+
renders: "my_app/profile", runs: "MyApp::ChangeName"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
It takes the same arguments as `SettingsHub.section` and every other rule above
|
|
238
|
+
still applies to it.
|
|
239
|
+
|
|
240
|
+
14. **Check it worked.** Start the app, sign in and visit the settings path. The
|
|
241
|
+
section appears in its area's list, the partial draws beside it, and
|
|
242
|
+
submitting saves and returns to the section. A refusal shows the message
|
|
243
|
+
above the partial and leaves the data unchanged.
|
|
244
|
+
|
|
245
|
+
## Conventions
|
|
246
|
+
|
|
247
|
+
**A section that was generated rather than written saves nothing yet.** Its
|
|
248
|
+
object answers `SettingsHub::Result.ok` without touching the person or the account,
|
|
249
|
+
its partial holds one field named after the section, and its test asserts only
|
|
250
|
+
that the object answers ok, so all three are edited before the section does
|
|
251
|
+
anything. Edit the files in place and keep the object's three keywords and its
|
|
252
|
+
result.
|
|
253
|
+
|
|
254
|
+
**A registration that is wrong stops the app rather than the person.** SettingsHub
|
|
255
|
+
raises `SettingsHub::BadRegistration` while registering for a key already taken in
|
|
256
|
+
that area, an object named in `runs:` that the app cannot find, and a capability
|
|
257
|
+
the app does not recognise, and the message names which. Fix the registration;
|
|
258
|
+
never rescue it.
|
|
259
|
+
|
|
260
|
+
**Keep a key unique across the whole page, not just its area.** A registration is
|
|
261
|
+
refused only when the key is taken in the same area, but a section is looked up
|
|
262
|
+
by key alone, so the same key in two areas leaves one of them unreachable.
|
|
263
|
+
|
|
264
|
+
**A section runs nothing unless `runs:` names something.** Submitting from the
|
|
265
|
+
page to a section that named no object, or to a name its hash does not hold, is
|
|
266
|
+
rejected and nothing runs.
|
|
267
|
+
|
|
268
|
+
**A JSON caller submits only to an action the listing gave it.** A section
|
|
269
|
+
running nothing is listed with no actions at all, and the two JSON addresses are
|
|
270
|
+
the whole of what a caller outside the browser gets.
|
|
271
|
+
|
|
272
|
+
**SettingsHub stores nothing.** It owns no table, so every field a section shows and
|
|
273
|
+
every change it makes belongs to whoever registered it.
|
|
274
|
+
|
|
275
|
+
**The shipped Profile section reads and writes `name` on whatever the host
|
|
276
|
+
returns for the signed-in person.** An app whose person record has no writable
|
|
277
|
+
`name` replaces that section with `SettingsHub.replace_section`.
|
|
278
|
+
|
|
279
|
+
**Out of scope.** Adding the gem, mounting the engine, the methods the host's
|
|
280
|
+
`ApplicationController` supplies, and declaring which capabilities the app
|
|
281
|
+
recognises all belong to settings_hub-install.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: settings_hub-info
|
|
3
|
+
description: Use to learn what settings_hub offers — the shared settings page, sections as registrations, and the vocabulary its other locals assume.
|
|
4
|
+
tools: Read
|
|
5
|
+
scope: settings — one settings page whose sections are registered by the app and by other gems
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local explains what settings_hub is and which of its other locals you want. It
|
|
9
|
+
changes nothing and gives no steps.
|
|
10
|
+
|
|
11
|
+
## What settings_hub is
|
|
12
|
+
|
|
13
|
+
SettingsHub is the settings page an app mounts instead of building. It is a mountable
|
|
14
|
+
Rails engine that owns the page itself, the list of what appears on it, the
|
|
15
|
+
address each entry is served at, and the check deciding who may see one. An app
|
|
16
|
+
or another gem adds to that page by registering a section, and a section owns
|
|
17
|
+
only its own fields and the things a person can do with them. Adding one changes
|
|
18
|
+
no routing, no navigation and no controller in the host.
|
|
19
|
+
|
|
20
|
+
Reach for it when a product needs one settings page that several parts of the
|
|
21
|
+
codebase contribute to — the app's own settings beside settings that belong to
|
|
22
|
+
gems the app installs. Those same registrations also answer a caller that speaks
|
|
23
|
+
JSON rather than asking for the page, so a client outside the browser reads and
|
|
24
|
+
changes settings without the app building a second interface for them. SettingsHub
|
|
25
|
+
owns no database table and stores nothing; every section's data belongs to
|
|
26
|
+
whoever registered it. Every page is drawn with keystone_ui, so a host that does
|
|
27
|
+
not use keystone_ui gets a settings page that does not match the rest of it.
|
|
28
|
+
|
|
29
|
+
## Interface
|
|
30
|
+
|
|
31
|
+
SettingsHub's surface is split between its other two locals and this one documents
|
|
32
|
+
none of it.
|
|
33
|
+
|
|
34
|
+
- **settings_hub-install** — putting the gem in, mounting the engine, the methods the
|
|
35
|
+
host's `ApplicationController` supplies for who is signed in and what settings
|
|
36
|
+
act on, and telling settings_hub which capabilities the app recognises.
|
|
37
|
+
- **settings_hub-develop** — writing a new section's starting files, registering a
|
|
38
|
+
section by hand, replacing one another gem registered, the object a section
|
|
39
|
+
hands a submitted change to, the result that object answers with, the locals
|
|
40
|
+
its partial is drawn with, and the addresses a JSON caller reads and submits
|
|
41
|
+
at.
|
|
42
|
+
|
|
43
|
+
## How to use it
|
|
44
|
+
|
|
45
|
+
- Putting settings_hub into an app for the first time, or an app has it and no section
|
|
46
|
+
is appearing — **settings_hub-install**.
|
|
47
|
+
- Starting a section from nothing, editing one that was generated, registering
|
|
48
|
+
one by hand, changing one that already exists, or calling settings from
|
|
49
|
+
outside the browser — **settings_hub-develop**.
|
|
50
|
+
|
|
51
|
+
Both, in that order, when an app is taking settings_hub and its first section in the
|
|
52
|
+
same pass.
|
|
53
|
+
|
|
54
|
+
## Conventions
|
|
55
|
+
|
|
56
|
+
**Section** — one registration, not a page and not a controller. It names what
|
|
57
|
+
it is called, which list it belongs in, what is drawn for it, and what runs when
|
|
58
|
+
a person submits it.
|
|
59
|
+
|
|
60
|
+
**Area** — the list a section appears in on the page, named by a symbol. The
|
|
61
|
+
person's own settings, a team's and an account's are the usual three, any symbol
|
|
62
|
+
is allowed, and sections sharing an area are shown together.
|
|
63
|
+
|
|
64
|
+
**Key** — the section's own name, used in its address under wherever the engine
|
|
65
|
+
is mounted. A registration taking a key already held in that area is refused
|
|
66
|
+
when the app starts rather than in front of a person, and a section is found by
|
|
67
|
+
key alone, so the same key in two areas leaves one of them unreachable.
|
|
68
|
+
|
|
69
|
+
**Action** — one thing a person can do in a section, named by a symbol and
|
|
70
|
+
answered by its own object. A section that offers several names each of them; a
|
|
71
|
+
section that offers one has that action named after the section itself, so a
|
|
72
|
+
caller addresses every action the same way whichever kind it is.
|
|
73
|
+
|
|
74
|
+
**Capability** — the product's word for what a person must hold to see a
|
|
75
|
+
section. A section that names none is shown to everyone signed in, and the host
|
|
76
|
+
answers whether the signed-in person holds one. The refusal to register a
|
|
77
|
+
capability the app does not recognise runs only once the app declares which
|
|
78
|
+
capabilities exist, which is what lets settings_hub be installed without the gem that
|
|
79
|
+
would normally supply them.
|
|
80
|
+
|
|
81
|
+
**Registration time** — registrations are made in the reload hook rather than at
|
|
82
|
+
boot, and settings_hub clears what it holds on each reload, so the set of sections is
|
|
83
|
+
rebuilt from scratch every time the code reloads.
|
|
84
|
+
|
|
85
|
+
**Refusal** — a submitted change that did not happen. The object behind the
|
|
86
|
+
section says so with a message, nothing is saved, and the host is not told a
|
|
87
|
+
change was made. A person is drawn the section again with the message above it,
|
|
88
|
+
and a JSON caller is answered that it did not happen and given the same message.
|
|
89
|
+
|
|
90
|
+
**Generated code is a starting point.** A section written from nothing runs and
|
|
91
|
+
its test passes as written, and everything in it is meant to be edited rather
|
|
92
|
+
than kept as it came out.
|