pin_flags 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 +244 -0
- data/Rakefile +8 -0
- data/app/assets/javascripts/pin_flags/alpine.min.js +5 -0
- data/app/assets/stylesheets/pin_flags/application.css +25 -0
- data/app/assets/stylesheets/pin_flags/bulma.min.css +21564 -0
- data/app/assets/stylesheets/pin_flags/forms.css +8 -0
- data/app/controllers/pin_flags/application_controller.rb +6 -0
- data/app/controllers/pin_flags/feature_tags/exports_controller.rb +18 -0
- data/app/controllers/pin_flags/feature_tags/feature_subscriptions_controller.rb +138 -0
- data/app/controllers/pin_flags/feature_tags/imports_controller.rb +65 -0
- data/app/controllers/pin_flags/feature_tags_controller.rb +141 -0
- data/app/helpers/pin_flags/application_helper.rb +20 -0
- data/app/helpers/pin_flags/feature_tags/feature_subscriptions_helper.rb +23 -0
- data/app/helpers/pin_flags/feature_tags/imports_helper.rb +7 -0
- data/app/helpers/pin_flags/feature_tags_helper.rb +12 -0
- data/app/jobs/pin_flags/application_job.rb +4 -0
- data/app/mailers/pin_flags/application_mailer.rb +6 -0
- data/app/models/concerns/pin_flags/feature_taggable.rb +39 -0
- data/app/models/pin_flags/application_record.rb +5 -0
- data/app/models/pin_flags/feature_subscription/bulk_processor.rb +80 -0
- data/app/models/pin_flags/feature_subscription.rb +21 -0
- data/app/models/pin_flags/feature_tag/cacheable.rb +44 -0
- data/app/models/pin_flags/feature_tag.rb +98 -0
- data/app/models/pin_flags/page.rb +48 -0
- data/app/views/layouts/pin_flags/_flash.html.erb +6 -0
- data/app/views/layouts/pin_flags/_modal.html.erb +41 -0
- data/app/views/layouts/pin_flags/application.html.erb +34 -0
- data/app/views/pin_flags/feature_tags/_empty_feature_tags_table.html.erb +7 -0
- data/app/views/pin_flags/feature_tags/_feature_tags_table.html.erb +82 -0
- data/app/views/pin_flags/feature_tags/_feature_tags_table_row.html.erb +50 -0
- data/app/views/pin_flags/feature_tags/_new_feature_subscription_link.html.erb +8 -0
- data/app/views/pin_flags/feature_tags/_new_feature_tag_link.html.erb +6 -0
- data/app/views/pin_flags/feature_tags/create.turbo_stream.erb +7 -0
- data/app/views/pin_flags/feature_tags/destroy.turbo_stream.erb +7 -0
- data/app/views/pin_flags/feature_tags/feature_subscriptions/_empty_feature_subscriptions_table.html.erb +7 -0
- data/app/views/pin_flags/feature_tags/feature_subscriptions/_feature_subscriptions_table.html.erb +83 -0
- data/app/views/pin_flags/feature_tags/feature_subscriptions/_feature_subscriptions_table_row.html.erb +37 -0
- data/app/views/pin_flags/feature_tags/feature_subscriptions/create.turbo_stream.erb +10 -0
- data/app/views/pin_flags/feature_tags/feature_subscriptions/destroy.turbo_stream.erb +7 -0
- data/app/views/pin_flags/feature_tags/feature_subscriptions/new.html.erb +83 -0
- data/app/views/pin_flags/feature_tags/imports/_form.html.erb +66 -0
- data/app/views/pin_flags/feature_tags/index.html.erb +123 -0
- data/app/views/pin_flags/feature_tags/new.html.erb +34 -0
- data/app/views/pin_flags/feature_tags/show.html.erb +89 -0
- data/app/views/pin_flags/feature_tags/update.html.erb +2 -0
- data/app/views/pin_flags/feature_tags/update.turbo_stream.erb +3 -0
- data/app/views/pin_flags/shared/_error_messages.html.erb +11 -0
- data/config/routes.rb +12 -0
- data/db/migrate/20250626042529_create_feature_tag.rb +12 -0
- data/db/migrate/20250626050146_create_feature_subscriptions.rb +15 -0
- data/lib/generators/pin_flags/install/install_generator.rb +21 -0
- data/lib/generators/pin_flags/install/templates/create_feature_subscriptions.rb +14 -0
- data/lib/generators/pin_flags/install/templates/create_feature_tag.rb +12 -0
- data/lib/pin_flags/engine.rb +13 -0
- data/lib/pin_flags/version.rb +3 -0
- data/lib/pin_flags.rb +13 -0
- data/lib/tasks/pin_flags_tasks.rake +4 -0
- metadata +192 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
<div class="container is-max-desktop">
|
2
|
+
<div class="columns is-centered">
|
3
|
+
<div class="column is-narrow">
|
4
|
+
<%= form_with url: feature_tags_imports_path, data: { turbo: false } do |form| %>
|
5
|
+
<div class="field mb-4">
|
6
|
+
<div class="is-flex is-align-items-flex-start">
|
7
|
+
<%= form.label :json_file, 'FeatureTag JSON File', class: "label" %>
|
8
|
+
<span class="has-text-danger ml-1">*</span>
|
9
|
+
</div>
|
10
|
+
<p class="help is-info mb-2">
|
11
|
+
File must be <strong>less than <%= number_to_human_size(PinFlags::FeatureTags::ImportsController::MAX_FILE_SIZE) %></strong>.
|
12
|
+
</p>
|
13
|
+
<div class="control"
|
14
|
+
x-data="{
|
15
|
+
fileName: 'No file selected',
|
16
|
+
showError: false,
|
17
|
+
maxSize: <%= PinFlags::FeatureTags::ImportsController::MAX_FILE_SIZE %>,
|
18
|
+
validate(event) {
|
19
|
+
const file = event.target.files[0];
|
20
|
+
|
21
|
+
if (file) {
|
22
|
+
this.fileName = file.name;
|
23
|
+
|
24
|
+
if (file.size > this.maxSize) {
|
25
|
+
this.showError = true;
|
26
|
+
event.target.classList.add('is-danger');
|
27
|
+
event.target.value = '';
|
28
|
+
this.fileName = 'No file selected';
|
29
|
+
} else {
|
30
|
+
this.showError = false;
|
31
|
+
event.target.classList.remove('is-danger');
|
32
|
+
}
|
33
|
+
} else {
|
34
|
+
this.fileName = 'No file selected';
|
35
|
+
this.showError = false;
|
36
|
+
event.target.classList.remove('is-danger');
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}">
|
40
|
+
<div class="file has-name is-fullwidth is-boxed">
|
41
|
+
<label class="file-label">
|
42
|
+
<%= form.file_field :json_file, class: "file-input",
|
43
|
+
required: true, accept: ".json",
|
44
|
+
"@change": "validate($event)"
|
45
|
+
%>
|
46
|
+
<span class="file-cta">
|
47
|
+
<span class="file-label">Choose a file…</span>
|
48
|
+
</span>
|
49
|
+
<span class="file-name" x-text="fileName"></span>
|
50
|
+
</label>
|
51
|
+
</div>
|
52
|
+
<p class="help is-danger" x-show="showError">
|
53
|
+
File size must be less than <%= number_to_human_size(PinFlags::FeatureTags::ImportsController::MAX_FILE_SIZE) %>.
|
54
|
+
</p>
|
55
|
+
</div>
|
56
|
+
</div>
|
57
|
+
|
58
|
+
<div class="field is-grouped">
|
59
|
+
<div class="control is-expanded">
|
60
|
+
<%= form.submit "Upload", class: "button is-primary is-fullwidth" %>
|
61
|
+
</div>
|
62
|
+
</div>
|
63
|
+
<% end %>
|
64
|
+
</div>
|
65
|
+
</div>
|
66
|
+
</div>
|
@@ -0,0 +1,123 @@
|
|
1
|
+
<% content_for :title, "Feature Tags" %>
|
2
|
+
|
3
|
+
<nav class="navbar" role="navigation" aria-label="main navigation"
|
4
|
+
x-data="{ mobileMenuOpen: false }">
|
5
|
+
<div class="navbar-brand">
|
6
|
+
<%= link_to pin_flags.root_path, class: "navbar-item" do %>
|
7
|
+
<span class="is-size-2 is-size-4-mobile has-text-weight-bold has-text-white">
|
8
|
+
⛳ Pin Flags
|
9
|
+
</span>
|
10
|
+
<% end %>
|
11
|
+
|
12
|
+
<a role="button" class="navbar-burger" aria-label="menu"
|
13
|
+
:aria-expanded="mobileMenuOpen"
|
14
|
+
:class="{ 'is-active': mobileMenuOpen }"
|
15
|
+
@click="mobileMenuOpen = !mobileMenuOpen">
|
16
|
+
<span aria-hidden="true"></span>
|
17
|
+
<span aria-hidden="true"></span>
|
18
|
+
<span aria-hidden="true"></span>
|
19
|
+
<span aria-hidden="true"></span>
|
20
|
+
</a>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<div class="navbar-menu" :class="{ 'is-active': mobileMenuOpen }">
|
24
|
+
<div class="navbar-end">
|
25
|
+
<div class="navbar-item">
|
26
|
+
<div class="buttons">
|
27
|
+
<button class="button is-primary is-dark"
|
28
|
+
@click="$dispatch('open-modal', { modalId: '<%= display_import_form_modal_id %>' })">
|
29
|
+
Import
|
30
|
+
</button>
|
31
|
+
<%= link_to "Export", pin_flags.feature_tags_exports_path(format: :json),
|
32
|
+
class: "button is-info is-dark" %>
|
33
|
+
</div>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
</nav>
|
38
|
+
|
39
|
+
<div class="mt-4">
|
40
|
+
<%= turbo_frame_tag :new_feature_tag do %>
|
41
|
+
<%= render 'new_feature_tag_link' %>
|
42
|
+
<% end %>
|
43
|
+
</div>
|
44
|
+
|
45
|
+
<div class="mt-5">
|
46
|
+
<section x-data="{ timeout: null, delay: 500 }">
|
47
|
+
<%= form_with url: pin_flags.feature_tags_path, method: :get,
|
48
|
+
data: { turbo_frame: :feature_tags_table } do |form| %>
|
49
|
+
<div class="columns">
|
50
|
+
<div class="column">
|
51
|
+
<%= form.search_field :filter,
|
52
|
+
autofocus: true,
|
53
|
+
placeholder: "Filter by name...",
|
54
|
+
autocomplete: "off",
|
55
|
+
value: @filter_param || "",
|
56
|
+
maxlength: PinFlags::FeatureTag::MAX_NAME_LENGTH,
|
57
|
+
pattern: "[^\s]+.*",
|
58
|
+
class: "input is-medium",
|
59
|
+
aria: { label: "Search Feature Tags" },
|
60
|
+
"x-ref": "filterField",
|
61
|
+
"@input": "clearTimeout(timeout); timeout = setTimeout(() => { $el.closest('form').requestSubmit() }, delay)"
|
62
|
+
%>
|
63
|
+
</div>
|
64
|
+
<div class="column">
|
65
|
+
<div class="select is-medium mr-3">
|
66
|
+
<%= form.select :enabled,
|
67
|
+
options_for_select([
|
68
|
+
['Enabled?', ''],
|
69
|
+
['Enabled', 'true'],
|
70
|
+
['Disabled', 'false']
|
71
|
+
], params[:enabled]),
|
72
|
+
{},
|
73
|
+
{
|
74
|
+
aria: { label: "Filter by enabled status" },
|
75
|
+
"x-ref": "enabledSelect",
|
76
|
+
"@change": "clearTimeout(timeout); timeout = setTimeout(() => { $el.closest('form').requestSubmit() }, delay)"
|
77
|
+
}
|
78
|
+
%>
|
79
|
+
</div>
|
80
|
+
<div class="select is-medium">
|
81
|
+
<%= form.select :subscriptions,
|
82
|
+
options_for_select([
|
83
|
+
['Subscriptions?', ''],
|
84
|
+
['Has Subscriptions', 'true'],
|
85
|
+
['No Subscriptions', 'false']
|
86
|
+
], params[:subscriptions]),
|
87
|
+
{},
|
88
|
+
{
|
89
|
+
aria: { label: "Filter by subscriptions status" },
|
90
|
+
"x-ref": "subscriptionsSelect",
|
91
|
+
"@change": "clearTimeout(timeout); timeout = setTimeout(() => { $el.closest('form').requestSubmit() }, delay)"
|
92
|
+
}
|
93
|
+
%>
|
94
|
+
</div>
|
95
|
+
</div>
|
96
|
+
<div class="column is-narrow">
|
97
|
+
<button type="button"
|
98
|
+
class="button is-light"
|
99
|
+
aria-label="Clear all filters"
|
100
|
+
@click="
|
101
|
+
$refs.filterField.value = '';
|
102
|
+
$refs.enabledSelect.value = '';
|
103
|
+
$refs.subscriptionsSelect.value = '';
|
104
|
+
$el.closest('form').requestSubmit();
|
105
|
+
">
|
106
|
+
Clear Filters
|
107
|
+
</button>
|
108
|
+
</div>
|
109
|
+
</div>
|
110
|
+
<% end %>
|
111
|
+
</section>
|
112
|
+
|
113
|
+
<% if @feature_tags.any? %>
|
114
|
+
<%= turbo_frame_tag :feature_tags_table do %>
|
115
|
+
<%= render 'feature_tags_table', feature_tags: @feature_tags, paginator: @paginator,
|
116
|
+
filter_param: @filter_param, enabled_param: @enabled_param, subscriptions_param: @subscriptions_param %>
|
117
|
+
<% end %>
|
118
|
+
<% else %>
|
119
|
+
<%= turbo_frame_tag :feature_tags_table do %>
|
120
|
+
<%= render 'empty_feature_tags_table', filter_param: @filter_param, enabled_param: @enabled_param, subscriptions_param: @subscriptions_param %>
|
121
|
+
<% end %>
|
122
|
+
<% end %>
|
123
|
+
</div>
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<%= turbo_frame_tag :new_feature_tag do %>
|
2
|
+
<%= form_with model: @feature_tag, url: pin_flags.feature_tags_path do |form| %>
|
3
|
+
<%= render "pin_flags/shared/error_messages", resource: @feature_tag %>
|
4
|
+
|
5
|
+
<div class="field">
|
6
|
+
<%= form.label :name, class: "label" %>
|
7
|
+
<div class="control">
|
8
|
+
<%= form.text_field :name, class: "input",
|
9
|
+
placeholder: "Enter feature tag name",
|
10
|
+
minlength: PinFlags::FeatureTag::MIN_NAME_LENGTH,
|
11
|
+
maxlength: PinFlags::FeatureTag::MAX_NAME_LENGTH,
|
12
|
+
autofocus: true, required: true %>
|
13
|
+
</div>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<div class="field">
|
17
|
+
<div class="control">
|
18
|
+
<label class="checkbox">
|
19
|
+
<%= form.check_box :enabled %>
|
20
|
+
Enabled?
|
21
|
+
</label>
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<div class="field is-grouped">
|
26
|
+
<div class="control">
|
27
|
+
<%= form.submit "Create Feature Tag", class: "button is-success is-outlined" %>
|
28
|
+
</div>
|
29
|
+
<div class="control">
|
30
|
+
<%= link_to "Cancel", pin_flags.feature_tags_path, class: "button is-danger is-outlined" %>
|
31
|
+
</div>
|
32
|
+
</div>
|
33
|
+
<% end %>
|
34
|
+
<% end %>
|
@@ -0,0 +1,89 @@
|
|
1
|
+
<% content_for :title, "Feature Tag #{@feature_tag.name}" %>
|
2
|
+
|
3
|
+
<nav class="navbar" role="navigation" aria-label="main navigation">
|
4
|
+
<div class="navbar-brand">
|
5
|
+
<%= link_to pin_flags.root_path, class: "navbar-item" do %>
|
6
|
+
<span class="is-size-2 is-size-4-mobile has-text-weight-bold has-text-white">
|
7
|
+
⛳ Pin Flags
|
8
|
+
</span>
|
9
|
+
<% end %>
|
10
|
+
</div>
|
11
|
+
</nav>
|
12
|
+
|
13
|
+
<p class="is-size-4 is-size-5-mobile has-text-weight-bold has-text-white is-clipped">
|
14
|
+
<%= @feature_tag.name %>
|
15
|
+
</p>
|
16
|
+
|
17
|
+
<div class="mt-5">
|
18
|
+
<%= turbo_frame_tag :new_feature_subscription do %>
|
19
|
+
<%= render 'new_feature_subscription_link', feature_tag: @feature_tag %>
|
20
|
+
<% end %>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<div class="container">
|
24
|
+
<div class="mt-6 mb-4">
|
25
|
+
<span class="has-text-warning has-text-weight-semibold is-size-5">
|
26
|
+
Filters
|
27
|
+
</span>
|
28
|
+
<hr class="has-background-warning" style="height: 2px; width: 50%;">
|
29
|
+
</div>
|
30
|
+
|
31
|
+
<section x-data="{ timeout: null, delay: 500 }">
|
32
|
+
<%= form_with url: pin_flags.feature_tag_path(@feature_tag), method: :get,
|
33
|
+
data: { turbo_frame: :feature_subscriptions_table } do |form| %>
|
34
|
+
<div class="columns">
|
35
|
+
<div class="column is-half">
|
36
|
+
<div class="field mb-4">
|
37
|
+
<%= form.label :feature_taggable_type, 'Taggable Type', class: "label has-text-left" %>
|
38
|
+
<div class="control">
|
39
|
+
<div class="select is-fullwidth is-medium">
|
40
|
+
<%= form.select :feature_taggable_type,
|
41
|
+
PinFlags::FeatureTag.feature_taggable_options_for_select,
|
42
|
+
{
|
43
|
+
prompt: 'All Types',
|
44
|
+
selected: @feature_taggable_type,
|
45
|
+
include_blank: @feature_taggable_type.present?
|
46
|
+
},
|
47
|
+
"@change": "clearTimeout(timeout); timeout = setTimeout(() => { $el.closest('form').requestSubmit() }, delay)"
|
48
|
+
%>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
|
53
|
+
<div class="field">
|
54
|
+
<%= form.label :filter, 'Taggable ID', class: 'label has-text-left' %>
|
55
|
+
<div class="control">
|
56
|
+
<%= form.search_field :filter, class: "input is-medium",
|
57
|
+
placeholder: "Filter feature subscription by ID...",
|
58
|
+
autocomplete: "off",
|
59
|
+
autofocus: true,
|
60
|
+
value: @filter_param || "",
|
61
|
+
maxlength: PinFlags::FeatureTag::MAX_NAME_LENGTH,
|
62
|
+
pattern: "[^\s]+.*",
|
63
|
+
"@input": "clearTimeout(timeout); timeout = setTimeout(() => { $el.closest('form').requestSubmit() }, delay)"
|
64
|
+
%>
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
</div>
|
68
|
+
</div>
|
69
|
+
<% end %>
|
70
|
+
</section>
|
71
|
+
|
72
|
+
<div class="mt-5">
|
73
|
+
<% if @feature_subscriptions.any? %>
|
74
|
+
<%= turbo_frame_tag :feature_subscriptions_table do %>
|
75
|
+
<%= render "pin_flags/feature_tags/feature_subscriptions/feature_subscriptions_table",
|
76
|
+
feature_tag: @feature_tag, feature_subscriptions: @feature_subscriptions, paginator: @paginator,
|
77
|
+
filter_param: @filter_param, feature_taggable_type: @feature_taggable_type
|
78
|
+
%>
|
79
|
+
<% end %>
|
80
|
+
<% else %>
|
81
|
+
<%= turbo_frame_tag :feature_subscriptions_table do %>
|
82
|
+
<%= render 'pin_flags/feature_tags/feature_subscriptions/empty_feature_subscriptions_table',
|
83
|
+
filter_param: @filter_param,
|
84
|
+
feature_taggable_type: @feature_taggable_type
|
85
|
+
%>
|
86
|
+
<% end %>
|
87
|
+
<% end %>
|
88
|
+
</div>
|
89
|
+
</div>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<% if resource.errors.any? %>
|
2
|
+
<article id="error_explanation" data-turbo-cache="false" class="message is-danger" role="alert">
|
3
|
+
<div class="message-body">
|
4
|
+
<ul>
|
5
|
+
<% resource.errors.full_messages.each do |message| %>
|
6
|
+
<li><%= message %></li>
|
7
|
+
<% end %>
|
8
|
+
</ul>
|
9
|
+
</div>
|
10
|
+
</article>
|
11
|
+
<% end %>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
PinFlags::Engine.routes.draw do
|
2
|
+
namespace :feature_tags do
|
3
|
+
resources :imports, only: %i[create]
|
4
|
+
resources :exports, only: %i[index]
|
5
|
+
end
|
6
|
+
|
7
|
+
resources :feature_tags, except: %i[edit] do
|
8
|
+
resources :feature_subscriptions, only: %i[new create destroy], module: :feature_tags
|
9
|
+
end
|
10
|
+
|
11
|
+
root "feature_tags#index"
|
12
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateFeatureTag < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :pin_flags_feature_tags do |t|
|
4
|
+
t.string :name, null: false
|
5
|
+
t.boolean :enabled, default: true, null: false
|
6
|
+
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
add_index :pin_flags_feature_tags, :name, unique: true
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateFeatureSubscriptions < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :pin_flags_feature_subscriptions do |t|
|
4
|
+
t.references :feature_tag, null: false, foreign_key: { to_table: :pin_flags_feature_tags }
|
5
|
+
t.references :feature_taggable, polymorphic: true, null: false
|
6
|
+
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
add_index :pin_flags_feature_subscriptions,
|
11
|
+
%i[feature_tag_id feature_taggable_type feature_taggable_id],
|
12
|
+
unique: true,
|
13
|
+
name: 'idx_pf_subs_on_tag_and_taggable'
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module PinFlags
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
|
8
|
+
def copy_migrations
|
9
|
+
migration_template "create_feature_tag.rb", "db/migrate/create_feature_tag.rb"
|
10
|
+
migration_template "create_feature_subscriptions.rb", "db/migrate/create_feature_subscriptions.rb"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.next_migration_number(dirname)
|
14
|
+
# Increment by 1 second each time to ensure unique timestamps
|
15
|
+
@migration_number ||= Time.current.utc.strftime("%Y%m%d%H%M%S").to_i
|
16
|
+
@migration_number += 1
|
17
|
+
@migration_number.to_s
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateFeatureSubscriptions < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :pin_flags_feature_subscriptions do |t|
|
4
|
+
t.references :feature_tag, null: false, foreign_key: { to_table: :pin_flags_feature_tags }
|
5
|
+
t.references :feature_taggable, polymorphic: true, null: false
|
6
|
+
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
add_index :pin_flags_feature_subscriptions, [ :feature_taggable_type, :feature_taggable_id ]
|
11
|
+
add_index :pin_flags_feature_subscriptions, [ :feature_tag_id, :feature_taggable_type, :feature_taggable_id ],
|
12
|
+
unique: true, name: "index_feature_subscriptions_unique"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateFeatureTag < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :pin_flags_feature_tags do |t|
|
4
|
+
t.string :name, null: false
|
5
|
+
t.boolean :enabled, default: true, null: false
|
6
|
+
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
add_index :pin_flags_feature_tags, :name, unique: true
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module PinFlags
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace PinFlags
|
4
|
+
|
5
|
+
initializer "pin_flags.assets.precompile" do |app|
|
6
|
+
app.config.assets.precompile += %w[
|
7
|
+
pin_flags/alpine.min.js
|
8
|
+
pin_flags/bulma.min.css
|
9
|
+
pin_flags/application.css
|
10
|
+
]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/pin_flags.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "pin_flags/version"
|
2
|
+
require "pin_flags/engine"
|
3
|
+
require "active_support/core_ext/numeric/time"
|
4
|
+
|
5
|
+
module PinFlags
|
6
|
+
mattr_accessor :cache_prefix, default: "pin_flags"
|
7
|
+
mattr_accessor :cache_expiry, default: 12.hours
|
8
|
+
|
9
|
+
def self.config
|
10
|
+
yield self if block_given?
|
11
|
+
self
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pin_flags
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Demetrious Wilson
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '8.0'
|
19
|
+
- - ">="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 8.0.2
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - "~>"
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '8.0'
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 8.0.2
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: turbo-rails
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - "~>"
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '2.0'
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - "~>"
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '2.0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: debug
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - "~>"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '1.9'
|
53
|
+
type: :development
|
54
|
+
prerelease: false
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '1.9'
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: rubocop-performance
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '1.21'
|
67
|
+
type: :development
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '1.21'
|
74
|
+
- !ruby/object:Gem::Dependency
|
75
|
+
name: rubocop-rails-omakase
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '1.0'
|
81
|
+
type: :development
|
82
|
+
prerelease: false
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '1.0'
|
88
|
+
- !ruby/object:Gem::Dependency
|
89
|
+
name: propshaft
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - "~>"
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '1.0'
|
95
|
+
type: :development
|
96
|
+
prerelease: false
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - "~>"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '1.0'
|
102
|
+
description: A lightweight Rails engine for managing entity-based feature flags with
|
103
|
+
built-in caching support. Pin features to any ActiveRecord model with polymorphic
|
104
|
+
associations, providing a flexible alternative to traditional feature flags. Includes
|
105
|
+
a configurable cache expiry, and isolated namespace to avoid conflicts.
|
106
|
+
email:
|
107
|
+
- demetriouswilson@gmail.com
|
108
|
+
executables: []
|
109
|
+
extensions: []
|
110
|
+
extra_rdoc_files: []
|
111
|
+
files:
|
112
|
+
- MIT-LICENSE
|
113
|
+
- README.md
|
114
|
+
- Rakefile
|
115
|
+
- app/assets/javascripts/pin_flags/alpine.min.js
|
116
|
+
- app/assets/stylesheets/pin_flags/application.css
|
117
|
+
- app/assets/stylesheets/pin_flags/bulma.min.css
|
118
|
+
- app/assets/stylesheets/pin_flags/forms.css
|
119
|
+
- app/controllers/pin_flags/application_controller.rb
|
120
|
+
- app/controllers/pin_flags/feature_tags/exports_controller.rb
|
121
|
+
- app/controllers/pin_flags/feature_tags/feature_subscriptions_controller.rb
|
122
|
+
- app/controllers/pin_flags/feature_tags/imports_controller.rb
|
123
|
+
- app/controllers/pin_flags/feature_tags_controller.rb
|
124
|
+
- app/helpers/pin_flags/application_helper.rb
|
125
|
+
- app/helpers/pin_flags/feature_tags/feature_subscriptions_helper.rb
|
126
|
+
- app/helpers/pin_flags/feature_tags/imports_helper.rb
|
127
|
+
- app/helpers/pin_flags/feature_tags_helper.rb
|
128
|
+
- app/jobs/pin_flags/application_job.rb
|
129
|
+
- app/mailers/pin_flags/application_mailer.rb
|
130
|
+
- app/models/concerns/pin_flags/feature_taggable.rb
|
131
|
+
- app/models/pin_flags/application_record.rb
|
132
|
+
- app/models/pin_flags/feature_subscription.rb
|
133
|
+
- app/models/pin_flags/feature_subscription/bulk_processor.rb
|
134
|
+
- app/models/pin_flags/feature_tag.rb
|
135
|
+
- app/models/pin_flags/feature_tag/cacheable.rb
|
136
|
+
- app/models/pin_flags/page.rb
|
137
|
+
- app/views/layouts/pin_flags/_flash.html.erb
|
138
|
+
- app/views/layouts/pin_flags/_modal.html.erb
|
139
|
+
- app/views/layouts/pin_flags/application.html.erb
|
140
|
+
- app/views/pin_flags/feature_tags/_empty_feature_tags_table.html.erb
|
141
|
+
- app/views/pin_flags/feature_tags/_feature_tags_table.html.erb
|
142
|
+
- app/views/pin_flags/feature_tags/_feature_tags_table_row.html.erb
|
143
|
+
- app/views/pin_flags/feature_tags/_new_feature_subscription_link.html.erb
|
144
|
+
- app/views/pin_flags/feature_tags/_new_feature_tag_link.html.erb
|
145
|
+
- app/views/pin_flags/feature_tags/create.turbo_stream.erb
|
146
|
+
- app/views/pin_flags/feature_tags/destroy.turbo_stream.erb
|
147
|
+
- app/views/pin_flags/feature_tags/feature_subscriptions/_empty_feature_subscriptions_table.html.erb
|
148
|
+
- app/views/pin_flags/feature_tags/feature_subscriptions/_feature_subscriptions_table.html.erb
|
149
|
+
- app/views/pin_flags/feature_tags/feature_subscriptions/_feature_subscriptions_table_row.html.erb
|
150
|
+
- app/views/pin_flags/feature_tags/feature_subscriptions/create.turbo_stream.erb
|
151
|
+
- app/views/pin_flags/feature_tags/feature_subscriptions/destroy.turbo_stream.erb
|
152
|
+
- app/views/pin_flags/feature_tags/feature_subscriptions/new.html.erb
|
153
|
+
- app/views/pin_flags/feature_tags/imports/_form.html.erb
|
154
|
+
- app/views/pin_flags/feature_tags/index.html.erb
|
155
|
+
- app/views/pin_flags/feature_tags/new.html.erb
|
156
|
+
- app/views/pin_flags/feature_tags/show.html.erb
|
157
|
+
- app/views/pin_flags/feature_tags/update.html.erb
|
158
|
+
- app/views/pin_flags/feature_tags/update.turbo_stream.erb
|
159
|
+
- app/views/pin_flags/shared/_error_messages.html.erb
|
160
|
+
- config/routes.rb
|
161
|
+
- db/migrate/20250626042529_create_feature_tag.rb
|
162
|
+
- db/migrate/20250626050146_create_feature_subscriptions.rb
|
163
|
+
- lib/generators/pin_flags/install/install_generator.rb
|
164
|
+
- lib/generators/pin_flags/install/templates/create_feature_subscriptions.rb
|
165
|
+
- lib/generators/pin_flags/install/templates/create_feature_tag.rb
|
166
|
+
- lib/pin_flags.rb
|
167
|
+
- lib/pin_flags/engine.rb
|
168
|
+
- lib/pin_flags/version.rb
|
169
|
+
- lib/tasks/pin_flags_tasks.rake
|
170
|
+
homepage: https://github.com/taywils/pin_flags
|
171
|
+
licenses:
|
172
|
+
- MIT
|
173
|
+
metadata:
|
174
|
+
source_code_uri: https://github.com/taywils/pin_flags
|
175
|
+
rdoc_options: []
|
176
|
+
require_paths:
|
177
|
+
- lib
|
178
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
179
|
+
requirements:
|
180
|
+
- - ">="
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: '3.0'
|
183
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
requirements: []
|
189
|
+
rubygems_version: 3.6.9
|
190
|
+
specification_version: 4
|
191
|
+
summary: "⛳PinFlags is a granular, polymorphic feature access system"
|
192
|
+
test_files: []
|