diff --git a/.circleci/config.yml b/.circleci/config.yml index 6a1d4db48..e7a36f5b0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,7 @@ orbs: browser-tools: circleci/browser-tools@1.4.1 aliases: - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.2.1-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application @@ -29,7 +29,7 @@ jobs: steps: - checkout - browser-tools/install-browser-tools: - firefox-version: 108.0.1 + firefox-version: "112.0" - ruby/install-deps: clean-bundle: true @@ -62,7 +62,7 @@ jobs: - checkout: path: ~/project - browser-tools/install-browser-tools: - firefox-version: 108.0.1 + firefox-version: "112.0" # TODO: This is a workaround to get `git clone` working, but it shouldn't be here. # https://github.com/CircleCI-Public/browser-tools-orb/issues/62 @@ -114,7 +114,7 @@ jobs: - checkout: path: ~/project - browser-tools/install-browser-tools: - firefox-version: 108.0.1 + firefox-version: "112.0" # TODO: This is a workaround to get `git clone` working, but it shouldn't be here. # https://github.com/CircleCI-Public/browser-tools-orb/issues/62 diff --git a/.standard.yml b/.standard.yml index 1f3be5330..0dae34fd5 100644 --- a/.standard.yml +++ b/.standard.yml @@ -22,13 +22,5 @@ ignore: - Lint/RescueException # TODO would it be okay to rescue `StandardError`? - '*/lib/scaffolding/transformer.rb': - Layout/EndAlignment - # TODO Fix these files up for Standard Ruby. - - '*/lib/bullet_train/super_scaffolding/scaffolders/oauth_provider_scaffolder.rb' - - '*/lib/tasks/bullet_train/themes/light_tasks.rake': - - Style/CommandLiteral - '*/config/routes.rb': - Lint/UselessAssignment - - '*/app/helpers/theme_helper.rb': - - Style/GlobalVars - - '*/app/models/concerns/webhooks/outgoing/uri_filtering.rb': - - Lint/ShadowedException diff --git a/Gemfile b/Gemfile index d5d0c3bc7..4a0dacc4f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby "3.2.1" +ruby "3.2.2" gem "standard" diff --git a/Gemfile.lock b/Gemfile.lock index a65e56af1..07c29b3b0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -35,13 +35,15 @@ GEM PLATFORMS arm64-darwin-20 arm64-darwin-21 + arm64-darwin-22 + ruby x86_64-linux DEPENDENCIES standard RUBY VERSION - ruby 3.2.1p31 + ruby 3.2.2p53 BUNDLED WITH 2.3.8 diff --git a/bin/checkout-and-link-starter-repo b/bin/checkout-and-link-starter-repo index 7900654a9..3438c528e 100755 --- a/bin/checkout-and-link-starter-repo +++ b/bin/checkout-and-link-starter-repo @@ -5,10 +5,15 @@ STARTER_REPO_BRANCH="main" # Look for a matching branch on the starter repository when running tests on CircleCI. CI_BRANCH=$CIRCLE_BRANCH -if [[ -v CI_BRANCH ]]; then - BRANCH_RESPONSE=$(curl --head -H "Accept: application.vnd.github+json" https://api.github.com/repos/bullet-train-co/bullet_train/branches/$CI_BRANCH) +if [[ -v CI_BRANCH ]] +then + BRANCH_RESPONSE=$(curl --verbose -H "Accept: application.vnd.github+json" https://api.github.com/repos/bullet-train-co/bullet_train/branches/$CI_BRANCH) - if echo $BRANCH_RESPONSE | grep "200"; then + echo "Branch response ====================" + echo $BRANCH_RESPONSE + + # If the branch is missing in the repo the response will not contain the branch name + if echo $BRANCH_RESPONSE | grep "$CIRCLE_BRANCH"; then STARTER_REPO_BRANCH=$CI_BRANCH fi fi @@ -16,6 +21,7 @@ fi echo "Cloning from ${STARTER_REPO_BRANCH}..." git clone -b $STARTER_REPO_BRANCH --depth 1 https://github.com/bullet-train-co/bullet_train.git . +# TODO: Maybe generate this list automatically based on the subdirectories in core that contain a .gemspec? packages=( "bullet_train" "bullet_train-api" diff --git a/bullet_train-api/.circleci/config.yml b/bullet_train-api/.circleci/config.yml index 6414b4b77..05f6b4988 100644 --- a/bullet_train-api/.circleci/config.yml +++ b/bullet_train-api/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-api/app/controllers/account/platform/access_tokens_controller.rb b/bullet_train-api/app/controllers/account/platform/access_tokens_controller.rb index 597fe8471..9fa5d5b46 100644 --- a/bullet_train-api/app/controllers/account/platform/access_tokens_controller.rb +++ b/bullet_train-api/app/controllers/account/platform/access_tokens_controller.rb @@ -70,7 +70,6 @@ def destroy include strong_parameters_from_api def process_params(strong_params) - assign_date_and_time(strong_params, :last_used_at) # πŸš… super scaffolding will insert processing for new fields above this line. end end diff --git a/bullet_train-api/app/controllers/api/open_api_controller.rb b/bullet_train-api/app/controllers/api/open_api_controller.rb index 29455208a..78b239d0b 100644 --- a/bullet_train-api/app/controllers/api/open_api_controller.rb +++ b/bullet_train-api/app/controllers/api/open_api_controller.rb @@ -1,128 +1,5 @@ -module OpenApiHelper - def indent(string, count) - lines = string.lines - first_line = lines.shift - lines = lines.map { |line| (" " * count).to_s + line } - lines.unshift(first_line).join.html_safe - end - - # TODO: Remove this method? It's not being used anywhere - def components_for(model) - for_model model do - indent(render("api/#{@version}/open_api/#{model.name.underscore.pluralize}/components"), 2) - end - end - - def current_model - @model_stack.last - end - - def for_model(model) - @model_stack ||= [] - @model_stack << model - result = yield - @model_stack.pop - result - end - - def gem_paths - @gem_paths ||= `bundle show --paths`.lines.map { |gem_path| gem_path.chomp } - end - - def automatic_paths_for(model, parent, except: []) - output = render("api/#{@version}/open_api/shared/paths", except: except) - output = Scaffolding::Transformer.new(model.name, [parent&.name]).transform_string(output).html_safe - indent(output, 1) - end - - def automatic_components_for(model, locals: {}) - path = "app/views/api/#{@version}" - paths = ([path] + gem_paths.map { |gem_path| "#{gem_path}/#{path}" }) - jbuilder = Jbuilder::Schema.renderer(paths, locals: { - # If we ever get to the point where we need a real model here, we should implement an example team in seeds that we can source it from. - model.name.underscore.split("/").last.to_sym => model.new, - # Same here, if we ever need this to be a real object, this should be `test@example.com` with an `SecureRandom.hex` password. - :current_user => User.new - }.merge(locals)) - - schema_json = jbuilder.json( - model.new, - title: I18n.t("#{model.name.underscore.pluralize}.label"), - # TODO Improve this. We don't have a generic description for models we can use here. - description: I18n.t("#{model.name.underscore.pluralize}.label"), - ) - - attributes_output = JSON.parse(schema_json) - - # Rails attachments aren't technically attributes in a model, - # so we add the attributes manually to make them available in the API. - if model.attachment_reflections.any? - model.attachment_reflections.each do |reflection| - attribute_name = reflection.first - - attributes_output["properties"][attribute_name] = { - "type" => "object", - "description" => attribute_name.titleize.to_s - } - - attributes_output["example"].merge!({attribute_name.to_s => nil}) - end - end - - if has_strong_parameters?("Api::#{@version.upcase}::#{model.name.pluralize}Controller".constantize) - strong_params_module = "Api::#{@version.upcase}::#{model.name.pluralize}Controller::StrongParameters".constantize - strong_parameter_keys = BulletTrain::Api::StrongParametersReporter.new(model, strong_params_module).report - if strong_parameter_keys.last.is_a?(Hash) - strong_parameter_keys += strong_parameter_keys.pop.keys - end - - parameters_output = JSON.parse(schema_json) - parameters_output["required"].select! { |key| strong_parameter_keys.include?(key.to_sym) } - parameters_output["properties"].select! { |key, value| strong_parameter_keys.include?(key.to_sym) } - - ( - indent(attributes_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Attributes:"), 3) + - indent(" " + parameters_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Parameters:"), 3) - ).html_safe - else - - indent(attributes_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Attributes:"), 3) - .html_safe - end - end - - def paths_for(model) - for_model model do - indent(render("api/#{@version}/open_api/#{model.name.underscore.pluralize}/paths"), 1) - end - end - - def attribute(attribute) - heading = t("#{current_model.name.underscore.pluralize}.fields.#{attribute}.heading") - attribute_data = current_model.columns_hash[attribute.to_s] - - # Default to `string` when the type returns nil. - type = attribute_data.nil? ? "string" : attribute_data.type - - attribute_block = <<~YAML - #{attribute}: - description: "#{heading}" - type: #{type} - YAML - indent(attribute_block.chomp, 2) - end - alias_method :parameter, :attribute - - private - - def has_strong_parameters?(controller) - methods = controller.action_methods - methods.include?("create") || methods.include?("update") - end -end - class Api::OpenApiController < ApplicationController - helper :open_api + helper "api/open_api" def set_default_response_format request.format = :yaml diff --git a/bullet_train-api/app/controllers/concerns/api/controllers/base.rb b/bullet_train-api/app/controllers/concerns/api/controllers/base.rb index 33d00618b..e18f498bd 100644 --- a/bullet_train-api/app/controllers/concerns/api/controllers/base.rb +++ b/bullet_train-api/app/controllers/concerns/api/controllers/base.rb @@ -72,7 +72,6 @@ def current_user end # TODO Remove this rescue once workspace clusters can write to this column on the identity server. - # TODO Make this logic configurable so that downstream developers can write different methods for this column getting updated. if doorkeeper_token begin doorkeeper_token.update(last_used_at: Time.zone.now) diff --git a/bullet_train-api/app/controllers/concerns/api/v1/users/controller_base.rb b/bullet_train-api/app/controllers/concerns/api/v1/users/controller_base.rb index 695126e32..014a12ce5 100644 --- a/bullet_train-api/app/controllers/concerns/api/v1/users/controller_base.rb +++ b/bullet_train-api/app/controllers/concerns/api/v1/users/controller_base.rb @@ -14,7 +14,8 @@ def user_params :first_name, :last_name, :time_zone, - :locale + :locale, + :profile_photo_id ] selected_fields = if params.is_a?(BulletTrain::Api::StrongParametersReporter) diff --git a/bullet_train-api/app/helpers/api/open_api_helper.rb b/bullet_train-api/app/helpers/api/open_api_helper.rb new file mode 100644 index 000000000..80f50a1ef --- /dev/null +++ b/bullet_train-api/app/helpers/api/open_api_helper.rb @@ -0,0 +1,151 @@ +module Api + module OpenApiHelper + def indent(string, count) + lines = string.lines + first_line = lines.shift + lines = lines.map { |line| (" " * count).to_s + line } + lines.unshift(first_line).join.html_safe + end + + # TODO: Remove this method? It's not being used anywhere + def components_for(model) + for_model model do + indent(render("api/#{@version}/open_api/#{model.name.underscore.pluralize}/components"), 2) + end + end + + def current_model + @model_stack.last + end + + def for_model(model) + @model_stack ||= [] + @model_stack << model + result = yield + @model_stack.pop + result + end + + def gem_paths + @gem_paths ||= `bundle show --paths`.lines.map { |gem_path| gem_path.chomp } + end + + def automatic_paths_for(model, parent, except: []) + output = render("api/#{@version}/open_api/shared/paths", except: except) + output = Scaffolding::Transformer.new(model.name, [parent&.name]).transform_string(output).html_safe + + custom_actions_file_path = "api/#{@version}/open_api/#{model.name.underscore.pluralize}/paths" + output += render(custom_actions_file_path) if lookup_context.exists?(custom_actions_file_path, [], true) + + # There are some placeholders specific to this method that we still need to transform. + model_symbol = model.name.underscore.tr("/", "_").to_sym + + if (get_example = FactoryBot.get_example(model_symbol, version: @version)) + output.gsub!("πŸš… get_example", get_example) + end + + if (post_parameters = FactoryBot.post_parameters(model_symbol, version: @version)) + output.gsub!("πŸš… post_parameters", post_parameters) + end + + if (post_examples = FactoryBot.post_examples(model_symbol, version: @version)) + output.gsub!("πŸš… post_examples", post_examples) + end + + if (put_parameters = FactoryBot.put_parameters(model_symbol, version: @version)) + output.gsub!("πŸš… put_parameters", put_parameters) + end + + if (put_example = FactoryBot.put_example(model_symbol, version: @version)) + output.gsub!("πŸš… put_example", put_example) + end + + indent(output, 1) + end + + def automatic_components_for(model, locals: {}) + path = "app/views/api/#{@version}" + paths = ([path] + gem_paths.map { |gem_path| "#{gem_path}/#{path}" }) + jbuilder = Jbuilder::Schema.renderer(paths, locals: { + # If we ever get to the point where we need a real model here, we should implement an example team in seeds that we can source it from. + model.name.underscore.split("/").last.to_sym => model.new, + # Same here, if we ever need this to be a real object, this should be `test@example.com` with an `SecureRandom.hex` password. + :current_user => User.new + }.merge(locals)) + + schema_json = jbuilder.json( + model.new, + title: I18n.t("#{model.name.underscore.pluralize}.label"), + # TODO Improve this. We don't have a generic description for models we can use here. + description: I18n.t("#{model.name.underscore.pluralize}.label"), + ) + + attributes_output = JSON.parse(schema_json) + + # Rails attachments aren't technically attributes in a model, + # so we add the attributes manually to make them available in the API. + if model.attachment_reflections.any? + model.attachment_reflections.each do |reflection| + attribute_name = reflection.first + + attributes_output["properties"][attribute_name] = { + "type" => "object", + "description" => attribute_name.titleize.to_s + } + + attributes_output["example"].merge!({attribute_name.to_s => nil}) + end + end + + if has_strong_parameters?("Api::#{@version.upcase}::#{model.name.pluralize}Controller".constantize) + strong_params_module = "Api::#{@version.upcase}::#{model.name.pluralize}Controller::StrongParameters".constantize + strong_parameter_keys = BulletTrain::Api::StrongParametersReporter.new(model, strong_params_module).report + if strong_parameter_keys.last.is_a?(Hash) + strong_parameter_keys += strong_parameter_keys.pop.keys + end + + parameters_output = JSON.parse(schema_json) + parameters_output["required"].select! { |key| strong_parameter_keys.include?(key.to_sym) } + parameters_output["properties"].select! { |key, value| strong_parameter_keys.include?(key.to_sym) } + + ( + indent(attributes_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Attributes:"), 3) + + indent(" " + parameters_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Parameters:"), 3) + ).html_safe + else + + indent(attributes_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Attributes:"), 3) + .html_safe + end + end + + def paths_for(model) + for_model model do + indent(render("api/#{@version}/open_api/#{model.name.underscore.pluralize}/paths"), 1) + end + end + + def attribute(attribute) + heading = t("#{current_model.name.underscore.pluralize}.fields.#{attribute}.heading") + attribute_data = current_model.columns_hash[attribute.to_s] + + # Default to `string` when the type returns nil. + type = attribute_data.nil? ? "string" : attribute_data.type + + attribute_block = <<~YAML + #{attribute}: + description: "#{heading}" + type: #{type} + YAML + indent(attribute_block.chomp, 2) + end + alias_method :parameter, :attribute + + private + + def has_strong_parameters?(controller) + methods = controller.action_methods + methods.include?("create") || methods.include?("update") + end + end +end diff --git a/bullet_train-api/app/models/platform/access_token.rb b/bullet_train-api/app/models/platform/access_token.rb index 6bd8807dc..e80198fa5 100644 --- a/bullet_train-api/app/models/platform/access_token.rb +++ b/bullet_train-api/app/models/platform/access_token.rb @@ -1,4 +1,4 @@ -class Platform::AccessToken < ApplicationRecord +class Platform::AccessToken < BulletTrain::Api.base_class.constantize self.table_name = "oauth_access_tokens" include Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken diff --git a/bullet_train-api/app/models/platform/application.rb b/bullet_train-api/app/models/platform/application.rb index cc6a8f6dd..d0eedefa1 100644 --- a/bullet_train-api/app/models/platform/application.rb +++ b/bullet_train-api/app/models/platform/application.rb @@ -1,4 +1,4 @@ -class Platform::Application < ApplicationRecord +class Platform::Application < BulletTrain::Api.base_class.constantize self.table_name = "oauth_applications" include Doorkeeper::Orm::ActiveRecord::Mixins::Application diff --git a/bullet_train-api/app/views/account/platform/access_tokens/_index.html.erb b/bullet_train-api/app/views/account/platform/access_tokens/_index.html.erb index 63e611825..897bdd98d 100644 --- a/bullet_train-api/app/views/account/platform/access_tokens/_index.html.erb +++ b/bullet_train-api/app/views/account/platform/access_tokens/_index.html.erb @@ -8,7 +8,7 @@ <% pagy, access_tokens = pagy(access_tokens, page_param: :access_tokens_page) %> <%= action_model_select_controller do %> - <%= updates_for context, collection do %> + <%= cable_ready_updates_for context, collection do %> <%= render 'account/shared/box', pagy: pagy do |box| %> <% box.title t(".contexts.#{context.class.name.underscore}.header") %> <% box.description t(".contexts.#{context.class.name.underscore}.description") %> diff --git a/bullet_train-api/app/views/account/platform/access_tokens/new.html.erb b/bullet_train-api/app/views/account/platform/access_tokens/new.html.erb index 909322550..8c2af2407 100644 --- a/bullet_train-api/app/views/account/platform/access_tokens/new.html.erb +++ b/bullet_train-api/app/views/account/platform/access_tokens/new.html.erb @@ -1,7 +1,7 @@ <%= render 'account/shared/page' do |page| %> <% page.title t('.section') %> <% page.body.render 'account/shared/box', divider: true do |box| %> - <% box.t :description, title: t('.header') %> + <% box.t :description, title: '.header' %> <% box.body.render 'form', access_token: @access_token %> <% end %> <% end %> diff --git a/bullet_train-api/app/views/account/platform/access_tokens/show.html.erb b/bullet_train-api/app/views/account/platform/access_tokens/show.html.erb index 7b3ed546b..ee4d713ce 100644 --- a/bullet_train-api/app/views/account/platform/access_tokens/show.html.erb +++ b/bullet_train-api/app/views/account/platform/access_tokens/show.html.erb @@ -1,7 +1,7 @@ <%= render 'account/shared/page' do |page| %> <% page.title t('.section') %> <% page.body do %> - <%= updates_for @access_token do %> + <%= cable_ready_updates_for @access_token do %> <%= render 'account/shared/box', divider: true do |box| %> <% box.title t('.header') %> <% box.description do %> diff --git a/bullet_train-api/app/views/api/v1/open_api/shared/_paths.yaml.erb b/bullet_train-api/app/views/api/v1/open_api/shared/_paths.yaml.erb index 9fe0d0216..d08ee6a4e 100644 --- a/bullet_train-api/app/views/api/v1/open_api/shared/_paths.yaml.erb +++ b/bullet_train-api/app/views/api/v1/open_api/shared/_paths.yaml.erb @@ -4,7 +4,7 @@ <% unless except.include?(:index) %> get: tags: - - "Scaffolding/Completely Concrete/Tangible Things" + - Scaffolding::CompletelyConcrete::TangibleThing summary: "List Tangible Things" operationId: listScaffoldingCompletelyConcreteTangibleThings parameters: @@ -30,11 +30,13 @@ type: array items: $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingAttributes" + example: + πŸš… get_example <% end %> <% unless except.include?(:create) %> post: tags: - - "Scaffolding/Completely Concrete/Tangible Things" + - Scaffolding::CompletelyConcrete::TangibleThing summary: "Create Tangible Thing" operationId: createScaffoldingCompletelyConcreteTangibleThings parameters: @@ -54,6 +56,8 @@ scaffolding_completely_concrete_tangible_thing: type: object $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingParameters" + example: + πŸš… post_parameters responses: "404": description: "Not Found" @@ -63,6 +67,8 @@ application/json: schema: $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingAttributes" + example: + πŸš… post_examples <% end %> <% end %> <% unless except.include?(:show) && except.include?(:update) && except.include?(:destroy) %> @@ -70,7 +76,7 @@ <% unless except.include?(:show) %> get: tags: - - "Scaffolding/Completely Concrete/Tangible Things" + - Scaffolding::CompletelyConcrete::TangibleThing summary: "Fetch Tangible Thing" operationId: getScaffoldingCompletelyConcreteTangibleThings parameters: @@ -84,11 +90,13 @@ application/json: schema: $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingAttributes" + example: + πŸš… get_example <% end %> <% unless except.include?(:update) %> put: tags: - - "Scaffolding/Completely Concrete/Tangible Things" + - Scaffolding::CompletelyConcrete::TangibleThing summary: "Update Tangible Thing" operationId: updateScaffoldingCompletelyConcreteTangibleThings parameters: @@ -104,6 +112,8 @@ scaffolding_completely_concrete_tangible_thing: type: object $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingParameters" + example: + πŸš… put_parameters responses: "404": description: "Not Found" @@ -113,11 +123,13 @@ application/json: schema: $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingAttributes" + example: + πŸš… put_example <% end %> <% unless except.include?(:destroy) %> delete: tags: - - "Scaffolding/Completely Concrete/Tangible Things" + - Scaffolding::CompletelyConcrete::TangibleThing summary: "Remove Tangible Thing" operationId: removeScaffoldingCompletelyConcreteTangibleThings parameters: diff --git a/bullet_train-api/app/views/api/v1/open_api/teams/_paths.yaml.erb b/bullet_train-api/app/views/api/v1/open_api/teams/_paths.yaml.erb index 638f0d9d8..a2f63c204 100644 --- a/bullet_train-api/app/views/api/v1/open_api/teams/_paths.yaml.erb +++ b/bullet_train-api/app/views/api/v1/open_api/teams/_paths.yaml.erb @@ -1,7 +1,7 @@ /teams: get: tags: - - Teams + - Team summary: "List Teams" operationId: listTeams parameters: @@ -22,10 +22,13 @@ type: array items: $ref: "#/components/schemas/TeamAttributes" + example: + <%= FactoryBot.get_examples(:team, version: @version) %> + /teams/{id}: get: tags: - - Teams + - Team summary: "Fetch Team" operationId: fetchTeam parameters: @@ -39,9 +42,11 @@ application/json: schema: $ref: "#/components/schemas/TeamAttributes" + example: + <%= FactoryBot.get_example(:team, version: @version) %> put: tags: - - Teams + - Team summary: "Update Team" operationId: updateTeam parameters: @@ -57,6 +62,8 @@ team: type: object $ref: "#/components/schemas/TeamParameters" + example: + <%= FactoryBot.put_parameters(:team, version: @version) %> responses: "404": description: "Not Found" @@ -66,3 +73,5 @@ application/json: schema: $ref: "#/components/schemas/TeamAttributes" + example: + <%= FactoryBot.put_example(:team, version: @version) %> diff --git a/bullet_train-api/app/views/api/v1/open_api/users/_paths.yaml.erb b/bullet_train-api/app/views/api/v1/open_api/users/_paths.yaml.erb index 409946080..82c7e31e0 100644 --- a/bullet_train-api/app/views/api/v1/open_api/users/_paths.yaml.erb +++ b/bullet_train-api/app/views/api/v1/open_api/users/_paths.yaml.erb @@ -1,7 +1,7 @@ /users: get: tags: - - Users + - User summary: "List Users" operationId: listUsers parameters: @@ -22,10 +22,12 @@ type: array items: $ref: "#/components/schemas/UserAttributes" + example: + <%= FactoryBot.get_examples(:user, version: @version) %> /users/{id}: get: tags: - - Users + - User summary: "Fetch User" operationId: fetchUser parameters: @@ -39,9 +41,11 @@ application/json: schema: $ref: "#/components/schemas/UserAttributes" + example: + <%= FactoryBot.get_example(:user, version: @version) %> put: tags: - - Users + - User summary: "Update User" operationId: updateUser parameters: @@ -57,6 +61,8 @@ user: type: object $ref: "#/components/schemas/UserParameters" + example: + <%= FactoryBot.put_parameters(:user, version: @version) %> responses: "404": description: "Not Found" @@ -66,3 +72,5 @@ application/json: schema: $ref: "#/components/schemas/UserAttributes" + example: + <%= FactoryBot.put_example(:user, version: @version) %> diff --git a/bullet_train-api/bullet_train-api.gemspec b/bullet_train-api/bullet_train-api.gemspec index 4dc40daae..8280dfa62 100644 --- a/bullet_train-api/bullet_train-api.gemspec +++ b/bullet_train-api/bullet_train-api.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::Api::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-api" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-api" spec.summary = "Bullet Train API" spec.description = spec.summary spec.license = "MIT" @@ -30,6 +30,7 @@ Gem::Specification.new do |spec| spec.add_dependency "rack-cors" spec.add_dependency "doorkeeper" spec.add_dependency "jbuilder-schema", ">= 2.0.0" + spec.add_dependency "factory_bot" spec.add_dependency "bullet_train" end diff --git a/bullet_train-api/config/routes.rb b/bullet_train-api/config/routes.rb index f35620dce..09ac685f2 100644 --- a/bullet_train-api/config/routes.rb +++ b/bullet_train-api/config/routes.rb @@ -17,9 +17,7 @@ end end - if ENV["TESTING_PROVISION_KEY"].present? - get "/testing/provision", to: "account/platform/applications#provision" - end + get "/testing/provision", to: "account/platform/applications#provision" namespace :api do match "*version/openapi.yaml" => "open_api#index", :via => :get diff --git a/bullet_train-api/lib/bullet_train/api.rb b/bullet_train-api/lib/bullet_train/api.rb index 77f0b8ab9..1f64ccb97 100644 --- a/bullet_train-api/lib/bullet_train/api.rb +++ b/bullet_train-api/lib/bullet_train/api.rb @@ -1,6 +1,7 @@ require "bullet_train/api/version" require "bullet_train/api/engine" require "bullet_train/api/strong_parameters_reporter" +require "bullet_train/api/example_bot" require "bullet_train/platform/connection_workflow" # require "wine_bouncer" @@ -15,6 +16,7 @@ module BulletTrain module Api + mattr_accessor :base_class, default: "ApplicationRecord" mattr_accessor :endpoints, default: [] mattr_accessor :current_version, default: "v1" mattr_accessor :initial_version, default: "v1" diff --git a/bullet_train-api/lib/bullet_train/api/example_bot.rb b/bullet_train-api/lib/bullet_train/api/example_bot.rb new file mode 100644 index 000000000..229e21b81 --- /dev/null +++ b/bullet_train-api/lib/bullet_train/api/example_bot.rb @@ -0,0 +1,146 @@ +require_relative "../../../app/helpers/api/open_api_helper" + +module FactoryBot + module ExampleBot + attr_accessor :tables_to_reset + + def example(model, **options) + @tables_to_reset = [model.to_s.pluralize] + + object = nil + + ActiveRecord::Base.transaction do + instance = FactoryBot.create(factory(model), **options) + object = deep_clone(instance) + + raise ActiveRecord::Rollback + end + + reset_tables! + object + end + + def example_list(model, quantity, **options) + @tables_to_reset = [model.to_s.pluralize] + + objects = [] + + ActiveRecord::Base.transaction do + instances = FactoryBot.create_list(factory(model), quantity, **options) + + instances.each do |instance| + objects << deep_clone(instance) + end + + raise ActiveRecord::Rollback + end + + reset_tables! + objects + end + + %i[get_examples get_example post_examples post_parameters put_example put_parameters patch_example patch_parameters].each do |method| + define_method(method) do |model, **options| + _path_examples(method.to_s, model, **options) + end + end + + private + + def factory(model) + factories = FactoryBot.factories.instance_variable_get(:@items).keys + factories.include?("#{model}_example") ? "#{model}_example".to_sym : model + end + + def reset_tables! + @tables_to_reset.each do |name| + ActiveRecord::Base.connection.reset_pk_sequence!(name) if ActiveRecord::Base.connection.table_exists?(name) + end + end + + def deep_clone(instance) + clone = instance.clone + + instance.class.reflections.each do |name, reflection| + if reflection.macro == :has_many + associations = instance.send(name).map { |association| association.clone } + clone.send("#{name}=", associations) + @tables_to_reset << name + elsif %i[belongs_to has_one].include?(reflection.macro) + clone.send("#{name}=", instance.send(name).clone) + @tables_to_reset << name.pluralize + end + end + + clone + end + + include ::Api::OpenApiHelper + def _path_examples(method, model, **options) + version = options.delete(:version) || "v1" + + case method.split("_").first + when "get" + count = (options.delete(:count) || method == "get_examples") ? 2 : 1 + template, class_name, var_name, values = _set_values(method, model, count) + else + template, class_name, var_name, values = _set_values("get_example", model) + + unless %w[example examples].include?(method.split("_").last) + if has_strong_parameters?("::Api::#{version.upcase}::#{class_name.pluralize}Controller".constantize) + strong_params_module = "::Api::#{version.upcase}::#{class_name.pluralize}Controller::StrongParameters".constantize + strong_parameter_keys = BulletTrain::Api::StrongParametersReporter.new(class_name.constantize, strong_params_module).report + if strong_parameter_keys.last.is_a?(Hash) + strong_parameter_keys += strong_parameter_keys.pop.keys + end + + output = _json_output(template, version, class_name, var_name, values) + + parameters_output = JSON.parse(output) + parameters_output&.select! { |key| strong_parameter_keys.include?(key.to_sym) } + + return indent(parameters_output.to_yaml.delete_prefix("---\n"), 6).html_safe + end + return nil + end + end + + _yaml_output(template, version, class_name, var_name, values) + end + + def _set_values(method, model, count = 1) + if count > 1 + values = FactoryBot.example_list(model, count) + class_name = values.first.class.name + var_name = class_name.demodulize.underscore.pluralize + else + values = FactoryBot.example(model) + class_name = values.class.name + var_name = class_name.demodulize.underscore + end + + template = (method == "get_examples") ? "index" : "show" + + [template, class_name, var_name, values] + end + + def _json_output(template, version, class_name, var_name, values) + ActionController::Base.render( + template: "api/#{version}/#{class_name.underscore.pluralize}/#{template}", + assigns: {"#{var_name}": values}, + formats: :json + ) + end + + def _yaml_output(template, version, class_name, var_name, values) + indent( + JSON.parse( + _json_output(template, version, class_name, var_name, values) + ).to_yaml + .delete_prefix("---\n"), 7 + ).html_safe + end + end + + extend ExampleBot +end diff --git a/bullet_train-api/lib/bullet_train/api/version.rb b/bullet_train-api/lib/bullet_train/api/version.rb index 2a8ac751a..204156b52 100644 --- a/bullet_train-api/lib/bullet_train/api/version.rb +++ b/bullet_train-api/lib/bullet_train/api/version.rb @@ -1,5 +1,5 @@ module BulletTrain module Api - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-api/lib/tasks/bullet_train/api_tasks.rake b/bullet_train-api/lib/tasks/bullet_train/api_tasks.rake index f88f2a971..2e7becd9c 100644 --- a/bullet_train-api/lib/tasks/bullet_train/api_tasks.rake +++ b/bullet_train-api/lib/tasks/bullet_train/api_tasks.rake @@ -1,6 +1,9 @@ require "scaffolding" require "scaffolding/file_manipulator" +require "faraday" +require "tempfile" + namespace :bullet_train do namespace :api do desc "Bump the current version of application's API" @@ -88,5 +91,42 @@ namespace :bullet_train do puts "Finished bumping to #{new_version}" end + + desc "Bump the current version of application's API" + task push_to_redocly: :environment do + include Rails.application.routes.url_helpers + + raise "You need to set REDOCLY_ORGANIZATION_ID in your environment. You can fetch it from the URL when you're on your Redocly dashboard." unless ENV["REDOCLY_ORGANIZATION_ID"].present? + raise "You need to set REDOCLY_API_KEY in your environment. You can create one at https://app.redocly.com/org/#{ENV["REDOCLY_ORGANIZATION_ID"]}/settings/api-keys ." unless ENV["REDOCLY_API_KEY"].present? + + # Create a new Faraday connection + conn = Faraday.new(api_url(version: BulletTrain::Api.current_version)) + + # Fetch the file + response = conn.get + + # Check if the request was successful + if response.status == 200 + # Create a temp file + temp_file = Tempfile.new(["openapi-", ".yaml"]) + + # Write the file content to the temp file + temp_file.binmode + temp_file.write(response.body) + temp_file.rewind + + # Close and delete the temp file when the script exits + temp_file.close + puts "File downloaded and saved to: #{temp_file.path}" + + puts `echo "#{ENV["REDOCLY_API_KEY"]}" | redocly login` + + puts `redocly push #{temp_file.path} "@#{ENV["REDOCLY_ORGANIZATION_ID"]}/#{I18n.t("application.name")}@#{BulletTrain::Api.current_version}" --public --upsert` + + temp_file.unlink + else + puts "Failed to download the OpenAPI Document. Status code: #{response.status}" + end + end end end diff --git a/bullet_train-fields/.circleci/config.yml b/bullet_train-fields/.circleci/config.yml index c4ec6c5b8..4fdb8b598 100644 --- a/bullet_train-fields/.circleci/config.yml +++ b/bullet_train-fields/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-fields/Gemfile.lock b/bullet_train-fields/Gemfile.lock index 34ebf746d..497c58644 100644 --- a/bullet_train-fields/Gemfile.lock +++ b/bullet_train-fields/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - bullet_train-fields (1.2.21) + bullet_train-fields (1.2.27) chronic cloudinary phonelib @@ -79,7 +79,7 @@ GEM aws_cf_signer (0.1.3) builder (3.2.4) chronic (0.10.2) - cloudinary (1.25.0) + cloudinary (1.26.0) aws_cf_signer rest-client (>= 2.0.0) concurrent-ruby (1.1.10) @@ -112,7 +112,7 @@ GEM mini_mime (1.1.2) mini_portile2 (2.8.1) minitest (5.16.2) - net-imap (0.3.4) + net-imap (0.3.7) date net-protocol net-pop (0.1.2) @@ -122,14 +122,14 @@ GEM net-smtp (0.3.3) net-protocol netrc (0.11.0) - nio4r (2.5.8) + nio4r (2.5.9) nokogiri (1.13.7) mini_portile2 (~> 2.8.0) racc (~> 1.4) parallel (1.22.1) parser (3.1.2.0) ast (~> 2.4.1) - phonelib (0.7.7) + phonelib (0.8.2) racc (1.6.0) rack (2.2.4) rack-test (2.0.2) @@ -196,23 +196,24 @@ GEM standard (1.14.0) rubocop (= 1.32.0) rubocop-performance (= 1.14.3) - thor (1.2.1) - timeout (0.3.2) + thor (1.2.2) + timeout (0.4.0) tzinfo (2.0.4) concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext unf_ext (0.0.8.2) unicode-display_width (2.2.0) - websocket-driver (0.7.5) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.6.7) + zeitwerk (2.6.9) PLATFORMS arm64-darwin-20 arm64-darwin-21 arm64-darwin-22 + ruby x86_64-darwin-21 x86_64-linux diff --git a/bullet_train-fields/app/controllers/concerns/fields/date_and_time_support.rb b/bullet_train-fields/app/controllers/concerns/fields/date_and_time_support.rb index 81a6be701..3bd25e017 100644 --- a/bullet_train-fields/app/controllers/concerns/fields/date_and_time_support.rb +++ b/bullet_train-fields/app/controllers/concerns/fields/date_and_time_support.rb @@ -2,12 +2,22 @@ module Fields::DateAndTimeSupport extend ActiveSupport::Concern def assign_date_and_time(strong_params, attribute) + deprecator = ActiveSupport::Deprecation.new("2.0", "BulletTrain::Fields") + deprecator.deprecation_warning( + "assign_date_and_time", + "Please assign an ISO8601 datetime string as form field value instead and remove all assign_date_and_time assignments, + see https://ruby-doc.org/3.2.2/exts/date/DateTime.html" + ) attribute = attribute.to_s time_zone_attribute = "#{attribute}_time_zone" if strong_params.dig(attribute).present? - time_zone = ActiveSupport::TimeZone.new(strong_params[time_zone_attribute] || current_team.time_zone) - strong_params.delete(time_zone_attribute) - strong_params[attribute] = time_zone.strptime(strong_params[attribute], t("global.formats.date_and_time")) + begin + strong_params[attribute] = DateTime.iso8601(strong_params[attribute]) + rescue ArgumentError + time_zone = ActiveSupport::TimeZone.new(strong_params[time_zone_attribute] || current_team.time_zone) + strong_params.delete(time_zone_attribute) + strong_params[attribute] = time_zone.strptime(strong_params[attribute], t("global.formats.date_and_time")) + end end end end diff --git a/bullet_train-fields/app/controllers/concerns/fields/date_support.rb b/bullet_train-fields/app/controllers/concerns/fields/date_support.rb index 4536bf50a..0906dc5ca 100644 --- a/bullet_train-fields/app/controllers/concerns/fields/date_support.rb +++ b/bullet_train-fields/app/controllers/concerns/fields/date_support.rb @@ -2,11 +2,21 @@ module Fields::DateSupport extend ActiveSupport::Concern def assign_date(strong_params, attribute) + deprecator = ActiveSupport::Deprecation.new("2.0", "BulletTrain::Fields") + deprecator.deprecation_warning( + "assign_date", + "Please assign an ISO8601 date string as field value instead and remove all assign_date assignments, + see https://ruby-doc.org/3.2.2/exts/date/Date.html" + ) attribute = attribute.to_s if strong_params.dig(attribute).present? - parsed_value = Chronic.parse(strong_params[attribute]) - return nil unless parsed_value - strong_params[attribute] = parsed_value.to_date + begin + strong_params[attribute] = Date.iso8601(strong_params[attribute]) + rescue ArgumentError + parsed_value = Chronic.parse(strong_params[attribute]) + return nil unless parsed_value + strong_params[attribute] = parsed_value.to_date + end end end end diff --git a/bullet_train-fields/app/javascript/controllers/fields/date_controller.js b/bullet_train-fields/app/javascript/controllers/fields/date_controller.js index 6a27a9a72..e8def6860 100644 --- a/bullet_train-fields/app/javascript/controllers/fields/date_controller.js +++ b/bullet_train-fields/app/javascript/controllers/fields/date_controller.js @@ -3,14 +3,18 @@ require("daterangepicker/daterangepicker.css"); // requires jQuery, moment, might want to consider a vanilla JS alternative import 'daterangepicker'; +import moment from 'moment-timezone' export default class extends Controller { - static targets = [ "field", "clearButton", "currentTimeZoneWrapper", "timeZoneButtons", "timeZoneSelectWrapper", "timeZoneField" ] + static targets = [ "field", "displayField", "clearButton", "currentTimeZoneWrapper", "timeZoneButtons", "timeZoneSelectWrapper", "timeZoneField", "timeZoneSelect" ] static values = { includeTime: Boolean, defaultTimeZones: Array, - cancelButtonLabel: { type: String, default: "Cancel" }, - applyButtonLabel: { type: String, default: "Apply" } + dateFormat: String, + timeFormat: String, + currentTimeZone: String, + isAmPm: Boolean, + pickerLocale: { type: Object, default: {} } } connect() { @@ -26,13 +30,33 @@ export default class extends Controller { event.preventDefault() $(this.fieldTarget).val('') + $(this.displayFieldTarget).val('') + } + + currentTimeZone(){ + return ( + ( this.hasTimeZoneSelectWrapperTarget && $(this.timeZoneSelectWrapperTarget).is(":visible") && this.timeZoneSelectTarget.value ) || + ( this.hasTimeZoneFieldTarget && this.timeZoneFieldTarget.value ) || + this.currentTimeZoneValue + ) } applyDateToField(event, picker) { - const format = this.includeTimeValue ? 'MM/DD/YYYY h:mm A' : 'MM/DD/YYYY' - $(this.fieldTarget).val(picker.startDate.format(format)) + const format = this.includeTimeValue ? this.timeFormatValue : this.dateFormatValue + const newTimeZone = this.currentTimeZone() + const momentVal = ( + picker ? + moment(picker.startDate.toISOString()).tz(newTimeZone, true) : + moment.tz(moment(this.fieldTarget.value, "YYYY-MM-DDTHH:mm").format("YYYY-MM-DDTHH:mm"), newTimeZone) + ) + const displayVal = momentVal.format(format) + const dataVal = this.includeTimeValue ? momentVal.toISOString(true) : momentVal.format('YYYY-MM-DD') + $(this.displayFieldTarget).val(displayVal) + $(this.fieldTarget).val(dataVal) // bubble up a change event when the input is updated for other listeners - this.fieldTarget.dispatchEvent(new CustomEvent('change', { detail: { picker: picker }})) + if(picker){ + this.displayFieldTarget.dispatchEvent(new CustomEvent('change', { detail: { picker: picker }})) + } } showTimeZoneButtons(event) { @@ -43,15 +67,18 @@ export default class extends Controller { $(this.timeZoneButtonsTarget).toggleClass('hidden') } + // triggered on other click from the timezone buttons showTimeZoneSelectWrapper(event) { // don't follow the anchor event.preventDefault() $(this.timeZoneButtonsTarget).toggleClass('hidden') - if (this.hasTimeZoneSelectWrapperTarget) { $(this.timeZoneSelectWrapperTarget).toggleClass('hidden') } + if(!["", null].includes(this.fieldTarget.value)){ + $(this.displayFieldTarget).trigger("apply.daterangepicker"); + } } resetTimeZoneUI(e) { @@ -65,39 +92,70 @@ export default class extends Controller { } } + // triggered on selecting a new timezone using the buttons setTimeZone(event) { // don't follow the anchor event.preventDefault() - const currentTimeZoneEl = this.currentTimeZoneWrapperTarget.querySelector('a') - const {value} = event.target.dataset - - $(this.timeZoneFieldTarget).val(value) - $(currentTimeZoneEl).text(value) - + $(this.timeZoneFieldTarget).val(event.target.dataset.value) + $(currentTimeZoneEl).text(event.target.dataset.label) $('.time-zone-button').removeClass('button').addClass('button-alternative') $(event.target).removeClass('button-alternative').addClass('button') + this.resetTimeZoneUI() + if(!["", null].includes(this.fieldTarget.value)){ + $(this.displayFieldTarget).trigger("apply.daterangepicker"); + } + } + + // triggered on selecting a new timezone from the timezone picker + selectTimeZoneChange(event) { + if(!["", null].includes(this.fieldTarget.value)){ + $(this.displayFieldTarget).trigger("apply.daterangepicker"); + } + } + // triggered on cancel click from the timezone picker + cancelSelect(event) { + event.preventDefault() this.resetTimeZoneUI() + if(!["", null].includes(this.fieldTarget.value)){ + $(this.displayFieldTarget).trigger("apply.daterangepicker") + } + } + + displayFieldChange(event) { + const newTimeZone = this.currentTimeZone() + const format = this.includeTimeValue ? this.timeFormatValue : this.dateFormatValue + const momentParsed = moment(this.displayFieldTarget.value, format, false) + if(momentParsed.isValid()){ + const momentVal = moment.tz(momentParsed.format("YYYY-MM-DDTHH:mm"), newTimeZone) + const dataVal = this.includeTimeValue ? momentVal.toISOString(true) : momentVal.format('YYYY-MM-DD') + $(this.fieldTarget).val(dataVal) + } else { + // nullify field value when the display format is wrong + $(this.fieldTarget).val("") + } } initPluginInstance() { - $(this.fieldTarget).daterangepicker({ + const localeValues = this.pickerLocaleValue + const isAmPm = this.isAmPmValue + localeValues['format'] = this.includeTimeValue ? this.timeFormatValue : this.dateFormatValue + + $(this.displayFieldTarget).daterangepicker({ singleDatePicker: true, timePicker: this.includeTimeValue, timePickerIncrement: 5, autoUpdateInput: false, - locale: { - cancelLabel: this.cancelButtonLabelValue, - applyLabel: this.applyButtonLabelValue, - format: this.includeTimeValue ? 'MM/DD/YYYY h:mm A' : 'MM/DD/YYYY' - } + locale: localeValues, + timePicker24Hour: !isAmPm, }) - $(this.fieldTarget).on('apply.daterangepicker', this.applyDateToField.bind(this)) - $(this.fieldTarget).on('cancel.daterangepicker', this.clearDate.bind(this)) + $(this.displayFieldTarget).on('apply.daterangepicker', this.applyDateToField.bind(this)) + $(this.displayFieldTarget).on('cancel.daterangepicker', this.clearDate.bind(this)) + $(this.displayFieldTarget).on('input', this,this.displayFieldChange.bind(this)); - this.pluginMainEl = this.fieldTarget + this.pluginMainEl = this.displayFieldTarget this.plugin = $(this.pluginMainEl).data('daterangepicker') // weird // Init time zone select @@ -113,7 +171,6 @@ export default class extends Controller { $(this.timeZoneSelect).on('change.select2', function(event) { const currentTimeZoneEl = self.currentTimeZoneWrapperTarget.querySelector('a') const {value} = event.target - $(self.timeZoneFieldTarget).val(value) $(currentTimeZoneEl).text(value) @@ -126,7 +183,6 @@ export default class extends Controller { } else { // deselect any selected button $('.time-zone-button').removeClass('button').addClass('button-alternative') - selectedOptionTimeZoneButton.text(value) selectedOptionTimeZoneButton.attr('data-value', value).removeAttr('hidden') selectedOptionTimeZoneButton.removeClass(['hidden', 'button-alternative']).addClass('button') @@ -139,10 +195,8 @@ export default class extends Controller { teardownPluginInstance() { if (this.plugin === undefined) { return } - $(this.pluginMainEl).off('apply.daterangepicker') $(this.pluginMainEl).off('cancel.daterangepicker') - // revert to original markup, remove any event listeners this.plugin.remove() diff --git a/bullet_train-fields/bullet_train-fields.gemspec b/bullet_train-fields/bullet_train-fields.gemspec index 9ddc038a2..4c8c68bb0 100644 --- a/bullet_train-fields/bullet_train-fields.gemspec +++ b/bullet_train-fields/bullet_train-fields.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::Fields::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-fields" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-fields" spec.summary = "Bullet Train Fields" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-fields/lib/bullet_train/fields/version.rb b/bullet_train-fields/lib/bullet_train/fields/version.rb index bd2b42e7c..55aceb7ec 100644 --- a/bullet_train-fields/lib/bullet_train/fields/version.rb +++ b/bullet_train-fields/lib/bullet_train/fields/version.rb @@ -1,5 +1,5 @@ module BulletTrain module Fields - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-fields/package.json b/bullet_train-fields/package.json index 1fbf7d079..89a0f1337 100644 --- a/bullet_train-fields/package.json +++ b/bullet_train-fields/package.json @@ -47,6 +47,7 @@ "cp-cli": "^2.0.0", "emoji-mart": "^5.1.0", "microbundle": "^0.13.0", + "moment-timezone": "^0.5.43", "np": "^7.6.0", "npm-watch": "^0.11.0", "rimraf": "^3.0.2", diff --git a/bullet_train-fields/yarn.lock b/bullet_train-fields/yarn.lock index 26cdbdd6e..ce20a8109 100644 --- a/bullet_train-fields/yarn.lock +++ b/bullet_train-fields/yarn.lock @@ -3445,6 +3445,18 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +moment-timezone@^0.5.43: + version "0.5.43" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.43.tgz#3dd7f3d0c67f78c23cd1906b9b2137a09b3c4790" + integrity sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ== + dependencies: + moment "^2.29.4" + +moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + moment@^2.9.0: version "2.29.1" resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz" diff --git a/bullet_train-has_uuid/bullet_train-has_uuid.gemspec b/bullet_train-has_uuid/bullet_train-has_uuid.gemspec index 914885914..db6727939 100644 --- a/bullet_train-has_uuid/bullet_train-has_uuid.gemspec +++ b/bullet_train-has_uuid/bullet_train-has_uuid.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::HasUuid::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-has_uuid" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-has_uuid" spec.summary = "Bullet Train Has UUID" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-has_uuid/lib/bullet_train/has_uuid/version.rb b/bullet_train-has_uuid/lib/bullet_train/has_uuid/version.rb index 270d757e3..4a02b87ee 100644 --- a/bullet_train-has_uuid/lib/bullet_train/has_uuid/version.rb +++ b/bullet_train-has_uuid/lib/bullet_train/has_uuid/version.rb @@ -1,5 +1,5 @@ module BulletTrain module HasUuid - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-incoming_webhooks/bullet_train-incoming_webhooks.gemspec b/bullet_train-incoming_webhooks/bullet_train-incoming_webhooks.gemspec index a3c78df42..67ce8d513 100644 --- a/bullet_train-incoming_webhooks/bullet_train-incoming_webhooks.gemspec +++ b/bullet_train-incoming_webhooks/bullet_train-incoming_webhooks.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::IncomingWebhooks::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-incoming_webhooks" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-incoming_webhooks" spec.summary = "Bullet Train Incoming Webhooks" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-incoming_webhooks/lib/bullet_train/incoming_webhooks/version.rb b/bullet_train-incoming_webhooks/lib/bullet_train/incoming_webhooks/version.rb index 139400754..52776221e 100644 --- a/bullet_train-incoming_webhooks/lib/bullet_train/incoming_webhooks/version.rb +++ b/bullet_train-incoming_webhooks/lib/bullet_train/incoming_webhooks/version.rb @@ -1,5 +1,5 @@ module BulletTrain module IncomingWebhooks - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-integrations-stripe/bullet_train-integrations-stripe.gemspec b/bullet_train-integrations-stripe/bullet_train-integrations-stripe.gemspec index 0bb8b02e7..9e0efacdb 100644 --- a/bullet_train-integrations-stripe/bullet_train-integrations-stripe.gemspec +++ b/bullet_train-integrations-stripe/bullet_train-integrations-stripe.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::Integrations::Stripe::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-integrations-stripe" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-integrations-stripe" spec.summary = "Example Stripe platform integration for Bullet Train applications." spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-integrations-stripe/lib/bullet_train/integrations/stripe/version.rb b/bullet_train-integrations-stripe/lib/bullet_train/integrations/stripe/version.rb index ab33e43a5..9108c98c8 100644 --- a/bullet_train-integrations-stripe/lib/bullet_train/integrations/stripe/version.rb +++ b/bullet_train-integrations-stripe/lib/bullet_train/integrations/stripe/version.rb @@ -1,7 +1,7 @@ module BulletTrain module Integrations module Stripe - VERSION = "1.2.21" + VERSION = "1.2.27" end end end diff --git a/bullet_train-integrations/bullet_train-integrations.gemspec b/bullet_train-integrations/bullet_train-integrations.gemspec index e19ce6ef7..beffae28a 100644 --- a/bullet_train-integrations/bullet_train-integrations.gemspec +++ b/bullet_train-integrations/bullet_train-integrations.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::Integrations::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-integrations" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-integrations" spec.summary = "Bullet Train Integrations" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-integrations/lib/bullet_train/integrations/version.rb b/bullet_train-integrations/lib/bullet_train/integrations/version.rb index d062d93de..5b6771b16 100644 --- a/bullet_train-integrations/lib/bullet_train/integrations/version.rb +++ b/bullet_train-integrations/lib/bullet_train/integrations/version.rb @@ -1,5 +1,5 @@ module BulletTrain module Integrations - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-obfuscates_id/bullet_train-obfuscates_id.gemspec b/bullet_train-obfuscates_id/bullet_train-obfuscates_id.gemspec index 603dced26..ac0a9e07f 100644 --- a/bullet_train-obfuscates_id/bullet_train-obfuscates_id.gemspec +++ b/bullet_train-obfuscates_id/bullet_train-obfuscates_id.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::ObfuscatesId::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-obfuscates_id" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-obfuscates_id" spec.summary = "Bullet Train Obfuscates ID" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-obfuscates_id/lib/bullet_train/obfuscates_id/version.rb b/bullet_train-obfuscates_id/lib/bullet_train/obfuscates_id/version.rb index e8a879d4a..f14e8fbf0 100644 --- a/bullet_train-obfuscates_id/lib/bullet_train/obfuscates_id/version.rb +++ b/bullet_train-obfuscates_id/lib/bullet_train/obfuscates_id/version.rb @@ -1,5 +1,5 @@ module BulletTrain module ObfuscatesId - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-outgoing_webhooks/.circleci/config.yml b/bullet_train-outgoing_webhooks/.circleci/config.yml index 0671bce72..8c8ad63a8 100644 --- a/bullet_train-outgoing_webhooks/.circleci/config.yml +++ b/bullet_train-outgoing_webhooks/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-outgoing_webhooks/app/controllers/account/webhooks/outgoing/deliveries_controller.rb b/bullet_train-outgoing_webhooks/app/controllers/account/webhooks/outgoing/deliveries_controller.rb index 657d5f5be..e5722c3ad 100644 --- a/bullet_train-outgoing_webhooks/app/controllers/account/webhooks/outgoing/deliveries_controller.rb +++ b/bullet_train-outgoing_webhooks/app/controllers/account/webhooks/outgoing/deliveries_controller.rb @@ -63,17 +63,13 @@ def destroy # Never trust parameters from the scary internet, only allow the white list through. def delivery_params - strong_params = params.require(:webhooks_outgoing_delivery).permit( + params.require(:webhooks_outgoing_delivery).permit( :event_id, :endpoint_url, :delivered_at, # πŸš… super scaffolding will insert new fields above this line. # πŸš… super scaffolding will insert new arrays above this line. ) - - assign_date_and_time(strong_params, :delivered_at) # πŸš… super scaffolding will insert processing for new fields above this line. - - strong_params end end diff --git a/bullet_train-outgoing_webhooks/app/controllers/api/v1/webhooks/outgoing/endpoints_controller.rb b/bullet_train-outgoing_webhooks/app/controllers/api/v1/webhooks/outgoing/endpoints_controller.rb index 2b54b0b03..a92b5f312 100644 --- a/bullet_train-outgoing_webhooks/app/controllers/api/v1/webhooks/outgoing/endpoints_controller.rb +++ b/bullet_train-outgoing_webhooks/app/controllers/api/v1/webhooks/outgoing/endpoints_controller.rb @@ -43,7 +43,7 @@ def endpoint_params *permitted_fields, :url, :name, - :version, + :api_version, :scaffolding_absolutely_abstract_creative_concept_id, # πŸš… super scaffolding will insert new fields above this line. *permitted_arrays, diff --git a/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb b/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb index 8dfcd2afd..6c7a2b736 100644 --- a/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb +++ b/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb @@ -46,13 +46,19 @@ def attempt end # Net::HTTP will consider the url invalid (and not deliver the webhook) unless it ends with a '/' - unless uri.path.end_with?("/") - uri.path = uri.path + "/" + if uri.path == "" + uri.path = "/" end http = Net::HTTP.new(hostname, uri.port) - http.use_ssl = true if uri.scheme == "https" - request = Net::HTTP::Post.new(uri.path) + if uri.scheme == "https" + http.use_ssl = true + if BulletTrain::OutgoingWebhooks.http_verify_mode + # Developers might need to set this to `OpenSSL::SSL::VERIFY_NONE` in some cases. + http.verify_mode = BulletTrain::OutgoingWebhooks.http_verify_mode + end + end + request = Net::HTTP::Post.new(uri.request_uri) request.add_field("Host", uri.host) request.add_field("Content-Type", "application/json") request.body = delivery.event.payload.to_json diff --git a/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/issuing_model.rb b/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/issuing_model.rb index 377d7dc92..59b7bea29 100644 --- a/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/issuing_model.rb +++ b/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/issuing_model.rb @@ -13,6 +13,7 @@ def skip_generate_webhook?(action) false end + # TODO This should probably be called `outgoing_webhooks_parent` to avoid colliding with downstream `parent` methods. def parent return unless respond_to? BulletTrain::OutgoingWebhooks.parent_association send(BulletTrain::OutgoingWebhooks.parent_association) diff --git a/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/uri_filtering.rb b/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/uri_filtering.rb index e3bbac9f3..441436358 100644 --- a/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/uri_filtering.rb +++ b/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/uri_filtering.rb @@ -81,7 +81,7 @@ def resolve_ip_from_authoritative(hostname) resource = authoritative_resolver.getresource(hostname, Resolv::DNS::Resource::IN::A) Rails.cache.write(cache_key, resource.address.to_s, expires_in: resource.ttl, race_condition_ttl: 5) resource.address.to_s - rescue IPAddr::InvalidAddressError, ArgumentError # standard:disable Lint/ShadowedException + rescue ArgumentError Rails.cache.write(cache_key, "invalid", expires_in: 10.minutes, race_condition_ttl: 5) nil end diff --git a/bullet_train-outgoing_webhooks/app/views/api/v1/open_api/index.yaml.erb b/bullet_train-outgoing_webhooks/app/views/api/v1/open_api/index.yaml.erb deleted file mode 100644 index bead12524..000000000 --- a/bullet_train-outgoing_webhooks/app/views/api/v1/open_api/index.yaml.erb +++ /dev/null @@ -1,33 +0,0 @@ -openapi: 3.1.0 -info: - title: Bullet Train API - description: | - The baseline API of a new Bullet Train application. - license: - name: MIT - url: https://opensource.org/licenses/MIT - version: "<%= @version.upcase %>" -servers: - - url: <%= ENV["BASE_URL"] %>/api/<%= @version %> -components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer - schemas: - <%= automatic_components_for Webhooks::Outgoing::Endpoint %> - <%= automatic_components_for Webhooks::Outgoing::Event %> - <%# πŸš… super scaffolding will insert new components above this line. %> - parameters: - id: - name: id - in: path - required: true - schema: - type: string -security: - - BearerAuth: [] -paths: - <%= automatic_paths_for Webhooks::Outgoing::Endpoint, Team %> - <%= automatic_paths_for Webhooks::Outgoing::Event, Team, except: %i[create update delete] %> - <%# πŸš… super scaffolding will insert new paths above this line. %> diff --git a/bullet_train-outgoing_webhooks/app/views/api/v1/webhooks/outgoing/events/_event.json.jbuilder b/bullet_train-outgoing_webhooks/app/views/api/v1/webhooks/outgoing/events/_event.json.jbuilder index d3e22ab9f..cea24b753 100644 --- a/bullet_train-outgoing_webhooks/app/views/api/v1/webhooks/outgoing/events/_event.json.jbuilder +++ b/bullet_train-outgoing_webhooks/app/views/api/v1/webhooks/outgoing/events/_event.json.jbuilder @@ -1,12 +1,9 @@ -json.data schema: {object: OpenStruct.new, object_title: I18n.t("webhooks/outgoing/events.fields.data.heading"), object_description: I18n.t("webhooks/outgoing/events.fields.data.heading")} do - json.id schema: {type: :integer, description: I18n.t("webhooks/outgoing/events.fields.data.id.heading")} - json.name schema: {type: :string, description: I18n.t("webhooks/outgoing/events.fields.data.name.heading")} - json.description schema: {type: :string, description: I18n.t("webhooks/outgoing/events.fields.data.description.heading")} - json.created_at schema: {type: :string, format: "date-time", description: I18n.t("webhooks/outgoing/events.fields.data.created_at.heading")} - json.updated_at schema: {type: :string, format: "date-time", description: I18n.t("webhooks/outgoing/events.fields.data.updated_at.heading")} -end - -json.event_id schema: {type: :string, description: I18n.t("webhooks/outgoing/events.fields.event_id.heading")} -json.event_type schema: {type: :integer, description: I18n.t("webhooks/outgoing/events.fields.event_type.heading")} -json.subject_id schema: {type: :integer, description: I18n.t("webhooks/outgoing/events.fields.subject_id.heading")} -json.subject_type schema: {type: :string, description: I18n.t("webhooks/outgoing/events.fields.subject_type.heading")} +json.extract! event, + :id, + :team_id, + :uuid, + :event_type_id, + :subject_id, + :subject_type, + :data, + :created_at diff --git a/bullet_train-outgoing_webhooks/bullet_train-outgoing_webhooks.gemspec b/bullet_train-outgoing_webhooks/bullet_train-outgoing_webhooks.gemspec index d1eb8dfc9..46e8750bb 100644 --- a/bullet_train-outgoing_webhooks/bullet_train-outgoing_webhooks.gemspec +++ b/bullet_train-outgoing_webhooks/bullet_train-outgoing_webhooks.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::OutgoingWebhooks::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-outgoing_webhooks" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-outgoing_webhooks" spec.summary = "Allow users of your Rails application to subscribe and receive webhooks when activity takes place in your application." spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-outgoing_webhooks/config/locales/en/webhooks/outgoing/events.en.yml b/bullet_train-outgoing_webhooks/config/locales/en/webhooks/outgoing/events.en.yml index 412558939..ec3f21312 100644 --- a/bullet_train-outgoing_webhooks/config/locales/en/webhooks/outgoing/events.en.yml +++ b/bullet_train-outgoing_webhooks/config/locales/en/webhooks/outgoing/events.en.yml @@ -2,45 +2,23 @@ en: webhooks/outgoing/events: &events label: &label Webhooks Events fields: &fields - data: - _: &data Event Data - label: *data - heading: *data - - id: - _: &id Event ID - label: *id - heading: *id - - name: - _: &name Event Name - label: *name - heading: *name - - description: - _: &description Event Description - label: *description - heading: *description - - created_at: - _: &created_at DateTime Event was added - label: *created_at - heading: *created_at - updated_at: - _: &updated_at DateTime Event was updated - label: *updated_at - heading: *updated_at + id: + _: &id Event ID + label: *id + heading: *id - event_id: - _: &event_id Event UUID - label: *event_id - heading: *event_id + team_id: + _: &team_id Team ID + label: *team_id + heading: *team_id - event_type: + event_type_id: &event_type_id _: &event_type Event Type label: *event_type heading: *event_type + event_type: *event_type_id + subject_id: _: &subject_id Subject ID label: *subject_id @@ -70,3 +48,13 @@ en: _: &event_type_name Event Type Name label: *event_type_name heading: *event_type_name + + data: + _: &data Object Data + label: *data + heading: *data + + created_at: + _: &created_at Happened At + label: *created_at + heading: *created_at diff --git a/bullet_train-outgoing_webhooks/db/migrate/20221230223200_add_api_version_to_webhooks_outgoing_endpoints.rb b/bullet_train-outgoing_webhooks/db/migrate/20221230223200_add_api_version_to_webhooks_outgoing_endpoints.rb new file mode 100644 index 000000000..ecbd43c1a --- /dev/null +++ b/bullet_train-outgoing_webhooks/db/migrate/20221230223200_add_api_version_to_webhooks_outgoing_endpoints.rb @@ -0,0 +1,5 @@ +class AddApiVersionToWebhooksOutgoingEndpoints < ActiveRecord::Migration[7.0] + def change + add_column :webhooks_outgoing_endpoints, :api_version, :integer, null: false, default: 1 + end +end diff --git a/bullet_train-outgoing_webhooks/db/migrate/20221230235326_add_api_version_to_webhooks_outgoing_events.rb b/bullet_train-outgoing_webhooks/db/migrate/20221230235326_add_api_version_to_webhooks_outgoing_events.rb new file mode 100644 index 000000000..04f131b46 --- /dev/null +++ b/bullet_train-outgoing_webhooks/db/migrate/20221230235326_add_api_version_to_webhooks_outgoing_events.rb @@ -0,0 +1,5 @@ +class AddApiVersionToWebhooksOutgoingEvents < ActiveRecord::Migration[7.0] + def change + add_column :webhooks_outgoing_events, :api_version, :integer, null: false, default: 1 + end +end diff --git a/bullet_train-outgoing_webhooks/db/migrate/20221231003437_remove_default_from_webhooks_outgoing_endpoints.rb b/bullet_train-outgoing_webhooks/db/migrate/20221231003437_remove_default_from_webhooks_outgoing_endpoints.rb new file mode 100644 index 000000000..5b95aff63 --- /dev/null +++ b/bullet_train-outgoing_webhooks/db/migrate/20221231003437_remove_default_from_webhooks_outgoing_endpoints.rb @@ -0,0 +1,5 @@ +class RemoveDefaultFromWebhooksOutgoingEndpoints < ActiveRecord::Migration[7.0] + def change + change_column_default :webhooks_outgoing_endpoints, :api_version, from: 1, to: nil + end +end diff --git a/bullet_train-outgoing_webhooks/db/migrate/20221231003438_remove_default_from_webhooks_outgoing_events.rb b/bullet_train-outgoing_webhooks/db/migrate/20221231003438_remove_default_from_webhooks_outgoing_events.rb new file mode 100644 index 000000000..9145f68d6 --- /dev/null +++ b/bullet_train-outgoing_webhooks/db/migrate/20221231003438_remove_default_from_webhooks_outgoing_events.rb @@ -0,0 +1,5 @@ +class RemoveDefaultFromWebhooksOutgoingEvents < ActiveRecord::Migration[7.0] + def change + change_column_default :webhooks_outgoing_events, :api_version, from: 1, to: nil + end +end diff --git a/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks.rb b/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks.rb index 86c7b2c87..7f63e6687 100644 --- a/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks.rb +++ b/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks.rb @@ -10,6 +10,7 @@ def self.default_for(klass, method, default_value) mattr_accessor :parent_class, default: default_for(BulletTrain, :parent_class, "Team") mattr_accessor :base_class, default: default_for(BulletTrain, :base_class, "ApplicationRecord") mattr_accessor :advanced_hostname_security, default: false + mattr_accessor :http_verify_mode def self.parent_association parent_class.underscore.to_sym diff --git a/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks/version.rb b/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks/version.rb index b71fd9262..5beebb69a 100644 --- a/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks/version.rb +++ b/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks/version.rb @@ -1,5 +1,5 @@ module BulletTrain module OutgoingWebhooks - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-roles/.circleci/config.yml b/bullet_train-roles/.circleci/config.yml index d44d95878..6edcc984d 100644 --- a/bullet_train-roles/.circleci/config.yml +++ b/bullet_train-roles/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-roles/Gemfile.lock b/bullet_train-roles/Gemfile.lock index d1500a72c..4c349e752 100644 --- a/bullet_train-roles/Gemfile.lock +++ b/bullet_train-roles/Gemfile.lock @@ -9,7 +9,7 @@ GIT PATH remote: . specs: - bullet_train-roles (1.2.21) + bullet_train-roles (1.2.27) active_hash activesupport cancancan @@ -112,6 +112,7 @@ GEM marcel (1.0.2) method_source (1.0.0) mini_mime (1.1.2) + mini_portile2 (2.8.1) minitest (5.18.0) net-imap (0.3.4) date @@ -123,6 +124,9 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) + nokogiri (1.14.2) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) nokogiri (1.14.2-arm64-darwin) racc (~> 1.4) nokogiri (1.14.2-x86_64-darwin) @@ -199,6 +203,7 @@ PLATFORMS arm64-darwin-20 arm64-darwin-21 arm64-darwin-22 + ruby x86_64-darwin-21 x86_64-linux diff --git a/bullet_train-roles/README.md b/bullet_train-roles/README.md index c1de40f81..24ac81571 100644 --- a/bullet_train-roles/README.md +++ b/bullet_train-roles/README.md @@ -58,7 +58,7 @@ The provided `Role` model is backed by a Yaml configuration in `config/models/ro To help explain this configuration and its options, we'll provide the following hypothetical example: -``` +```yaml default: models: Project: read @@ -92,6 +92,8 @@ Here's a breakdown of the structure of the configuration file: - `manageable_roles` provides a list of roles that can be assigned to other users by members that have the role being defined. - `includes` provides a list of other roles whose permissions should also be made available to members with the role being defined. - `manage`, `read`, etc. are all CanCanCan-defined actions that can be granted. + - `crud` is a special value that we substitute for the 4 CRUD actions - create, read, update and destroy. + This is instead of `manage` which covers all actions - 4 CRUD actions _and_ any extra actions the controller may respond to The following things are true given the example configuration above: @@ -111,7 +113,7 @@ The following things are true given the example configuration above: You can also grant more granular permissions by supplying a list of the specific actions per resource, like so: -``` +```yaml editor: models: project: @@ -123,7 +125,7 @@ editor: All of these definitions are interpreted and translated into CanCanCan directives when we invoke the following Bullet Train helper in `app/models/ability.rb`: -``` +```ruby permit user, through: :memberships, parent: :team ``` @@ -136,7 +138,7 @@ In the example above: To illustrate the flexibility of this approach, consider that you may want to grant non-administrative team members different permissions for different `Project` objects on a `Team`. In that case, `permit` actually allows us to re-use the same role definitions to assign permissions that are scoped by a specific resource, like this: -``` +```ruby permit user, through: :projects_collaborators, parent: :project ``` @@ -149,7 +151,7 @@ In some situations, you don't want all roles to be available to all Grant Models By default all Grant Models will show all roles as options. If you want to limit the roles available to a model, use the `roles_only` class method: -``` +```ruby class Membership < ApplicationRecord include Roles::Support roles_only :admin, :editor, :reader # Add this line to restrict the Membership model to only these roles @@ -158,7 +160,7 @@ end To access the array of all roles available for a particular model, use the `assignable_roles` class method. For example, in your Membership form, you probably _only_ want to show the assignable_roles as options. Your view could look like this: -``` +```erb <% Membership.assignable_roles.each do |role| %> <% if role.manageable_by?(current_membership.roles) %> @@ -166,13 +168,72 @@ To access the array of all roles available for a particular model, use the `assi <% end %> ``` +## Checking user permissions + +Generally the CanCanCan helper method (`account_load_and_authorize_resource`) at the top of each controller will handle checking user permissions and will only load resources appropriate for the current user. + +However, you may also want to check if a user can perform a specific action. For example, in a view you may want to only show the edit button if the current user has permissions to edit the object. For this, you can use regular CanCanCan helpers. For example: + +``` +<%= link_to "Edit", [:account, @document] if can? :edit, @document %> +``` + +Sometimes, you might want to check for the presence of a specific role. We provide a helper to check for the admin role: +``` +@membership.admin? +``` + +For all other roles, you can check for their presence like this: + +``` +@membership.roles.include?(Role.find("developer")) +``` + +However, when you do that, you're only checking the roles that have been directly assigned to that membership. + +Imagine a scenario like this: +``` +# roles.yml +admin: + includes: + - editor + - billing + +# somewhere else in your app: +@membership.roles << Role.admin +@membership.roles.include?(Role.find("editor")) +=> false +``` + +While that's technically correct that the user doesn't have the editor role, we probably want that to return true if we're checking what the user can and can't do. For this situation, we really want to check if the user can perform a role rather than if they've had that role assigned to them. + +``` +# roles.yml + +admin: + includes: + - editor + - billing + +# somewhere else in your app: + +@membership.roles << Role.admin +@membership.roles.can_perform_role?(Role.find("editor")) +=> true + +# You can also pass the role key as a symbol for a more concise syntax +@membership.roles.can_perform_role?(:editor) +=> true +``` + ## Debugging + If you want to see what CanCanCan directives are being created by your permit calls, you can add the `debug: true` option to your `permit` statement in `app/models/ability.rb`. Likewise, to see what abilities are being added for a certain user, you can run the following on the Rails console: -``` +```ruby user = User.first Ability.new(user).permit user, through: :projects_collaborators, parent: :project, debug: true ``` diff --git a/bullet_train-roles/bullet_train-roles.gemspec b/bullet_train-roles/bullet_train-roles.gemspec index e53b3e35c..5e0d63df0 100644 --- a/bullet_train-roles/bullet_train-roles.gemspec +++ b/bullet_train-roles/bullet_train-roles.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |spec| spec.summary = "Yaml-backed ApplicationHash for CanCan Roles" spec.description = "Yaml-backed ApplicationHash for CanCan Roles" - spec.homepage = "https://github.com/bullet-train-co/bullet_train-roles" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-roles" spec.license = "MIT" spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0") diff --git a/bullet_train-roles/lib/bullet_train/roles/version.rb b/bullet_train-roles/lib/bullet_train/roles/version.rb index 584f01595..b9502916f 100644 --- a/bullet_train-roles/lib/bullet_train/roles/version.rb +++ b/bullet_train-roles/lib/bullet_train/roles/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Roles - VERSION = "1.2.21" + VERSION = "1.2.27" end diff --git a/bullet_train-roles/lib/models/role.rb b/bullet_train-roles/lib/models/role.rb index 7ae51bd8f..f59528efe 100644 --- a/bullet_train-roles/lib/models/role.rb +++ b/bullet_train-roles/lib/models/role.rb @@ -19,9 +19,15 @@ def self.default find_by_key("default") end + # Allow us to use either symbols or strings when searching + def self.find_by_key(key) + super(key.to_s) + end + def self.includes(role_or_key) - role_key = role_or_key.is_a?(Role) ? role_or_key.key : role_or_key + role_key = role_or_key.is_a?(Role) ? role_or_key.key : role_or_key.to_s role = Role.find_by_key(role_key) + return [] if role.nil? return Role.all.select(&:assignable?) if role.default? result = [] all.each do |role| @@ -35,7 +41,7 @@ def self.assignable end def self.find(key) - all.find { |role| role.key == key } + all.find { |role| role.key == key.to_s } end # We don't want to ever use the automatically generated ids from ActiveYaml. These are created based on the order of objects in the yml file diff --git a/bullet_train-roles/lib/roles/support.rb b/bullet_train-roles/lib/roles/support.rb index 411b7d7b7..de3c735b0 100644 --- a/bullet_train-roles/lib/roles/support.rb +++ b/bullet_train-roles/lib/roles/support.rb @@ -70,6 +70,20 @@ def roles Role::Collection.new(self, (self.class.default_roles + roles_without_defaults).compact.uniq) end + # Tests if the user can perform a given role + # They can have the role assigned directly, or the role can be included in another role they have + def can_perform_role?(role_or_key) + role_key = role_or_key.is_a?(Role) ? role_or_key.key : role_or_key + role = Role.find_by_key(role_key) + return true if roles.include?(role) + # Check all the roles that this role is included into + Role.includes(role_key).each do |included_in_role| + return true if roles.include?(included_in_role) + return true if can_perform_role?(included_in_role) + end + false + end + def roles=(roles) update(role_ids: roles.map(&:key)) end diff --git a/bullet_train-roles/test/dummy/config/models/roles.yml b/bullet_train-roles/test/dummy/config/models/roles.yml index 1cf53da44..edfd5d809 100644 --- a/bullet_train-roles/test/dummy/config/models/roles.yml +++ b/bullet_train-roles/test/dummy/config/models/roles.yml @@ -16,6 +16,14 @@ editor: - read - update +manager: + includes: + - editor + +supervisor: + includes: + - manager + admin: includes: - editor diff --git a/bullet_train-roles/test/lib/models/role_test.rb b/bullet_train-roles/test/lib/models/role_test.rb index 6d4d8da5e..cb4799e31 100644 --- a/bullet_train-roles/test/lib/models/role_test.rb +++ b/bullet_train-roles/test/lib/models/role_test.rb @@ -25,11 +25,11 @@ def setup end test "Role.includes works when given a string" do - assert_equal Role.includes("editor"), [Role.admin] + assert Role.includes("editor").include?(Role.admin) end test "Role.include works when given a role" do - assert_equal Role.includes(Role.find_by_key("editor")), [Role.admin] + assert Role.includes(Role.find_by_key("editor")).include? Role.admin end test "Role.assignable should not return the default role" do @@ -45,6 +45,8 @@ class InstanceMethodsTest < ActiveSupport::TestCase def setup @admin_user = FactoryBot.create :onboarded_user @membership = FactoryBot.create :membership, user: @admin_user, team: @admin_user.current_team, role_ids: [Role.admin.id] + @non_admin_user = FactoryBot.create :onboarded_user + @non_admin_membership = FactoryBot.create :membership, user: @non_admin_user, team: @non_admin_user.current_team, role_ids: [Role.find(:editor).id] end test "default_role#included_by returns the admin role" do @@ -131,6 +133,32 @@ def setup @membership.update_column(:role_ids, nil) assert_equal @membership.roles, [Role.find_by_key("default")] end + + test "#can_perform_role? returns true if the user has been assigned the role" do + assert @membership.can_perform_role?(Role.admin) + end + + test "#can_perform_role? returns true if the user has not been assigned the role, but it is included in another role they have" do + assert @membership.roles.include? Role.admin + refute @membership.roles.include?(Role.find(:editor)) + assert Role.admin.includes.include?("editor") + assert @membership.can_perform_role?(:editor) + end + + test "#can_perform_role? returns false if the user has not been assigned the role, and it is not included in another role they have" do + assert @non_admin_membership.roles.include?(Role.find(:editor)) + refute Role.find(:editor).includes.include?("crud_role") + assert Role.find :crud_role + refute @non_admin_membership.can_perform_role?(:crud_role) + end + + test "#can_perform_role? returns true if the role being tested is 2 layers deep" do + # supervisor includes manager, which includes editor + @supervisor_membership = FactoryBot.create :membership, user: @admin_user, team: @admin_user.current_team, role_ids: [] + refute @supervisor_membership.can_perform_role?(:editor) + @supervisor_membership.roles << Role.find(:supervisor) + assert @supervisor_membership.can_perform_role?(:editor) + end end class Role::AbilityGeneratorTest < ActiveSupport::TestCase diff --git a/bullet_train-scope_questions/bullet_train-scope_questions.gemspec b/bullet_train-scope_questions/bullet_train-scope_questions.gemspec index 6c7fc3f6d..300bf6b85 100644 --- a/bullet_train-scope_questions/bullet_train-scope_questions.gemspec +++ b/bullet_train-scope_questions/bullet_train-scope_questions.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::ScopeQuestions::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-scope_questions" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-scope_questions" spec.summary = "Bullet Train Scope Questions" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-scope_questions/lib/bullet_train/scope_questions/version.rb b/bullet_train-scope_questions/lib/bullet_train/scope_questions/version.rb index 07fec41e7..2fb45a799 100644 --- a/bullet_train-scope_questions/lib/bullet_train/scope_questions/version.rb +++ b/bullet_train-scope_questions/lib/bullet_train/scope_questions/version.rb @@ -1,5 +1,5 @@ module BulletTrain module ScopeQuestions - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-scope_validator/Gemfile.lock b/bullet_train-scope_validator/Gemfile.lock index a087af729..8acbbbc58 100644 --- a/bullet_train-scope_validator/Gemfile.lock +++ b/bullet_train-scope_validator/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - bullet_train-scope_validator (1.2.21) + bullet_train-scope_validator (1.2.27) GEM remote: https://rubygems.org/ diff --git a/bullet_train-scope_validator/README.md b/bullet_train-scope_validator/README.md index 8061f9670..d2b8d3877 100644 --- a/bullet_train-scope_validator/README.md +++ b/bullet_train-scope_validator/README.md @@ -50,7 +50,7 @@ end ### Example Form -``` +```erb <%= form.collection_select(:customer_id, @team.customers, :id, :name) %> ``` @@ -101,7 +101,7 @@ end If you're wondering what the connection between `validates :customer, scope: true` and `def valid_customers` is, it's just a convention that the former will call the latter based on the name of the attibute being validated. We've favored a full-blown method definition for this instead of simply passing in a proc into the validator because having a method allows us to also DRY up our form view to use the same definition of valid options, like so: -``` +```erb <%= form.collection_select(:customer_id, form.object.valid_customers, :id, :name) %> ``` diff --git a/bullet_train-scope_validator/bullet_train-scope_validator.gemspec b/bullet_train-scope_validator/bullet_train-scope_validator.gemspec index a012a8391..182ffad5f 100644 --- a/bullet_train-scope_validator/bullet_train-scope_validator.gemspec +++ b/bullet_train-scope_validator/bullet_train-scope_validator.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |spec| spec.summary = "Protect `belongs_to` attributes from ID stuffing." spec.description = spec.summary - spec.homepage = "https://github.com/bullet-train-co/bullet_train-scope_validator" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-scope_validator" spec.license = "MIT" spec.required_ruby_version = ">= 2.6.0" diff --git a/bullet_train-scope_validator/lib/bullet_train/scope_validator/version.rb b/bullet_train-scope_validator/lib/bullet_train/scope_validator/version.rb index c8e544cc4..3c3e3b325 100644 --- a/bullet_train-scope_validator/lib/bullet_train/scope_validator/version.rb +++ b/bullet_train-scope_validator/lib/bullet_train/scope_validator/version.rb @@ -2,6 +2,6 @@ module BulletTrain module ScopeValidator - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-sortable/.circleci/config.yml b/bullet_train-sortable/.circleci/config.yml index 2dd19bf05..eac14a502 100644 --- a/bullet_train-sortable/.circleci/config.yml +++ b/bullet_train-sortable/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-sortable/bullet_train-sortable.gemspec b/bullet_train-sortable/bullet_train-sortable.gemspec index 32b622093..743a692da 100644 --- a/bullet_train-sortable/bullet_train-sortable.gemspec +++ b/bullet_train-sortable/bullet_train-sortable.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::Sortable::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-sortable" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-sortable" spec.summary = "Bullet Train Sortable" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-sortable/lib/bullet_train/sortable/version.rb b/bullet_train-sortable/lib/bullet_train/sortable/version.rb index eb3edbf2a..09dfe267c 100644 --- a/bullet_train-sortable/lib/bullet_train/sortable/version.rb +++ b/bullet_train-sortable/lib/bullet_train/sortable/version.rb @@ -1,5 +1,5 @@ module BulletTrain module Sortable - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-super_load_and_authorize_resource/bullet_train-super_load_and_authorize_resource.gemspec b/bullet_train-super_load_and_authorize_resource/bullet_train-super_load_and_authorize_resource.gemspec index f7bd260ea..9397a7ca2 100644 --- a/bullet_train-super_load_and_authorize_resource/bullet_train-super_load_and_authorize_resource.gemspec +++ b/bullet_train-super_load_and_authorize_resource/bullet_train-super_load_and_authorize_resource.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::SuperLoadAndAuthorizeResource::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-super_load_and_authorize_resource" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-super_load_and_authorize_resource" spec.summary = "Bullet Train Super Load And Authorize Resource" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-super_load_and_authorize_resource/lib/bullet_train/super_load_and_authorize_resource/version.rb b/bullet_train-super_load_and_authorize_resource/lib/bullet_train/super_load_and_authorize_resource/version.rb index 63503f8c5..bf9e8875e 100644 --- a/bullet_train-super_load_and_authorize_resource/lib/bullet_train/super_load_and_authorize_resource/version.rb +++ b/bullet_train-super_load_and_authorize_resource/lib/bullet_train/super_load_and_authorize_resource/version.rb @@ -1,5 +1,5 @@ module BulletTrain module SuperLoadAndAuthorizeResource - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-super_scaffolding/.circleci/config.yml b/bullet_train-super_scaffolding/.circleci/config.yml index ee2749a97..09c6ef7f5 100644 --- a/bullet_train-super_scaffolding/.circleci/config.yml +++ b/bullet_train-super_scaffolding/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-super_scaffolding/.standard.yml b/bullet_train-super_scaffolding/.standard.yml index 3d51bfbed..747454221 100644 --- a/bullet_train-super_scaffolding/.standard.yml +++ b/bullet_train-super_scaffolding/.standard.yml @@ -1,5 +1,3 @@ ignore: - 'lib/scaffolding/transformer.rb': - Layout/EndAlignment - # TODO Fix these files up for Standard Ruby. - - 'lib/bullet_train/super_scaffolding/scaffolders/oauth_provider_scaffolder.rb' diff --git a/bullet_train-super_scaffolding/app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb b/bullet_train-super_scaffolding/app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb index 2b142ab7d..484c1b9f8 100644 --- a/bullet_train-super_scaffolding/app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb +++ b/bullet_train-super_scaffolding/app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb @@ -68,7 +68,6 @@ def destroy def process_params(strong_params) # πŸš… skip this section when scaffolding. assign_boolean(strong_params, :boolean_button_value) - assign_date_and_time(strong_params, :date_and_time_field_value) assign_checkboxes(strong_params, :multiple_button_values) assign_checkboxes(strong_params, :multiple_option_values) assign_select_options(strong_params, :multiple_super_select_values) diff --git a/bullet_train-super_scaffolding/app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller.rb b/bullet_train-super_scaffolding/app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller.rb index a61cac235..f8d1970c6 100644 --- a/bullet_train-super_scaffolding/app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller.rb +++ b/bullet_train-super_scaffolding/app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller.rb @@ -53,7 +53,6 @@ def tangible_thing_params :cloudinary_image_value, :date_field_value, :date_and_time_field_value, - :date_and_time_field_value_time_zone, :email_field_value, :file_field_value, :file_field_value_removal, diff --git a/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/_index.html.erb b/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/_index.html.erb index e187190b9..ffff23cbd 100644 --- a/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/_index.html.erb +++ b/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/_index.html.erb @@ -6,7 +6,7 @@ <% pagy ||= nil %> <% pagy, creative_concepts = pagy(creative_concepts, page_param: :creative_concepts_page) unless pagy %> -<%= updates_for context, :scaffolding_absolutely_abstract_creative_concepts do %> +<%= cable_ready_updates_for context, :scaffolding_absolutely_abstract_creative_concepts do %> <%= render 'account/shared/box' do |box| %> <% box.title t(".contexts.#{context.class.name.underscore}.header") %> <% box.description do %> @@ -27,9 +27,7 @@ - <% creative_concepts.each do |creative_concept| %> - <%= render 'account/scaffolding/absolutely_abstract/creative_concepts/creative_concept', creative_concept: creative_concept %> - <% end %> + <%= render partial: 'account/scaffolding/absolutely_abstract/creative_concepts/creative_concept', collection: creative_concepts %> <% end %> @@ -43,7 +41,7 @@ <%# πŸš… super scaffolding will insert new targets one parent action model buttons above this line. %> <%# πŸš… super scaffolding will insert new bulk action model buttons above this line. %> - <%= render "shared/bulk_action_select" %> + <%= render "shared/bulk_action_select" if creative_concepts.many? %> <%= link_to t('global.buttons.back'), [:account, context], class: "#{first_button_primary(:absolutely_abstract_creative_concept)} back" unless hide_back %> <% end %> diff --git a/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/show.html.erb b/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/show.html.erb index 42a737277..107a33685 100644 --- a/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/show.html.erb +++ b/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/show.html.erb @@ -1,7 +1,7 @@ <%= render 'account/shared/page' do |page| %> <% page.title t('.section') %> <% page.body do %> - <%= updates_for @creative_concept do %> + <%= cable_ready_updates_for @creative_concept do %> <%= render 'account/shared/box', divider: true do |box| %> <% box.t :description, title: '.header' %> <% box.body do %> diff --git a/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb b/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb index af35588ba..40799f6f7 100644 --- a/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb +++ b/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb @@ -9,7 +9,7 @@ <% pagy, tangible_things = pagy(tangible_things, page_param: :tangible_things_page) unless pagy %> <%= action_model_select_controller do %> - <% updates_for context, collection do %> + <% cable_ready_updates_for context, collection do %> <%= render 'account/shared/box', pagy: pagy do |box| %> <% box.title t(".contexts.#{context.class.name.underscore}.header") %> <% box.description do %> @@ -35,9 +35,7 @@ - <% tangible_things.each do |tangible_thing| %> - <%= render 'account/scaffolding/completely_concrete/tangible_things/tangible_thing', tangible_thing: tangible_thing %> - <% end %> + <%= render partial: 'account/scaffolding/completely_concrete/tangible_things/tangible_thing', collection: tangible_things %> <% end %> @@ -53,7 +51,7 @@ <%# πŸš… super scaffolding will insert new targets one parent action model buttons above this line. %> <%# πŸš… super scaffolding will insert new bulk action model buttons above this line. %> - <%= render "shared/bulk_action_select" %> + <%= render "shared/bulk_action_select" if tangible_things.many? %> <% unless hide_back %> <%= link_to t('global.buttons.back'), [:account, context], class: "#{first_button_primary(:completely_concrete_tangible_thing)} back" %> diff --git a/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/show.html.erb b/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/show.html.erb index c93903e1e..612b076c1 100644 --- a/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/show.html.erb +++ b/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/show.html.erb @@ -1,7 +1,7 @@ <%= render 'account/shared/page' do |page| %> <% page.title t('.section') %> <% page.body do %> - <%= updates_for @tangible_thing do %> + <%= cable_ready_updates_for @tangible_thing do %> <%= render 'account/shared/box', divider: true do |box| %> <% box.title t('.header') %> <% box.description do %> diff --git a/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/absolutely_abstract/creative_concepts/_paths.yaml.erb b/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/absolutely_abstract/creative_concepts/_paths.yaml.erb new file mode 100644 index 000000000..b58b39a30 --- /dev/null +++ b/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/absolutely_abstract/creative_concepts/_paths.yaml.erb @@ -0,0 +1,93 @@ +/teams/{team_id}/scaffolding/absolutely_abstract/creative_concepts: + get: + tags: + - "Scaffolding/Absolutely Abstract/Creative Concept" + summary: "List Creative Concept" + operationId: listScaffoldingAbsolutelyAbstractCreativeConcepts + parameters: + - name: team_id + in: path + required: true + schema: + type: string + responses: + "404": + description: "Not Found" + "200": + description: "OK" + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/ScaffoldingAbsolutelyAbstractCreativeConceptAttributes" + has_more: + type: boolean + post: + tags: + - "Scaffolding/Absolutely Abstract/Creative Concept" + summary: "Create Creative Concept" + operationId: createScaffoldingAbsolutelyAbstractCreativeConcepts + parameters: + - name: team_id + in: path + required: true + schema: + type: string + responses: + "404": + description: "Not Found" + "201": + description: "Created" + content: + application/json: + schema: + $ref: "#/components/schemas/ScaffoldingAbsolutelyAbstractCreativeConceptParameters" +/scaffolding/absolutely_abstract/creative_concepts/{id}: + get: + tags: + - "Scaffolding/Absolutely Abstract/Creative Concept" + summary: "Fetch Creative Concept" + operationId: getScaffoldingAbsolutelyAbstractCreativeConcepts + parameters: + - $ref: "#/components/parameters/id" + responses: + "404": + description: "Not Found" + "200": + description: "OK" + content: + application/json: + schema: + $ref: "#/components/schemas/ScaffoldingAbsolutelyAbstractCreativeConceptAttributes" + put: + tags: + - "Scaffolding/Absolutely Abstract/Creative Concept" + summary: "Update Creative Concept" + operationId: updateScaffoldingAbsolutelyAbstractCreativeConcepts + parameters: + - $ref: "#/components/parameters/id" + responses: + "404": + description: "Not Found" + "200": + description: "OK" + content: + application/json: + schema: + $ref: "#/components/schemas/ScaffoldingAbsolutelyAbstractCreativeConceptParameters" + delete: + tags: + - "Scaffolding/Absolutely Abstract/Creative Concept" + summary: "Remove Creative Concept" + operationId: removeScaffoldingAbsolutelyAbstractCreativeConcepts + parameters: + - $ref: "#/components/parameters/id" + responses: + "404": + description: "Not Found" + "200": + description: "OK" diff --git a/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_components.yaml.erb b/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_components.yaml.erb deleted file mode 100644 index 62ffbcfd2..000000000 --- a/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_components.yaml.erb +++ /dev/null @@ -1,32 +0,0 @@ -Scaffolding::CompletelyConcrete::TangibleThing::Attributes: - type: object - properties: - <%= attribute :id %> - <%= attribute :absolutely_abstract_creative_concept_id %> - <%# πŸš… skip this section when scaffolding. %> - <%= attribute :text_field_value %> - <%= attribute :button_value %> - <%= attribute :boolean_button_value %> - <%= attribute :cloudinary_image_value %> - <%= attribute :date_field_value %> - <%= attribute :email_field_value %> - <%= attribute :file_field_value %> - <%= attribute :password_field_value %> - <%= attribute :phone_field_value %> - <%= attribute :option_value %> - <%= attribute :multiple_option_values %> - <%= attribute :super_select_value %> - <%= attribute :text_area_value %> - <%= attribute :action_text_value %> - <%# πŸš… stop any skipping we're doing now. %> - <%# πŸš… super scaffolding will insert new attributes above this line. %> - <%= attribute :created_at %> - <%= attribute :updated_at %> - -Scaffolding::CompletelyConcrete::TangibleThing::Parameters: - type: object - properties: - <%# πŸš… skip this section when scaffolding. %> - <%= parameter :text_field_value %> - <%# πŸš… stop any skipping we're doing now. %> - <%# πŸš… super scaffolding will insert new parameter above this line. %> diff --git a/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_paths.yaml.erb b/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_paths.yaml.erb deleted file mode 100644 index d9e154c9f..000000000 --- a/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_paths.yaml.erb +++ /dev/null @@ -1,93 +0,0 @@ -/scaffolding/absolutely_abstract/creative_concepts/{absolutely_abstract_creative_concept_id}/completely_concrete/tangible_things: - get: - tags: - - "Scaffolding/Completely Concrete/Tangible Things" - summary: "List Tangible Things" - operationId: listScaffoldingCompletelyConcreteTangibleThings - parameters: - - name: absolutely_abstract_creative_concept_id - in: path - required: true - schema: - type: string - responses: - "404": - description: "Not Found" - "200": - description: "OK" - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingAttributes" - has_more: - type: boolean - post: - tags: - - "Scaffolding/Completely Concrete/Tangible Things" - summary: "Create Tangible Thing" - operationId: createScaffoldingCompletelyConcreteTangibleThings - parameters: - - name: absolutely_abstract_creative_concept_id - in: path - required: true - schema: - type: string - responses: - "404": - description: "Not Found" - "201": - description: "Created" - content: - application/json: - schema: - $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingParameters" -/scaffolding/completely_concrete/tangible_things/{id}: - get: - tags: - - "Scaffolding/Completely Concrete/Tangible Things" - summary: "Fetch Tangible Thing" - operationId: getScaffoldingCompletelyConcreteTangibleThings - parameters: - - $ref: "#/components/parameters/id" - responses: - "404": - description: "Not Found" - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingAttributes" - put: - tags: - - "Scaffolding/Completely Concrete/Tangible Things" - summary: "Update Tangible Thing" - operationId: updateScaffoldingCompletelyConcreteTangibleThings - parameters: - - $ref: "#/components/parameters/id" - responses: - "404": - description: "Not Found" - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingParameters" - delete: - tags: - - "Scaffolding/Completely Concrete/Tangible Things" - summary: "Remove Tangible Thing" - operationId: removeScaffoldingCompletelyConcreteTangibleThings - parameters: - - $ref: "#/components/parameters/id" - responses: - "404": - description: "Not Found" - "200": - description: "OK" diff --git a/bullet_train-super_scaffolding/app/views/api/v1/scaffolding/absolutely_abstract/creative_concepts/_creative_concept.json.jbuilder b/bullet_train-super_scaffolding/app/views/api/v1/scaffolding/absolutely_abstract/creative_concepts/_creative_concept.json.jbuilder index fbaf36249..922d243f7 100644 --- a/bullet_train-super_scaffolding/app/views/api/v1/scaffolding/absolutely_abstract/creative_concepts/_creative_concept.json.jbuilder +++ b/bullet_train-super_scaffolding/app/views/api/v1/scaffolding/absolutely_abstract/creative_concepts/_creative_concept.json.jbuilder @@ -1,5 +1,6 @@ json.extract! creative_concept, :id, + :team_id, :name, :description, :created_at, diff --git a/bullet_train-super_scaffolding/bullet_train-super_scaffolding.gemspec b/bullet_train-super_scaffolding/bullet_train-super_scaffolding.gemspec index e8e8e3b60..1009ab9a8 100644 --- a/bullet_train-super_scaffolding/bullet_train-super_scaffolding.gemspec +++ b/bullet_train-super_scaffolding/bullet_train-super_scaffolding.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::SuperScaffolding::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-super_scaffolding" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-super_scaffolding" spec.summary = "Bullet Train Super Scaffolding" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-super_scaffolding/config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml b/bullet_train-super_scaffolding/config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml index ba88da778..ef7cbdaa4 100644 --- a/bullet_train-super_scaffolding/config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml +++ b/bullet_train-super_scaffolding/config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml @@ -18,6 +18,8 @@ en: confirmations: # TODO customize for your use-case. destroy: Are you sure you want to remove %{tangible_thing_name}? This will also remove any child resources and can't be undone. + tangible_thing: + buttons: *buttons fields: &fields id: heading: Tangible Thing ID diff --git a/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/crud_scaffolder.rb b/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/crud_scaffolder.rb index 807d6a33d..d066e177b 100644 --- a/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/crud_scaffolder.rb +++ b/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/crud_scaffolder.rb @@ -12,8 +12,8 @@ def run puts " bin/super-scaffold crud Site Team name:text_field url:text_area" puts "" puts "E.g. a Section belongs to a Page, which belongs to a Site, which belongs to a Team:" - puts " rails g model Section page:references title:text body:text" - puts " bin/super-scaffold crud Section Page,Site,Team title:text_area body:text_area" + puts " rails g model Section page:references title:string body:text" + puts " bin/super-scaffold crud Section Page,Site,Team title:text_field body:text_area" puts "" puts "E.g. an Image belongs to either a Page or a Site:" puts " Doable! See https://bit.ly/2NvO8El for a step by step guide." diff --git a/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/oauth_provider_scaffolder.rb b/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/oauth_provider_scaffolder.rb index 3cb2a34c3..37f6ddd18 100644 --- a/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/oauth_provider_scaffolder.rb +++ b/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/oauth_provider_scaffolder.rb @@ -5,13 +5,15 @@ class OauthProviderScaffolder < Scaffolder def run unless argv.count >= 5 puts "" - puts "πŸš… usage: bin/super-scaffold oauth-provider [options]" + puts "πŸš… usage: bin/super-scaffold oauth-provider [options]" puts "" puts "E.g. what we'd do to start Stripe off (if we didn't already do it):" puts " bin/super-scaffold oauth-provider omniauth-stripe-connect stripe_connect Oauth::StripeAccount STRIPE_CLIENT_ID STRIPE_SECRET_KEY --icon=ti-money" + puts " (Please note here that the STRIPE_CLIENT_ID and STRIPE_SECRET_KEY strings are not the actual values, just the names we give to the environment variables.)" puts "" puts "E.g. what we actually did to start Shopify off:" puts " bin/super-scaffold oauth-provider omniauth-shopify-oauth2 shopify Oauth::ShopifyAccount SHOPIFY_API_KEY SHOPIFY_API_SECRET_KEY --icon=ti-shopping-cart" + puts " (Please note here that the SHOPIFY_API_KEY and SHOPIFY_API_SECRET_KEY strings are not the actual values, just the names we give to the environment variables.)" puts "" puts "Options:" puts "" @@ -24,7 +26,7 @@ def run _, omniauth_gem, gems_provider_name, our_provider_name, api_key, api_secret = *ARGV - unless match = our_provider_name.match(/Oauth::(.*)Account/) + unless (match = our_provider_name.match(/Oauth::(.*)Account/)) puts "\n🚨 Your provider name must match the pattern of `Oauth::{Name}Account`, e.g. `Oauth::StripeAccount`\n".red return end @@ -60,7 +62,6 @@ def run puts "but after you hit enter I'll open a page where you can view other icon options." puts "When you find one you like, hover your mouse over it and then come back here and" puts "and enter the name of the icon you want to use." - response = $stdin.gets.chomp if TerminalCommands.can_open? TerminalCommands.open_file_or_link("http://light.pinsupreme.com/icon_fonts_themefy.html") else @@ -80,6 +81,8 @@ def run options[:icon] = icon_name + empty_transformer = Scaffolding::Transformer.new("", "") + [ # User OAuth. @@ -110,7 +113,7 @@ def run "./app/controllers/webhooks/incoming/oauth/stripe_account_webhooks_controller.rb" ].each do |name| - if File.directory?(legacy_resolve_template_path(name)) + if File.directory?(empty_transformer.resolve_template_path(name)) oauth_scaffold_directory(name, options) else oauth_scaffold_file(name, options) @@ -135,16 +138,16 @@ def run # find the database migration that defines this relationship. migration_file_name = `grep "create_table #{oauth_transform_string(":oauth_stripe_accounts", options)}" db/migrate/*`.split(":").first - legacy_replace_in_file(migration_file_name, "null: false", "null: true") + empty_transformer.replace_in_file(migration_file_name, "null: false", "null: true") migration_file_name = `grep "create_table #{oauth_transform_string(":integrations_stripe_installations", options)}" db/migrate/*`.split(":").first - legacy_replace_in_file(migration_file_name, + empty_transformer.replace_in_file(migration_file_name, oauth_transform_string("t.references :oauth_stripe_account, null: false, foreign_key: true", options), oauth_transform_string('t.references :oauth_stripe_account, null: false, foreign_key: true, index: {name: "index_stripe_installations_on_oauth_stripe_account_id"}', options)) migration_file_name = `grep "create_table #{oauth_transform_string(":webhooks_incoming_oauth_stripe_account_webhooks", options)}" db/migrate/*`.split(":").first - legacy_replace_in_file(migration_file_name, "null: false", "null: true") - legacy_replace_in_file(migration_file_name, "foreign_key: true", 'foreign_key: true, index: {name: "index_stripe_webhooks_on_oauth_stripe_account_id"}') + empty_transformer.replace_in_file(migration_file_name, "null: false", "null: true") + empty_transformer.replace_in_file(migration_file_name, "foreign_key: true", oauth_transform_string('foreign_key: true, index: {name: "index_stripe_webhooks_on_oauth_stripe_account_id"}')) puts "" puts "πŸŽ‰" diff --git a/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/version.rb b/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/version.rb index 84e6ef754..cc32472f0 100644 --- a/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/version.rb +++ b/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/version.rb @@ -1,5 +1,5 @@ module BulletTrain module SuperScaffolding - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-super_scaffolding/lib/scaffolding/block_manipulator.rb b/bullet_train-super_scaffolding/lib/scaffolding/block_manipulator.rb index 302976966..c5fda6c6d 100644 --- a/bullet_train-super_scaffolding/lib/scaffolding/block_manipulator.rb +++ b/bullet_train-super_scaffolding/lib/scaffolding/block_manipulator.rb @@ -6,7 +6,7 @@ class << self # # Wrap a block of ruby code with another block on the outside. # - # @param [String] `starting` A string to search for at the start of the block. Eg "<%= updates_for context, collection do" + # @param [String] `starting` A string to search for at the start of the block. Eg "<%= cable_ready_updates_for context, collection do" # @param [Array] `with` An array with two String elements. The text that should wrap the block. Eg ["<%= action_model_select_controller do %>", "<% end %>"] # def wrap_block(starting:, with:, lines:) diff --git a/bullet_train-super_scaffolding/lib/scaffolding/oauth_providers.rb b/bullet_train-super_scaffolding/lib/scaffolding/oauth_providers.rb index 1357228d7..1ee284062 100755 --- a/bullet_train-super_scaffolding/lib/scaffolding/oauth_providers.rb +++ b/bullet_train-super_scaffolding/lib/scaffolding/oauth_providers.rb @@ -1,79 +1,6 @@ -def legacy_resolve_template_path(file) - # Figure out the actual location of the file. - # Originally all the potential source files were in the repository alongside the application. - # Now the files could be provided by an included Ruby gem, so we allow those Ruby gems to register their base - # path and then we check them in order to see which template we should use. - BulletTrain::SuperScaffolding.template_paths.map do |base_path| - base_path = Pathname.new(base_path) - resolved_path = base_path.join(file).to_s - File.exist?(resolved_path) ? resolved_path : nil - end.compact.first || raise("Couldn't find the Super Scaffolding template for `#{file}` in any of the following locations:\n\n#{BulletTrain::SuperScaffolding.template_paths.join("\n")}") -end - -def legacy_replace_in_file(file, before, after) - puts "Replacing in '#{file}'." unless silence_logs? - target_file_content = File.read(file) - target_file_content.gsub!(before, after) - File.write(file, target_file_content) -end - -def legacy_add_line_to_file(file, content, hook, child, parent, options = {}) - increase_indent = options[:increase_indent] - add_before = options[:add_before] - add_after = options[:add_after] - - transformed_file_name = file - transformed_content = content - transform_hook = hook - - target_file_content = File.read(transformed_file_name) - - if target_file_content.include?(transformed_content) - puts "No need to update '#{transformed_file_name}'. It already has '#{transformed_content}'." - else - new_target_file_content = [] - target_file_content.split("\n").each do |line| - if /#{Regexp.escape(transform_hook)}\s*$/.match?(line) - - if add_before - new_target_file_content << "#{line} #{add_before}" - else - unless options[:prepend] - new_target_file_content << line - end - end - - # get leading whitespace. - line =~ /^(\s*).*#{Regexp.escape(transform_hook)}.*/ - leading_whitespace = $1 - new_target_file_content << "#{leading_whitespace}#{" " if increase_indent}#{transformed_content}" - - new_target_file_content << "#{leading_whitespace}#{add_after}" if add_after - - if options[:prepend] - new_target_file_content << line - end - else - new_target_file_content << line - end - end - - puts "Updating '#{transformed_file_name}'." unless silence_logs? - - File.write(transformed_file_name, new_target_file_content.join("\n") + "\n") - end -end - -def encode_double_replacement_fix(string) - string.chars.join("~!@BT@!~") -end - -def decode_double_replacement_fix(string) - string.gsub("~!@BT@!~", "") -end - def oauth_scaffold_directory(directory, options) transformed_directory_name = oauth_transform_string(directory, options) + empty_transformer = Scaffolding::Transformer.new("", "") begin Dir.mkdir(transformed_directory_name) rescue Errno::EEXIST => _ @@ -82,9 +9,9 @@ def oauth_scaffold_directory(directory, options) puts "Proceeding to generate '#{transformed_directory_name}'." end - Dir.foreach(legacy_resolve_template_path(directory)) do |file| + Dir.foreach(empty_transformer.resolve_template_path(directory)) do |file| file = "#{directory}/#{file}" - unless File.directory?(legacy_resolve_template_path(file)) + unless File.directory?(empty_transformer.resolve_template_path(file)) oauth_scaffold_file(file, options) end end @@ -94,9 +21,10 @@ def oauth_scaffold_directory(directory, options) def oauth_scaffold_file(file, options) transformed_file_name = oauth_transform_string(file, options) transformed_file_content = [] + empty_transformer = Scaffolding::Transformer.new("", "") skipping = false - File.open(legacy_resolve_template_path(file)).each_line do |line| + File.open(empty_transformer.resolve_template_path(file)).each_line do |line| if line.include?("# πŸš… skip when scaffolding.") next end @@ -144,34 +72,36 @@ def oauth_scaffold_file(file, options) def oauth_transform_string(string, options) name = options[:our_provider_name] + empty_transformer = Scaffolding::Transformer.new("", "") # get these out of the way first. - string = string.gsub("stripe_connect: Stripe", encode_double_replacement_fix(options[:gems_provider_name] + ": " + name.titleize)) - string = string.gsub("ti-money", encode_double_replacement_fix(options[:icon])) if options[:icon] - string = string.gsub("omniauth-stripe-connect", encode_double_replacement_fix(options[:omniauth_gem])) - string = string.gsub("stripe_connect", encode_double_replacement_fix(options[:gems_provider_name])) - string = string.gsub("STRIPE_CLIENT_ID", encode_double_replacement_fix(options[:api_key])) - string = string.gsub("STRIPE_SECRET_KEY", encode_double_replacement_fix(options[:api_secret])) + string = string.gsub("stripe_connect: Stripe", empty_transformer.encode_double_replacement_fix(options[:gems_provider_name] + ": " + name.titleize)) + string = string.gsub("ti-money", empty_transformer.encode_double_replacement_fix(options[:icon])) if options[:icon] + string = string.gsub("omniauth-stripe-connect", empty_transformer.encode_double_replacement_fix(options[:omniauth_gem])) + string = string.gsub("stripe_connect", empty_transformer.encode_double_replacement_fix(options[:gems_provider_name])) + string = string.gsub("STRIPE_CLIENT_ID", empty_transformer.encode_double_replacement_fix(options[:api_key])) + string = string.gsub("STRIPE_SECRET_KEY", empty_transformer.encode_double_replacement_fix(options[:api_secret])) # then try for some matches that give us a little more context on what they're looking for. - string = string.gsub("stripe-account", encode_double_replacement_fix(name.underscore.dasherize + "_account")) - string = string.gsub("stripe_account", encode_double_replacement_fix(name.underscore + "_account")) - string = string.gsub("StripeAccount", encode_double_replacement_fix(name + "Account")) - string = string.gsub("Stripe Account", encode_double_replacement_fix(name.titleize + " Account")) - string = string.gsub("Stripe account", encode_double_replacement_fix(name.titleize + " account")) - string = string.gsub("with Stripe", encode_double_replacement_fix("with " + name.titleize)) + string = string.gsub("stripe-account", empty_transformer.encode_double_replacement_fix(name.underscore.dasherize + "_account")) + string = string.gsub("stripe_account", empty_transformer.encode_double_replacement_fix(name.underscore + "_account")) + string = string.gsub("StripeAccount", empty_transformer.encode_double_replacement_fix(name + "Account")) + string = string.gsub("Stripe Account", empty_transformer.encode_double_replacement_fix(name.titleize + " Account")) + string = string.gsub("Stripe account", empty_transformer.encode_double_replacement_fix(name.titleize + " account")) + string = string.gsub("with Stripe", empty_transformer.encode_double_replacement_fix("with " + name.titleize)) # finally, just do the simplest string replace. it's possible this can produce weird results. # if they do, try adding more context aware replacements above, e.g. what i did with 'with'. - string = string.gsub("stripe", encode_double_replacement_fix(name.underscore)) - string = string.gsub("Stripe", encode_double_replacement_fix(name)) + string = string.gsub("stripe", empty_transformer.encode_double_replacement_fix(name.underscore)) + string = string.gsub("Stripe", empty_transformer.encode_double_replacement_fix(name)) - decode_double_replacement_fix(string) + empty_transformer.decode_double_replacement_fix(string) end def oauth_scaffold_add_line_to_file(file, content, after, options, additional_options = {}) + empty_transformer = Scaffolding::Transformer.new("", "") file = oauth_transform_string(file, options) content = oauth_transform_string(content, options) after = oauth_transform_string(after, options) - legacy_add_line_to_file(file, content, after, nil, nil, additional_options) + empty_transformer.add_line_to_file(file, content, after, additional_options) end diff --git a/bullet_train-super_scaffolding/lib/scaffolding/script.rb b/bullet_train-super_scaffolding/lib/scaffolding/script.rb index e84dbb118..53bdc2f9c 100644 --- a/bullet_train-super_scaffolding/lib/scaffolding/script.rb +++ b/bullet_train-super_scaffolding/lib/scaffolding/script.rb @@ -1,6 +1,3 @@ -# TODO these methods were removed from the global scope in super scaffolding and moved to `Scaffolding::Transformer`, -# but oauth provider scaffolding hasn't been updated yet. - require "scaffolding" require "scaffolding/transformer" require "scaffolding/block_manipulator" @@ -97,7 +94,7 @@ def check_required_options_for_attributes(scaffolding_type, attributes, child, p def show_usage puts "" - puts "πŸš… usage: bin/super-scaffold [type] (... | --help)" + puts "πŸš… usage: bin/super-scaffold [type] (... | --help | --field-partials)" puts "" puts "Supported types of scaffolding:" puts "" @@ -122,11 +119,48 @@ def show_usage puts "To use the original Super Scaffolding that you know and love, use the `crud` option.".yellow show_usage -else - if ARGV.first.present? +elsif ARGV.first.present? + case ARGV.first + when "--field-partials" + puts "Bullet Train uses the following field partials for Super Scaffolding".blue puts "" + field_partials = { + boolean: "boolean", + buttons: "string", + cloudinary_image: "string", + color_picker: "string", + date_and_time_field: "datetime", + date_field: "date_field", + email_field: "string", + emoji_field: "string", + file_field: "attachment", + options: "string", + password_field: "string", + phone_field: "string", + super_select: "string", + text_area: "text", + text_field: "string", + number_field: "integer", + trix_editor: "text" + } + + max_name_length = 0 + field_partials.each do |key, value| + if key.to_s.length > max_name_length + max_name_length = key.to_s.length + end + end + + printf "\t%#{max_name_length}s:Data Type\n".bold, "Field Partial Name" + field_partials.each { |key, value| printf "\t%#{max_name_length}s:#{value}\n", key } + + puts "" + puts "For more details, check out the documentation:" + puts "https://bullettrain.co/docs/field-partials" + when "--help" + show_usage + else puts "Invalid scaffolding type \"#{ARGV.first}\".".red + show_usage end - - show_usage end diff --git a/bullet_train-super_scaffolding/lib/scaffolding/transformer.rb b/bullet_train-super_scaffolding/lib/scaffolding/transformer.rb index 2600f1ae4..140741e7d 100644 --- a/bullet_train-super_scaffolding/lib/scaffolding/transformer.rb +++ b/bullet_train-super_scaffolding/lib/scaffolding/transformer.rb @@ -76,9 +76,7 @@ def decode_double_replacement_fix(string) end def transform_string(string) - [ - - # full class name plural. + full_class_name = [ "Scaffolding::AbsolutelyAbstract::CreativeConcepts", "Scaffolding::CompletelyConcrete::TangibleThings", "ScaffoldingAbsolutelyAbstractCreativeConcepts", @@ -94,41 +92,19 @@ def transform_string(string) "scaffolding_completely_concrete_tangible_things", "scaffolding-absolutely-abstract-creative-concepts", "scaffolding-completely-concrete-tangible-things", + "scaffolding.completely_concrete.tangible_things" + ] - # full class name singular. - "Scaffolding::AbsolutelyAbstract::CreativeConcept", - "Scaffolding::CompletelyConcrete::TangibleThing", - "ScaffoldingAbsolutelyAbstractCreativeConcept", - "ScaffoldingCompletelyConcreteTangibleThing", - "Scaffolding Absolutely Abstract Creative Concept", - "Scaffolding Completely Concrete Tangible Thing", - "Scaffolding/Absolutely Abstract/Creative Concept", - "Scaffolding/Completely Concrete/Tangible Thing", - "scaffolding/absolutely_abstract/creative_concept", - "scaffolding/completely_concrete/tangible_thing", - "scaffolding_absolutely_abstract_creative_concept", - "scaffolding_completely_concrete_tangible_thing", - "scaffolding-absolutely-abstract-creative-concept", - "scaffolding-completely-concrete-tangible-thing", - "scaffolding.completely_concrete.tangible_things", - - # class name in context plural. + class_name_with_context = [ "absolutely_abstract_creative_concepts", "completely_concrete_tangible_things", "absolutely_abstract/creative_concepts", "completely_concrete/tangible_things", "absolutely-abstract-creative-concepts", "completely-concrete-tangible-things", + ] - # class name in context singular. - "absolutely_abstract_creative_concept", - "completely_concrete_tangible_thing", - "absolutely_abstract/creative_concept", - "completely_concrete/tangible_thing", - "absolutely-abstract-creative-concept", - "completely-concrete-tangible-thing", - - # just class name singular. + class_name = [ "creative_concepts", "tangible_things", "creative-concepts", @@ -139,24 +115,14 @@ def transform_string(string) "Tangible things", "creative concepts", "tangible things", + ] - # just class name plural. - "creative_concept", - "tangible_thing", - "creative-concept", - "tangible-thing", - "Creative Concept", - "Tangible Thing", - "Creative concept", - "Tangible thing", - "creative concept", - "tangible thing", - - # Account namespace vs. others. - ":account", - "/account/" - - ].each do |needle| + ( + full_class_name + full_class_name.map(&:singularize) + + class_name_with_context + class_name_with_context.map(&:singularize) + + class_name + class_name.map(&:singularize) + + [":account", "/account/"] # Account namespace vs. others. + ).each do |needle| string = string.gsub(needle, encode_double_replacement_fix(class_names_transformer.replacement_for(needle))) end @@ -175,9 +141,6 @@ def transform_string(string) def resolve_template_path(file) # Figure out the actual location of the file. - # Originally all the potential source files were in the repository alongside the application. - # Now the files could be provided by an included Ruby gem, so we allow those Ruby gems to register their base - # path and then we check them in order to see which template we should use. BulletTrain::SuperScaffolding.template_paths.map do |base_path| base_path = Pathname.new(base_path) resolved_path = base_path.join(file).to_s @@ -878,22 +841,14 @@ def valid_#{collection_name} field_options[:color_picker_options] = "t('#{child.pluralize.underscore}.fields.#{name}.options')" end - # TODO: This feels incorrect. - # Should we adjust the partials to only use `{multiple: true}` or `html_options: {multiple_true}`? - if is_multiple - if type == "super_select" - field_options[:multiple] = "true" - else - field_attributes[:multiple] = "true" - end - end - valid_values = if is_id "valid_#{name_without_id.pluralize}" elsif is_ids "valid_#{collection_name}" end + field_options[:multiple] = "true" if is_multiple + # https://stackoverflow.com/questions/21582464/is-there-a-ruby-hashto-s-equivalent-for-the-new-hash-syntax if field_options.any? || options.any? field_options_key = if ["buttons", "super_select", "options"].include?(type) @@ -1106,10 +1061,6 @@ def valid_#{collection_name} end special_processing = case type - when "date_field" - "assign_date(strong_params, :#{name})" - when "date_and_time_field" - "assign_date_and_time(strong_params, :#{name})" when "buttons" if boolean_buttons "assign_boolean(strong_params, :#{name})" @@ -1264,7 +1215,6 @@ def valid_#{collection_name} replace_in_file(migration_file_name, "foreign_key: true", "foreign_key: {to_table: \"#{attribute_options[:class_name].tableize.tr("/", "_")}\"}", /t\.references :#{name_without_id}/) replace_in_file(migration_file_name, "foreign_key: true", "foreign_key: {to_table: \"#{attribute_options[:class_name].tableize.tr("/", "_")}\"}", /add_reference :#{child.underscore.pluralize.tr("/", "_")}, :#{name_without_id}/) - # TODO also solve the 60 character long index limitation. modified_migration = true else add_additional_step :yellow, "We would have expected there to be a migration that defined `#{expected_reference}`, but we didn't find one. Where was the reference added to this model? It's _probably_ the original creation of the table. Either way, you need to rollback, change \"foreign_key: true\" to \"foreign_key: {to_table: '#{attribute_options[:class_name].tableize.tr("/", "_")}'}\" for this column, and re-run the migration." @@ -1300,14 +1250,17 @@ def valid_#{collection_name} # Add `default: false` to boolean migrations. if boolean_buttons - confirmation_reference = "create_table :#{class_names_transformer.table_name}" - confirmation_migration_file_name = `grep "#{confirmation_reference}" db/migrate/*`.split(":").first + # Give priority to crud-field migrations if they exist. + add_column_reference = "add_column :#{class_names_transformer.table_name}, :#{name}" + create_table_reference = "create_table :#{class_names_transformer.table_name}" + confirmation_migration_file_name = `grep "#{add_column_reference}" db/migrate/*`.split(":").first + confirmation_migration_file_name ||= `grep "#{create_table_reference}" db/migrate/*`.split(":").first old_line, new_line = nil File.open(confirmation_migration_file_name) do |migration_file| old_lines = migration_file.readlines old_lines.each do |line| - target_attribute = line.match?(/\s*t\.boolean :#{name}/) + target_attribute = line.match?(/:#{class_names_transformer.table_name}, :#{name}, :boolean/) || line.match?(/\s*t\.boolean :#{name}/) if target_attribute old_line = line new_line = "#{old_line.chomp}, default: false\n" @@ -1429,7 +1382,7 @@ def scaffold_crud(attributes) unless cli_options["skip-model"] # find the database migration that defines this relationship. - migration_file_name = `grep "create_table :#{class_names_transformer.table_name} do |t|" db/migrate/*`.split(":").first + migration_file_name = `grep "create_table :#{class_names_transformer.table_name}.*do |t|$" db/migrate/*`.split(":").first unless migration_file_name.present? raise "No migration file seems to exist for creating the table `#{class_names_transformer.table_name}`.\n" \ "Please run the following command first and try Super Scaffolding again:\n" \ @@ -1532,6 +1485,8 @@ def scaffold_crud(attributes) # add sortability. if cli_options["sortable"] + scaffold_replace_line_in_file("./app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb", transform_string("\">"), "") + unless cli_options["skip-model"] scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "def collection\n absolutely_abstract_creative_concept.completely_concrete_tangible_things\nend\n\n", METHODS_HOOK, prepend: true) scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "include Sortable\n", CONCERNS_HOOK, prepend: true) @@ -1543,10 +1498,6 @@ def scaffold_crud(attributes) Scaffolding::FileManipulator.write(migration, new_lines) end - unless cli_options["skip-table"] - scaffold_replace_line_in_file("./app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb", transform_string("\">"), "") - end - unless cli_options["skip-controller"] scaffold_add_line_to_file("./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb", "include SortableActions\n", "Account::ApplicationController", increase_indent: true) end @@ -1642,7 +1593,9 @@ def scaffold_crud(attributes) icon_name = cli_options["sidebar"] else puts "" - puts "Hey, models that are scoped directly off of a Team (or nothing) are eligible to be added to the sidebar." + # TODO: Update this help text letting developers know they can Super Scaffold + # models without a parent after the `--skip-parent` logic is implemented. + puts "Hey, models that are scoped directly off of a Team are eligible to be added to the navbar." puts "Do you want to add this resource to the sidebar menu? (y/N)" response = $stdin.gets.chomp if response.downcase[0] == "y" @@ -1693,7 +1646,7 @@ def scaffold_crud(attributes) end end - add_additional_step :yellow, transform_string("If you would like the table view you've just generated to reactively update when a Tangible Thing is updated on the server, please edit `app/models/scaffolding/absolutely_abstract/creative_concept.rb`, locate the `has_many :completely_concrete_tangible_things`, and add `enable_updates: true` to it.") + add_additional_step :yellow, transform_string("If you would like the table view you've just generated to reactively update when a Tangible Thing is updated on the server, please edit `app/models/scaffolding/absolutely_abstract/creative_concept.rb`, locate the `has_many :completely_concrete_tangible_things`, and add `enable_cable_ready_updates: true` to it.") restart_server unless ENV["CI"].present? end diff --git a/bullet_train-themes-light/.circleci/config.yml b/bullet_train-themes-light/.circleci/config.yml index 186d2a97d..95672bc25 100644 --- a/bullet_train-themes-light/.circleci/config.yml +++ b/bullet_train-themes-light/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-themes-light/app/assets/stylesheets/light/application.css b/bullet_train-themes-light/app/assets/stylesheets/light/application.css index 828ae95cf..0d33925c1 100644 --- a/bullet_train-themes-light/app/assets/stylesheets/light/application.css +++ b/bullet_train-themes-light/app/assets/stylesheets/light/application.css @@ -9,18 +9,6 @@ @import './fields'; @import './turn'; -/* TODO We should be able to do this in Tailwind CSS. */ -.hover-indent-child { - .indent-child { - transition: transform 0.2s ease; - } - &:hover { - .indent-child { - transform:translateX(8px); - } - } -} - form.button_to { @apply inline-block; input[type=submit] { @@ -33,3 +21,9 @@ form.button_to { /* hide the first breadcrumb chevron */ ol.breadcrumb li:first-child svg { display: none; } + +/* Override Turbo's progress bar color with the application's theme. */ +.turbo-progress-bar { + height: 5px; + @apply bg-primary-500; +} diff --git a/bullet_train-themes-light/app/assets/stylesheets/light/fields/super_select.css b/bullet_train-themes-light/app/assets/stylesheets/light/fields/super_select.css index ad7d44644..46505a3ec 100644 --- a/bullet_train-themes-light/app/assets/stylesheets/light/fields/super_select.css +++ b/bullet_train-themes-light/app/assets/stylesheets/light/fields/super_select.css @@ -39,6 +39,10 @@ } } +.select2-results__option--highlighted { + @apply bg-primary-400 !important; +} + @layer components { .select2-container { @apply w-full; @@ -89,7 +93,7 @@ .select2-container .select2-dropdown { @apply border-2 border-slate-300 shadow-sm overflow-hidden !important; - border-color: #5897fb !important; + @apply border-primary-500 !important; } .select2-container--default .select2-results__option[aria-selected=true] { diff --git a/bullet_train-themes-light/app/assets/stylesheets/light/fields/trix_editor.css b/bullet_train-themes-light/app/assets/stylesheets/light/fields/trix_editor.css index 855b68fdb..721a9e663 100644 --- a/bullet_train-themes-light/app/assets/stylesheets/light/fields/trix_editor.css +++ b/bullet_train-themes-light/app/assets/stylesheets/light/fields/trix_editor.css @@ -9,6 +9,27 @@ trix-toolbar { } } +/* Override Tailwind's preflight styles */ +/* https://github.com/tailwindlabs/tailwindcss/issues/989#issuecomment-506555308 */ +trix-editor ul { + list-style-type: disc; + padding-left: 2.5rem; +} + +trix-editor ol { + list-style-type: decimal; + padding-left: 2.5rem; +} + +.trix-button { + @apply dark:bg-slate-800 !important; +} + +/* Prevent red ring on Trix Editor */ +:-moz-focusring { + outline-style: none; +} + a[href^="bullettrain://"], span.tribute-reference { border-radius: 4px; display: inline-block; @@ -91,4 +112,64 @@ trix-editor { .trix-content { @apply text-base md:text-sm; -} \ No newline at end of file +} + +/* The Trix Editor uses SVGs for its buttons, so we manually override them here for dark mode */ +/* Icons taken from https://react-icons.github.io/react-icons/icons?name=md */ +@media (prefers-color-scheme: dark) { + .trix-button--icon-bold::before { + background-image: url('data:image/svg+xml;utf-8,') !important; + } + + .trix-button--icon-italic::before { + background-image: url('data:image/svg+xml;utf-8,') !important; + } + + .trix-button--icon-strike::before { + background-image: url('data:image/svg+xml;utf8,') !important; + } + + .trix-button--icon-link::before { + background-image: url('data:image/svg+xml;utf8,') !important; + } + + .trix-button--icon-heading-1::before { + background-image: url('data:image/svg+xml;utf8,') !important; + } + + .trix-button--icon-quote::before { + background-image: url('data:image/svg+xml;utf8,') !important; + } + + .trix-button--icon-code::before { + background-image: url('data:image/svg+xml;utf8,') !important; + } + + .trix-button--icon-bullet-list::before { + background-image: url('data:image/svg+xml;utf8,') !important; + } + + .trix-button--icon-number-list::before { + background-image: url('data:image/svg+xml;utf8,') !important; + } + + .trix-button--icon-decrease-nesting-level::before { + background-image: url('data:image/svg+xml;utf8,') !important; + } + + .trix-button--icon-increase-nesting-level::before { + background-image: url('data:image/svg+xml;utf8,') !important; + } + + .trix-button--icon-attach::before { + background-image: url('data:image/svg+xml;utf8,') !important; + } + + .trix-button--icon-undo::before { + background-image: url('data:image/svg+xml;utf8,') !important; + } + + .trix-button--icon-redo::before { + background-image: url('data:image/svg+xml;utf8,') !important; + } +} diff --git a/bullet_train-themes-light/app/assets/stylesheets/light/tailwind/utilities/billing.css b/bullet_train-themes-light/app/assets/stylesheets/light/tailwind/utilities/billing.css index 859e82ac6..0c0dd4f62 100644 --- a/bullet_train-themes-light/app/assets/stylesheets/light/tailwind/utilities/billing.css +++ b/bullet_train-themes-light/app/assets/stylesheets/light/tailwind/utilities/billing.css @@ -26,8 +26,8 @@ top: -200px; width: 1000px; height: 600px; - background-image: linear-gradient(90deg, rgba(227,231,248,0) 0%, #E8F2FC 100%); z-index: -1; + @apply bg-primary-200 bg-gradient-to-r from-white } position: absolute; @@ -35,9 +35,9 @@ top: 0px; width: 800px; height: 600px; - background-image: linear-gradient(90deg, rgba(227,231,248,0) 0%, #8BBAF5 100%); transform: translate(-360px, 0px) rotate(-45deg); z-index: -1; + @apply bg-primary-400 bg-gradient-to-r from-white } .brand-title { @@ -66,6 +66,6 @@ background-attachment: scroll; background-clip: text; background-color: rgba(0,0,0,0); - background-image: -webkit-linear-gradient(-75deg, #c094de 10%, #65A8F1 50%, #006EF4 95%); + @apply bg-slate-800 bg-gradient-to-r from-primary-500 } -} \ No newline at end of file +} diff --git a/bullet_train-themes-light/app/views/showcase/engine/_head.html.erb b/bullet_train-themes-light/app/views/showcase/engine/_head.html.erb new file mode 100644 index 000000000..530458797 --- /dev/null +++ b/bullet_train-themes-light/app/views/showcase/engine/_head.html.erb @@ -0,0 +1,7 @@ + diff --git a/bullet_train-themes-light/app/views/showcase/previews/partials/_breadcrumb.html.erb b/bullet_train-themes-light/app/views/showcase/previews/partials/_breadcrumb.html.erb index cdb46747f..9480e2d52 100644 --- a/bullet_train-themes-light/app/views/showcase/previews/partials/_breadcrumb.html.erb +++ b/bullet_train-themes-light/app/views/showcase/previews/partials/_breadcrumb.html.erb @@ -5,10 +5,9 @@ <% end %> -<%# TODO: Find a suitable url_for GET route here that every BulletTrain app would have. %> -<%# showcase.sample "With model url_for" do %> -<%#= render "shared/breadcrumb", url: [:account, Scaffolding::CompletelyConcrete::TangibleThing.new(id: 1)], label: "Concept" %> -<%# end %> +<% showcase.sample "With model url_for" do %> + <%= render 'account/shared/breadcrumb', label: t('memberships.label'), url: [:account, current_team, :memberships] %> +<% end %> <% showcase.options do |o| %> <% o.required :label, "The breadcrumb text, either shown in the link or the plain label" %> diff --git a/bullet_train-themes-light/app/views/showcase/previews/tailwind/utilities/_spacing.html.erb b/bullet_train-themes-light/app/views/showcase/previews/tailwind/utilities/_spacing.html.erb index dab9656fa..e1b2ea6ee 100644 --- a/bullet_train-themes-light/app/views/showcase/previews/tailwind/utilities/_spacing.html.erb +++ b/bullet_train-themes-light/app/views/showcase/previews/tailwind/utilities/_spacing.html.erb @@ -18,4 +18,20 @@ <% end %> -<% # TODO Add `gap-y` and `gap-x`. %> \ No newline at end of file +<% showcase.sample "Vertical Gaps" do %> +
+
Item
+
Item
+
Item
+
Item
+
+<% end %> + +<% showcase.sample "Horizontal Gaps" do %> +
+
Item
+
Item
+
Item
+
Item
+
+<% end %> diff --git a/bullet_train-themes-light/app/views/themes/light/_alert.html.erb b/bullet_train-themes-light/app/views/themes/light/_alert.html.erb index 285b480c7..f6e362a78 100644 --- a/bullet_train-themes-light/app/views/themes/light/_alert.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/_alert.html.erb @@ -7,8 +7,8 @@ when :yellow div_classes = "bg-amber-100 border-amber-200" header_classes = "text-amber-800" when :red - div_classes = "bg-red-300 border-red-400" - header_classes = "text-red-700" + div_classes = "bg-red-100 dark:bg-red-900 border-red-200 dark:border-red-700" + header_classes = "text-red-900 dark:text-red-100" end %> diff --git a/bullet_train-themes-light/app/views/themes/light/actions/_action.html.erb b/bullet_train-themes-light/app/views/themes/light/actions/_action.html.erb index 8248e8754..73675033d 100644 --- a/bullet_train-themes-light/app/views/themes/light/actions/_action.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/actions/_action.html.erb @@ -34,22 +34,22 @@ <% if action.completed? %> <% if action.is_a?(Actions::HasProgress) %>
- Processed <%= action.performed_count %> of <%= action.target_count %> <%= display_date_and_time(action.completed_at) %> + <%= t('action_models.processed_of', performed_count: action.performed_count, target_count: action.target_count, at: display_date_and_time(action.completed_at)) %>
<% else %>
- Processed <%= display_date_and_time(action.completed_at) %> + <%= t('action_models.processed', at: display_date_and_time(action.completed_at)) %>
<% end %> <% elsif action.is_a?(Actions::RequiresApproval) && !action.approved? %>
- Awaiting approval + <%= t('action_models.awaiting') %>
<% elsif action.is_a?(Actions::SupportsScheduling) && action.scheduled_for && !action.started? %>
- Scheduled for <%= display_date_and_time(action.scheduled_for) %> + <%= t('action_models.scheduled_for', at: display_date_and_time(action.scheduled_for)) %>
<% elsif action.is_a?(Actions::HasProgress) && action.started? %> @@ -58,17 +58,17 @@
- Processing <%= action.performed_count %> of <%= action.target_count %> + <%= t('action_models.processing_of', performed_count: action.performed_count, target_count: action.target_count) %>
<% elsif action.is_a?(Actions::HasProgress) %>
- Preparing to process <%= action.performed_count %> of <%= action.target_count %> + <%= t('action_models.preparing_to_process_of', performed_count: action.performed_count, target_count: action.target_count) %>
<% else %>
- Preparing to process + <%= t('action_models.preparing_to_process') %>
<% end %> diff --git a/bullet_train-themes-light/app/views/themes/light/attributes/_block.html.erb b/bullet_train-themes-light/app/views/themes/light/attributes/_block.html.erb index e555f6d64..ffd2944eb 100644 --- a/bullet_train-themes-light/app/views/themes/light/attributes/_block.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/attributes/_block.html.erb @@ -2,8 +2,8 @@ <% strategy ||= current_attributes_strategy || :none %> <% url ||= nil %> -<% if object.send(attribute).present? %> +<% if object.public_send(attribute).present? %> <%= render 'shared/attributes/attribute', object: object, attribute: attribute, strategy: strategy, url: url do %> -
<%= object.send(attribute) %>
+
<%= object.public_send(attribute) %>
<% end %> <% end %> diff --git a/bullet_train-themes-light/app/views/themes/light/attributes/_progress_bar.html.erb b/bullet_train-themes-light/app/views/themes/light/attributes/_progress_bar.html.erb index a25db3ccd..a38c0835c 100644 --- a/bullet_train-themes-light/app/views/themes/light/attributes/_progress_bar.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/attributes/_progress_bar.html.erb @@ -4,7 +4,7 @@ <% hide_completed ||= false %> <% if object.send(total).present? %> - <% completion_percent = (object.send(attribute).to_f / object.send(total).to_f) * 100.0 %> + <% completion_percent = (object.public_send(attribute).to_f / object.send(total).to_f) * 100.0 %> <% unless completion_percent == 100 && hide_completed %> <%= render 'shared/attributes/attribute', object: object, attribute: "#{attribute}_over_#{total}".to_sym, strategy: strategy, url: url do %> diff --git a/bullet_train-themes-light/app/views/themes/light/conversations/_card.html.erb b/bullet_train-themes-light/app/views/themes/light/conversations/_card.html.erb index 1c5e9c433..f07aa664f 100644 --- a/bullet_train-themes-light/app/views/themes/light/conversations/_card.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/conversations/_card.html.erb @@ -7,7 +7,7 @@ <%= current_membership.name %>
-

