flipside 0.2.1 → 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 +11 -0
- data/README.md +137 -33
- data/lib/flipside/config/settings.rb +10 -0
- data/lib/flipside/feature_presenter.rb +2 -39
- data/lib/flipside/importmap.rb +26 -0
- data/lib/flipside/public/index.js +2 -2
- data/lib/flipside/public/inline_edit_controller.js +25 -0
- data/lib/flipside/version.rb +1 -1
- data/lib/flipside/views/_datetime_modal.erb +1 -1
- data/lib/flipside/views/_feature_item.erb +8 -4
- data/lib/flipside/views/_toggle_button.erb +9 -9
- data/lib/flipside/views/feature_entities.erb +4 -4
- data/lib/flipside/views/feature_roles.erb +4 -4
- data/lib/flipside/views/index.erb +6 -4
- data/lib/flipside/views/layout.erb +2 -9
- data/lib/flipside/views/show.erb +33 -10
- data/lib/flipside/web.rb +108 -82
- data/lib/flipside.rb +19 -10
- metadata +46 -3
- data/lib/flipside/public/toggle_controller.js +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d4d7b79a7cff6585a0858dfeb93ca2a79511458ae211bd27e5cf70c5f114ff79
|
4
|
+
data.tar.gz: b526c4c4eee41a72f6576d8fb3044815b3d58aa515a38280d754d504e4d55b15
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 63a30c74eb4b138808e24b79316806325b47a42dabc17fa9d59550e2967edd8a106b85c80612bc2dfa7549a61f329ff2952331f25034bde125c3c9912d99db45
|
7
|
+
data.tar.gz: fbd4ac70e3e00e6935201925f51e7eb7bce04f8c4735267e4bba8e919d2f1214818bee38c2829691bbf7981543f28f0469379c47df8b63b3a7faca157e3150d0
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
## [0.3.0] - 2025-05-14
|
2
|
+
- Rewrite Sinatra UI to Roda
|
3
|
+
- Fix links not respecting mount point
|
4
|
+
- Add Cache-Control header for js files
|
5
|
+
|
6
|
+
## [0.2.2] - 2025-04-25
|
7
|
+
|
8
|
+
- Add disabled? method
|
9
|
+
- Edit feature description from UI
|
10
|
+
- Add configuration for setting default_object
|
11
|
+
|
1
12
|
## [0.2.1] - 2025-01-30
|
2
13
|
|
3
14
|
- Option to create missing features
|
data/README.md
CHANGED
@@ -33,41 +33,72 @@ This will create a migration file. Run the migration, to add the flipside tables
|
|
33
33
|
|
34
34
|
## Usage
|
35
35
|
|
36
|
-
|
36
|
+
### Defining Features
|
37
37
|
|
38
38
|
Features are created by running this (in a console or from code):
|
39
39
|
```ruby
|
40
|
-
Flipside
|
40
|
+
Flipside.create(
|
41
41
|
name: "MyFeature",
|
42
42
|
description: "Some optional description about what this feature do"
|
43
43
|
)
|
44
44
|
```
|
45
45
|
|
46
|
-
By default features are turned off. If
|
46
|
+
By default features are turned off. If we would like it turned on from the get go we could pass in `enabled: true`.
|
47
47
|
```ruby
|
48
|
-
Flipside
|
48
|
+
Flipside.create(name: "MyFeature", enabled: true)
|
49
49
|
```
|
50
50
|
|
51
51
|
Features can be active during a given period. Set `activated_at` and/or `deactivated_at` to define this period.
|
52
52
|
Note: A feature is always disabled outside of the active period.
|
53
|
+
Note: A nil value means that its active. I.e. `activated_at = nil` means active from the start, `deactivated_at = nil` means never deactivates.
|
53
54
|
```ruby
|
54
|
-
Flipside
|
55
|
+
Flipside.create(
|
55
56
|
name: "MyFeature",
|
56
57
|
activated_at: 1.week.from_now,
|
57
58
|
deactivated_at: 2.weeks.from_now
|
58
59
|
)
|
59
60
|
```
|
60
61
|
|
61
|
-
|
62
|
+
### Checking Feature Status
|
63
|
+
|
64
|
+
#### Globally
|
65
|
+
|
66
|
+
Check if a feature is enabled globally:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
Flipside.enabled? "MyFeature"
|
70
|
+
```
|
71
|
+
|
72
|
+
We can also check if a feature is disabled:
|
73
|
+
```ruby
|
74
|
+
Flipside.disabled? "MyFeature"
|
75
|
+
```
|
76
|
+
|
77
|
+
#### For a Specific Record
|
78
|
+
|
79
|
+
Check if a feature is enabled for a specific record (e.g. a user):
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
Flipside.enabled? "MyFeature", user
|
83
|
+
```
|
84
|
+
|
85
|
+
We can also check multiple records. `.enabled?` will return `true` if any of them have the feature enabled:
|
86
|
+
```ruby
|
87
|
+
Flipside.enabled? "MyFeature", user, company, location
|
88
|
+
```
|
89
|
+
|
90
|
+
### Enabling features for specific records
|
91
|
+
|
92
|
+
Features can be enabled for a certain record, typically a certain user or organization. These records are called entities. To enable a feature for a given record use `.add_entity`:
|
62
93
|
```ruby
|
63
94
|
user = User.first
|
64
|
-
Flipside.enabled? user # => false
|
95
|
+
Flipside.enabled? "MyFeature", user # => false
|
65
96
|
Flipside.add_entity(name: "MyFeature", user)
|
66
|
-
Flipside.enabled? user # => true
|
97
|
+
Flipside.enabled? "MyFeature", user # => true
|
67
98
|
```
|
68
99
|
|
69
100
|
Features can be enabled for records responding true to a certain method. This is called a "role". Given that User records have an admin? method. A feature can then be enabled
|
70
|
-
for all users
|
101
|
+
for all users who are admins, using the `.add_role` method:
|
71
102
|
```ruby
|
72
103
|
user1 = User.new(admin: false)
|
73
104
|
user2 = User.new(admin: true)
|
@@ -76,43 +107,69 @@ Flipside.add_role(
|
|
76
107
|
class_name: "User",
|
77
108
|
method_name: :admin?
|
78
109
|
)
|
79
|
-
Flipside.enabled? user1 # => false
|
80
|
-
Flipside.enabled? user2 # => true
|
110
|
+
Flipside.enabled? "MyFeature", user1 # => false
|
111
|
+
Flipside.enabled? "MyFeature", user2 # => true
|
112
|
+
Flipside.enabled? "MyFeature", user1, user2 # => true, enabled for at least one.
|
81
113
|
```
|
82
114
|
|
83
115
|
|
84
|
-
|
116
|
+
## UI
|
117
|
+
Flipside comes with a Roda web ui to mange feature flags. To mount this roda app in Rails add the following to your routes.rb file.
|
118
|
+
```ruby
|
119
|
+
mount Flipside::Web, at: '/flipside'
|
120
|
+
```
|
121
|
+
Note: you probably want to wrap this inside a constraints block to provide some authentication.
|
85
122
|
|
86
|
-
|
123
|
+

