diff --git a/Gemfile.lock b/Gemfile.lock index 25d8376de2..a3086f7bab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -443,9 +443,6 @@ GEM prettyprint (0.2.0) prop_initializer (0.2.0) zeitwerk (>= 2.6.18) - psych (5.2.3) - date - stringio public_suffix (6.0.1) puma (6.6.0) nio4r (~> 2.0) @@ -505,8 +502,7 @@ GEM ffi (~> 1.0) rbs (3.8.1) logger - rdoc (6.12.0) - psych (>= 4.0.0) + rdoc (6.11.0) redis (5.3.0) redis-client (>= 0.22.0) redis-client (0.23.2) @@ -643,7 +639,6 @@ GEM standard-performance (1.6.0) lint_roller (~> 1.1) rubocop-performance (~> 1.23.0) - stringio (3.1.2) syntax_tree (6.2.0) prettier_print (>= 1.2.0) terminal-table (4.0.0) @@ -698,6 +693,7 @@ GEM PLATFORMS arm64-darwin-23 + arm64-darwin-24 x86_64-linux DEPENDENCIES diff --git a/app/components/avo/discreet_information_component.html.erb b/app/components/avo/discreet_information_component.html.erb new file mode 100644 index 0000000000..a584da6c09 --- /dev/null +++ b/app/components/avo/discreet_information_component.html.erb @@ -0,0 +1,7 @@ +
+ <% items.each do |item| %> + <%= content_tag element_tag(item), **element_attributes(item), class: "flex gap-1 text-xs font-normal text-gray-600 hover:text-gray-900", title: item.tooltip, data: {tippy: :tooltip, **data(item)} do %> + <%= item.label if item.label.present? %> <%= helpers.svg item.icon, class: "text-2xl h-4" %> + <% end %> + <% end %> +
diff --git a/app/components/avo/discreet_information_component.rb b/app/components/avo/discreet_information_component.rb new file mode 100644 index 0000000000..b356c22d51 --- /dev/null +++ b/app/components/avo/discreet_information_component.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class Avo::DiscreetInformationComponent < Avo::BaseComponent + prop :payload + + def items + @payload.items.compact + end + + def element_tag(item) + if item.url.present? + :a + else + :div + end + end + + def element_attributes(item) + if item.url.present? + {href: item.url, target: item.url_target} + else + {} + end + end + + def data(item) = item.data || {} +end diff --git a/app/components/avo/items/panel_component.rb b/app/components/avo/items/panel_component.rb index f8a17cc974..da67910778 100644 --- a/app/components/avo/items/panel_component.rb +++ b/app/components/avo/items/panel_component.rb @@ -35,7 +35,8 @@ def args data: {panel_id: "main"}, cover_photo: @resource.cover_photo, profile_photo: @resource.profile_photo, - external_link: @resource.get_external_link + external_link: @resource.get_external_link, + discreet_information: @resource.discreet_information } else {name: @item.name, description: @item.description, index: @index} diff --git a/app/components/avo/panel_component.html.erb b/app/components/avo/panel_component.html.erb index a289ee8bc7..5b6e9fb4fc 100644 --- a/app/components/avo/panel_component.html.erb +++ b/app/components/avo/panel_component.html.erb @@ -5,7 +5,8 @@ description: @description, display_breadcrumbs: @display_breadcrumbs, profile_photo: @profile_photo, - external_link: @external_link + external_link: @external_link, + discreet_information: @discreet_information ) do |header| %> <% if name_slot.present? %> <% header.with_name_slot do %> diff --git a/app/components/avo/panel_component.rb b/app/components/avo/panel_component.rb index 2774450acc..88a8b3cceb 100644 --- a/app/components/avo/panel_component.rb +++ b/app/components/avo/panel_component.rb @@ -19,16 +19,17 @@ class Avo::PanelComponent < Avo::BaseComponent prop :body_classes prop :data, default: {}.freeze prop :display_breadcrumbs, default: false + prop :discreet_information prop :index prop :classes prop :profile_photo prop :cover_photo prop :args, kind: :**, default: {}.freeze + prop :external_link def after_initialize @name = @args.dig(:name) || @args.dig(:title) end - prop :external_link def classes class_names(@classes, "has-cover-photo": @cover_photo.present?, "has-profile-photo": @profile_photo.present?) diff --git a/app/components/avo/panel_header_component.html.erb b/app/components/avo/panel_header_component.html.erb index 7ff8ad9938..be42a8a39e 100644 --- a/app/components/avo/panel_header_component.html.erb +++ b/app/components/avo/panel_header_component.html.erb @@ -19,6 +19,9 @@ <%= svg "heroicons/outline/arrow-top-right-on-square", class: "ml-2 text-2xl h-4" %> <% end %> <% end %> + <% if @discreet_information.present? %> + <%= render Avo::DiscreetInformationComponent.new(payload: @discreet_information) %> + <% end %> <% end %> <% end %> <% end %> diff --git a/app/components/avo/panel_header_component.rb b/app/components/avo/panel_header_component.rb index 168cc60c2f..ca358a6251 100644 --- a/app/components/avo/panel_header_component.rb +++ b/app/components/avo/panel_header_component.rb @@ -10,6 +10,7 @@ class Avo::PanelHeaderComponent < Avo::BaseComponent prop :external_link prop :description prop :display_breadcrumbs, default: false + prop :discreet_information prop :profile_photo private diff --git a/app/components/avo/panel_name_component.html.erb b/app/components/avo/panel_name_component.html.erb index a245215de4..121ea85aa0 100644 --- a/app/components/avo/panel_name_component.html.erb +++ b/app/components/avo/panel_name_component.html.erb @@ -1,4 +1,4 @@ -
- <%= link_to_if @url.present?, @name, @url, target: @target, class: class_names("text-gray-800", @classes) %> +
+ <%= link_to_if @url.present?, @name, @url, target: @target, class: class_names("text-gray-800", @classes) %> <%= body %>
diff --git a/app/helpers/avo/turbo_stream_actions_helper.rb b/app/helpers/avo/turbo_stream_actions_helper.rb index a274f02dcb..c888e35303 100644 --- a/app/helpers/avo/turbo_stream_actions_helper.rb +++ b/app/helpers/avo/turbo_stream_actions_helper.rb @@ -15,6 +15,10 @@ def avo_close_modal target: Avo::MODAL_FRAME_ID, template: @view_context.turbo_frame_tag(Avo::MODAL_FRAME_ID) end + + def avo_turbo_reload + turbo_stream_action_tag :turbo_reload + end end end diff --git a/app/javascript/avo.base.js b/app/javascript/avo.base.js index 8c3fb9491e..e79868c5eb 100644 --- a/app/javascript/avo.base.js +++ b/app/javascript/avo.base.js @@ -25,7 +25,7 @@ Mousetrap.bind('r r r', () => { // Capture scroll position scrollTop = document.scrollingElement.scrollTop - Turbo.visit(window.location.href, { action: 'replace' }) + window.StreamActions.turbo_reload() }) function isMac() { @@ -56,6 +56,7 @@ document.addEventListener('keyup', (event) => { function initTippy() { tippy('[data-tippy="tooltip"]', { theme: 'light', + allowHTML: true, content(reference) { const title = reference.getAttribute('title') reference.removeAttribute('title') diff --git a/app/javascript/js/custom-stream-actions.js b/app/javascript/js/custom-stream-actions.js index 205f5b965b..bab75873dd 100644 --- a/app/javascript/js/custom-stream-actions.js +++ b/app/javascript/js/custom-stream-actions.js @@ -8,6 +8,11 @@ StreamActions.close_filters_dropdown = function () { document.querySelector('.filters-dropdown-selector').classList.add('hidden') } +// Uses Turbo to refresh the page +StreamActions.turbo_reload = function () { + window.Turbo.visit(window.location.href, { action: 'replace' }) +} + StreamActions.open_filter = function () { const id = this.getAttribute('unique-id') setTimeout(() => { diff --git a/app/views/avo/actions/show.html.erb b/app/views/avo/actions/show.html.erb index 68cdaf5f75..d8ba706b7c 100644 --- a/app/views/avo/actions/show.html.erb +++ b/app/views/avo/actions/show.html.erb @@ -5,7 +5,7 @@ <%= turbo_frame_tag Avo::MODAL_FRAME_ID do %>
" - data-action-no-confirmation-value="<%= @action.no_confirmation %>" + data-action-no-confirmation-value="<%= @action.no_confirmation? %>" data-action-resource-name-value="<%= @resource.model_key %>" data-resource-id="<%= params[:id] %>" class="hidden text-slate-800" diff --git a/lib/avo/base_action.rb b/lib/avo/base_action.rb index bc3958e944..c55dd0515c 100644 --- a/lib/avo/base_action.rb +++ b/lib/avo/base_action.rb @@ -4,6 +4,8 @@ class BaseAction include Avo::Concerns::HasActionStimulusControllers include Avo::Concerns::Hydration + DATA_ATTRIBUTES = {turbo_frame: Avo::MODAL_FRAME_ID} + class_attribute :name, default: nil class_attribute :message class_attribute :confirm_button_label @@ -59,8 +61,8 @@ def to_param to_s end - def link_arguments(resource:, arguments: {}, **args) - path = Avo::Services::URIService.parse(resource.record&.persisted? ? resource.record_path : resource.records_path) + def path(resource:, arguments: {}, **args) + Avo::Services::URIService.parse(resource.record&.persisted? ? resource.record_path : resource.records_path) .append_paths("actions") .append_query( **{ @@ -70,8 +72,10 @@ def link_arguments(resource:, arguments: {}, **args) }.compact ) .to_s + end - [path, {turbo_frame: Avo::MODAL_FRAME_ID}] + def link_arguments(resource:, arguments: {}, **args) + [path(resource:, arguments:, **args), DATA_ATTRIBUTES] end # Encrypt the arguments so we can pass sensible data as a query param. @@ -366,6 +370,16 @@ def disabled? !enabled? end + def no_confirmation? + Avo::ExecutionContext.new( + target: no_confirmation, + action: self, + resource: @resource, + view: @view, + arguments: + ).handle + end + private def add_message(body, type = :info) diff --git a/lib/avo/concerns/has_discreet_information.rb b/lib/avo/concerns/has_discreet_information.rb new file mode 100644 index 0000000000..6b521b75f5 --- /dev/null +++ b/lib/avo/concerns/has_discreet_information.rb @@ -0,0 +1,15 @@ +module Avo + module Concerns + module HasDiscreetInformation + extend ActiveSupport::Concern + + included do + class_attribute :discreet_information, instance_accessor: false + end + + def discreet_information + ::Avo::DiscreetInformation.new resource: self + end + end + end +end diff --git a/lib/avo/discreet_information.rb b/lib/avo/discreet_information.rb new file mode 100644 index 0000000000..3e56f7f3d4 --- /dev/null +++ b/lib/avo/discreet_information.rb @@ -0,0 +1,62 @@ +class Avo::DiscreetInformation + extend PropInitializer::Properties + include ActionView::Helpers::TagHelper + + prop :resource, reader: :public + + delegate :record, :view, to: :resource + + def items + Array.wrap(resource.class.discreet_information).map do |item| + if item == :timestamps + timestamp_item(item) + else + parse_payload(item) + end + end + end + + private + + def timestamp_item(item) + return if record.created_at.blank? && record.updated_at.blank? + + time_format = "%Y-%m-%d %H:%M:%S" + created_at = record.created_at.strftime(time_format) + updated_at = record.updated_at.strftime(time_format) + + created_at_tag = if record.created_at.present? + I18n.t("avo.created_at_timestamp", created_at:) + end + + updated_at_tag = if record.updated_at.present? + I18n.t("avo.updated_at_timestamp", updated_at:) + end + + DiscreetInformationItem.new( + tooltip: tag.div([created_at_tag, updated_at_tag].compact.join(tag.br), style: "text-align: right;"), + icon: "heroicons/outline/clock" + ) + end + + def parse_payload(item) + return unless item.is_a?(Hash) + + args = { + record:, + resource:, + view: + } + + DiscreetInformationItem.new( + tooltip: Avo::ExecutionContext.new(target: item[:tooltip], **args).handle, + icon: Avo::ExecutionContext.new(target: item[:icon], **args).handle, + url: Avo::ExecutionContext.new(target: item[:url], **args).handle, + url_target: Avo::ExecutionContext.new(target: item[:url_target], **args).handle, + data: Avo::ExecutionContext.new(target: item[:data], **args).handle, + label: Avo::ExecutionContext.new(target: item[:label], **args).handle + ) + end + + DiscreetInformationItem = Struct.new(:tooltip, :icon, :url, :url_target, :data, :label, keyword_init: true) unless defined?(DiscreetInformationItem) +end diff --git a/lib/avo/execution_context.rb b/lib/avo/execution_context.rb index d3b8657c84..dedb477403 100644 --- a/lib/avo/execution_context.rb +++ b/lib/avo/execution_context.rb @@ -1,4 +1,7 @@ module Avo + # = Avo Execution Context + # + # The ExecutionContext class is used to evaluate blocks in isolation. class ExecutionContext include Avo::Concerns::HasHelpers @@ -36,7 +39,32 @@ def initialize(**args) delegate :result, to: :card delegate :authorize, to: Avo::Services::AuthorizationService - # Return target if target is not callable, otherwise, execute target on this instance context + # Executes the target and returns the result. + # It takes in a target which usually is a block. If it's something else, it will return it. + # + # It automatically has access to the view context, current user, request, main app, avo, locale, and params. + # It also has a +delegate_missing_to+ which allows it to delegate missing methods to the view context for a more natural experience. + # You may pass extra arguments to the initialize method to have them available in the block that will be executed. + # You may pass extra modules to extend the class with. + # + # ==== Examples + # + # ===== Normal use + # + # Avo::ExecutionContext.new(target: -> { "Hello, world!" }).handle + # => "Hello, world!" + # + # ===== Providing a record + # + # Avo::ExecutionContext.new(target: -> { record.name }, record: @record).handle + # => "John Doe" + # + # ===== Providing a module + # + # This will include the SanitizeHelper module in the class and so have the +sanitize+ method available. + # + # Avo::ExecutionContext.new(target: -> { sanitize "#{record.name}" } record: @record, include: [ActionView::Helpers::SanitizeHelper]).handle + # => "John Doe" def handle target.respond_to?(:call) ? instance_exec(&target) : target end diff --git a/lib/avo/resources/base.rb b/lib/avo/resources/base.rb index e24a9e914c..c3de2b23f1 100644 --- a/lib/avo/resources/base.rb +++ b/lib/avo/resources/base.rb @@ -16,6 +16,7 @@ class Base include Avo::Concerns::HasHelpers include Avo::Concerns::Hydration include Avo::Concerns::Pagination + include Avo::Concerns::HasDiscreetInformation include Avo::Concerns::RowControlsConfiguration # Avo::Current methods diff --git a/spec/dummy/app/avo/actions/toggle_published.rb b/spec/dummy/app/avo/actions/toggle_published.rb index 03e4d98026..9f0b136bf9 100644 --- a/spec/dummy/app/avo/actions/toggle_published.rb +++ b/spec/dummy/app/avo/actions/toggle_published.rb @@ -3,6 +3,7 @@ class Avo::Actions::TogglePublished < Avo::BaseAction self.message = "Are you sure, sure?" self.confirm_button_label = "Toggle" self.cancel_button_label = "Don't toggle yet" + self.no_confirmation = -> { arguments[:no_confirmation] || false } def fields field :notify_user, as: :boolean, default: true diff --git a/spec/dummy/app/avo/resources/event.rb b/spec/dummy/app/avo/resources/event.rb index 38b060970b..d1b1576fc9 100644 --- a/spec/dummy/app/avo/resources/event.rb +++ b/spec/dummy/app/avo/resources/event.rb @@ -17,6 +17,7 @@ class Avo::Resources::Event < Avo::BaseResource self.profile_photo = { source: :profile_photo } + self.discreet_information = :timestamps self.row_controls_config = { float: true, diff --git a/spec/dummy/app/avo/resources/post.rb b/spec/dummy/app/avo/resources/post.rb index c7a89c8e55..a84b25c998 100644 --- a/spec/dummy/app/avo/resources/post.rb +++ b/spec/dummy/app/avo/resources/post.rb @@ -33,6 +33,28 @@ class Avo::Resources::Post < Avo::BaseResource main_app.post_path(record) } + self.discreet_information = [ + :timestamps, + { + tooltip: -> { sanitize("Product is #{record.published_at ? "published" : "draft"}", tags: %w[strong]) }, + icon: -> { "heroicons/outline/#{record.published_at ? "eye" : "eye-slash"}" } + }, + { + label: -> { record.published_at ? "✅" : "🙄" }, + tooltip: -> { "Post is #{record.published_at ? "published" : "draft"}. Click to toggle." }, + url: -> { + Avo::Actions::TogglePublished.path( + resource: resource, + arguments: { + records: Array.wrap(record.id), + no_confirmation: true + } + ) + }, + data: Avo::BaseAction::DATA_ATTRIBUTES, + } + ] + def fields field :id, as: :id field :name, required: true, sortable: true diff --git a/spec/dummy/app/avo/resources/product.rb b/spec/dummy/app/avo/resources/product.rb index 467cd83c87..ba974b0d0f 100644 --- a/spec/dummy/app/avo/resources/product.rb +++ b/spec/dummy/app/avo/resources/product.rb @@ -5,12 +5,12 @@ class Avo::Resources::Product < Avo::BaseResource self.grid_view = { card: -> do { - cover_url: record.image.attached? ? main_app.url_for(record.image.variant(resize: "300x300")) : nil, + cover_url: record.image.attached? ? main_app.url_for(record.image.variant(resize_to_fill: [300, 300])) : nil, title: record.title, body: simple_format(record.description), - badge_label: (record.updated_at < 1.week.ago) ? "New" : "Updated", - badge_color: (record.updated_at < 1.week.ago) ? "green" : "orange", - badge_title: (record.updated_at < 1.week.ago) ? "New product here" : "Updated product here" + badge_label: (record.status == :new) ? "New" : "Updated", + badge_color: (record.status == :new) ? "green" : "orange", + badge_title: (record.status == :new) ? "New product here" : "Updated product here" } end, html: -> do @@ -28,6 +28,16 @@ class Avo::Resources::Product < Avo::BaseResource self.index_query = -> { query.includes image_attachment: :blob } + self.discreet_information = [ + { + tooltip: -> { sanitize("Product is #{record.status}", tags: %w[strong]) }, + icon: -> { "heroicons/outline/#{(record.status == :new) ? "arrow-trending-up" : "arrow-trending-down"}" } + }, + :timestamps + ] + self.profile_photo = { + source: -> { record.image.attached? ? main_app.url_for(record.image.variant(resize_to_fill: [300, 300])) : nil } + } def fields field :id, as: :id diff --git a/spec/dummy/app/models/product.rb b/spec/dummy/app/models/product.rb index 1cc7d59cc9..4fcfd9cfcf 100644 --- a/spec/dummy/app/models/product.rb +++ b/spec/dummy/app/models/product.rb @@ -31,4 +31,10 @@ class Product < ApplicationRecord has_one_attached :image has_many_attached :images + + def status + return :new if id.nil? + return :new if id.even? + :updated + end end diff --git a/spec/dummy/config/locales/avo.en.yml b/spec/dummy/config/locales/avo.en.yml index 5f22ca6f1f..45d166e75c 100644 --- a/spec/dummy/config/locales/avo.en.yml +++ b/spec/dummy/config/locales/avo.en.yml @@ -38,6 +38,8 @@ en: delete_item: Delete %{item} detach_item: detach %{item} details: details + created_at_timestamp: "Created at %{created_at}" + updated_at_timestamp: "Updated at %{updated_at}" download: Download download_file: Download file download_item: Download %{item} diff --git a/yarn.lock b/yarn.lock index daedefbc64..fd1c09877b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1689,9 +1689,9 @@ prettier ">=2.3.0" "@rails/actioncable@^7.0": - version "7.0.4" - resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.0.4.tgz#70a3ca56809f7aaabb80af2f9c01ae51e1a8ed41" - integrity sha512-tz4oM+Zn9CYsvtyicsa/AwzKZKL+ITHWkhiu7x+xF77clh2b4Rm+s6xnOgY/sGDWoFWZmtKsE95hxBPkgQQNnQ== + version "7.2.201" + resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.2.201.tgz#bfb3da01b3e2462f5a18f372c52dedd7de76037f" + integrity sha512-wsTdWoZ5EfG5k3t7ORdyQF0ZmDEgN4aVPCanHAiNEwCROqibSZMXXmCbH7IDJUVri4FOeAVwwbPINI7HVHPKBw== "@rails/activestorage@^6.1.710": version "6.1.710" @@ -5221,8 +5221,16 @@ stimulus-use@^0.50.0: dependencies: hotkeys-js ">=3" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0: - name string-width-cjs +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5322,8 +5330,14 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: - name strip-ansi-cjs +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5723,8 +5737,16 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==