<%= "#{time_ago_in_words conversation.last_message.created_at} ago" unless conversation.last_message.nil? %>

+

<%= "#{t('global.time_ago', time: time_ago_in_words(conversation.last_message.created_at))}" if conversation.last_message.present? %>

<%= conversation.subject&.label_string %>

<%= conversation.last_message&.user&.label_string %> diff --git a/bullet_train-themes-light/app/views/themes/light/conversations/_comment.html.erb b/bullet_train-themes-light/app/views/themes/light/conversations/_comment.html.erb index b0483db4e..0094119ba 100644 --- a/bullet_train-themes-light/app/views/themes/light/conversations/_comment.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/conversations/_comment.html.erb @@ -7,7 +7,8 @@
<%= avatar %>
-
<%= message.user.name %> • <%= time_ago_in_words message.created_at %> ago
+
<%= message.user.name %> • <%= t("global.time_ago", time: time_ago_in_words(message.created_at)) %> +
<%= trix_sanitize message.body %>
<% if message.replies.any? %>
diff --git a/bullet_train-themes-light/app/views/themes/light/conversations/_message.html.erb b/bullet_train-themes-light/app/views/themes/light/conversations/_message.html.erb index f4181d78c..d28e8322c 100644 --- a/bullet_train-themes-light/app/views/themes/light/conversations/_message.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/conversations/_message.html.erb @@ -34,7 +34,7 @@ <% rounding_modifier = message_corner_class(next_message_in_series, current_user_message) %> -<% timestamp = next_message_in_series || show_as_thread ? '' : time_ago_in_words(message.created_at) + " ago" %> +<% timestamp = next_message_in_series || show_as_thread ? '' : t("global.time_ago", time: time_ago_in_words(message.created_at)) %> <% threaded_message = message.threaded? %> <% thread_started_by_current_user = message.thread_origin_user == current_author %> @@ -126,7 +126,7 @@
-
<%= reply.membership.label_string %> reply:
+
<%= reply.membership.label_string %> <%= t('conversations.message.reply') %>
<%= reply.body.html_safe %>
diff --git a/bullet_train-themes-light/app/views/themes/light/fields/_field.html.erb b/bullet_train-themes-light/app/views/themes/light/fields/_field.html.erb index 4f044e03c..e8289f7eb 100644 --- a/bullet_train-themes-light/app/views/themes/light/fields/_field.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/fields/_field.html.erb @@ -1,4 +1,11 @@ -<% yield %> +<% +%i[label field error help after_help].each do |key| + if (content = content_for(key).presence) + flush_content_for key + partial.section key, content + end +end +%> <% form ||= current_fields_form @@ -12,7 +19,7 @@ other_options ||= {} other_options[:help] = [other_options[:help], labels.help].compact.join(" ") errors = [method, method.to_s.gsub(/_id$/, '').to_sym].uniq.map { |attribute| form.object.errors.full_messages_for(attribute) }.flatten -has_errors = errors.any? || content_for(:error).present? || other_options[:error].present? +has_errors = errors.any? || partial.error? || other_options[:error].present? options[:class] = "#{options[:class]} block w-full rounded-md shadow-sm font-light text-sm dark:bg-slate-800 dark:text-slate-300" @@ -28,9 +35,8 @@ end <% # the label. %> <% unless other_options[:hide_label] == true %> - <% if content_for? :label %> - <%= yield :label %> - <% flush_content_for :label %> + <% if partial.label? %> + <%= partial.label %> <% else %> <% # allow the label to be defined via an inline option or else one of the locale yaml definitions. %> <% label = (other_options[:label].presence || labels.label || legacy_label_for(form, method)) %> @@ -39,24 +45,20 @@ end <% end %>
- <% # the actual field. %> - <% if content_for? :field %> - <%= yield :field %> - <% flush_content_for :field %> + <% if partial.field? %> + <%= partial.field %> <% else %> <% # e.g. form.text_field(method, options) %> <%= form.send(helper, method, options) %> <% end %> -
<% # any error messages. %> <% if has_errors %>