|
87
124
|
|
88
|
-
Check if a feature is enabled globally:
|
89
125
|
|
90
|
-
|
91
|
-
Flipside.enabled? "MyFeature"
|
92
|
-
```
|
126
|
+
### Configuration
|
93
127
|
|
94
|
-
|
128
|
+
The Flipside UI can be configured by calling some class methods on `Flipside` (see below).
|
95
129
|
|
96
|
-
|
130
|
+
`ui_back_path` is used to set a path to return to from the Flipside UI. If this is set, then the UI shows a "Back" button,
|
131
|
+
targeting this path/url. By default no back button is shown.
|
132
|
+
|
133
|
+
|
134
|
+
If `create_missing_features` is set to true, then features will automatically be created, whenever a check for an unknown feature is done.
|
135
|
+
This can be a convenient way of makes features "show up" in the Flipside UI. However, the description will not reveal much about what this features does.
|
136
|
+
(it will simply point to the place in the code where this check was done). So these features should be manually updated with a better description.
|
137
|
+
By default, features are not added and code like `Flipside.enabled? "Some unknown feature"` will simply return `false`.
|
97
138
|
|
139
|
+
`default_object` can be used to avoid the need to always pass in a record when checking if a feature is enabled. For example, say that we
|
140
|
+
always want to check if a feature is enabled for the currently logged in user (`Current.user`). Then we might add this configuration:
|
98
141
|
```ruby
|
99
|
-
Flipside.
|
142
|
+
Flipside.default_object = -> { Current.user }
|
100
143
|
```
|
101
|
-
|
102
|
-
## UI
|
103
|
-
Flipside comes with a sinatra web ui to mange feature flags. To mount this sinatra app in Rails add the following to your routes.rb file.
|
144
|
+
Now we can check if a feature is enabled without needing to pass in `Current.user`:
|
104
145
|
```ruby
|
105
|
-
|
146
|
+
# With `default_object` set then all we need is this
|
147
|
+
Flipside.enabled? :some_feature
|
148
|
+
|
149
|
+
# Which will be the same as
|
150
|
+
Flipside.enabled? :some_feature, Current.user
|
151
|
+
|
152
|
+
# Note: if we do pass in an argument, then `default_object ` will not be used:
|
153
|
+
Flipside.enabled? :some_feature, Current.company # check current company instead of user.
|
106
154
|
```
|
107
|
-
Note: you probably want to wrap this inside a constraints block to provide some authentication.
|
108
155
|
|
109
|
-

|
110
156
|
|
157
|
+
Typically this configuration should be declared in an initializer file.
|
111
158
|
|
112
|
-
|
159
|
+
```ruby
|
160
|
+
# config/initializers/flipside.rb
|
161
|
+
require 'flipside'
|
113
162
|
|
114
|
-
|
115
|
-
|
163
|
+
Flipside.ui_back_path = "/"
|
164
|
+
Flipside.create_missing_features = true
|
165
|
+
Flipside.default_object = -> { Current.user }
|
166
|
+
```
|
167
|
+
|
168
|
+
#### Entities
|
169
|
+
|
170
|
+
Entities can be added to a feature by searching for records.
|
171
|
+
|
172
|
+

|
116
173
|
|
117
174
|
To make this work, some configuration is required. Use the class method `Flipside.register_entity` for this.
|
118
175
|
```ruby
|
@@ -123,12 +180,59 @@ Flipside.register_entity(
|
|
123
180
|
identified_by: :id
|
124
181
|
)
|
125
182
|
```
|
126
|
-
Typically this should be configured in an initializer file.
|
127
183
|
|
128
|
-
The `.register_entity` method should be called once for each class that may be used as feature
|
129
|
-
The `search_by` keyword argument, which may be a
|
184
|
+
The `.register_entity` method should be called once for each class that may be used as a feature enabler.
|
185
|
+
The `search_by` keyword argument, which may be a `Symbol` or a `Proc`, dictates how records are found from searching in the ui.
|
186
|
+
When a `Symbol` is given, e.g. `:name`, then entities with an exact match on the corresponding attribute are returned. I.e. `User.where(name: query)`.
|
187
|
+
When a `Proc` is given, then this `Proc` is called with the search string and is expected to return an object responding to `to_a` (e.g. an AR collection).
|
188
|
+
This gives us the flexibility to decide how to search for entities. For example, to search for users with matching first name or last name or an email
|
189
|
+
starting with _query_, something like this could be used.
|
190
|
+
```ruby
|
191
|
+
Flipside.register_entity(
|
192
|
+
class_name: "User",
|
193
|
+
search_by: ->(str) { User.where("lower(first_name) = :name OR lower(last_name) = :name or email LIKE :str", name: str.downcase, str: "#{str}%") },
|
194
|
+
)
|
195
|
+
|
196
|
+
```
|
197
|
+
|
198
|
+
The `identified_by` keyword argument, sets the column used as primary key for the corresponding table. This defaults to `:id` and typically does need to be change.
|
199
|
+
Currently composite keys are not supported.
|
200
|
+
|
201
|
+
The `display_as` keyword argument, is used to configure how these entities show up in the combobox. When set to a `Symbol`, then this value is sent to the corresponding entity.
|
202
|
+
For example, given the following setup. Users will be displayed with first name and last name:
|
203
|
+
```ruby
|
204
|
+
class User < ApplicationRecord
|
205
|
+
def name
|
206
|
+
[first_name, last_name].compact.map(&:capitalize).join(" ")
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
Flipside.register_entity(
|
211
|
+
class_name: "User",
|
212
|
+
display_as: :name,
|
213
|
+
)
|
214
|
+
```
|
215
|
+
|
216
|
+
When a `Proc` is given, then it is expected to take an entity as input and return a string used for displaying the entity. The config above could then instead be done using:
|
217
|
+
```ruby
|
218
|
+
Flipside.register_entity(
|
219
|
+
class_name: "User",
|
220
|
+
display_as: ->(user) { [user.first_name, user.last_name].compact.map(&:capitalize).join(" ") }
|
221
|
+
)
|
222
|
+
```
|
130
223
|
|
131
|
-
|
224
|
+
#### Roles
|
225
|
+
|
226
|
+
Features can be enabled for certain roles, by searching for roles (by method name).
|
227
|
+
|
228
|
+

|
229
|
+
|
230
|
+
This is configured by calling the class method `Flipside.register_role` for each role to be added.
|
231
|
+
Note a role consists of a class and a corresponding instance method.
|
232
|
+
```ruby
|
233
|
+
Flipside.register_role(class_name: "User", method_name: :admin?)
|
234
|
+
Flipside.register_role(class_name: "User", method_name: :awesome?)
|
235
|
+
```
|
132
236
|
|
133
237
|
## Development
|
134
238
|
|
@@ -2,6 +2,16 @@ module Flipside
|
|
2
2
|
module Config
|
3
3
|
module Settings
|
4
4
|
attr_accessor :ui_back_path, :create_missing_features
|
5
|
+
attr_writer :default_object
|
6
|
+
|
7
|
+
def default_object
|
8
|
+
case @default_object
|
9
|
+
when Proc
|
10
|
+
@default_object.call
|
11
|
+
else
|
12
|
+
@default_object
|
13
|
+
end
|
14
|
+
end
|
5
15
|
end
|
6
16
|
end
|
7
17
|
end
|
@@ -2,49 +2,12 @@ require 'forwardable'
|
|
2
2
|
|
3
3
|
class FeaturePresenter
|
4
4
|
extend Forwardable
|
5
|
-
attr_reader :feature
|
5
|
+
attr_reader :feature
|
6
6
|
|
7
7
|
def_delegators :@feature, :name, :description, :enabled, :entities, :roles
|
8
8
|
|
9
|
-
def initialize(feature
|
9
|
+
def initialize(feature)
|
10
10
|
@feature = feature
|
11
|
-
@base_path = base_path
|
12
|
-
end
|
13
|
-
|
14
|
-
def href
|
15
|
-
File.join(base_path, "feature", ERB::Util.url_encode(name))
|
16
|
-
end
|
17
|
-
|
18
|
-
def toggle_path
|
19
|
-
File.join(href, "toggle")
|
20
|
-
end
|
21
|
-
|
22
|
-
def back_path
|
23
|
-
base_path
|
24
|
-
end
|
25
|
-
|
26
|
-
def entities_path
|
27
|
-
File.join(href, "entities")
|
28
|
-
end
|
29
|
-
|
30
|
-
def add_entity_path
|
31
|
-
File.join(href, "add_entity")
|
32
|
-
end
|
33
|
-
|
34
|
-
def remove_entity_path
|
35
|
-
File.join(href, "remove_entity")
|
36
|
-
end
|
37
|
-
|
38
|
-
def roles_path
|
39
|
-
File.join(href, "roles")
|
40
|
-
end
|
41
|
-
|
42
|
-
def add_role_path
|
43
|
-
File.join(href, "add_role")
|
44
|
-
end
|
45
|
-
|
46
|
-
def remove_role_path
|
47
|
-
File.join(href, "remove_role")
|
48
11
|
end
|
49
12
|
|
50
13
|
def status
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Flipside
|
2
|
+
module Importmap
|
3
|
+
LIBRARIES = {
|
4
|
+
"@hotwired/stimulus": "https://cdn.jsdelivr.net/npm/@hotwired/stimulus/dist/stimulus.js",
|
5
|
+
}.freeze
|
6
|
+
|
7
|
+
def importmap_tags
|
8
|
+
JSON.generate({imports:})
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def imports
|
14
|
+
LIBRARIES.merge(controllers)
|
15
|
+
end
|
16
|
+
|
17
|
+
def controllers
|
18
|
+
pattern = File.join(__dir__, "public/*_controller.js")
|
19
|
+
|
20
|
+
Dir.glob(pattern).to_h do |controller|
|
21
|
+
name = File.basename(controller, ".js")
|
22
|
+
[name, public_path("#{name}.js")]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
import { Application } from "@hotwired/stimulus"
|
2
|
-
import ToggleController from "toggle_controller"
|
3
2
|
import SearchController from "search_controller"
|
4
3
|
import ModalController from "modal_controller"
|
4
|
+
import InlineEditController from "inline_edit_controller"
|
5
5
|
|
6
6
|
const application = Application.start()
|
7
|
-
application.register("toggle", ToggleController)
|
8
7
|
application.register("search", SearchController)
|
9
8
|
application.register("modal", ModalController)
|
9
|
+
application.register("inline-edit", InlineEditController)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class InlineEditController extends Controller {
|
4
|
+
static targets = ["read", "edit"];
|
5
|
+
|
6
|
+
edit() {
|
7
|
+
this.readTargets.forEach(element => {
|
8
|
+
element.classList.add("hidden")
|
9
|
+
})
|
10
|
+
|
11
|
+
this.editTargets.forEach(element => {
|
12
|
+
element.classList.remove("hidden")
|
13
|
+
})
|
14
|
+
}
|
15
|
+
|
16
|
+
cancel() {
|
17
|
+
this.editTargets.forEach(element => {
|
18
|
+
element.classList.add("hidden")
|
19
|
+
})
|
20
|
+
this.readTargets.forEach(element => {
|
21
|
+
element.classList.remove("hidden")
|
22
|
+
})
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
data/lib/flipside/version.rb
CHANGED
@@ -16,7 +16,7 @@
|
|
16
16
|
</button>
|
17
17
|
</div>
|
18
18
|
<div class="mt-4">
|
19
|
-
<form action="<%= feature
|
19
|
+
<form action="<%= feature_path(feature) %>" method="post">
|
20
20
|
<input type="hidden" name="_method" value="put" />
|
21
21
|
<%# <input type="hidden" name="feature_name" value="<%= feature.name %1>" /> %>
|
22
22
|
<label class="font-bold text-slate-800">Update <%= attribute %></label>
|
@@ -1,6 +1,10 @@
|
|
1
|
-
<div class="p-4 grid grid-cols-12 gap-4
|
2
|
-
<div class="col-span-
|
3
|
-
|
1
|
+
<div class="p-4 grid grid-cols-12 gap-4 items-center text-slate-300">
|
2
|
+
<div class="col-span-10">
|
3
|
+
<a href=<%= feature_path(feature) %> class="grid grid-cols-9 cursor-pointer hover:text-slate-400">
|
4
|
+
<div class="col-span-3 text-left text-xl"><%= feature.name %></div>
|
5
|
+
<div class="col-span-6 text-center text-lg"><%= feature.description&.capitalize %></div>
|
6
|
+
</a>
|
7
|
+
</div>
|
4
8
|
<div class="col-span-1 text-right text-lg font-normal">
|
5
9
|
<span
|
6
10
|
class="inline-block py-1 px-2 min-w-20 text-center rounded-lg <%= feature.status_color %>"
|
@@ -9,6 +13,6 @@
|
|
9
13
|
</span>
|
10
14
|
</div>
|
11
15
|
<div class="text-right">
|
12
|
-
<%=
|
16
|
+
<%= render :_toggle_button, locals: {feature:} %>
|
13
17
|
</div>
|
14
18
|
</div>
|
@@ -1,12 +1,12 @@
|
|
1
1
|
<% color = feature.enabled ? "bg-blue-700" : "bg-gray-200" %>
|
2
2
|
<% translate = feature.enabled ? "translate-x-5" : "translate-x-0" %>
|
3
|
+
<%# FIXME: How to prevent clicking the submit button from propageting? E.g. link navigation on index page. %>
|
3
4
|
|
4
|
-
<
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
</button>
|
5
|
+
<form action="<%= feature_path(feature, "toggle") %>" method="post">
|
6
|
+
<input type="hidden" name="_method" value="put" />
|
7
|
+
<button type="submit"
|
8
|
+
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent <%= color %>"
|
9
|
+
>
|
10
|
+
<span aria-hidden="true" class="pointer-events-none inline-block size-5 <%= translate %> transform rounded-full bg-white shadow"></span>
|
11
|
+
</button>
|
12
|
+
</form>
|
@@ -1,10 +1,10 @@
|
|
1
1
|
<div class="m-12">
|
2
|
-
<a href="<%= feature
|
2
|
+
<a href="<%= feature_path(feature) %>" class="p-2 rounded-lg text-xl text-slate-300 bg-blue-900 border border-blue-950 hover:bg-blue-800">Back</a>
|
3
3
|
<div class="mt-12 w-1/3 m-auto bg-gray-800 text-slate-400 p-8 rounded-lg shadow-lg">
|
4
4
|
<h1 class="text-2xl font-bold mb-6">Entities for <span class="font-extrabold text-slate-300"><%= feature.name %></span></h1>
|
5
5
|
<h2 class="text-xl font-bold">Add entity</h2>
|
6
|
-
<form action="<%= feature
|
7
|
-
<div data-controller="search" data-search-url-value="
|
6
|
+
<form action="<%= feature_path(feature, "add_entity") %>" method="post">
|
7
|
+
<div data-controller="search" data-search-url-value="<%= search_entity_path %>" class="w-full flex p-0 gap-2 items-center">
|
8
8
|
<select
|
9
9
|
name="class_name"
|
10
10
|
data-search-target="param"
|
@@ -54,7 +54,7 @@
|
|
54
54
|
<%= Flipside.display_entity(entity.flippable) %> (<%= entity.flippable_type %>)
|
55
55
|
</div>
|
56
56
|
<div>
|
57
|
-
<form action="<%= feature
|
57
|
+
<form action="<%= feature_path(feature, "remove_entity") %>" method="post">
|
58
58
|
<input type="hidden" name="entity_id" value="<%= entity.id %>" />
|
59
59
|
<button type="submit" class="p-2 bg-gray-300 text-gray-700 font-semibold rounded hover:bg-gray-400">Remove</button>
|
60
60
|
</form>
|
@@ -1,10 +1,10 @@
|
|
1
1
|
<div class="m-12">
|
2
|
-
<a href="<%= feature
|
2
|
+
<a href="<%= feature_path(feature) %>" class="p-2 rounded-lg text-xl text-slate-300 bg-blue-900 border border-blue-950 hover:bg-blue-800">Back</a>
|
3
3
|
<div class="mt-12 w-1/3 m-auto bg-gray-800 text-slate-400 p-8 rounded-lg shadow-lg">
|
4
4
|
<h1 class="text-2xl font-bold mb-6">Roles for <span class="font-extrabold text-slate-300"><%= feature.name %></span></h1>
|
5
5
|
<h2 class="text-xl font-bold">Add Role</h2>
|
6
|
-
<form action="<%= feature
|
7
|
-
<div data-controller="search" data-search-url-value="
|
6
|
+
<form action="<%= feature_path(feature, "add_role") %>" method="post">
|
7
|
+
<div data-controller="search" data-search-url-value="<%= search_role_path %>" class="w-full flex p-0 gap-2 items-center">
|
8
8
|
<select
|
9
9
|
name="class_name"
|
10
10
|
data-search-target="param"
|
@@ -54,7 +54,7 @@
|
|
54
54
|
<%= "#{role.class_name}##{role.method}" %>
|
55
55
|
</div>
|
56
56
|
<div>
|
57
|
-
<form action="<%= feature
|
57
|
+
<form action="<%= feature_path(feature, "remove_role") %>" method="post">
|
58
58
|
<input type="hidden" name="role_id" value="<%= role.id %>" />
|
59
59
|
<button type="submit" class="p-2 bg-gray-300 text-gray-700 font-semibold rounded hover:bg-gray-400">Remove</button>
|
60
60
|
</form>
|
@@ -7,11 +7,13 @@
|
|
7
7
|
|
8
8
|
<div class="mt-12 m-auto">
|
9
9
|
<ul>
|
10
|
+
<li class="hidden only:block text-center">
|
11
|
+
<p>No feature added yet!</p><p>Create a new feature from the console by typing:</p>
|
12
|
+
<code class="inline-block p-1 text-green-700 bg-slate-900">Flipside.create(name: "my_feature", description: "Some decription...")</code>
|
13
|
+
</li>
|
10
14
|
<% features.each do |feature| %>
|
11
|
-
<li class="border-b border-slate-500
|
12
|
-
|
13
|
-
<%= erb :_feature_item, locals: {feature:} %>
|
14
|
-
</a>
|
15
|
+
<li class="border-b border-slate-500">
|
16
|
+
<%= render :_feature_item, locals: {feature:} %>
|
15
17
|
</li>
|
16
18
|
<% end %>
|
17
19
|
</ul>
|
@@ -5,14 +5,7 @@
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
6
|
<script src="https://cdn.tailwindcss.com"></script>
|
7
7
|
<script type="importmap">
|
8
|
-
|
9
|
-
"imports": {
|
10
|
-
"@hotwired/stimulus": "https://cdn.jsdelivr.net/npm/@hotwired/stimulus/dist/stimulus.js",
|
11
|
-
"toggle_controller": "/flipside/toggle_controller.js",
|
12
|
-
"search_controller": "/flipside/search_controller.js",
|
13
|
-
"modal_controller": "/flipside/modal_controller.js"
|
14
|
-
}
|
15
|
-
}
|
8
|
+
<%= importmap_tags %>
|
16
9
|
</script>
|
17
10
|
</head>
|
18
11
|
<body class="w-full h-full bg-slate-600">
|
@@ -20,6 +13,6 @@
|
|
20
13
|
<%= yield %>
|
21
14
|
</div>
|
22
15
|
|
23
|
-
<script src="
|
16
|
+
<script src="<%= public_path("index.js") %>" type="module" ></script>
|
24
17
|
</body>
|
25
18
|
</html>
|
data/lib/flipside/views/show.erb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
<div class="m-12">
|
2
|
-
<a href="<%=
|
2
|
+
<a href="<%= base_path %>" class="p-2 rounded-lg text-xl text-slate-300 bg-blue-900 border border-blue-950 hover:bg-blue-800">Back</a>
|
3
3
|
<div class="mt-12 w-1/2 m-auto bg-gray-800 text-slate-400 p-8 rounded-lg shadow-lg">
|
4
4
|
<h1 class="text-2xl font-bold mb-6">Feature Details</h1>
|
5
5
|
<div class="mt-4 grid grid-cols-1 gap-4">
|
@@ -8,25 +8,47 @@
|
|
8
8
|
<span class="font-semibold">Name:</span>
|
9
9
|
<span class="font-bold text-lg text-slate-300"><%= feature.name %></span>
|
10
10
|
</div>
|
11
|
-
|
12
|
-
<
|
13
|
-
<
|
11
|
+
<div class="mt-2 flex justify-between items-center text-center" data-controller="inline-edit">
|
12
|
+
<div class="font-semibold">Description:</div>
|
13
|
+
<div data-inline-edit-target="read"><%= feature.description %></div>
|
14
|
+
<div class="ml-2 " data-inline-edit-target="read">
|
15
|
+
<button
|
16
|
+
class="px-2 py-1 rounded-lg bg-blue-900 text-xl text-slate-400 border border-blue-950 hover:bg-blue-800"
|
17
|
+
data-action="inline-edit#edit" >
|
18
|
+
Edit
|
19
|
+
</button>
|
20
|
+
</div>
|
21
|
+
<form action="<%= feature_path(feature) %>" method="post" class="hidden ml-4 grow flex gap-2" data-inline-edit-target="edit">
|
22
|
+
<input type="hidden" name="_method" value="put" />
|
23
|
+
<input
|
24
|
+
type="text"
|
25
|
+
value="<%= feature.description %>"
|
26
|
+
name="description"
|
27
|
+
class="grow border text-slate-600 border-slate-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
28
|
+
<button
|
29
|
+
type="button"
|
30
|
+
class="p-2 bg-slate-600 text-slate-300 font-semibold rounded hover:bg-slate-500"
|
31
|
+
data-action="inline-edit#cancel" >
|
32
|
+
Cancel
|
33
|
+
</button>
|
34
|
+
<button type="submit" class="p-2 bg-blue-900 font-semibold rounded hover:bg-blue-800">Update</button>
|
35
|
+
</form>
|
14
36
|
</div>
|
15
37
|
</div>
|
16
38
|
<div class="mt-2 flex justify-between items-center">
|
17
39
|
<span class="font-semibold">Enabled for all:</span>
|
18
|
-
<span><%=
|
40
|
+
<span><%= render :_toggle_button, locals: {feature:} %></span>
|
19
41
|
</div>
|
20
42
|
<div class="mt-2 flex justify-between items-center">
|
21
43
|
<span class="font-semibold">Active From:</span>
|
22
44
|
<span>
|
23
|
-
<%=
|
45
|
+
<%= render :_datetime_modal, locals: {feature:, attribute: :activated_at} %>
|
24
46
|
</span>
|
25
47
|
</div>
|
26
48
|
<div class="mt-2 flex justify-between items-center">
|
27
49
|
<span class="font-semibold">Active Until:</span>
|
28
50
|
<span>
|
29
|
-
<%=
|
51
|
+
<%= render :_datetime_modal, locals: {feature:, attribute: :deactivated_at} %>
|
30
52
|
</span>
|
31
53
|
</div>
|
32
54
|
<div class="mt-2 grid grid-cols-3 items-center">
|
@@ -34,17 +56,18 @@
|
|
34
56
|
<span class="text-center"><%= feature.entity_count_str %></span>
|
35
57
|
<span class="text-right text-xl text-slate-400">
|
36
58
|
<a
|
37
|
-
href="<%= feature
|
59
|
+
href="<%= feature_path(feature, "entities") %>"
|
38
60
|
class="px-2 py-1 rounded-lg bg-blue-900 border border-blue-950 hover:bg-blue-800" >
|
39
61
|
Edit
|
40
62
|
</a>
|
41
63
|
</span>
|
42
|
-
</div
|
64
|
+
</div>
|
65
|
+
<div class="mt-2 grid grid-cols-3 items-center">
|
43
66
|
<span class="font-semibold">Roles:</span>
|
44
67
|
<span class="text-center"><%= feature.role_count_str %></span>
|
45
68
|
<span class="text-right text-xl text-slate-400">
|
46
69
|
<a
|
47
|
-
href="<%= feature
|
70
|
+
href="<%= feature_path(feature, "roles") %>"
|
48
71
|
class="px-2 py-1 rounded-lg bg-blue-900 border border-blue-950 hover:bg-blue-800" >
|
49
72
|
Edit
|
50
73
|
</a>
|
data/lib/flipside/web.rb
CHANGED
@@ -1,99 +1,125 @@
|
|
1
|
-
require
|
1
|
+
require 'roda'
|
2
2
|
require "uri"
|
3
3
|
require "flipside/feature_presenter"
|
4
|
+
require "flipside/importmap"
|
5
|
+
require "rack/method_override"
|
4
6
|
|
5
7
|
module Flipside
|
6
|
-
class Web <
|
7
|
-
|
8
|
-
|
8
|
+
class Web < Roda
|
9
|
+
include Flipside::Importmap
|
10
|
+
use Rack::MethodOverride
|
11
|
+
|
12
|
+
opts[:add_script_name] = true
|
13
|
+
|
14
|
+
plugin :r
|
15
|
+
plugin :json
|
16
|
+
plugin :halt
|
17
|
+
plugin :path
|
18
|
+
plugin :all_verbs
|
19
|
+
plugin :unescape_path
|
20
|
+
plugin :render, views: File.expand_path("views", __dir__)
|
21
|
+
plugin :public,
|
22
|
+
root: File.expand_path("public", __dir__),
|
23
|
+
headers: {"Cache-Control" => "public, max-age=14400"}
|
24
|
+
|
25
|
+
path(:public) { |name| "/#{name}" }
|
26
|
+
path(:base, "/")
|
27
|
+
path(:feature) do |feature, *segments, **query|
|
28
|
+
path = "/feature/#{ERB::Util.url_encode(feature.name)}"
|
29
|
+
path = "#{path}/#{segments.join("/")}" if segments.any?
|
30
|
+
path = "#{path}?#{query.map { |k,v| "#{k}=#{v}" }.join("&")}" if query.keys.any?
|
31
|
+
path
|
9
32
|
end
|
33
|
+
path(:search_entity, "/search_entity")
|
34
|
+
path(:search_role, "/search_role")
|
10
35
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
erb :index, locals: {features:}
|
36
|
+
def load_feature(name)
|
37
|
+
Flipside::Feature.find_by!(name:)
|
38
|
+
rescue
|
39
|
+
r.halt 404, view(:not_found)
|
17
40
|
end
|
18
41
|
|
19
|
-
|
20
|
-
|
21
|
-
end
|
42
|
+
route do |r|
|
43
|
+
r.public
|
22
44
|
|
23
|
-
|
24
|
-
|
45
|
+
r.root do
|
46
|
+
features = Flipside::Feature.order(:name).map do |feature|
|
47
|
+
FeaturePresenter.new(feature)
|
48
|
+
end
|
25
49
|
|
26
|
-
|
27
|
-
204
|
28
|
-
else
|
29
|
-
[422, "Failed to update feature"]
|
50
|
+
view :index, locals: {features:}
|
30
51
|
end
|
31
|
-
end
|
32
|
-
|
33
|
-
put "/feature/:name" do
|
34
|
-
kwargs = params.slice("activated_at", "deactivated_at")
|
35
|
-
feature.update(**kwargs)
|
36
|
-
redirect to("/feature/#{params["name"]}"), 303
|
37
|
-
end
|
38
|
-
|
39
|
-
get "/feature/:name/entities" do
|
40
|
-
erb :feature_entities, locals: {feature: FeaturePresenter.new(feature, base_path)}
|
41
|
-
end
|
42
|
-
|
43
|
-
get '/search_entity' do
|
44
|
-
class_name = params[:class_name]
|
45
|
-
query = URI.decode_www_form_component(params[:q])
|
46
|
-
result = Flipside.search_entity(class_name:, query:)
|
47
|
-
|
48
|
-
erb :_search_result, locals: {result:, class_name:, query:}
|
49
|
-
end
|
50
|
-
|
51
|
-
post "/feature/:name/add_entity" do
|
52
|
-
name, class_name, identifier = params.values_at("name", "class_name", "identifier")
|
53
|
-
|
54
|
-
entity = Flipside.find_entity(class_name:, identifier:)
|
55
|
-
Flipside.add_entity(name: , entity:)
|
56
|
-
redirect to("/feature/#{name}/entities"), 303
|
57
|
-
end
|
58
|
-
|
59
|
-
post "/feature/:name/remove_entity" do
|
60
|
-
Flipside.remove_entity(name: params["name"], entity_id: params["entity_id"])
|
61
|
-
redirect to("/feature/#{params["name"]}/entities"), 303
|
62
|
-
end
|
63
|
-
|
64
|
-
get "/feature/:name/roles" do
|
65
|
-
erb :feature_roles, locals: {feature: FeaturePresenter.new(feature, base_path)}
|
66
|
-
end
|
67
|
-
|
68
|
-
get '/search_role' do
|
69
|
-
class_name = params[:class_name]
|
70
|
-
query = URI.decode_www_form_component(params[:q])
|
71
|
-
result = Flipside.search_role(class_name:, query:)
|
72
|
-
|
73
|
-
erb :_search_result, locals: {result:, class_name:, query:}
|
74
|
-
end
|
75
|
-
|
76
|
-
post "/feature/:name/add_role" do
|
77
|
-
name, class_name, method_name = params.values_at("name", "class_name", "identifier")
|
78
|
-
Flipside.add_role(name:, class_name:, method_name:)
|
79
|
-
|
80
|
-
redirect to("/feature/#{name}/roles"), 303
|
81
|
-
end
|
82
|
-
|
83
|
-
post "/feature/:name/remove_role" do
|
84
|
-
Flipside.remove_role(name: params["name"], role_id: params["role_id"])
|
85
52
|
|
86
|
-
|
87
|
-
|
53
|
+
r.on "feature", String do |name|
|
54
|
+
feature = load_feature(name)
|
55
|
+
|
56
|
+
r.is do
|
57
|
+
r.get do
|
58
|
+
view(:show, locals: { feature: FeaturePresenter.new(feature) })
|
59
|
+
end
|
60
|
+
|
61
|
+
r.put do
|
62
|
+
kwargs = r.params.slice("description", "activated_at", "deactivated_at")
|
63
|
+
feature.update(**kwargs)
|
64
|
+
r.redirect r.path, 303
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
r.put "toggle" do
|
69
|
+
if feature.update(enabled: !feature.enabled)
|
70
|
+
referer = r.env["HTTP_REFERER"]
|
71
|
+
r.redirect (referer || feature_path(feature)), 303
|
72
|
+
else
|
73
|
+
response.status = 422
|
74
|
+
"Failed to update feature"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
r.get "entities" do
|
79
|
+
view(:feature_entities, locals: { feature: FeaturePresenter.new(feature) })
|
80
|
+
end
|
81
|
+
|
82
|
+
r.post "add_entity" do
|
83
|
+
class_name, identifier = r.params.values_at("class_name", "identifier")
|
84
|
+
entity = Flipside.find_entity(class_name:, identifier:)
|
85
|
+
Flipside.add_entity(feature:, entity:)
|
86
|
+
r.redirect feature_path(feature, "entities"), 303
|
87
|
+
end
|
88
|
+
|
89
|
+
r.post "remove_entity" do
|
90
|
+
Flipside.remove_entity(feature:, entity_id: r.params["entity_id"])
|
91
|
+
r.redirect feature_path(feature, "entities"), 303
|
92
|
+
end
|
93
|
+
|
94
|
+
r.get "roles" do
|
95
|
+
view(:feature_roles, locals: { feature: FeaturePresenter.new(feature) })
|
96
|
+
end
|
97
|
+
|
98
|
+
r.post "add_role" do
|
99
|
+
class_name, method_name = r.params.values_at("class_name", "identifier")
|
100
|
+
Flipside.add_role(name:, class_name:, method_name:)
|
101
|
+
r.redirect feature_path(feature, "roles"), 303
|
102
|
+
end
|
103
|
+
|
104
|
+
r.post "remove_role" do
|
105
|
+
Flipside.remove_role(name:, role_id: r.params["role_id"])
|
106
|
+
r.redirect feature_path(feature, "roles"), 303
|
107
|
+
end
|
108
|
+
end
|
88
109
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
110
|
+
r.get "search_entity" do
|
111
|
+
class_name = r.params["class_name"]
|
112
|
+
query = URI.decode_www_form_component(r.params["q"])
|
113
|
+
result = Flipside.search_entity(class_name:, query:)
|
114
|
+
view(:_search_result, locals: { result:, class_name:, query: })
|
115
|
+
end
|
94
116
|
|
95
|
-
|
96
|
-
|
117
|
+
r.get "search_role" do
|
118
|
+
class_name = r.params["class_name"]
|
119
|
+
query = URI.decode_www_form_component(r.params["q"])
|
120
|
+
result = Flipside.search_role(class_name:, query:)
|
121
|
+
view(:_search_result, locals: { result:, class_name:, query: })
|
122
|
+
end
|
97
123
|
end
|
98
124
|
end
|
99
125
|
end
|
data/lib/flipside.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_record"
|
3
4
|
require "flipside/version"
|
4
5
|
require "flipside/web"
|
5
6
|
require "flipside/config/settings"
|
@@ -25,32 +26,36 @@ module Flipside
|
|
25
26
|
feature = find_by(name:)
|
26
27
|
return false unless feature
|
27
28
|
|
28
|
-
objects
|
29
|
+
objects = [default_object] if objects.empty?
|
29
30
|
objects.any? { |object| feature.enabled? object }
|
30
31
|
end
|
31
32
|
|
33
|
+
def disabled?(...)
|
34
|
+
!enabled?(...)
|
35
|
+
end
|
36
|
+
|
32
37
|
def enable!(name)
|
33
38
|
feature = find_by!(name:)
|
34
39
|
feature.update(enabled: true)
|
35
40
|
end
|
36
41
|
|
37
|
-
def add_entity(
|
38
|
-
feature
|
42
|
+
def add_entity(entity:, feature: nil, name: nil)
|
43
|
+
feature ||= find_by!(name:)
|
39
44
|
Entity.find_or_create_by(feature:, flippable: entity)
|
40
45
|
end
|
41
46
|
|
42
|
-
def remove_entity(
|
43
|
-
feature
|
47
|
+
def remove_entity(entity_id:, feature: nil, name: nil)
|
48
|
+
feature ||= find_by!(name:)
|
44
49
|
feature.entities.find_by(id: entity_id)&.destroy
|
45
50
|
end
|
46
51
|
|
47
|
-
def add_role(
|
48
|
-
feature
|
52
|
+
def add_role(class_name:, method_name:, feature: nil, name: nil)
|
53
|
+
feature ||= find_by!(name:)
|
49
54
|
Role.find_or_create_by(feature:, class_name:, method: method_name)
|
50
55
|
end
|
51
56
|
|
52
|
-
def remove_role(
|
53
|
-
feature
|
57
|
+
def remove_role(role_id:, feature: nil, name: nil)
|
58
|
+
feature ||= find_by!(name:)
|
54
59
|
feature.roles.find_by(id: role_id)&.destroy
|
55
60
|
end
|
56
61
|
|
@@ -64,11 +69,15 @@ module Flipside
|
|
64
69
|
find_by(name:) || raise(NoSuchFeauture.new(name))
|
65
70
|
end
|
66
71
|
|
72
|
+
def create(name:, description: nil)
|
73
|
+
Feature.create(name: name, description: description)
|
74
|
+
end
|
75
|
+
|
67
76
|
def create_missing(name)
|
68
77
|
trace = caller.find { |trace| !trace.start_with? __FILE__ }
|
69
78
|
source, line, _ = trace.split(":")
|
70
79
|
source = [source, line].join(":") if line.match?(/\d+/)
|
71
|
-
|
80
|
+
create(name:, description: "Created from #{source}")
|
72
81
|
end
|
73
82
|
end
|
74
83
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flipside
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sammy Henningsson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-05-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: roda
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: byebug
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +66,20 @@ dependencies:
|
|
52
66
|
- - ">="
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rackup
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.2'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.2'
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
name: rspec
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +108,20 @@ dependencies:
|
|
80
108
|
- - ">="
|
81
109
|
- !ruby/object:Gem::Version
|
82
110
|
version: '2.1'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: puma
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '6.5'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '6.5'
|
83
125
|
description: Create simple feature toggles.
|
84
126
|
email:
|
85
127
|
- sammy.henningsson@hey.com
|
@@ -105,10 +147,11 @@ files:
|
|
105
147
|
- lib/flipside/config/roles.rb
|
106
148
|
- lib/flipside/config/settings.rb
|
107
149
|
- lib/flipside/feature_presenter.rb
|
150
|
+
- lib/flipside/importmap.rb
|
108
151
|
- lib/flipside/public/index.js
|
152
|
+
- lib/flipside/public/inline_edit_controller.js
|
109
153
|
- lib/flipside/public/modal_controller.js
|
110
154
|
- lib/flipside/public/search_controller.js
|
111
|
-
- lib/flipside/public/toggle_controller.js
|
112
155
|
- lib/flipside/search_result.rb
|
113
156
|
- lib/flipside/version.rb
|
114
157
|
- lib/flipside/views/_datetime_modal.erb
|
@@ -1,29 +0,0 @@
|
|
1
|
-
import { Controller } from "@hotwired/stimulus";
|
2
|
-
|
3
|
-
export default class ToggleController extends Controller {
|
4
|
-
static targets = ["switch"]
|
5
|
-
static values = {
|
6
|
-
url: String,
|
7
|
-
enabled: Boolean
|
8
|
-
}
|
9
|
-
|
10
|
-
async switch(event) {
|
11
|
-
try {
|
12
|
-
const data = {enable: !this.enabledValue}
|
13
|
-
const response = await fetch(this.urlValue, {
|
14
|
-
method: "PUT",
|
15
|
-
headers: {"Content-Type": "application/json"},
|
16
|
-
body: JSON.stringify(data)
|
17
|
-
})
|
18
|
-
|
19
|
-
if (response.ok) {
|
20
|
-
location.reload()
|
21
|
-
} else {
|
22
|
-
const text = await response.text()
|
23
|
-
console.error("Failed to update:", text)
|
24
|
-
}
|
25
|
-
} catch (error) {
|
26
|
-
console.error("Error during the PUT request:", error)
|
27
|
-
}
|
28
|
-
}
|
29
|
-
}
|