From 8f854b906ad2e532337762315f096d81fc847c05 Mon Sep 17 00:00:00 2001 From: Paul Bob Date: Fri, 25 Aug 2023 16:22:38 +0300 Subject: [PATCH 1/2] feature: helpers --- docs/.vitepress/config.js | 1 + docs/3.0/field-options.md | 5 +++++ docs/3.0/fields.md | 2 ++ docs/3.0/helpers.md | 8 ++++++++ docs/3.0/resources.md | 2 ++ 5 files changed, 18 insertions(+) create mode 100644 docs/3.0/helpers.md diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 19227b31..5490c174 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -206,6 +206,7 @@ const config = { {text: "Testing", link: "/3.0/testing"}, {text: "Avo::ApplicationController", link: "/3.0/avo-application-controller"}, {text: "Avo.asset_manager", link: "/3.0/asset-manager"}, + {text: "Helpers", link: "/3.0/helpers"}, ], }, // { diff --git a/docs/3.0/field-options.md b/docs/3.0/field-options.md index bae71bf8..623df603 100644 --- a/docs/3.0/field-options.md +++ b/docs/3.0/field-options.md @@ -43,6 +43,9 @@ field :is_featured, as: :boolean, visible: -> { resource.record.published_at.pre On form submissions, the `visible` block is evaluated in the `create` and `update` controller actions. That's why you have to check if the `resource.record` object is present before trying to use it. ::: +[Check how to use your application's helpers within any field context.](./helpers) + + ```ruby # `resource.record` is nil when submitting the form on resource creation field :name, as: :text, visible -> { resource.record.enabled? } @@ -55,6 +58,8 @@ field :name, as: :text, visible -> { resource.record&.enabled? } You might need to show a field with a value you don't have in a database row. In that case, you may compute the value using a block that receives the `record` (the actual database record), the `resource` (the configured Avo resource), and the current `view`. With that information, you can compute what to show on the field in the and views. +[Check how to use your application's helpers within any computed field context.](./helpers) + ```ruby field 'Has posts', as: :boolean do record.posts.present? diff --git a/docs/3.0/fields.md b/docs/3.0/fields.md index 655c9320..a8f23f32 100644 --- a/docs/3.0/fields.md +++ b/docs/3.0/fields.md @@ -21,6 +21,8 @@ Through fields you tell Avo what to fetch from the database and how to display i Avo ships with various simple fields like `text`, `textarea`, `number`, `password`, `boolean`, `select`, and more complex ones like `markdown`, `key_value`, `trix`, `tags`, and `code`. +[Check how to use your application's helpers within any field context.](./helpers) + ## Declaring fields You add fields to a resource through the `fields` method using the `field DATABASE_COLUMN, as: FIELD_TYPE, **FIELD_OPTIONS` notation. diff --git a/docs/3.0/helpers.md b/docs/3.0/helpers.md new file mode 100644 index 00000000..972cdfc4 --- /dev/null +++ b/docs/3.0/helpers.md @@ -0,0 +1,8 @@ +# Helpers +Avo offers access to your application's helpers in various scenarios. You have the opportunity to leverage your application's helper methods within the context of resources, fields, as well as within every block and lambda function. If you encounter any situations where helpers are not accessible, kindly let us know so we can address them. + +## How to use it +To make use of your application's helpers, employ the syntax `helpers.method_name`. + +## How do it works? +The `helpers` method stems from a concern. Upon invocation, it initializes an anonymous class that inherits all the helpers located within your application's `app/helpers` directory. This anonymous class is cached for each request, ensuring efficiency. Our approach of encapsulating the helpers within an anonymous class mitigates conflicts between your helper methods and our internal methods. diff --git a/docs/3.0/resources.md b/docs/3.0/resources.md index 87bc5ba8..7e3173d1 100644 --- a/docs/3.0/resources.md +++ b/docs/3.0/resources.md @@ -10,6 +10,8 @@ Each `Resource` maps out one of your models. There can be multiple `Resource`s a All resources are located in the `app/avo/resources` directory. +[Check how to use your application's helpers within any resource context.](./helpers) + ## Resources from model generation ```bash From 9112fe963c75441d3e90b5cdf209f6504fb3efff Mon Sep 17 00:00:00 2001 From: Adrian Date: Sun, 27 Aug 2023 09:18:00 +0300 Subject: [PATCH 2/2] tweaks --- docs/.vitepress/config.js | 8 +- docs/3.0/avo-current.md | 34 ++++++ ...associations_attach_scope_option_common.md | 2 +- .../associations_scope_option_common.md | 2 +- docs/3.0/evaluation-hosts.md | 89 -------------- docs/3.0/execution-context.md | 109 ++++++++++++++++++ docs/3.0/fields/tags.md | 2 +- docs/3.0/helpers.md | 8 -- 8 files changed, 152 insertions(+), 102 deletions(-) create mode 100644 docs/3.0/avo-current.md delete mode 100644 docs/3.0/evaluation-hosts.md create mode 100644 docs/3.0/execution-context.md delete mode 100644 docs/3.0/helpers.md diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 5490c174..2621e29c 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -61,6 +61,9 @@ const config = { ['meta', { name:"msapplication-config", content:"/favicons/browserconfig.xml" }], ['meta', { name:"theme-color", content:"#ffffff" }], ], + rewrites: { + "/3.0/evaluation-host": "/3.0/execution-context" + }, themeConfig: { siteTitle: false, logo: "/logo.svg", @@ -188,7 +191,6 @@ const config = { {text: "Custom fields", link: "/3.0/custom-fields"}, {text: "Resource tools", link: "/3.0/resource-tools"}, {text: "Stimulus JS integration", link: "/3.0/stimulus-integration"}, - // {text: "Evaluation hosts", link: "/3.0/evaluation-hosts"}, {text: "Custom asset pipeline", link: "/3.0/custom-asset-pipeline"}, ], }, @@ -204,9 +206,11 @@ const config = { text: "Internals", items: [ {text: "Testing", link: "/3.0/testing"}, + {text: "Avo::Current", link: "/3.0/avo-current"}, + {text: "Avo::ExecutionContext", link: "/3.0/execution-context"}, {text: "Avo::ApplicationController", link: "/3.0/avo-application-controller"}, + // {text: "Application Helpers", link: "/3.0/helpers"}, {text: "Avo.asset_manager", link: "/3.0/asset-manager"}, - {text: "Helpers", link: "/3.0/helpers"}, ], }, // { diff --git a/docs/3.0/avo-current.md b/docs/3.0/avo-current.md new file mode 100644 index 00000000..0ae78d0c --- /dev/null +++ b/docs/3.0/avo-current.md @@ -0,0 +1,34 @@ +# `Avo::Current` + +`Avo::Current` is based on the `Current` pattern Rails exposes using [`ActiveSupport/CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html). + +On each request Avo will set some values on it. + +:::option `user` +This is what will be returned by the [`current_user_method`](./authentication.html#customize-the-current-user-method) that you've set in your initializer. +::: + +:::option `params` +Equivalent of `request.params`. +::: + +:::option `request` +The Rails `request`. +::: + +:::option `context` +The [`context`](./customization.html#context) that you configured in your initializer evaluated in `Avo::ApplicationController`. +::: + +:::option `view_context` +An instance of [`ActionView::Rendering`](https://api.rubyonrails.org/classes/ActionView/Rendering.html#method-i-view_context) off of which you can run any methods or variables that are available in your partials. + +```ruby +view_context.link_to "Avo", "https://avohq.io" +``` +::: + +:::option `locale` +The `locale` of the app. +::: + diff --git a/docs/3.0/common/associations_attach_scope_option_common.md b/docs/3.0/common/associations_attach_scope_option_common.md index 923d9739..f8f7d633 100644 --- a/docs/3.0/common/associations_attach_scope_option_common.md +++ b/docs/3.0/common/associations_attach_scope_option_common.md @@ -13,5 +13,5 @@ field :user, attach_scope: -> { query.non_admins } ``` -Pass in a block where you attach scopes to the `query` object. The block is executed in the [`AssociationScopeHost`](./../evaluation-hosts.html#associationscopehost), so follow the docs to see what variables you have access to. +Pass in a block where you attach scopes to the `query` object. The block is executed in the [`ExecutionContext`](./../execution-context). ::: diff --git a/docs/3.0/common/associations_scope_option_common.md b/docs/3.0/common/associations_scope_option_common.md index 3ad183bd..23737883 100644 --- a/docs/3.0/common/associations_scope_option_common.md +++ b/docs/3.0/common/associations_scope_option_common.md @@ -13,5 +13,5 @@ field :user, scope: -> { query.approved } ``` -Pass in a block where you attach scopes to the `query` object. The block gets executed in the [`AssociationScopeHost`](./../evaluation-hosts.html#associationscopehost), so follow the docs to see what variables you have access to. +Pass in a block where you attach scopes to the `query` object. The block gets executed in the [`ExecutionContext`](./../execution-context). ::: diff --git a/docs/3.0/evaluation-hosts.md b/docs/3.0/evaluation-hosts.md deleted file mode 100644 index b686d635..00000000 --- a/docs/3.0/evaluation-hosts.md +++ /dev/null @@ -1,89 +0,0 @@ -# Evaluation hosts - -Avo is a package that does a lot of meta-programming. That means we have a lot of custom functionality passed from the host app to Avo to be executed at different points in time. That functionality can't always be performed in void but requires some pieces of state. We're going to talk all about them below. -You'll probably never be going to implement the hosts yourself, but you'll want to know what they contain and how they work. - -Usually, this happens using lambdas. That's why we created the concept of a `Host`. - -## What's a host? - -A `Host` is an object that holds some pieces of state on which we execute a lambda function. - -```ruby -require "dry-initializer" - -# This object holds some data that is usually needed to compute blocks around the app. -module Avo - module Hosts - class BaseHost - extend Dry::Initializer - - option :context, default: proc { Avo::App.context } - option :params, default: proc { Avo::App.params } - option :view_context, default: proc { Avo::App.view_context } - option :current_user, default: proc { Avo::App.current_user } - # This is optional because we might instantiate the `Host` first and later hydrate it with a block. - option :block, optional: true - delegate :authorize, to: Avo::Services::AuthorizationService - - def handle - instance_exec(&block) - end - end - end -end - -# Use it like so. -Avo::Hosts::BaseHost.new(block: &some_block).handle -``` - -## `BaseHost` - -The `BaseHost` holds some of the most basic pieces of state like the request `params`, Avo's [`context`](customization.html#context) object, the [`view_context`](https://apidock.com/rails/AbstractController/Rendering/view_context) object, and the `current_user`. - -As the name states, this is the base host. All other hosts are inherited from it. - -### `params` - -The `params` object is the regular [`params`](https://guides.rubyonrails.org/action_controller_overview.html#parameters) object you are used to. - -### Avo's `context` object - -As you progress throughout building your app, you'll probably configure a [`context`](customization.html#context) object to hold some custom state you need. For example, in `BaseHost` you have access to this object. - -### The `view_context` object - -The [`view_context`](https://apidock.com/rails/AbstractController/Rendering/view_context) object can be used to create the route helpers you are used to (EX: `posts_path`, `new_comment_path`, etc.). - -When dealing with the `view_context` you have to lean on the object to get those paths. Also, because we are operating under an engine (Avo), the paths must be prefixed with the engine name. Rails' is `main_app`. So if you'd like to output a route to your app `/comments/5/edit`, instead of writing `edit_comment_path 5`, you'd write `view_context.main_app.edit_comment_path 5`. - -### The current user - -Provided that you set up the [`:current_user_method`](authentication.html#customize-the-current-user-method), you'll have access to that output using `current_user` in this block. - -## Evaluating the block - -We talked about the host and the pieces of state it holds; now, let's talk about how we can use it. - -You're not going to use it when building with Avo. Instead, it's used internally when you pass a block to customize the behavior. For example, it's used when declaring the `visibility` block on [`dashboards`](dashboards.html#dashboards-visibility) or when you try pre-filling the suggestions for the `tags` field. - -Not all blocks you declare in Avo will be executed in a `Host`. We started implementing `Host`s since v2.0 after the experience gained with v1.0. We plan on refactoring the old block to hosts at some point, but that's going to be a breaking change, so probably in v3.0. -You'll probably be prompted in the docs on each block if it's a `Host` or not. - -Different hosts have different pieces of state. - -## `RecordHost` - -The `RecordHost` inherits from `BaseHost` and has the `record` available. The `record` is the model class instantiated with the DB information (like doing `User.find 1`) in that context. - -## `ViewRecordHost` - -The `ViewRecordHost` inherits from `RecordHost` and has the `view` object available too. - -## `ResourceViewRecordHost` - -The `ResourceViewRecordHost` inherits from `ViewRecordHost` and has the `resource` object available too. - -## `AssociationScopeHost` - -The `AssociationScopeHost` inherits from `BaseHost` and has the `parent` and the `query` objects available. The `parent` is the instantiated model on which the block is given and the `query` is the actual query that is going to run. diff --git a/docs/3.0/execution-context.md b/docs/3.0/execution-context.md new file mode 100644 index 00000000..93d4872e --- /dev/null +++ b/docs/3.0/execution-context.md @@ -0,0 +1,109 @@ +# Execution context + +Avo enables developers to hook into different points of the application lifecycle using blocks. +That functionality can't always be performed in void but requires some pieces of state to set up some context. + +Computed fields are one example. + +```ruby +field :full_name, as: :text do + "#{record.first_name} #{record.last_name}" +end +``` + +In that block we need to pass the `record` so you can compile that value. We send more information than just the `record`, we pass on the `resource`, `view`, `view_context`, `request`, `current_user` and more depending on the block that's being run. + +## How does the `ExecutionContext` work? + +The `ExecutionContext` is an object that holds some pieces of state on which we execute a lambda function. + +```ruby +module Avo + class ExecutionContext + + attr_accessor :target, :context, :params, :view_context, :current_user, :request + + def initialize(**args) + # If target don't respond to call, handle will return target + # In that case we don't need to initialize the others attr_accessors + return unless (@target = args[:target]).respond_to? :call + + args.except(:target).each do |key,value| + singleton_class.class_eval { attr_accessor "#{key}" } + instance_variable_set("@#{key}", value) + end + + # Set defaults on not initialized accessors + @context ||= Avo::Current.context + @params ||= Avo::Current.params + @view_context ||= Avo::Current.view_context + @current_user ||= Avo::Current.current_user + @request ||= Avo::Current.request + end + + delegate :authorize, to: Avo::Services::AuthorizationService + + # Return target if target is not callable, otherwise, execute target on this instance context + def handle + target.respond_to?(:call) ? instance_exec(&target) : target + end + end +end + +# Use it like so. +SOME_BLOCK = -> { + "#{record.first_name} #{record.last_name}" +} + +Avo::ExecutionContext.new(target: &SOME_BLOCK, record: User.first).handle +``` + +This means you could throw any type of object at it and it it responds to a `call` method wil will be called with all those objects. + +:::option `target` +The block you'll pass to be evaluated. It may be anything but will only be evaluated if it responds to a `call` method. +::: + +:::option `context` +Aliased to [`Avo::Current.context`](./avo-current#context). +::: + +:::option `current_user` +Aliased to [`Avo::Current.user`](./avo-current#user). +::: + +:::option `view_context` +Aliased to [`Avo::Current.view_context`](./avo-current#view_context). +::: + +:::option `request` +Aliased to [`Avo::Current.request`](./avo-current#request). +::: + +:::option `params` +Aliased to [`Avo::Current.params`](./avo-current#params). +::: + +:::option Custom variables +You can pass any variable to the `ExecutionContext` and it will be available in that block. +This is how we can expose `view`, `record`, and `resource` in the computed field example. + +```ruby +Avo::ExecutionContext.new(target: &SOME_BLOCK, record: User.first, view: :index, resource: resource).handle +``` +::: + +:::option `helpers` +Within the `ExecutionContext` you might want to use some of your already defined helpers. You can do that using the `helpers` object. + +```ruby +# products_helper.rb +class ProductsHelper + # Strips the "CODE_" prefix from the name + def simple_name(name) + name.gsub "CODE_", "" + end +end + +field :name, as: :text, format_using: -> { helpers.simple_name(value) } +::: diff --git a/docs/3.0/fields/tags.md b/docs/3.0/fields/tags.md index 7b3c1182..b6474659 100644 --- a/docs/3.0/fields/tags.md +++ b/docs/3.0/fields/tags.md @@ -46,7 +46,7 @@ end The `suggestions` option can be an array of strings, an object with the keys `value`, `label`, and (optionally) `avatar`, or a lambda that returns an array of that type of object. -The lambda is run inside a [`RecordHost`](./../evaluation-hosts.html#recordhost), so it has access to the `record` along with other things (check the link). +The lambda is run inside a [`ExecutionContext`](./../execution-context.html), so it has access to the `record`, `resource`, `request`, `params`, `view`, and `view_context` along with other things. ```ruby{5-21} # app/models/post.rb diff --git a/docs/3.0/helpers.md b/docs/3.0/helpers.md deleted file mode 100644 index 972cdfc4..00000000 --- a/docs/3.0/helpers.md +++ /dev/null @@ -1,8 +0,0 @@ -# Helpers -Avo offers access to your application's helpers in various scenarios. You have the opportunity to leverage your application's helper methods within the context of resources, fields, as well as within every block and lambda function. If you encounter any situations where helpers are not accessible, kindly let us know so we can address them. - -## How to use it -To make use of your application's helpers, employ the syntax `helpers.method_name`. - -## How do it works? -The `helpers` method stems from a concern. Upon invocation, it initializes an anonymous class that inherits all the helpers located within your application's `app/helpers` directory. This anonymous class is cached for each request, ensuring efficiency. Our approach of encapsulating the helpers within an anonymous class mitigates conflicts between your helper methods and our internal methods.