<%= errors.map { |error| error + ". " }.join %> - <%= yield :error %> - <% flush_content_for :error %> + <%= partial.error %> <% if other_options[:hide_custom_error].blank? %> <%= other_options[:error]&.html_safe %> <% end %> @@ -64,13 +66,11 @@ end <% end %> <% # any help text. %> - <% if content_for?(:help) || other_options[:help].present? || content_for?(:after_help) %> + <% if partial.help? || other_options[:help].present? || partial.after_help? %>

- <%= yield :help %> - <% flush_content_for :help %> + <%= partial.help %> <%= other_options[:help]&.html_safe %> - <%= yield :after_help %> - <% flush_content_for :after_help %> + <%= partial.after_help %>

<% end %> diff --git a/bullet_train-themes-light/app/views/themes/light/layouts/_devise.html.erb b/bullet_train-themes-light/app/views/themes/light/layouts/_devise.html.erb index e6a502f49..0c0cd5c3c 100644 --- a/bullet_train-themes-light/app/views/themes/light/layouts/_devise.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/layouts/_devise.html.erb @@ -1,4 +1,5 @@ +"> <%= render 'shared/layouts/head' %> diff --git a/bullet_train-themes-light/app/views/themes/light/layouts/_pricing.html.erb b/bullet_train-themes-light/app/views/themes/light/layouts/_pricing.html.erb index 9c41388fa..ab40492ed 100644 --- a/bullet_train-themes-light/app/views/themes/light/layouts/_pricing.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/layouts/_pricing.html.erb @@ -1,5 +1,5 @@ - +"> <%= render 'shared/layouts/head' %> @@ -13,7 +13,7 @@
<%= link_to @back, class: 'button-secondary hover:no-underline group' do %> - Back + <%= t('navigation.back') %> <% end %>
<% end %> diff --git a/bullet_train-themes-light/app/views/themes/light/menu/_close.html.erb b/bullet_train-themes-light/app/views/themes/light/menu/_close.html.erb index f5bc8161b..824a9f0fc 100644 --- a/bullet_train-themes-light/app/views/themes/light/menu/_close.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/menu/_close.html.erb @@ -3,7 +3,7 @@ id="mobile-menu-close" data-action="mobile-menu#close" > - Close Application Menu + <%= t('navigation.close_app_menu') %> diff --git a/bullet_train-themes-light/app/views/themes/light/menu/_item.html.erb b/bullet_train-themes-light/app/views/themes/light/menu/_item.html.erb index 0cb602f0b..c8d23276f 100644 --- a/bullet_train-themes-light/app/views/themes/light/menu/_item.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/menu/_item.html.erb @@ -1,8 +1,8 @@ <% method ||= nil %> <% active ||= request.path == url %> -<%= send (method ? :button_to : :link_to), url, class: "block group hover:text-white hover:no-underline #{'bg-primary-900 dark:bg-black dark:bg-opacity-10' if active} text-white #{@menu_orientation == :top ? "px-5 py-5" : "px-2 py-2 rounded-md hover-indent-child"} #{"rounded-tl-lg" unless @not_first} dark:text-white", data: {"desktop-menu-target": "menuItemLink"}, tabIndex: 0, method: method do %> -
+<%= send (method ? :button_to : :link_to), url, class: "block group/item hover:text-white hover:no-underline #{'bg-primary-900 dark:bg-black dark:bg-opacity-10' if active} text-white #{@menu_orientation == :top ? "px-5 py-5" : "px-3 py-2 rounded-md"} #{"rounded-tl-lg" unless @not_first || BulletTrain::Themes::Light.show_logo_in_account} dark:text-white", data: {"desktop-menu-target": "menuItemLink"}, tabIndex: 0, method: method do %> +
flex items-center"> <% if partial.icon? %> @@ -14,4 +14,4 @@ <% end %> <% # Ugly, but works. %> -<% @not_first = true %> \ No newline at end of file +<% @not_first = true %> diff --git a/bullet_train-themes-light/app/views/themes/light/menu/_open.html.erb b/bullet_train-themes-light/app/views/themes/light/menu/_open.html.erb index ea11a750b..c9016a851 100644 --- a/bullet_train-themes-light/app/views/themes/light/menu/_open.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/menu/_open.html.erb @@ -2,8 +2,8 @@ id="mobile-menu-open" data-action="mobile-menu#open" > - Open Application Menu + <%= t('navigation.open_app_menu') %> - \ No newline at end of file + diff --git a/bullet_train-themes-light/app/views/themes/light/menu/_subsection.html.erb b/bullet_train-themes-light/app/views/themes/light/menu/_subsection.html.erb index 759ca9c19..56d504ccb 100644 --- a/bullet_train-themes-light/app/views/themes/light/menu/_subsection.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/menu/_subsection.html.erb @@ -10,7 +10,7 @@
<% else %>
-
+
<%= render 'account/shared/menu/heading' do %> <%= title %> diff --git a/bullet_train-themes-light/bullet_train-themes-light.gemspec b/bullet_train-themes-light/bullet_train-themes-light.gemspec index 319fb74e5..584df696e 100644 --- a/bullet_train-themes-light/bullet_train-themes-light.gemspec +++ b/bullet_train-themes-light/bullet_train-themes-light.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::Themes::Light::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-themes-light" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-themes-light" spec.summary = "Bullet Train Themes: Light" spec.description = "Bullet Train Themes: Light" spec.license = "MIT" diff --git a/bullet_train-themes-light/config/locales/en/action_models.en.yml b/bullet_train-themes-light/config/locales/en/action_models.en.yml new file mode 100644 index 000000000..278bb7d97 --- /dev/null +++ b/bullet_train-themes-light/config/locales/en/action_models.en.yml @@ -0,0 +1,9 @@ +en: + action_models: + processed: Processed %{at} + processed_of: Processed %{performed_count} of %{target_count} %{at} + awaiting: Awaiting approval + scheduled_for: Scheduled for %{for} + processing_of: Processing %{performed_count} of %{target_count} + preparing_to_process: Preparing to process + preparing_to_process_of: Preparing to process %{performed_count} of %{target_count} diff --git a/bullet_train-themes-light/config/locales/en/conversations.en.yml b/bullet_train-themes-light/config/locales/en/conversations.en.yml new file mode 100644 index 000000000..df1a5e852 --- /dev/null +++ b/bullet_train-themes-light/config/locales/en/conversations.en.yml @@ -0,0 +1,4 @@ +en: + conversations: + message: + reply: "reply:" diff --git a/bullet_train-themes-light/config/locales/en/navigation.en.yml b/bullet_train-themes-light/config/locales/en/navigation.en.yml new file mode 100644 index 000000000..244197091 --- /dev/null +++ b/bullet_train-themes-light/config/locales/en/navigation.en.yml @@ -0,0 +1,5 @@ +en: + navigation: + close_app_menu: Close Application Menu + open_app_menu: Open Application Menu + back: Back diff --git a/bullet_train-themes-light/lib/bullet_train/themes/light.rb b/bullet_train-themes-light/lib/bullet_train/themes/light.rb index e0db43985..f37bd361d 100644 --- a/bullet_train-themes-light/lib/bullet_train/themes/light.rb +++ b/bullet_train-themes-light/lib/bullet_train/themes/light.rb @@ -14,7 +14,6 @@ module Light mattr_accessor :logo_color_shift, default: false mattr_accessor :show_logo_in_account, default: false mattr_accessor :navigation, default: :top - mattr_accessor :original_devise_path # TODO: Obsolete: remove after shipping a new BulletTrain version with usage removed. class Theme < BulletTrain::Themes::TailwindCss::Theme def directory_order diff --git a/bullet_train-themes-light/lib/bullet_train/themes/light/version.rb b/bullet_train-themes-light/lib/bullet_train/themes/light/version.rb index ffa9b037f..686d7d0e4 100644 --- a/bullet_train-themes-light/lib/bullet_train/themes/light/version.rb +++ b/bullet_train-themes-light/lib/bullet_train/themes/light/version.rb @@ -1,7 +1,7 @@ module BulletTrain module Themes module Light - VERSION = "1.2.21" + VERSION = "1.2.27" end end end diff --git a/bullet_train-themes-light/lib/tasks/application.rb b/bullet_train-themes-light/lib/tasks/application.rb index 8ba49f5ac..f76bdb121 100644 --- a/bullet_train-themes-light/lib/tasks/application.rb +++ b/bullet_train-themes-light/lib/tasks/application.rb @@ -48,6 +48,7 @@ def self.eject_theme(theme_name, ejected_theme_name) end %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/#{theme_name}/#{ejected_theme_name}/g" #{Rails.root}/app/views/themes/#{ejected_theme_name}/layouts/_head.html.erb) + %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/#{theme_name}/#{ejected_theme_name}/g" #{Rails.root}/app/views/themes/#{ejected_theme_name}/layouts/_mailer.html.erb) puts "Cutting local `Procfile.dev` over from `#{theme_name}` to `#{ejected_theme_name}`." %x(sed -i #{'""' if `echo $OSTYPE`.include?("darwin")} "s/#{theme_name}/#{ejected_theme_name}/g" #{Rails.root}/Procfile.dev) diff --git a/bullet_train-themes-tailwind_css/.circleci/config.yml b/bullet_train-themes-tailwind_css/.circleci/config.yml index 0e91c60cc..802d06e10 100644 --- a/bullet_train-themes-tailwind_css/.circleci/config.yml +++ b/bullet_train-themes-tailwind_css/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/attributes/_code.html.erb b/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/attributes/_code.html.erb index e3b06388c..08b1560c3 100644 --- a/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/attributes/_code.html.erb +++ b/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/attributes/_code.html.erb @@ -3,10 +3,10 @@ <% source ||= nil %> <% url ||= nil %> <% secret ||= false %> -<% if object.send(attribute).present? %> +<% if object.public_send(attribute).present? %> <%= render 'shared/attributes/attribute', object: object, attribute: attribute, strategy: strategy, url: url do %> <% if secret && source.nil? %> - <% attribute = object.send(attribute) %> + <% attribute = object.public_send(attribute) %> <% attribute_short = attribute[0..4] %>
diff --git a/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/attributes/_file.erb b/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/attributes/_file.erb index 3659d6478..dced2640b 100644 --- a/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/attributes/_file.erb +++ b/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/attributes/_file.erb @@ -2,11 +2,11 @@ <% strategy ||= current_attributes_strategy || :none %> <% url ||= nil %> -<% if object.send(attribute).attached? %> +<% if object.public_send(attribute).attached? %> <%= render 'shared/attributes/attribute', object: object, attribute: attribute, strategy: strategy, url: url do %> - <%= link_to url_for(object.send(attribute)), class: 'button download-file' do %> + <%= link_to url_for(object.public_send(attribute)), class: 'button download-file' do %> - Download File + <%= t("global.file_fields.download") %> <% end %> <% end %> <% end %> diff --git a/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_buttons.html.erb b/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_buttons.html.erb index 1d7452bae..0ccc68ba9 100644 --- a/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_buttons.html.erb +++ b/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_buttons.html.erb @@ -1,5 +1,3 @@ -<% yield %> - <% stimulus_controller = 'fields--button-toggle' form ||= current_fields_form diff --git a/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_cloudinary_image.html.erb b/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_cloudinary_image.html.erb index c22198ef7..94a4ed9a4 100644 --- a/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_cloudinary_image.html.erb +++ b/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_cloudinary_image.html.erb @@ -1,5 +1,3 @@ -<% yield %> - <% if cloudinary_enabled? %> <% diff --git a/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_color_picker.html.erb b/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_color_picker.html.erb index a8afe5852..6224537e0 100644 --- a/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_color_picker.html.erb +++ b/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_color_picker.html.erb @@ -1,5 +1,3 @@ -<% yield %> - <% stimulus_controller = 'fields--color-picker' diff --git a/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_date_and_time_field.html.erb b/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_date_and_time_field.html.erb index 8ec0cb6f6..a965a6819 100644 --- a/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_date_and_time_field.html.erb +++ b/bullet_train-themes-tailwind_css/app/views/themes/tailwind_css/fields/_date_and_time_field.html.erb @@ -1,45 +1,73 @@ -<% yield %> - <% +stimulus_controller = 'fields--date' + form ||= current_fields_form options ||= {} -options[:id] ||= form.field_id(method) +user_tz = ActiveSupport::TimeZone.find_tzinfo(current_user.time_zone).name +team_tz = ActiveSupport::TimeZone.find_tzinfo(current_user.current_team.time_zone).name + +# data options +data_options ||= {} +data_options[:id] ||= form.field_id(method) +data_options[:class] = "hidden" +data_options[:value] = form.object.send(method)&.in_time_zone(current_time_zone)&.iso8601 +data_options = data_options.merge({ data: {"#{stimulus_controller}-target": 'field' }}) + +# localized display options +options[:id] ||= "#{form.field_id(method)}_display" options[:class] = "form-control single-daterange w-full border-slate-300 dark:bg-slate-800 dark:border-slate-900 #{options[:class]}".strip -options[:value] = form.object.send(method)&.in_time_zone(current_team.time_zone)&.strftime(t('global.formats.date_and_time')) -options = options.merge({ data: {'fields--date-target': 'field' }}) +raw_value = form.object.send(method)&.in_time_zone(current_user.time_zone || current_team.time_zone || "UTC") +options[:value] = raw_value && I18n.l(raw_value, format: :date_and_time_field) +options = options.merge({ data: {"#{stimulus_controller}-target": 'displayField' }}) + other_options ||= {} %> <%= render 'shared/fields/field', form: form, method: method, options: options, other_options: other_options do %> <% content_for :field do %> -
"> +
-include-time-value="true" + data-<%= stimulus_controller %>-date-format-value="<%= I18n.t('date.formats.date_controller') %>" + data-<%= stimulus_controller %>-time-format-value="<%= I18n.t('time.formats.date_controller') %>" + data-<%= stimulus_controller %>-is-am-pm-value="<%= am_pm? %>" + data-<%= stimulus_controller %>-picker-locale-value="<%= I18n.t("daterangepicker").deep_transform_keys { |k| k.to_s.camelize(:lower) }.to_json %>" + data-<%= stimulus_controller %>-default-time-zones-value="<%= "[\"#{current_user.time_zone}\",\"#{current_user.current_team.time_zone}\"]" %>" + data-<%= stimulus_controller %>-current-time-zone-value="<%= current_time_zone %>" + > <%= form.text_field method, options %> + <%= form.text_field method, data_options %> <% unless options[:disabled] %> <% end %> <% if current_user.time_zone != current_user.current_team.time_zone %>
- <%= form.hidden_field "#{method}_time_zone".to_sym, value: current_user.current_team.time_zone, data: {'fields--date-target': 'timeZoneField'} %> -
- <%= link_to current_user.current_team.time_zone, '#', class: 'button-secondary p-0', data: {action: 'fields--date#showTimeZoneButtons'} %> + -target="timeZoneField" value="<%= current_time_zone %>"> +
-target="currentTimeZoneWrapper"> + <%= link_to (current_user.time_zone || current_user.current_team.time_zone || "UTC"), '#', class: 'button-secondary p-0', data: {action: "#{stimulus_controller}#showTimeZoneButtons"} %>
- diff --git a/bullet_train-themes/app/views/themes/base/fields/_phone_field.html.erb b/bullet_train-themes/app/views/themes/base/fields/_phone_field.html.erb index 8a5ef57f7..a1f399396 100644 --- a/bullet_train-themes/app/views/themes/base/fields/_phone_field.html.erb +++ b/bullet_train-themes/app/views/themes/base/fields/_phone_field.html.erb @@ -1,5 +1,3 @@ -<% yield %> - <% stimulus_controller = 'fields--phone' diff --git a/bullet_train-themes/app/views/themes/base/fields/_super_select.html.erb b/bullet_train-themes/app/views/themes/base/fields/_super_select.html.erb index 7ce594e9d..6c6323ba2 100644 --- a/bullet_train-themes/app/views/themes/base/fields/_super_select.html.erb +++ b/bullet_train-themes/app/views/themes/base/fields/_super_select.html.erb @@ -1,5 +1,3 @@ -<% yield %> - <% stimulus_controller = 'fields--super-select' diff --git a/bullet_train-themes/app/views/themes/base/fields/_text_area.html.erb b/bullet_train-themes/app/views/themes/base/fields/_text_area.html.erb index fd18d12f9..b0350d3b4 100644 --- a/bullet_train-themes/app/views/themes/base/fields/_text_area.html.erb +++ b/bullet_train-themes/app/views/themes/base/fields/_text_area.html.erb @@ -1,5 +1,3 @@ -<% yield %> - <% form ||= current_fields_form options ||= {} diff --git a/bullet_train-themes/app/views/themes/base/fields/_text_field.html.erb b/bullet_train-themes/app/views/themes/base/fields/_text_field.html.erb index c7d68f2fa..e857e069b 100644 --- a/bullet_train-themes/app/views/themes/base/fields/_text_field.html.erb +++ b/bullet_train-themes/app/views/themes/base/fields/_text_field.html.erb @@ -1,5 +1,3 @@ -<% yield %> - <% form ||= current_fields_form options ||= {} diff --git a/bullet_train-themes/bullet_train-themes.gemspec b/bullet_train-themes/bullet_train-themes.gemspec index 5ecb62c88..553476972 100644 --- a/bullet_train-themes/bullet_train-themes.gemspec +++ b/bullet_train-themes/bullet_train-themes.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::Themes::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-themes" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-themes" spec.summary = "Bullet Train Themes" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-themes/config/locales/en/fields.en.yml b/bullet_train-themes/config/locales/en/fields.en.yml new file mode 100644 index 000000000..126fe158e --- /dev/null +++ b/bullet_train-themes/config/locales/en/fields.en.yml @@ -0,0 +1,3 @@ +en: + fields: + choose_emoji: Choose an Emoji diff --git a/bullet_train-themes/lib/bullet_train/themes.rb b/bullet_train-themes/lib/bullet_train/themes.rb index c824ab2b7..89576c0fb 100644 --- a/bullet_train-themes/lib/bullet_train/themes.rb +++ b/bullet_train-themes/lib/bullet_train/themes.rb @@ -9,7 +9,6 @@ module Themes mattr_reader :partial_paths, default: {} - # TODO Do we want this to be configurable by downstream applications? INVOCATION_PATTERNS = [ # ❌ This path is included for legacy purposes, but you shouldn't reference partials like this in new code. /^account\/shared\//, diff --git a/bullet_train-themes/lib/bullet_train/themes/version.rb b/bullet_train-themes/lib/bullet_train/themes/version.rb index e1f7e2df5..e7256a1c4 100644 --- a/bullet_train-themes/lib/bullet_train/themes/version.rb +++ b/bullet_train-themes/lib/bullet_train/themes/version.rb @@ -1,5 +1,5 @@ module BulletTrain module Themes - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train/.circleci/config.yml b/bullet_train/.circleci/config.yml index 2333ab6ae..cef477bd3 100644 --- a/bullet_train/.circleci/config.yml +++ b/bullet_train/.circleci/config.yml @@ -49,7 +49,7 @@ aliases: paths: - tmp/starter/node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train/Gemfile.lock b/bullet_train/Gemfile.lock index 34803e663..af570fdb9 100644 --- a/bullet_train/Gemfile.lock +++ b/bullet_train/Gemfile.lock @@ -8,7 +8,7 @@ GIT PATH remote: . specs: - bullet_train (1.2.21) + bullet_train (1.2.27) awesome_print bullet_train-fields bullet_train-has_uuid @@ -17,9 +17,9 @@ PATH bullet_train-scope_validator bullet_train-super_load_and_authorize_resource bullet_train-themes - cable_ready (= 5.0.0.pre9) + cable_ready (~> 5.0.0) cancancan - commonmarker + commonmarker (>= 1.0.0.pre10) devise devise-pwned_password extended_email_reply_parser @@ -33,6 +33,7 @@ PATH possessive premailer-rails rails (>= 6.0.0) + ruby-openai showcase-rails sidekiq unicode-emoji @@ -107,75 +108,83 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.1) + addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) awesome_print (1.9.2) aws_cf_signer (0.1.3) - bcrypt (3.1.18) + bcrypt (3.1.19) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) builder (3.2.4) - bullet_train-fields (1.2.20) + bullet_train-fields (1.2.26) chronic cloudinary phonelib rails (>= 6.0.0) - bullet_train-has_uuid (1.2.20) + bullet_train-has_uuid (1.2.26) rails (>= 6.0.0) - bullet_train-roles (1.2.20) + bullet_train-roles (1.2.26) active_hash activesupport cancancan bullet_train-routes (1.0.0) rails (>= 6.0.0) - bullet_train-scope_validator (1.2.20) - bullet_train-super_load_and_authorize_resource (1.2.20) + bullet_train-scope_validator (1.2.26) + bullet_train-super_load_and_authorize_resource (1.2.26) rails (>= 6.0.0) - bullet_train-themes (1.2.20) + bullet_train-themes (1.2.26) rails (>= 6.0.0) - cable_ready (5.0.0.pre9) - actioncable (>= 5.2) + cable_ready (5.0.1) actionpack (>= 5.2) actionview (>= 5.2) - activerecord (>= 5.2) activesupport (>= 5.2) railties (>= 5.2) thread-local (>= 1.1.0) cancancan (3.5.0) charlock_holmes (0.7.7) chronic (0.10.2) - cloudinary (1.25.0) + cloudinary (1.26.0) aws_cf_signer rest-client (>= 2.0.0) coderay (1.1.3) - commonmarker (0.23.8) - concurrent-ruby (1.1.10) - connection_pool (2.3.0) + commonmarker (1.0.0.pre10-aarch64-linux) + commonmarker (1.0.0.pre10-arm64-darwin) + commonmarker (1.0.0.pre10-x64-mingw-ucrt) + commonmarker (1.0.0.pre10-x86_64-darwin) + commonmarker (1.0.0.pre10-x86_64-linux) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) crass (1.0.6) css_parser (1.14.0) addressable date (3.3.3) debug_inspector (1.1.0) - devise (4.9.0) + devise (4.9.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-pwned_password (0.1.9) + devise-pwned_password (0.1.10) devise (~> 4) pwned (~> 2.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - email_reply_parser (0.5.10) - erubi (1.10.0) + email_reply_parser (0.5.11) + erubi (1.12.0) extended_email_reply_parser (0.5.1) activesupport charlock_holmes email_reply_parser (~> 0.5.9) mail - fastimage (2.2.6) + faraday (2.7.10) + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (3.0.2) + fastimage (2.2.7) figaro (1.2.0) thor (>= 0.14.0, < 2) globalid (1.1.0) @@ -190,7 +199,7 @@ GEM concurrent-ruby (~> 1.0) json (2.6.3) language_server-protocol (3.17.0.2) - loofah (2.18.0) + loofah (2.20.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.8.1) @@ -207,9 +216,10 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2023.0218.1) mini_mime (1.1.2) - mini_portile2 (2.8.0) - minitest (5.16.2) - net-imap (0.3.4) + mini_portile2 (2.8.1) + minitest (5.18.0) + multipart-post (2.3.0) + net-imap (0.3.7) date net-protocol net-pop (0.1.2) @@ -221,18 +231,18 @@ GEM netrc (0.11.0) nice_partials (0.9.3) actionview (>= 4.2.6) - nio4r (2.5.8) - nokogiri (1.13.8) + nio4r (2.5.9) + nokogiri (1.14.3) mini_portile2 (~> 2.8.0) racc (~> 1.4) orm_adapter (0.5.0) - pagy (6.0.2) + pagy (6.0.4) parallel (1.22.1) parser (3.1.3.0) ast (~> 2.4.1) - phonelib (0.7.7) + phonelib (0.8.2) possessive (1.0.1) - premailer (1.20.0) + premailer (1.21.0) addressable css_parser (>= 1.12.0) htmlentities (>= 4.0.0) @@ -246,11 +256,11 @@ GEM pry-stack_explorer (0.6.1) binding_of_caller (~> 1.0) pry (~> 0.13) - public_suffix (5.0.1) + public_suffix (5.0.3) pwned (2.0.2) - racc (1.6.0) - rack (2.2.4) - rack-test (2.0.2) + racc (1.6.2) + rack (2.2.6.4) + rack-test (2.1.0) rack (>= 1.3) rails (7.0.3.1) actioncable (= 7.0.3.1) @@ -269,8 +279,8 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.3) - loofah (~> 2.3) + rails-html-sanitizer (1.5.0) + loofah (~> 2.19, >= 2.19.1) railties (7.0.3.1) actionpack (= 7.0.3.1) activesupport (= 7.0.3.1) @@ -280,7 +290,7 @@ GEM zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) - redis-client (0.14.0) + redis-client (0.14.1) connection_pool regexp_parser (2.6.1) responders (3.1.0) @@ -307,14 +317,18 @@ GEM rubocop-performance (1.15.1) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) + ruby-openai (4.2.0) + faraday (>= 1) + faraday-multipart (>= 1) ruby-progressbar (1.11.0) - showcase-rails (0.2.6) + ruby2_keywords (0.0.5) + showcase-rails (0.4.5) rails (>= 6.1.0) - sidekiq (7.0.6) + sidekiq (7.1.2) concurrent-ruby (< 2) connection_pool (>= 2.3.0) rack (>= 2.2.4) - redis-client (>= 0.11.0) + redis-client (>= 0.14.0) simpleidn (0.2.1) unf (~> 0.1.4) sprockets (4.1.1) @@ -329,16 +343,16 @@ GEM language_server-protocol (~> 3.17.0.2) rubocop (= 1.39.0) rubocop-performance (= 1.15.1) - thor (1.2.1) + thor (1.2.2) thread-local (1.1.0) - timeout (0.3.2) - tzinfo (2.0.5) + timeout (0.4.0) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext unf_ext (0.0.8.2) unicode-display_width (2.3.0) - unicode-emoji (3.3.1) + unicode-emoji (3.3.2) unicode-version (~> 1.0) unicode-version (1.3.0) valid_email (0.1.4) @@ -347,11 +361,11 @@ GEM simpleidn warden (1.2.9) rack (>= 2.0.9) - websocket-driver (0.7.5) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xxhash (0.5.0) - zeitwerk (2.6.7) + zeitwerk (2.6.9) PLATFORMS ruby diff --git a/bullet_train/README.md b/bullet_train/README.md deleted file mode 100644 index 26ad6816e..000000000 --- a/bullet_train/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# BulletTrain -Short description and motivation. - -## Usage -How to use my plugin. - -## Installation -Add this line to your application's Gemfile: - -```ruby -gem "bullet_train" -``` - -And then execute: -```bash -bundle -``` - -Or install it yourself as: -```bash -gem install bullet_train -``` - -## Contributing - -See `RELEASE.md` for instructions on local development and for publishing both the gem and the npm package. - -## License -The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). diff --git a/bullet_train/app/controllers/concerns/account/controllers/base.rb b/bullet_train/app/controllers/concerns/account/controllers/base.rb index 43881bfc1..022aa6482 100644 --- a/bullet_train/app/controllers/concerns/account/controllers/base.rb +++ b/bullet_train/app/controllers/concerns/account/controllers/base.rb @@ -123,4 +123,6 @@ def ensure_onboarding_is_complete def set_last_seen_at current_user.update_attribute(:last_seen_at, Time.current) end + + ActiveSupport.run_load_hooks :bullet_train_account_controllers_base, self end diff --git a/bullet_train/app/controllers/concerns/account/invitations/controller_base.rb b/bullet_train/app/controllers/concerns/account/invitations/controller_base.rb index 8bdcce978..fed6bad0f 100644 --- a/bullet_train/app/controllers/concerns/account/invitations/controller_base.rb +++ b/bullet_train/app/controllers/concerns/account/invitations/controller_base.rb @@ -73,6 +73,17 @@ def accept end end + # POST /invitations/1/resend + def resend + @invitation = Invitation.find_by(uuid: params[:id]) + if @invitation + UserMailer.invited(params[:id]).deliver_later + redirect_to account_team_invitations_path(@invitation.membership.team), notice: I18n.t("invitations.notifications.resent") + else + redirect_to account_dashboard_path, alert: I18n.t("invitations.notifications.resent_error") + end + end + # GET /invitations/new def new @invitation.build_membership diff --git a/bullet_train/app/controllers/concerns/account/teams/controller_base.rb b/bullet_train/app/controllers/concerns/account/teams/controller_base.rb index 79a082256..9d71c481b 100644 --- a/bullet_train/app/controllers/concerns/account/teams/controller_base.rb +++ b/bullet_train/app/controllers/concerns/account/teams/controller_base.rb @@ -9,7 +9,7 @@ module Account::Teams::ControllerBase prepend_before_action do if params["action"] == "new" - current_user.current_team = nil + current_user&.current_team = nil end end diff --git a/bullet_train/app/controllers/concerns/sessions/controller_base.rb b/bullet_train/app/controllers/concerns/sessions/controller_base.rb index f004de9a5..05d2e8c89 100644 --- a/bullet_train/app/controllers/concerns/sessions/controller_base.rb +++ b/bullet_train/app/controllers/concerns/sessions/controller_base.rb @@ -1,6 +1,41 @@ module Sessions::ControllerBase extend ActiveSupport::Concern + # If user_return_to points to an oauth path we disable Turbo on the sign in form. + # This makes it work when we need to redirect to external sites and/or custom protocols. + # With Turbo enabled the browser will block those redirects with a CORS error. + # https://github.com/bullet-train-co/bullet_train/issues/384 + def user_return_to_is_oauth + session["user_return_to"]&.match(/^\/oauth/) + end + + included do + helper_method :user_return_to_is_oauth + end + + def new + # We allow people to pass in a URL to redirect to after sign in is complete. We have to do this because Safari + # doesn't allow them to set this in a session before a redirect if there isn't already a session. However, for + # security reasons we have to make sure we control the URL where we will redirect to, otherwise people could + # trick folks into redirecting to a fake destination in a phishing scheme. + if params[:return_url]&.start_with?(ENV["BASE_URL"]) + store_location_for(resource_name, params[:return_url]) + end + + super + end + + def destroy + if params.include?(:onboard_logout) + signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)) + set_flash_message! :notice, :signed_out if signed_out + yield if block_given? + redirect_to root_path + else + super + end + end + def pre_otp if (@email = params["user"]["email"].downcase.strip.presence) @user = User.find_by(email: @email) diff --git a/bullet_train/app/controllers/sessions_controller.rb b/bullet_train/app/controllers/sessions_controller.rb index 9778b5941..9b74b8fb3 100644 --- a/bullet_train/app/controllers/sessions_controller.rb +++ b/bullet_train/app/controllers/sessions_controller.rb @@ -1,35 +1,3 @@ class SessionsController < Devise::SessionsController include Sessions::ControllerBase - - # If user_return_to points to an oauth path we disable Turbo on the sign in form. - # This makes it work when we need to redirect to external sites and/or custom protocols. - # With Turbo enabled the browser will block those redirects with a CORS error. - # https://github.com/bullet-train-co/bullet_train/issues/384 - def user_return_to_is_oauth - session["user_return_to"]&.match(/^\/oauth/) - end - helper_method :user_return_to_is_oauth - - def new - # We allow people to pass in a URL to redirect to after sign in is complete. We have to do this because Safari - # doesn't allow them to set this in a session before a redirect if there isn't already a session. However, for - # security reasons we have to make sure we control the URL where we will redirect to, otherwise people could - # trick folks into redirecting to a fake destination in a phishing scheme. - if params[:return_url]&.start_with?(ENV["BASE_URL"]) - store_location_for(resource_name, params[:return_url]) - end - - super - end - - def destroy - if params.include?(:onboard_logout) - signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)) - set_flash_message! :notice, :signed_out if signed_out - yield if block_given? - redirect_to root_path - else - super - end - end end diff --git a/bullet_train/app/helpers/account/dates_helper.rb b/bullet_train/app/helpers/account/dates_helper.rb index 86c560d2d..37eefd5e5 100644 --- a/bullet_train/app/helpers/account/dates_helper.rb +++ b/bullet_train/app/helpers/account/dates_helper.rb @@ -37,4 +37,17 @@ def local_time(time) return time if current_user.time_zone.nil? time.in_time_zone(current_user.time_zone) end + + def am_pm? + !"#{I18n.t("time.am", fallback: false, default: "")}#{I18n.t("time.pm", fallback: false, default: "")}".empty? + end + + def time_zone_name_to_id + ActiveSupport::TimeZone.all.map { |tz| {tz.name.to_s => tz.tzinfo.name} }.reduce({}, :merge) + end + + def current_time_zone + current_time_zone_name = current_user&.time_zone || current_user&.current_team&.time_zone || "UTC" + ActiveSupport::TimeZone.find_tzinfo(current_time_zone_name).name + end end diff --git a/bullet_train/app/helpers/account/markdown_helper.rb b/bullet_train/app/helpers/account/markdown_helper.rb index bf8ae9c9f..c9fdbe9d8 100644 --- a/bullet_train/app/helpers/account/markdown_helper.rb +++ b/bullet_train/app/helpers/account/markdown_helper.rb @@ -1,5 +1,8 @@ module Account::MarkdownHelper def markdown(string) - CommonMarker.render_html(string, :UNSAFE, [:table]).html_safe + Commonmarker.to_html(string, options: { + plugins: {syntax_highlighter: {theme: "InspiredGitHub"}}, + render: {width: 120, unsafe: true} + }).html_safe end end diff --git a/bullet_train/app/helpers/attributes_helper.rb b/bullet_train/app/helpers/attributes_helper.rb index 386ec5d37..7a7a285df 100644 --- a/bullet_train/app/helpers/attributes_helper.rb +++ b/bullet_train/app/helpers/attributes_helper.rb @@ -1,32 +1,17 @@ module AttributesHelper def current_attributes_object - @_attributes_helper_objects&.last + @_current_attribute_settings&.dig(:object) end def current_attributes_strategy - @_attributes_helper_strategies&.last + @_current_attributes_settings&.dig(:strategy) end - def with_attribute_settings(options) - @_attributes_helper_objects ||= [] - @_attributes_helper_strategies ||= [] - - if options[:object] - @_attributes_helper_objects << options[:object] - end - - if options[:strategy] - @_attributes_helper_strategies << options[:strategy] - end - + def with_attribute_settings(object: current_attributes_object, strategy: current_attributes_strategy) + old_attribute_settings = @_current_attribute_settings + @_current_attribute_settings = {object: object, strategy: strategy} yield - - if options[:strategy] - @_attributes_helper_strategies.pop - end - - if options[:object] - @_attributes_helper_objects.pop - end + ensure + @_current_attribute_settings = old_attribute_settings end end diff --git a/bullet_train/app/helpers/invitation_only_helper.rb b/bullet_train/app/helpers/invitation_only_helper.rb index bf221e9fd..f1f3ee9d3 100644 --- a/bullet_train/app/helpers/invitation_only_helper.rb +++ b/bullet_train/app/helpers/invitation_only_helper.rb @@ -1,6 +1,14 @@ +require "active_support/security_utils" + module InvitationOnlyHelper def invited? - session[:invitation_key].present? && invitation_keys.include?(session[:invitation_key]) + return false unless session[:invitation_key].present? + + result = invitation_keys.find do |key| + ActiveSupport::SecurityUtils.secure_compare(key, session[:invitation_key]) + end + + result.present? end def show_sign_up_options? diff --git a/bullet_train/app/javascript/controllers/bulk_actions_controller.js b/bullet_train/app/javascript/controllers/bulk_actions_controller.js index 1852def78..659270b49 100644 --- a/bullet_train/app/javascript/controllers/bulk_actions_controller.js +++ b/bullet_train/app/javascript/controllers/bulk_actions_controller.js @@ -76,6 +76,7 @@ export default class extends Controller { } updateToggleLabel() { + if (!this.hasSelectableToggleTarget) { return } this.selectableToggleTarget.dispatchEvent(new CustomEvent('toggle', { detail: { useAlternate: this.selectableValue }} )) } diff --git a/bullet_train/app/models/billing/mock_limiter.rb b/bullet_train/app/models/billing/mock_limiter.rb index 9bf908a0e..7edaf68f4 100644 --- a/bullet_train/app/models/billing/mock_limiter.rb +++ b/bullet_train/app/models/billing/mock_limiter.rb @@ -10,7 +10,7 @@ def can?(action, model, count: 1) true end - def exhausted?(model) + def exhausted?(model, enforcement = "hard") false end end diff --git a/bullet_train/app/models/concerns/memberships/base.rb b/bullet_train/app/models/concerns/memberships/base.rb index 3ae621c0b..d4ccdc217 100644 --- a/bullet_train/app/models/concerns/memberships/base.rb +++ b/bullet_train/app/models/concerns/memberships/base.rb @@ -140,4 +140,6 @@ def first_name_last_initial def should_receive_notifications? invitation.present? || user.present? end + + ActiveSupport.run_load_hooks :bullet_train_memberships_base, self end diff --git a/bullet_train/app/models/concerns/records/base.rb b/bullet_train/app/models/concerns/records/base.rb index 4779a1aad..fabb2df00 100644 --- a/bullet_train/app/models/concerns/records/base.rb +++ b/bullet_train/app/models/concerns/records/base.rb @@ -21,7 +21,7 @@ module Records::Base end include CableReady::Updatable - enable_updates + enable_cable_ready_updates extend ActiveHash::Associations::ActiveRecordExtensions @@ -91,4 +91,6 @@ def to_api_json(api_version = BulletTrain::Api.current_version_numeric) end.attributes! end end + + ActiveSupport.run_load_hooks :bullet_train_records_base, self end diff --git a/bullet_train/app/models/concerns/teams/base.rb b/bullet_train/app/models/concerns/teams/base.rb index 0203b844d..18912d765 100644 --- a/bullet_train/app/models/concerns/teams/base.rb +++ b/bullet_train/app/models/concerns/teams/base.rb @@ -4,7 +4,7 @@ module Teams::Base included do # super scaffolding unless scaffolding_things_disabled? - has_many :scaffolding_absolutely_abstract_creative_concepts, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcept", dependent: :destroy, enable_updates: true + has_many :scaffolding_absolutely_abstract_creative_concepts, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcept", dependent: :destroy, enable_cable_ready_updates: true end # memberships and invitations @@ -27,10 +27,6 @@ module Teams::Base if defined?(Billing::Stripe::Subscription) has_many :billing_stripe_subscriptions, class_name: "Billing::Stripe::Subscription", dependent: :destroy, foreign_key: :team_id end - - if defined?(Billing::Usage::TeamSupport) - include Billing::Usage::TeamSupport - end end # validations @@ -39,9 +35,7 @@ module Teams::Base end def platform_agent_access_tokens - # TODO This could be written better. - platform_agent_user_ids = memberships.platform_agents.map(&:user_id).compact - Platform::AccessToken.joins(:application).where(resource_owner_id: platform_agent_user_ids, application: {team: nil}) + Platform::AccessToken.joins(:application).where(resource_owner_id: users.where.not(platform_agent_of_id: nil), application: {team: nil}) end def admins @@ -83,4 +77,6 @@ def needs_billing_subscription? billing_subscriptions.active.empty? end end + + ActiveSupport.run_load_hooks :bullet_train_teams_base, self end diff --git a/bullet_train/app/models/concerns/users/base.rb b/bullet_train/app/models/concerns/users/base.rb index 668dff49b..68576a865 100644 --- a/bullet_train/app/models/concerns/users/base.rb +++ b/bullet_train/app/models/concerns/users/base.rb @@ -9,7 +9,7 @@ module Users::Base end devise :omniauthable - devise :pwned_password if BulletTrain::Configuration.default.strong_passwords + devise :pwned_password if BulletTrain::Configuration.strong_passwords devise :registerable devise :recoverable devise :rememberable diff --git a/bullet_train/app/views/account/memberships/_index.html.erb b/bullet_train/app/views/account/memberships/_index.html.erb index 186973ed9..0be1f1969 100644 --- a/bullet_train/app/views/account/memberships/_index.html.erb +++ b/bullet_train/app/views/account/memberships/_index.html.erb @@ -2,7 +2,7 @@ <% hide_actions ||= false %> <% hide_back ||= false %> -<%= updates_for context, :memberships do %> +<%= cable_ready_updates_for context, :memberships do %> <%= render 'account/shared/box' do |box| %> <% box.title t(".contexts.#{context.class.name.underscore}.header") %> <% box.description do %> diff --git a/bullet_train/app/views/account/memberships/_membership.html.erb b/bullet_train/app/views/account/memberships/_membership.html.erb index 28a186cf6..07a7957c9 100644 --- a/bullet_train/app/views/account/memberships/_membership.html.erb +++ b/bullet_train/app/views/account/memberships/_membership.html.erb @@ -24,6 +24,9 @@ <% end %> + <% if membership.unclaimed? %> + <%= button_to t('.buttons.resend'), resend_account_invitation_path(membership.invitation.uuid), class: 'button-secondary button-smaller' %> + <% end %> <%= link_to t('.buttons.show'), [:account, membership], class: 'button-secondary button-smaller' %> diff --git a/bullet_train/app/views/account/onboarding/user_details/edit.html.erb b/bullet_train/app/views/account/onboarding/user_details/edit.html.erb index 280195a89..64b1d233f 100644 --- a/bullet_train/app/views/account/onboarding/user_details/edit.html.erb +++ b/bullet_train/app/views/account/onboarding/user_details/edit.html.erb @@ -6,6 +6,7 @@ <% within_fields_namespace(:self) do %> <%= form_for @user, url: account_onboarding_user_detail_path(@user), method: :put, html: {class: 'form'} do |f| %> <%= render 'account/shared/forms/errors', form: f %> + <%= render 'account/shared/notices', form: f %>
diff --git a/bullet_train/app/views/devise/registrations/new.html.erb b/bullet_train/app/views/devise/registrations/new.html.erb index 57c5a0bc2..aa234d63d 100644 --- a/bullet_train/app/views/devise/registrations/new.html.erb +++ b/bullet_train/app/views/devise/registrations/new.html.erb @@ -12,7 +12,7 @@ <% end %> <% end %> -
+
<%= render 'shared/fields/password_field', form: f, method: :password, options: {show_strength_indicator: true} %>
diff --git a/bullet_train/app/views/layouts/docs.html.erb b/bullet_train/app/views/layouts/docs.html.erb index 3e95e148d..4f46288bc 100644 --- a/bullet_train/app/views/layouts/docs.html.erb +++ b/bullet_train/app/views/layouts/docs.html.erb @@ -109,7 +109,7 @@ <% end %> <% end %> - <%= render 'account/shared/menu/item', url: '/docs/i18n', label: 'Internationalzation' do |p| %> + <%= render 'account/shared/menu/item', url: '/docs/i18n', label: 'Internationalization' do |p| %> <% p.icon do %> <% end %> @@ -152,6 +152,12 @@ <% end %> <% end %> + + <%= render 'account/shared/menu/item', url: '/docs/application-hash.md', label: 'Application Hash' do |p| %> + <% p.content_for :icon do %> + + <% end %> + <% end %> <% end %> <%= render 'account/shared/menu/section', title: 'Accounts & Teams' do %> @@ -204,6 +210,12 @@ <% end %> <% end %> + + <%= render 'account/shared/menu/item', url: 'https://github.com/bullet-train-co/showcase', label: 'Showcase' do |p| %> + <% p.icon do %> + + <% end %> + <% end %> <% end %> <%= render 'account/shared/menu/section', title: 'Billing' do %> @@ -212,6 +224,12 @@ <% end %> <% end %> + + <%= render 'account/shared/menu/item', url: '/docs/billing/usage', label: 'Usage' do |p| %> + <% p.icon do %> + + <% end %> + <% end %> <% end %> <%= render 'account/shared/menu/section', title: 'Integration' do %> @@ -286,7 +304,7 @@