flipside 0.2.1 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e49af6fc8d31128f0e88089ad6d46ce1745a223a68c4f5e4f2e5353f99091073
4
- data.tar.gz: cb1547b1c3892b363830aa5f75375998092b90f150e6a275b3224051b6eb3b19
3
+ metadata.gz: 3c0a3e61bb314f51e06511146b141050b6ca6da1bc7caefa8307f854e19deb78
4
+ data.tar.gz: f9465ceb62faf692a5a19addcd5d38251d76712d9ee39b3c634fa9b78be745f1
5
5
  SHA512:
6
- metadata.gz: cc95be97c95f5c6285d4eb57e6b378b47277d61e43f6606eb44bf9c9c6f914a2b6019c4a143c8b9a5058c64eac6ea0f4cffbecf7138ad3836ca0dcb196515c22
7
- data.tar.gz: fb5262d598dea36dd87e7c987ae1d263500b2bc7e39882d7da5a119a6c11aab009821d7981732a442176985054900355e6bcf2b8d2b0fa68eab99e03a6469337
6
+ metadata.gz: f779f75b475ae4b140a65dd6b1da62b9c77f8593000475ee0dec0a5bf7ad4d3170cd1af307d83927312d8c1ee389285d3bf001476a7190c4dff7392cdc7f4f0f
7
+ data.tar.gz: 3606e255c03e504d704f147d106783cb920d62c29b8a163b2869bdb50a8f204e4d8c45b0ba524683fe1fc6617b27e23da7b9ad55246df1d12f3e139c1872585d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.2.2] - 2025-04-25
2
+
3
+ - Add disabled? method
4
+ - Edit feature description from UI
5
+ - Add configuration for setting default_object
6
+
1
7
  ## [0.2.1] - 2025-01-30
2
8
 
3
9
  - 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
- 1. Defining Features
36
+ ### Defining Features
37
37
 
38
38
  Features are created by running this (in a console or from code):
39
39
  ```ruby
40
- Flipside::Feature.create(
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 you would like it turned on from the beginning you could pass in `enabled: true`.
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::Feature.create(name: "MyFeature", enabled: true)
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::Feature.create(
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
- Features can be enabled for a certain record, typically a certain user or organization. This records are called entities. To enable a feature for a given record use `.add_entity`:
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 how are admins, using the `.add_role` method:
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
- 2. Checking Feature Status
116
+ ## UI
117
+ 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.
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
- #### Globally
123
+ ![UI](/features.png)
87
124
 
88
- Check if a feature is enabled globally:
89
125
 
90
- ```ruby
91
- Flipside.enabled? "MyFeature"
92
- ```
126
+ ### Configuration
93
127
 
94
- #### For a Specific Record
128
+ The Flipside UI can be configured by calling some class methods on `Flipside` (see below).
95
129
 
96
- Check if a feature is enabled for a specific record (e.g. a user):
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.enabled? "MyFeature", user
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
- mount Flipside::Web, at: '/flipside'
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
- ![UI](/features.png)
110
156
 
157
+ Typically this configuration should be declared in an initializer file.
111
158
 
112
- ### Configuration
159
+ ```ruby
160
+ # config/initializers/flipside.rb
161
+ require 'flipside'
113
162
 
114
- Entities can be added to a feature by searching for records and adding them.
115
- ![Start screen](/add_entity.png)
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
+ ![Add an entity](/add_entity.png)
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 enablers.
129
- The `search_by` keyword argument, which may be a symbol or a proc, dictates how records are found from searching in the ui. When a symbol is given, then searches with an exact match on the corresponding attribute are returned. E.g. `User.where(name: query)`.
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
+ ```
130
197
 
131
- TODO
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
+ ```
223
+
224
+ #### Roles
225
+
226
+ Features can be enabled for certain roles, by searching for roles (by method name).
227
+
228
+ ![Add a role](/add_role.png)
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,8 +2,10 @@ import { Application } from "@hotwired/stimulus"
2
2
  import ToggleController from "toggle_controller"
3
3
  import SearchController from "search_controller"
4
4
  import ModalController from "modal_controller"
5
+ import InlineEditController from "inline_edit_controller"
5
6
 
6
7
  const application = Application.start()
7
8
  application.register("toggle", ToggleController)
8
9
  application.register("search", SearchController)
9
10
  application.register("modal", ModalController)
11
+ 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
+
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flipside
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.2"
5
5
  end
@@ -10,7 +10,8 @@
10
10
  "@hotwired/stimulus": "https://cdn.jsdelivr.net/npm/@hotwired/stimulus/dist/stimulus.js",
11
11
  "toggle_controller": "/flipside/toggle_controller.js",
12
12
  "search_controller": "/flipside/search_controller.js",
13
- "modal_controller": "/flipside/modal_controller.js"
13
+ "modal_controller": "/flipside/modal_controller.js",
14
+ "inline_edit_controller": "/flipside/inline_edit_controller.js"
14
15
  }
15
16
  }
16
17
  </script>
@@ -8,9 +8,31 @@
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
- <div class="mt-2 flex justify-between items-center">
12
- <span class="font-semibold">Description:</span>
13
- <span><%= feature.description %></span>
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.href %>" 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">
@@ -39,7 +61,8 @@
39
61
  Edit
40
62
  </a>
41
63
  </span>
42
- </div><div class="mt-2 grid grid-cols-3 items-center">
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">
data/lib/flipside/web.rb CHANGED
@@ -20,6 +20,12 @@ module Flipside
20
20
  erb :show, locals: {feature: FeaturePresenter.new(feature, base_path)}
21
21
  end
22
22
 
23
+ put "/feature/:name" do
24
+ feature.update(params.slice("description"))
25
+
26
+ redirect to(request.path_info), 303
27
+ end
28
+
23
29
  put "/feature/:name/toggle" do
24
30
  content_type :text
25
31
 
data/lib/flipside.rb CHANGED
@@ -25,10 +25,14 @@ module Flipside
25
25
  feature = find_by(name:)
26
26
  return false unless feature
27
27
 
28
- objects << nil if objects.empty?
28
+ objects = [default_object] if objects.empty?
29
29
  objects.any? { |object| feature.enabled? object }
30
30
  end
31
31
 
32
+ def disabled?(...)
33
+ !enabled?(...)
34
+ end
35
+
32
36
  def enable!(name)
33
37
  feature = find_by!(name:)
34
38
  feature.update(enabled: true)
@@ -64,11 +68,15 @@ module Flipside
64
68
  find_by(name:) || raise(NoSuchFeauture.new(name))
65
69
  end
66
70
 
71
+ def create(name:, description: nil)
72
+ Feature.create(name: name, description: description)
73
+ end
74
+
67
75
  def create_missing(name)
68
76
  trace = caller.find { |trace| !trace.start_with? __FILE__ }
69
77
  source, line, _ = trace.split(":")
70
78
  source = [source, line].join(":") if line.match?(/\d+/)
71
- Feature.create(name:, description: "Created from #{source}")
79
+ create(name:, description: "Created from #{source}")
72
80
  end
73
81
  end
74
82
  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.2.1
4
+ version: 0.2.2
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-01-30 00:00:00.000000000 Z
11
+ date: 2025-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -106,6 +106,7 @@ files:
106
106
  - lib/flipside/config/settings.rb
107
107
  - lib/flipside/feature_presenter.rb
108
108
  - lib/flipside/public/index.js
109
+ - lib/flipside/public/inline_edit_controller.js
109
110
  - lib/flipside/public/modal_controller.js
110
111
  - lib/flipside/public/search_controller.js
111
112
  - lib/flipside/public/toggle_controller.js