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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ad85780a6a73a02eb859164d7c912fb92229e357e63f0b52e90a7d1cf11c5929
|
|
4
|
+
data.tar.gz: c6e248ee131cd3b0a95b41907af314a106e999f117f6f7c8290d3d18780dfb4d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 38d3f21a99d9dd6b5b03c2f4be5a4d509b9d596923ca1f23abdcf626c36f58324194ec9e058ee559e0cc3df0b8d37362bd6ca60cf74a18658d92ce54584b9840
|
|
7
|
+
data.tar.gz: 23c5e8a36399efa81853564f9480cc780855b3452c3c3e43b6dd2b130037d960ac19160aec0fc4125e2e6b0fce83a40af02018c2561ce3bf25aab31263245f8c
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright tylercschneider
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# SettingsHub
|
|
2
|
+
Short description and motivation.
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
How to use my plugin.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
Add this line to your application's Gemfile:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem "settings_hub"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
And then execute:
|
|
15
|
+
```bash
|
|
16
|
+
$ bundle
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
```bash
|
|
21
|
+
$ gem install settings_hub
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Contributing
|
|
25
|
+
Contribution directions go here.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module SettingsHub
|
|
2
|
+
module Api
|
|
3
|
+
class SectionsController < SettingsHub::ApplicationController
|
|
4
|
+
before_action :refuse_without_capability, only: :update
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
render json: available_sections.map { |section| { key: section.key, actions: section.actions.keys } }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def update
|
|
11
|
+
return head :unprocessable_content unless requested_action
|
|
12
|
+
|
|
13
|
+
result = requested_action.new(person: current_person, account: account_settings_act_on, values: submitted_values).call
|
|
14
|
+
|
|
15
|
+
render json: { ok: result.ok?, message: result.message }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def refuse_without_capability
|
|
21
|
+
head :forbidden unless section && allowed?(section)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def requested_action
|
|
25
|
+
section.actions[params[:action_name].to_sym]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def section
|
|
29
|
+
SettingsHub.registry.find(params[:key])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def submitted_values
|
|
33
|
+
params.except(:controller, :action, :key, :action_name).permit!.to_h.symbolize_keys
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def available_sections
|
|
37
|
+
visible_areas.values.flatten
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module SettingsHub
|
|
2
|
+
class ApplicationController < ::ApplicationController
|
|
3
|
+
helper KeystoneUiHelper, SettingsHub::Engine.routes.url_helpers, SettingsHub::AppRoutesHelper
|
|
4
|
+
|
|
5
|
+
before_action { SettingsHub::AppRoutesHelper.define_app_route_helpers }
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def account_settings_act_on
|
|
10
|
+
SettingsAccount.of(self)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def visible_areas
|
|
14
|
+
SettingsHub.registry.areas
|
|
15
|
+
.transform_values { |sections| sections.select { |section| allowed?(section) } }
|
|
16
|
+
.reject { |_area, sections| sections.empty? }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def allowed?(section)
|
|
20
|
+
section.capability.nil? || can?(section.capability)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module SettingsHub
|
|
2
|
+
class SectionsController < ApplicationController
|
|
3
|
+
before_action :set_section
|
|
4
|
+
before_action :refuse_without_capability
|
|
5
|
+
|
|
6
|
+
def show
|
|
7
|
+
return redirect_to @section.at if @section.at
|
|
8
|
+
|
|
9
|
+
@person = current_person
|
|
10
|
+
@account = account_settings_act_on
|
|
11
|
+
@areas = visible_areas
|
|
12
|
+
@section_addresses = section_addresses
|
|
13
|
+
@selection = selection
|
|
14
|
+
|
|
15
|
+
render template: "settings_hub/settings/show"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def update
|
|
19
|
+
return head :unprocessable_content unless requested_action
|
|
20
|
+
|
|
21
|
+
result = requested_action.new(person: current_person, account: account_settings_act_on, values: submitted_values).call
|
|
22
|
+
return show_refusal(result.message) unless result.ok?
|
|
23
|
+
|
|
24
|
+
tell_the_app_it_ran
|
|
25
|
+
|
|
26
|
+
redirect_to section_path(params[:key])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def requested_action
|
|
32
|
+
return @section.actions[params[:action_name].to_sym] if params[:action_name]
|
|
33
|
+
|
|
34
|
+
@section.action
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def selection
|
|
38
|
+
params.except(:controller, :action, :key, :action_name).permit!.to_h.symbolize_keys
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def section_addresses
|
|
42
|
+
return { submit_url: section_path(@section.key) } unless @section.named_actions?
|
|
43
|
+
|
|
44
|
+
{ submit_urls: @section.actions.keys.to_h { |name| [ name, section_action_path(@section.key, name) ] } }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def show_refusal(message)
|
|
48
|
+
@person = current_person
|
|
49
|
+
@account = account_settings_act_on
|
|
50
|
+
@areas = visible_areas
|
|
51
|
+
@section_addresses = section_addresses
|
|
52
|
+
@selection = selection
|
|
53
|
+
@refusal = message
|
|
54
|
+
|
|
55
|
+
render template: "settings_hub/settings/show", status: :unprocessable_content
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def tell_the_app_it_ran
|
|
59
|
+
return unless respond_to?(:after_settings_change, true)
|
|
60
|
+
|
|
61
|
+
after_settings_change(section: @section, person: current_person)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def submitted_values
|
|
65
|
+
params.except(:controller, :action, :key, :signed_in_as).permit!.to_h.symbolize_keys
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def set_section
|
|
69
|
+
@section = SettingsHub.registry.find(params[:key])
|
|
70
|
+
|
|
71
|
+
raise ActionController::RoutingError, "No settings section named #{params[:key]}" unless @section
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def refuse_without_capability
|
|
75
|
+
head :forbidden unless allowed?(@section)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module SettingsHub
|
|
2
|
+
module AppRoutesHelper
|
|
3
|
+
def self.define_app_route_helpers
|
|
4
|
+
@app_route_helpers_defined ||= begin
|
|
5
|
+
(Rails.application.routes.named_routes.helper_names - SettingsHub::Engine.routes.named_routes.helper_names).each do |name|
|
|
6
|
+
define_method(name) { |*args, **options| main_app.public_send(name, *args, **options) }
|
|
7
|
+
end
|
|
8
|
+
true
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module SettingsHub
|
|
2
|
+
class ChangeName
|
|
3
|
+
def initialize(person:, account:, values:)
|
|
4
|
+
@person = person
|
|
5
|
+
@account = account
|
|
6
|
+
@values = values
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
return Result.refused(@person.errors.full_messages.to_sentence) unless @person.update(name: @values[:name])
|
|
11
|
+
|
|
12
|
+
Result.ok
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<%= ui_page(max_width: :xl) do %>
|
|
2
|
+
<%= ui_page_header(title: "Settings") %>
|
|
3
|
+
|
|
4
|
+
<%= ui_grid(cols: { default: 1, lg: 2 }, gap: :lg) do %>
|
|
5
|
+
<%= ui_section do %>
|
|
6
|
+
<% @areas.each do |area, sections| %>
|
|
7
|
+
<section aria-label="<%= area.to_s.titleize %>">
|
|
8
|
+
<%= ui_section(title: area.to_s.titleize) do %>
|
|
9
|
+
<%= ui_panel do %>
|
|
10
|
+
<nav aria-label="Settings">
|
|
11
|
+
<% sections.each do |section| %>
|
|
12
|
+
<%= link_to section.title, section.at || section_path(section.key), "aria-current": ("page" if section == @section) %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</nav>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% end %>
|
|
17
|
+
</section>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
21
|
+
<%= ui_section do %>
|
|
22
|
+
<%= ui_alert(message: @refusal, type: :error) if @refusal %>
|
|
23
|
+
<%= render partial: @section.renders, locals: {person: @person, account: @account, selection: @selection || {}, **(@section_addresses || {})} if @section %>
|
|
24
|
+
<% end %>
|
|
25
|
+
<% end %>
|
|
26
|
+
<% end %>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
SettingsHub::Engine.routes.draw do
|
|
2
|
+
get "/", to: "settings#show", as: :settings
|
|
3
|
+
namespace :api do
|
|
4
|
+
resources :sections, only: :index
|
|
5
|
+
patch "sections/:key/:action_name", to: "sections#update"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
get "/:key", to: "sections#show", as: :section
|
|
9
|
+
patch "/:key", to: "sections#update"
|
|
10
|
+
patch "/:key/:action_name", to: "sections#update", as: :section_action
|
|
11
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module SettingsHub
|
|
4
|
+
module Generators
|
|
5
|
+
class SectionGenerator < Rails::Generators::NamedBase
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
INITIALIZER = "config/initializers/settings_hub.rb".freeze
|
|
9
|
+
OPENING = "Rails.application.config.to_prepare do\n".freeze
|
|
10
|
+
|
|
11
|
+
def register_the_section
|
|
12
|
+
create_file INITIALIZER, "#{OPENING}end\n" unless initializer_exists?
|
|
13
|
+
inject_into_file INITIALIZER, registration, after: OPENING
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write_the_object
|
|
17
|
+
template "action.rb.tt", "app/models/save_#{file_name}.rb"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def write_the_partial
|
|
21
|
+
template "partial.html.erb.tt", "app/views/settings/_#{file_name}.html.erb"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def write_the_test
|
|
25
|
+
template "action_test.rb.tt", "test/models/save_#{file_name}_test.rb"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def registration
|
|
31
|
+
%( SettingsHub.section :#{file_name}, area: :user, title: "#{human_name}", renders: "settings/#{file_name}", runs: "#{save_object}"\n)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def save_object
|
|
35
|
+
"Save#{class_name}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initializer_exists?
|
|
39
|
+
File.exist?(File.join(destination_root, INITIALIZER))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
## SettingsHub
|
|
2
|
+
|
|
3
|
+
> **DO NOT** explore the settings_hub gem source code. This reference is the complete
|
|
4
|
+
> user-facing API, and settings_hub's locals are authored from it. Keep it the single
|
|
5
|
+
> source of truth: change it in the same pass that changes the interface, then
|
|
6
|
+
> author the locals again.
|
|
7
|
+
|
|
8
|
+
SettingsHub is the settings page, shared across apps: **a section is a registration,
|
|
9
|
+
not a page.** SettingsHub owns the page, the list of sections on it, the address each
|
|
10
|
+
section is served at, and the check deciding who may see one. A section owns
|
|
11
|
+
only its fields and the things a person can do with them. Adding a section
|
|
12
|
+
changes no routing, no navigation and no controller.
|
|
13
|
+
|
|
14
|
+
SettingsHub owns no database table. It is a mountable Rails engine and it draws every
|
|
15
|
+
page with keystone_ui.
|
|
16
|
+
|
|
17
|
+
### Installing it
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "settings_hub"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
mount SettingsHub::Engine => "/settings"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The host's `ApplicationController` supplies four things. Only the first is
|
|
28
|
+
required:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
def current_person # required — who is signed in
|
|
32
|
+
def settings_account # the account settings act on
|
|
33
|
+
def current_account # used when settings_account is not defined
|
|
34
|
+
def can?(capability) # required once any section names a capability
|
|
35
|
+
def after_settings_change(section:, person:) # optional, runs after a change
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`settings_account` is asked for first and `current_account` is the fallback, so
|
|
39
|
+
an app whose settings act on something other than the account a person last
|
|
40
|
+
switched to says which by defining `settings_account`. An app that defines
|
|
41
|
+
neither gets `nil`, which is correct for a product whose settings are all
|
|
42
|
+
personal.
|
|
43
|
+
|
|
44
|
+
### Generating a section
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
bin/rails generate settings_hub:section reminders
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
It registers the section in the app's settings_hub initializer, creating that file if
|
|
51
|
+
it is missing, and writes the object the section runs, the partial it draws and
|
|
52
|
+
a test for the object. The generated test passes as written, and the section
|
|
53
|
+
appears on the settings page with nothing further to wire. Everything it writes
|
|
54
|
+
is a starting point to edit.
|
|
55
|
+
|
|
56
|
+
### Registering a section
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
Rails.application.config.to_prepare do
|
|
60
|
+
SettingsHub.section :password, area: :user, title: "Password",
|
|
61
|
+
renders: "settings/password", runs: "ChangePassword"
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| Detail | Required | What it is |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| first argument | yes | the section's key, used in its address |
|
|
68
|
+
| `area:` | yes | which list it appears in — any symbol, commonly `:user`, `:team`, `:account` |
|
|
69
|
+
| `title:` | yes | what the list calls it |
|
|
70
|
+
| `renders:` | no | the partial drawn inside the settings page |
|
|
71
|
+
| `runs:` | no | the object or objects a submitted change is handed to |
|
|
72
|
+
| `capability:` | no | what a person must hold to see it |
|
|
73
|
+
| `at:` | no | an address to send a person to instead of drawing a partial |
|
|
74
|
+
|
|
75
|
+
A section with `at:` draws nothing and sends the person to that address. Use it
|
|
76
|
+
for a page settings_hub cannot serve, such as one a vendored engine owns.
|
|
77
|
+
|
|
78
|
+
Registrations go in `config.to_prepare`, which runs again on every code reload.
|
|
79
|
+
SettingsHub clears its registry each time, so registrations are made fresh rather
|
|
80
|
+
than added to what is already there.
|
|
81
|
+
|
|
82
|
+
### Replacing a registration
|
|
83
|
+
|
|
84
|
+
`SettingsHub.section` refuses a key already taken in that area. Code that means to
|
|
85
|
+
override a registered section says so:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
SettingsHub.replace_section :profile, area: :user, title: "Profile",
|
|
89
|
+
renders: "my_app/profile", runs: "MyApp::ChangeName"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### What settings_hub refuses when the app starts
|
|
93
|
+
|
|
94
|
+
Three mistakes raise `SettingsHub::BadRegistration` at registration rather than in
|
|
95
|
+
front of a person:
|
|
96
|
+
|
|
97
|
+
- a key already taken in that area, named in the message
|
|
98
|
+
- an object named in `runs:` that the app cannot find, named in the message
|
|
99
|
+
- a capability the app does not recognise, named in the message
|
|
100
|
+
|
|
101
|
+
The third runs only once the app declares what it recognises:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
SettingsHub.capabilities = -> { Citizen.capabilities }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
An app that declares nothing gets no capability check, which is what lets settings_hub
|
|
108
|
+
be installed without citizen.
|
|
109
|
+
|
|
110
|
+
### The object a section runs
|
|
111
|
+
|
|
112
|
+
`runs:` names a class as a string. The object takes three keywords and answers
|
|
113
|
+
`call`:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
class ChangeName
|
|
117
|
+
def initialize(person:, account:, values:)
|
|
118
|
+
@person = person
|
|
119
|
+
@account = account
|
|
120
|
+
@values = values
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def call
|
|
124
|
+
return SettingsHub::Result.refused(@person.errors.full_messages.to_sentence) unless @person.update(name: @values[:name])
|
|
125
|
+
|
|
126
|
+
SettingsHub::Result.ok
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`values` is what the person submitted, as a hash with symbol keys. The object
|
|
132
|
+
reads no request, no session and no params object, which is what lets a caller
|
|
133
|
+
other than the page run it.
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
SettingsHub::Result.ok # it worked
|
|
137
|
+
SettingsHub::Result.refused("why not") # it did not, and this is shown to the person
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A refusal re-draws the section with the message above it and saves nothing, and
|
|
141
|
+
the app is not told a change was made.
|
|
142
|
+
|
|
143
|
+
### The partial a section renders
|
|
144
|
+
|
|
145
|
+
The partial is handed locals and nothing else. It draws no page heading, no
|
|
146
|
+
frame and no layout, because it is rendered inside the settings page:
|
|
147
|
+
|
|
148
|
+
```erb
|
|
149
|
+
<%= ui_panel do %>
|
|
150
|
+
<%= ui_form(action: submit_url, method: :patch) do %>
|
|
151
|
+
<%= ui_form_field(attribute: "name", label: "Name", value: person.name, required: true) %>
|
|
152
|
+
<%= ui_button(label: "Save") %>
|
|
153
|
+
<% end %>
|
|
154
|
+
<% end %>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Local | What it is |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `person` | who is signed in |
|
|
160
|
+
| `account` | the account settings act on |
|
|
161
|
+
| `selection` | the query string, as a hash with symbol keys |
|
|
162
|
+
| `submit_url` | where to submit, for a section naming one object |
|
|
163
|
+
| `submit_urls` | where to submit each named object, for a section naming several |
|
|
164
|
+
|
|
165
|
+
`selection` is how a section holds a choice across a request without a
|
|
166
|
+
controller of its own — a section listing people links each one to
|
|
167
|
+
`?member_id=1` and reads `selection[:member_id]` to draw that person.
|
|
168
|
+
|
|
169
|
+
### Calling it from JSON
|
|
170
|
+
|
|
171
|
+
The same registrations and the same objects answer a caller that speaks JSON,
|
|
172
|
+
signed in as a person the same way the page is:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
GET /settings/api/sections
|
|
176
|
+
PATCH /settings/api/sections/:key/:action_name
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The listing gives back only the sections that person holds the capability for,
|
|
180
|
+
each naming the actions it offers. A section running one object names that
|
|
181
|
+
action after the section itself, so `profile` offers the action `profile`.
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
[{"key": "profile", "actions": ["profile"]},
|
|
185
|
+
{"key": "team", "actions": ["invite", "remove"]}]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
A change answers whether it worked and why not:
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{"ok": false, "message": "That name is spoken for"}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
A caller naming a section the person may not see is refused, and a refused
|
|
195
|
+
change saves nothing.
|
|
196
|
+
|
|
197
|
+
### A section that does several things
|
|
198
|
+
|
|
199
|
+
`runs:` takes a hash when a section offers more than one thing. Each gets its
|
|
200
|
+
own address, handed to the partial in `submit_urls`:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
SettingsHub.section :team, area: :team, title: "Team", capability: :manage_team,
|
|
204
|
+
renders: "citizen/members/team",
|
|
205
|
+
runs: {
|
|
206
|
+
invite: "Citizen::Invite",
|
|
207
|
+
remove: "Citizen::RemoveMember"
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```erb
|
|
212
|
+
<%= ui_form(action: submit_urls[:invite], method: :patch) do %>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Every object in the hash takes the same three keywords and returns the same
|
|
216
|
+
result as the single-object case.
|