flipside 0.3.4 → 0.5.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 +20 -0
- data/README.md +116 -36
- data/app/models/flipside/flippable.rb +109 -0
- data/lib/flipside/config/entities.rb +48 -3
- data/lib/flipside/config/flippables.rb +46 -0
- data/lib/flipside/config/registered_entity.rb +24 -26
- data/lib/flipside/config/roles.rb +14 -3
- data/lib/flipside/railtie.rb +18 -0
- data/lib/flipside/version.rb +1 -1
- data/lib/flipside/views/feature_entities.erb +1 -1
- data/lib/flipside.rb +30 -0
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4ecf5b08df6bcc2ac0bd7a9f2e4396d361fefb53b497323725ab543ed3d60a8
|
|
4
|
+
data.tar.gz: a64de96569181f04e4e7f50f9c7122299ee79581115c0abfbdf24aceef706f03
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b8b659ad364668c26e19e2666f78e5c354034c61b68b1c3b54f63a07f310749cc6295fe3cd77b36c61bfefa41f3266cb9c047ce459d688fed5951f4ee00d8ef
|
|
7
|
+
data.tar.gz: d61860274364b069a96b7547bc4f0c66b0de08b8eae4205f520084afa5b9fd1bb2e8b2d1e0022b05c47a8a8b47ef0421dafef87cb126152b3d7d70c39bb03d80
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## [0.5.0] - 2026-10-08
|
|
2
|
+
- Add `flipside_search_by`, `flipside_display_as` and `flipside_identified_by` to `Flipside::Flippable`. Each one registers the class as an entity, so `flipside_entity` is only needed for the defaults
|
|
3
|
+
- A block given to `flipside_search_by` runs in the context of the class, so it can call `where` directly
|
|
4
|
+
- An STI subclass inherits its parent's entity settings, and is registered as an entity of its own when it calls one of the macros itself
|
|
5
|
+
- Test against Ruby 3.4 and 4.0
|
|
6
|
+
|
|
7
|
+
### Deprecated
|
|
8
|
+
- The `search_by`, `display_as` and `identified_by` options of `flipside_entity`, in favour of `flipside_search_by`, `flipside_display_as` and `flipside_identified_by`. They will be removed in 1.0.
|
|
9
|
+
|
|
10
|
+
## [0.4.0] - 2026-09-25
|
|
11
|
+
- Show entities whose record has been deleted instead of raising in the UI
|
|
12
|
+
- Add `Flipside::Flippable` with `flipside_entity` and `flipside_role`, so models can register themselves (listed in `Flipside.flippables`); entities of a destroyed record are removed
|
|
13
|
+
- Add `Flipside.prune_orphaned_entities`
|
|
14
|
+
- Registering the same role twice no longer lists it twice
|
|
15
|
+
- Display STI subclass entities through their own or their base class's registration
|
|
16
|
+
- Verify `Flipside.flippables` at boot when a Rails app eager loads
|
|
17
|
+
|
|
18
|
+
### Deprecated
|
|
19
|
+
- `Flipside.register_entity` and `Flipside.register_role`, in favour of `flipside_entity` and `flipside_role` from `Flipside::Flippable`. They will be removed in 1.0.
|
|
20
|
+
|
|
1
21
|
## [0.3.4] - 2026-03-04
|
|
2
22
|
- Support an array of default objects
|
|
3
23
|
|
data/README.md
CHANGED
|
@@ -87,7 +87,7 @@ We can also check multiple records. `.enabled?` will return `true` if any of the
|
|
|
87
87
|
Flipside.enabled? "MyFeature", user, company, location
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
#### Enabling features for specific records
|
|
91
91
|
|
|
92
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`:
|
|
93
93
|
```ruby
|
|
@@ -97,6 +97,13 @@ Flipside.add_entity(name: "MyFeature", entity: user)
|
|
|
97
97
|
Flipside.enabled? "MyFeature", user # => true
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
Entities reference their record polymorphically, so there is no foreign key to clean them up when the record is deleted. A model registered as an entity (see [Registering from the model](#registering-from-the-model)) removes its entities when a record is destroyed.
|
|
101
|
+
|
|
102
|
+
Entities left behind by records deleted without callbacks (or before the concern was added) can be removed with:
|
|
103
|
+
```ruby
|
|
104
|
+
Flipside.prune_orphaned_entities # => number of entities deleted
|
|
105
|
+
```
|
|
106
|
+
|
|
100
107
|
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
|
|
101
108
|
for all users who are admins, using the `.add_role` method:
|
|
102
109
|
```ruby
|
|
@@ -123,7 +130,7 @@ Note: you probably want to wrap this inside a constraints block to provide some
|
|
|
123
130
|

|
|
124
131
|
|
|
125
132
|
|
|
126
|
-
|
|
133
|
+
## Configuration
|
|
127
134
|
|
|
128
135
|
Flipside can be configured by calling some class methods on `Flipside` (see below).
|
|
129
136
|
|
|
@@ -154,7 +161,7 @@ Flipside.enabled? :some_feature, Current.user
|
|
|
154
161
|
Flipside.default_object = -> { [Current.user, Current.organization] }
|
|
155
162
|
```
|
|
156
163
|
|
|
157
|
-
|
|
164
|
+
#### Note: if we do pass in an argument, then `default_object ` will not be used:
|
|
158
165
|
Flipside.enabled? :some_feature, Current.company # check current company instead of user.
|
|
159
166
|
```
|
|
160
167
|
|
|
@@ -170,75 +177,148 @@ Flipside.create_missing_features = true
|
|
|
170
177
|
Flipside.default_object = -> { Current.user }
|
|
171
178
|
```
|
|
172
179
|
|
|
173
|
-
|
|
180
|
+
### Registering from the model
|
|
181
|
+
|
|
182
|
+
Entities and roles are searched for in the UI, so Flipside needs to know which classes can be used. A model registers itself by including `Flipside::Flippable` and calling the entity macros (`flipside_entity`, `flipside_search_by`, `flipside_display_as`, `flipside_identified_by`) and/or `flipside_role`:
|
|
183
|
+
```ruby
|
|
184
|
+
class User < ApplicationRecord
|
|
185
|
+
include Flipside::Flippable
|
|
186
|
+
|
|
187
|
+
flipside_search_by :name
|
|
188
|
+
flipside_display_as :name
|
|
189
|
+
flipside_role :admin?
|
|
190
|
+
flipside_role :awesome?, display_as: "Awesome users"
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
List the class names in an initializer, so that Flipside can load them when the UI needs them (also when the app does not eager load):
|
|
195
|
+
```ruby
|
|
196
|
+
# config/initializers/flipside.rb
|
|
197
|
+
Flipside.flippables = %w[User]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
A class using these macros must be listed in `Flipside.flippables`, and a listed class must call at least one of them, otherwise Flipside raises.
|
|
201
|
+
|
|
202
|
+
### Entities
|
|
174
203
|
|
|
175
204
|
Entities can be added to a feature by searching for records.
|
|
176
205
|
|
|
177
206
|

|
|
178
207
|
|
|
179
|
-
|
|
208
|
+
Each class that may be used as a feature enabler is registered as an entity by calling `flipside_entity`, `flipside_search_by`, `flipside_display_as` or `flipside_identified_by` (any combination of them, in any order). `flipside_entity` is only needed to register with the defaults. A registered entity class also removes the entities of a record when it is destroyed. Note that this includes soft deletion that runs destroy callbacks (e.g. paranoia), so a restored record comes back without its features. Soft deletion that skips them (e.g. discard) keeps the entities.
|
|
180
209
|
```ruby
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
)
|
|
210
|
+
class User < ApplicationRecord
|
|
211
|
+
include Flipside::Flippable
|
|
212
|
+
|
|
213
|
+
flipside_entity
|
|
214
|
+
end
|
|
187
215
|
```
|
|
188
216
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
`flipside_identified_by` sets the column Flipside uses to identify records. This defaults to `:id` and typically does not need to be changed.
|
|
218
|
+
Currently composite keys are not supported.
|
|
219
|
+
```ruby
|
|
220
|
+
flipside_identified_by :uuid
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`flipside_search_by` dictates how records are found from searching in the ui. It takes a block, which is called with the search string in the context of the class and is expected to return an object responding to `to_a` (e.g. an AR collection).
|
|
193
224
|
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
|
|
194
225
|
starting with _query_, something like this could be used.
|
|
195
226
|
```ruby
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
227
|
+
flipside_search_by do |str|
|
|
228
|
+
where(
|
|
229
|
+
"lower(first_name) = :name OR lower(last_name) = :name OR email LIKE :str",
|
|
230
|
+
name: str.downcase,
|
|
231
|
+
str: "#{str}%"
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
```
|
|
200
235
|
|
|
236
|
+
Instead of a block, it may be given the name of a column, e.g. `:name`. Then records with an exact match on that column are returned, i.e. `User.where(name: query)`:
|
|
237
|
+
```ruby
|
|
238
|
+
flipside_search_by :name
|
|
201
239
|
```
|
|
202
240
|
|
|
203
|
-
|
|
204
|
-
Currently composite keys are not supported.
|
|
241
|
+
Without `flipside_search_by`, records are found by an exact match on `identified_by`.
|
|
205
242
|
|
|
206
|
-
|
|
243
|
+
`flipside_display_as` configures how these entities show up in the combobox. When given the name of an instance method, that method is called on the entity.
|
|
207
244
|
For example, given the following setup. Users will be displayed with first name and last name:
|
|
208
245
|
```ruby
|
|
209
246
|
class User < ApplicationRecord
|
|
247
|
+
include Flipside::Flippable
|
|
248
|
+
|
|
249
|
+
flipside_display_as :name
|
|
250
|
+
|
|
210
251
|
def name
|
|
211
252
|
[first_name, last_name].compact.map(&:capitalize).join(" ")
|
|
212
253
|
end
|
|
213
254
|
end
|
|
214
|
-
|
|
215
|
-
Flipside.register_entity(
|
|
216
|
-
class_name: "User",
|
|
217
|
-
display_as: :name,
|
|
218
|
-
)
|
|
219
255
|
```
|
|
220
256
|
|
|
221
|
-
When a
|
|
257
|
+
When given a block, it is called with the entity and is expected to return a string used for displaying the entity. The config above could then instead be done using:
|
|
222
258
|
```ruby
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
259
|
+
flipside_display_as do |user|
|
|
260
|
+
[user.first_name, user.last_name].compact.map(&:capitalize).join(" ")
|
|
261
|
+
end
|
|
227
262
|
```
|
|
228
263
|
|
|
229
|
-
|
|
264
|
+
Without `flipside_display_as`, entities are displayed by their `identified_by` value.
|
|
265
|
+
|
|
266
|
+
An STI subclass inherits these settings. When a subclass calls one of the macros itself, it is registered as an entity of its own, so it must also be listed in `Flipside.flippables` and shows up separately in the UI.
|
|
267
|
+
|
|
268
|
+
The `search_by`, `display_as` and `identified_by` keyword arguments of `flipside_entity` from Flipside 0.4.0 are deprecated in favour of these macros, and will be removed in 1.0.
|
|
269
|
+
|
|
270
|
+
### Roles
|
|
230
271
|
|
|
231
272
|
Features can be enabled for certain roles, by searching for roles (by method name).
|
|
232
273
|
|
|
233
274
|

|
|
234
275
|
|
|
235
|
-
This is configured by calling
|
|
236
|
-
Note a role consists of a class and a corresponding instance method.
|
|
276
|
+
This is configured by calling `flipside_role` for each role to be added. A role consists of a class and a corresponding instance method, and `display_as` optionally sets how it shows up in the UI.
|
|
237
277
|
```ruby
|
|
278
|
+
class User < ApplicationRecord
|
|
279
|
+
include Flipside::Flippable
|
|
280
|
+
|
|
281
|
+
flipside_role :admin?
|
|
282
|
+
flipside_role :awesome?, display_as: "Awesome users"
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Classes you cannot edit
|
|
287
|
+
|
|
288
|
+
A class from another gem can be registered by including the concern from `to_prepare`, which runs at boot and again after every reload. It must still be listed in `Flipside.flippables`:
|
|
289
|
+
```ruby
|
|
290
|
+
# config/initializers/flipside.rb
|
|
291
|
+
Flipside.flippables = %w[User OtherGem::Account]
|
|
292
|
+
|
|
293
|
+
Rails.application.config.to_prepare do
|
|
294
|
+
OtherGem::Account.include(Flipside::Flippable)
|
|
295
|
+
OtherGem::Account.flipside_display_as(:name)
|
|
296
|
+
end
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Upgrading from `register_entity` and `register_role`
|
|
300
|
+
|
|
301
|
+
`Flipside.register_entity` and `Flipside.register_role` are deprecated and will be removed in Flipside 1.0. Move each call into its model:
|
|
302
|
+
```ruby
|
|
303
|
+
# Before, in config/initializers/flipside.rb
|
|
304
|
+
Flipside.register_entity(class_name: "User", search_by: :name, display_as: :name)
|
|
238
305
|
Flipside.register_role(class_name: "User", method_name: :admin?)
|
|
239
|
-
|
|
306
|
+
|
|
307
|
+
# After, in config/initializers/flipside.rb
|
|
308
|
+
Flipside.flippables = %w[User]
|
|
309
|
+
|
|
310
|
+
# and in app/models/user.rb
|
|
311
|
+
class User < ApplicationRecord
|
|
312
|
+
include Flipside::Flippable
|
|
313
|
+
|
|
314
|
+
flipside_search_by :name
|
|
315
|
+
flipside_display_as :name
|
|
316
|
+
flipside_role :admin?
|
|
317
|
+
end
|
|
240
318
|
```
|
|
241
319
|
|
|
320
|
+
In a Rails 7.1+ app the deprecation warnings follow the app's own deprecation settings (`Rails.application.deprecators[:flipside]`).
|
|
321
|
+
|
|
242
322
|
## Development
|
|
243
323
|
|
|
244
324
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rspec` to run the tests.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
require "active_support/core_ext/class/attribute"
|
|
5
|
+
require "models/flipside/entity"
|
|
6
|
+
|
|
7
|
+
module Flipside
|
|
8
|
+
# Lets a model register itself with Flipside. The class must also be listed in
|
|
9
|
+
# Flipside.flippables, so that Flipside can load it when the UI needs it. The
|
|
10
|
+
# listing is checked when Flipside loads the classes rather than here, since
|
|
11
|
+
# a model may be loaded before the initializer that sets Flipside.flippables.
|
|
12
|
+
module Flippable
|
|
13
|
+
extend ActiveSupport::Concern
|
|
14
|
+
|
|
15
|
+
included do
|
|
16
|
+
# The class's Flipside::RegisteredEntity. Being a class attribute, it is
|
|
17
|
+
# inherited by STI subclasses and starts out empty in a reloaded class.
|
|
18
|
+
class_attribute :_flipside_registration, instance_accessor: false, instance_predicate: false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class_methods do
|
|
22
|
+
# Registers the class as an entity and removes its Flipside::Entity rows
|
|
23
|
+
# when a record is destroyed. Calling flipside_search_by,
|
|
24
|
+
# flipside_display_as or flipside_identified_by does the same, so this is
|
|
25
|
+
# only needed to register with the defaults.
|
|
26
|
+
def flipside_entity(search_by: nil, display_as: nil, identified_by: nil)
|
|
27
|
+
flipside_registration
|
|
28
|
+
deprecated_entity_options(search_by:, display_as:, identified_by:)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Sets how entities are found from a search in the UI, either with a block
|
|
32
|
+
# or the name of a column. The block is run in the context of the class,
|
|
33
|
+
# given the search string, and returns the matching records. A column
|
|
34
|
+
# matches the search string exactly. Registers the class as an entity, as
|
|
35
|
+
# flipside_entity does.
|
|
36
|
+
def flipside_search_by(column = nil, &block)
|
|
37
|
+
flipside_registration.search_by = name_or_block(column, block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Sets how entities are displayed in the UI, either with a block given the
|
|
41
|
+
# record or the name of an instance method. Either returns a string.
|
|
42
|
+
# Registers the class as an entity, as flipside_entity does.
|
|
43
|
+
def flipside_display_as(method_name = nil, &block)
|
|
44
|
+
flipside_registration.display_as = name_or_block(method_name, block)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Sets the column that identifies an entity, :id by default. Registers the
|
|
48
|
+
# class as an entity, as flipside_entity does.
|
|
49
|
+
def flipside_identified_by(column)
|
|
50
|
+
flipside_registration.identified_by = column.to_sym
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def flipside_role(method_name, display_as: nil)
|
|
54
|
+
register_flippable!
|
|
55
|
+
|
|
56
|
+
Flipside.add_registered_role(class_name: name, method_name:, display_as:)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def register_flippable!
|
|
62
|
+
raise Flipside::Error, "Flipside macros need a named class" if name.nil?
|
|
63
|
+
|
|
64
|
+
Flipside.register_flippable(name)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The registration this class owns, created on first use. An STI subclass
|
|
68
|
+
# starts from a copy of the settings it inherits.
|
|
69
|
+
def flipside_registration
|
|
70
|
+
return _flipside_registration if _flipside_registration&.class_name == name
|
|
71
|
+
|
|
72
|
+
register_flippable!
|
|
73
|
+
|
|
74
|
+
has_many :flipside_entities,
|
|
75
|
+
class_name: "Flipside::Entity",
|
|
76
|
+
as: :flippable,
|
|
77
|
+
dependent: :delete_all
|
|
78
|
+
|
|
79
|
+
self._flipside_registration = Flipside.add_registered_entity(
|
|
80
|
+
class_name: name,
|
|
81
|
+
**(_flipside_registration&.options || {})
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def name_or_block(name, block)
|
|
86
|
+
return name.to_sym if name && !block
|
|
87
|
+
return block if block && !name
|
|
88
|
+
|
|
89
|
+
raise ArgumentError, "pass either a name or a block"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def deprecated_entity_options(search_by:, display_as:, identified_by:)
|
|
93
|
+
return if search_by.nil? && display_as.nil? && identified_by.nil?
|
|
94
|
+
|
|
95
|
+
Flipside.deprecator.warn(
|
|
96
|
+
"The search_by, display_as and identified_by options of flipside_entity are " \
|
|
97
|
+
"deprecated and will be removed in Flipside #{Flipside.deprecator.deprecation_horizon}. " \
|
|
98
|
+
"Use flipside_search_by, flipside_display_as and flipside_identified_by in " \
|
|
99
|
+
"#{name} instead."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
registration = flipside_registration
|
|
103
|
+
registration.search_by = search_by if search_by
|
|
104
|
+
registration.display_as = display_as if display_as
|
|
105
|
+
registration.identified_by = identified_by if identified_by
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -4,6 +4,15 @@ module Flipside
|
|
|
4
4
|
module Config
|
|
5
5
|
module Entities
|
|
6
6
|
def register_entity(class_name:, search_by:, display_as:, identified_by: :id)
|
|
7
|
+
deprecator.warn(
|
|
8
|
+
"Flipside.register_entity is deprecated and will be removed in Flipside " \
|
|
9
|
+
"#{deprecator.deprecation_horizon}. Include Flipside::Flippable in " \
|
|
10
|
+
"#{class_name}, call flipside_entity there and list it in Flipside.flippables."
|
|
11
|
+
)
|
|
12
|
+
add_registered_entity(class_name:, search_by:, display_as:, identified_by:)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_registered_entity(class_name:, search_by: nil, display_as: nil, identified_by: :id) # :nodoc:
|
|
7
16
|
registered_entities[class_name.to_s] = RegisteredEntity.new(
|
|
8
17
|
class_name:,
|
|
9
18
|
search_by:,
|
|
@@ -13,25 +22,61 @@ module Flipside
|
|
|
13
22
|
end
|
|
14
23
|
|
|
15
24
|
def entity_classes
|
|
25
|
+
load_flippables
|
|
16
26
|
registered_entities.keys
|
|
17
27
|
end
|
|
18
28
|
|
|
19
29
|
def search_entity(class_name:, query:)
|
|
30
|
+
load_flippables
|
|
20
31
|
registered_entities.fetch(class_name.to_s).search(query)
|
|
21
32
|
end
|
|
22
33
|
|
|
23
34
|
def find_entity(class_name:, identifier:)
|
|
35
|
+
load_flippables
|
|
24
36
|
registered_entities.fetch(class_name.to_s).find(identifier)
|
|
25
37
|
end
|
|
26
38
|
|
|
39
|
+
# Accepts either a flippable record or a Flipside::Entity join row. Given
|
|
40
|
+
# a join row, it still renders when the record or its class has been
|
|
41
|
+
# deleted, or the class is no longer registered, so the UI can list it
|
|
42
|
+
# for removal.
|
|
27
43
|
def display_entity(entity)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
return display_flippable(entity) unless entity.is_a?(Flipside::Entity)
|
|
45
|
+
|
|
46
|
+
record = entity.flippable if entity.flippable_type.safe_constantize
|
|
47
|
+
label = "#{entity.flippable_type} ##{entity.flippable_id}"
|
|
48
|
+
|
|
49
|
+
if record.nil?
|
|
50
|
+
"#{label} (deleted)"
|
|
51
|
+
elsif (registration = registration_for(record.class))
|
|
52
|
+
registration.display(record)
|
|
53
|
+
else
|
|
54
|
+
label
|
|
55
|
+
end
|
|
31
56
|
end
|
|
32
57
|
|
|
33
58
|
private
|
|
34
59
|
|
|
60
|
+
def display_flippable(record)
|
|
61
|
+
registration = registration_for(record.class)
|
|
62
|
+
raise KeyError, "#{record.class} is not registered with Flipside" unless registration
|
|
63
|
+
|
|
64
|
+
registration.display(record)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The registration of the class or its closest registered superclass, so
|
|
68
|
+
# an STI subclass is displayed through the base class's registration
|
|
69
|
+
# unless it has one of its own.
|
|
70
|
+
def registration_for(klass)
|
|
71
|
+
klass.ancestors.each do |ancestor|
|
|
72
|
+
next unless ancestor.is_a?(Class)
|
|
73
|
+
|
|
74
|
+
registration = registered_entities[ancestor.name]
|
|
75
|
+
return registration if registration
|
|
76
|
+
end
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
35
80
|
def registered_entities
|
|
36
81
|
@registered_entities ||= {}
|
|
37
82
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Flipside
|
|
2
|
+
module Config
|
|
3
|
+
# Names of the classes that register themselves through the
|
|
4
|
+
# Flipside::Flippable macros. Listing them lets Flipside load them on demand,
|
|
5
|
+
# so the UI finds them whether or not the app eager loads.
|
|
6
|
+
module Flippables
|
|
7
|
+
attr_writer :flippables
|
|
8
|
+
|
|
9
|
+
def flippables
|
|
10
|
+
@flippables ||= []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def register_flippable(class_name) # :nodoc:
|
|
14
|
+
registered_flippables << class_name.to_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Raises unless Flipside.flippables and the classes calling the
|
|
18
|
+
# Flipside::Flippable macros agree. Runs whenever the UI reads the
|
|
19
|
+
# registry, and at boot in an app that eager loads (see Railtie).
|
|
20
|
+
def verify_flippables! # :nodoc:
|
|
21
|
+
load_flippables
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def load_flippables
|
|
27
|
+
flippables.each do |name|
|
|
28
|
+
name.constantize
|
|
29
|
+
next if registered_entities.key?(name) || registered_roles.key?(name)
|
|
30
|
+
|
|
31
|
+
raise Error, "#{name} is listed in Flipside.flippables but calls neither " \
|
|
32
|
+
"flipside_entity nor flipside_role"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
unlisted = registered_flippables.to_a - flippables
|
|
36
|
+
return if unlisted.empty?
|
|
37
|
+
|
|
38
|
+
raise Error, "#{unlisted.join(", ")} must be listed in Flipside.flippables"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def registered_flippables
|
|
42
|
+
@registered_flippables ||= Set.new
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -2,7 +2,8 @@ require "flipside/search_result"
|
|
|
2
2
|
|
|
3
3
|
module Flipside
|
|
4
4
|
class RegisteredEntity
|
|
5
|
-
attr_reader :class_name
|
|
5
|
+
attr_reader :class_name
|
|
6
|
+
attr_accessor :search_by, :display_as, :identified_by
|
|
6
7
|
|
|
7
8
|
def initialize(class_name:, identified_by: :id, search_by: nil, display_as: nil)
|
|
8
9
|
@class_name = class_name
|
|
@@ -11,8 +12,12 @@ module Flipside
|
|
|
11
12
|
@identified_by = identified_by
|
|
12
13
|
end
|
|
13
14
|
|
|
15
|
+
def options
|
|
16
|
+
{search_by:, display_as:, identified_by:}
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
def search(query)
|
|
15
|
-
Array(
|
|
20
|
+
Array(lookup(query)).map do |entity|
|
|
16
21
|
SearchResult.new(
|
|
17
22
|
entity,
|
|
18
23
|
display(entity),
|
|
@@ -22,39 +27,32 @@ module Flipside
|
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
def find(identifier)
|
|
25
|
-
|
|
30
|
+
klass.find_by!("#{identified_by}": identifier)
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
def display(entity)
|
|
29
|
-
|
|
34
|
+
case display_as
|
|
35
|
+
when Proc then display_as.call(entity)
|
|
36
|
+
when Symbol then entity.public_send(display_as)
|
|
37
|
+
when String then display_as
|
|
38
|
+
else entity.public_send(identified_by)
|
|
39
|
+
end
|
|
30
40
|
end
|
|
31
41
|
|
|
32
42
|
private
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
->(query) { class_name.constantize.where("#{identified_by}": query) }
|
|
43
|
-
end
|
|
44
|
+
# A block runs in the context of the class, so it can call where directly.
|
|
45
|
+
# A Symbol names a column to match exactly.
|
|
46
|
+
def lookup(query)
|
|
47
|
+
case search_by
|
|
48
|
+
when Proc then klass.instance_exec(query, &search_by)
|
|
49
|
+
when Symbol then klass.where("#{search_by}": query)
|
|
50
|
+
else klass.where("#{identified_by}": query)
|
|
51
|
+
end
|
|
44
52
|
end
|
|
45
53
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
case display_as
|
|
49
|
-
when Proc
|
|
50
|
-
display_as
|
|
51
|
-
when Symbol
|
|
52
|
-
->(entity) { entity.public_send(display_as) }
|
|
53
|
-
when String
|
|
54
|
-
->(entity) { display_as }
|
|
55
|
-
else
|
|
56
|
-
->(entity) { entity.public_send(identified_by) }
|
|
57
|
-
end
|
|
54
|
+
def klass
|
|
55
|
+
class_name.constantize
|
|
58
56
|
end
|
|
59
57
|
end
|
|
60
58
|
end
|
|
@@ -4,8 +4,17 @@ module Flipside
|
|
|
4
4
|
module Config
|
|
5
5
|
module Roles
|
|
6
6
|
def register_role(class_name:, method_name:, display_as: nil)
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
deprecator.warn(
|
|
8
|
+
"Flipside.register_role is deprecated and will be removed in Flipside " \
|
|
9
|
+
"#{deprecator.deprecation_horizon}. Include Flipside::Flippable in " \
|
|
10
|
+
"#{class_name}, call flipside_role there and list it in Flipside.flippables."
|
|
11
|
+
)
|
|
12
|
+
add_registered_role(class_name:, method_name:, display_as:)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_registered_role(class_name:, method_name:, display_as: nil) # :nodoc:
|
|
16
|
+
registered_roles[class_name.to_s] ||= {}
|
|
17
|
+
registered_roles[class_name.to_s][method_name.to_s] = RegisteredRole.new(
|
|
9
18
|
class_name:,
|
|
10
19
|
method_name:,
|
|
11
20
|
display_as:
|
|
@@ -13,11 +22,13 @@ module Flipside
|
|
|
13
22
|
end
|
|
14
23
|
|
|
15
24
|
def role_classes
|
|
25
|
+
load_flippables
|
|
16
26
|
registered_roles.keys
|
|
17
27
|
end
|
|
18
28
|
|
|
19
29
|
def search_role(class_name:, query:)
|
|
20
|
-
|
|
30
|
+
load_flippables
|
|
31
|
+
registered_roles.fetch(class_name.to_s).values.filter_map do |registered_role|
|
|
21
32
|
next unless registered_role.match? query
|
|
22
33
|
registered_role.to_result
|
|
23
34
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Flipside
|
|
2
|
+
# Wires Flipside into a Rails app. Only loaded when Rails is present.
|
|
3
|
+
class Railtie < ::Rails::Railtie
|
|
4
|
+
# Hands Flipside's deprecator to the app, so its deprecation settings (log,
|
|
5
|
+
# raise, silence) apply to Flipside's warnings too.
|
|
6
|
+
initializer "flipside.deprecator" do |app|
|
|
7
|
+
app.deprecators[:flipside] = Flipside.deprecator if app.respond_to?(:deprecators)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Eager loading runs before after_initialize, so every class calling the
|
|
11
|
+
# macros has registered by now. Checking here fails boot, and so CI, for a
|
|
12
|
+
# misconfigured list instead of the first visit to the UI. Without eager
|
|
13
|
+
# loading the check would only see whatever happens to be loaded.
|
|
14
|
+
config.after_initialize do |app|
|
|
15
|
+
Flipside.verify_flippables! if app.config.eager_load
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/flipside/version.rb
CHANGED
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
<li class="mt-2 p-2 bg-slate-700 text-slate-300 border border-slate-600 rounded-lg">
|
|
52
52
|
<div class="flex justify-between items-center">
|
|
53
53
|
<div>
|
|
54
|
-
<%= Flipside.display_entity(entity
|
|
54
|
+
<%= Flipside.display_entity(entity) %> (<%= entity.flippable_type %>)
|
|
55
55
|
</div>
|
|
56
56
|
<div>
|
|
57
57
|
<form action="<%= feature_path(feature, "remove_entity") %>" method="post">
|
data/lib/flipside.rb
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "active_record"
|
|
4
|
+
require "active_support/deprecation"
|
|
4
5
|
require "flipside/version"
|
|
5
6
|
require "flipside/web"
|
|
6
7
|
require "flipside/config/settings"
|
|
7
8
|
require "flipside/config/entities"
|
|
8
9
|
require "flipside/config/roles"
|
|
10
|
+
require "flipside/config/flippables"
|
|
9
11
|
require "models/flipside/feature"
|
|
12
|
+
require "models/flipside/flippable"
|
|
13
|
+
require "flipside/railtie" if defined?(Rails::Railtie)
|
|
10
14
|
|
|
11
15
|
module Flipside
|
|
12
16
|
extend Config::Settings
|
|
13
17
|
extend Config::Entities
|
|
14
18
|
extend Config::Roles
|
|
19
|
+
extend Config::Flippables
|
|
15
20
|
|
|
16
21
|
class Error < StandardError; end
|
|
17
22
|
|
|
@@ -22,6 +27,10 @@ module Flipside
|
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
class << self
|
|
30
|
+
def deprecator
|
|
31
|
+
@deprecator ||= ActiveSupport::Deprecation.new("1.0", "Flipside")
|
|
32
|
+
end
|
|
33
|
+
|
|
25
34
|
def enabled?(name, *objects)
|
|
26
35
|
feature = find_by(name:)
|
|
27
36
|
return false unless feature
|
|
@@ -40,6 +49,11 @@ module Flipside
|
|
|
40
49
|
feature.update(enabled: true)
|
|
41
50
|
end
|
|
42
51
|
|
|
52
|
+
def disable!(name)
|
|
53
|
+
feature = find_by!(name:)
|
|
54
|
+
feature.update(enabled: false)
|
|
55
|
+
end
|
|
56
|
+
|
|
43
57
|
def add_entity(entity:, feature: nil, name: nil)
|
|
44
58
|
feature ||= find_by!(name:)
|
|
45
59
|
Entity.find_or_create_by(feature:, flippable: entity)
|
|
@@ -50,6 +64,22 @@ module Flipside
|
|
|
50
64
|
feature.entities.find_by(id: entity_id)&.destroy
|
|
51
65
|
end
|
|
52
66
|
|
|
67
|
+
# Deletes entities whose record no longer exists, or whose class does not.
|
|
68
|
+
# Returns the number of entities deleted.
|
|
69
|
+
def prune_orphaned_entities
|
|
70
|
+
Entity.distinct.pluck(:flippable_type).sum do |type|
|
|
71
|
+
entities = Entity.where(flippable_type: type)
|
|
72
|
+
klass = type.safe_constantize
|
|
73
|
+
next entities.delete_all unless klass
|
|
74
|
+
|
|
75
|
+
existing = klass
|
|
76
|
+
.unscoped
|
|
77
|
+
.where(klass.primary_key => entities.pluck(:flippable_id))
|
|
78
|
+
.pluck(klass.primary_key)
|
|
79
|
+
entities.where.not(flippable_id: existing).delete_all
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
53
83
|
def add_role(class_name:, method_name:, feature: nil, name: nil)
|
|
54
84
|
feature ||= find_by!(name:)
|
|
55
85
|
Role.find_or_create_by(feature:, class_name:, method: method_name)
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flipside
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sammy Henningsson
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activerecord
|
|
@@ -136,12 +135,14 @@ files:
|
|
|
136
135
|
- app/models/flipside/entities.rb
|
|
137
136
|
- app/models/flipside/entity.rb
|
|
138
137
|
- app/models/flipside/feature.rb
|
|
138
|
+
- app/models/flipside/flippable.rb
|
|
139
139
|
- app/models/flipside/key.rb
|
|
140
140
|
- app/models/flipside/keys.rb
|
|
141
141
|
- app/models/flipside/role.rb
|
|
142
142
|
- app/models/flipside/roles.rb
|
|
143
143
|
- lib/flipside.rb
|
|
144
144
|
- lib/flipside/config/entities.rb
|
|
145
|
+
- lib/flipside/config/flippables.rb
|
|
145
146
|
- lib/flipside/config/registered_entity.rb
|
|
146
147
|
- lib/flipside/config/registered_role.rb
|
|
147
148
|
- lib/flipside/config/roles.rb
|
|
@@ -152,6 +153,7 @@ files:
|
|
|
152
153
|
- lib/flipside/public/inline_edit_controller.js
|
|
153
154
|
- lib/flipside/public/modal_controller.js
|
|
154
155
|
- lib/flipside/public/search_controller.js
|
|
156
|
+
- lib/flipside/railtie.rb
|
|
155
157
|
- lib/flipside/search_result.rb
|
|
156
158
|
- lib/flipside/version.rb
|
|
157
159
|
- lib/flipside/views/_datetime_modal.erb
|
|
@@ -174,7 +176,6 @@ metadata:
|
|
|
174
176
|
homepage_uri: https://github.com/apoex/flipside
|
|
175
177
|
source_code_uri: https://github.com/apoex/flipside
|
|
176
178
|
changelog_uri: https://github.com/apoex/flipside/blob/main/CHANGELOG.md
|
|
177
|
-
post_install_message:
|
|
178
179
|
rdoc_options: []
|
|
179
180
|
require_paths:
|
|
180
181
|
- app
|
|
@@ -190,8 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
190
191
|
- !ruby/object:Gem::Version
|
|
191
192
|
version: '0'
|
|
192
193
|
requirements: []
|
|
193
|
-
rubygems_version:
|
|
194
|
-
signing_key:
|
|
194
|
+
rubygems_version: 4.0.20
|
|
195
195
|
specification_version: 4
|
|
196
196
|
summary: Feature flags.
|
|
197
197
|
test_files: []
|