Merge in latest :master and resolve conflict in app/models/enterprise.rb

This commit is contained in:
Cillian O'Ruanaidh
2024-09-27 10:11:26 +01:00
303 changed files with 5620 additions and 8151 deletions

View File

@@ -11,6 +11,7 @@ assignees: ''
- [ ] Merge pull requests in the [Ready To Go] column
- [ ] Include translations: `script/release/update_locales`
- You need the [Transifex Client] installed on your local dev environement to run the script.
- [ ] Increment version number: `git push upstream HEAD:refs/tags/vX.Y.Z`
- Major: if server changes are required (eg. provision with ofn-install)
- Minor: larger change that is irreversible (eg. migration deleting data)
@@ -53,3 +54,4 @@ The full process is described at https://github.com/openfoodfoundation/openfoodn
[#global-community]: https://app.slack.com/client/T02G54U79/C59ADD8F2
[Create issue]: https://github.com/openfoodfoundation/openfoodnetwork/issues/new?assignees=&labels=&projects=&template=release.md&title=Release
[#core-devs]: https://openfoodnetwork.slack.com/archives/GK2T38QPJ
[Transifex Client]: https://developers.transifex.com/docs/cli

View File

@@ -86,6 +86,15 @@ jobs:
git show --no-patch # the commit being tested (which is often a merge due to actions/checkout@v3)
bin/rake knapsack_pro:rspec
- name: Save SimpleCov file
uses: actions/upload-artifact@v4
with:
name: simplecov-chunk-controllers-${{ matrix.ci_node_index }}
path: coverage/*.*
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
if-no-files-found: ignore
include-hidden-files: true
models:
runs-on: ubuntu-22.04
services:
@@ -145,6 +154,15 @@ jobs:
run: |
bin/rake knapsack_pro:rspec
- name: Save SimpleCov file
uses: actions/upload-artifact@v4
with:
name: simplecov-chunk-models-${{ matrix.ci_node_index }}
path: coverage/*.*
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
if-no-files-found: ignore
include-hidden-files: true
system_admin:
runs-on: ubuntu-22.04
services:
@@ -214,11 +232,20 @@ jobs:
run: |
bin/rake knapsack_pro:queue:rspec
- name: Save SimpleCov file
uses: actions/upload-artifact@v4
with:
name: simplecov-chunk-system-admin-${{ matrix.ci_node_index }}
path: coverage/*.*
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
if-no-files-found: ignore
include-hidden-files: true
- name: Archive failed tests screenshots
if: failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: failed-tests-screenshots
name: failed-admin_${{ matrix.ci_node_index }}-tests-screenshots
path: tmp/capybara/screenshots/*.png
retention-days: 7
if-no-files-found: ignore
@@ -292,11 +319,20 @@ jobs:
run: |
bin/rake knapsack_pro:queue:rspec
- name: Save SimpleCov file
uses: actions/upload-artifact@v4
with:
name: simplecov-chunk-system-consumer-${{ matrix.ci_node_index }}
path: coverage/*.*
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
if-no-files-found: ignore
include-hidden-files: true
- name: Archive failed tests screenshots
if: failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: failed-tests-screenshots
name: failed-consumer_${{ matrix.ci_node_index }}-tests-screenshots
path: tmp/capybara/screenshots/*.png
retention-days: 7
if-no-files-found: ignore
@@ -371,14 +407,14 @@ jobs:
run: |
bin/rake knapsack_pro:rspec
- name: Archive failed tests screenshots
if: failure()
uses: actions/upload-artifact@v3
- name: Save SimpleCov file
uses: actions/upload-artifact@v4
with:
name: failed-tests-screenshots
path: tmp/capybara/screenshots/*.png
retention-days: 7
name: simplecov-chunk-engines-${{ matrix.ci_node_index }}
path: coverage/*.*
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
if-no-files-found: ignore
include-hidden-files: true
test_the_rest:
runs-on: ubuntu-22.04
@@ -448,6 +484,15 @@ jobs:
run: |
bin/rake knapsack_pro:rspec
- name: Save SimpleCov file
uses: actions/upload-artifact@v4
with:
name: simplecov-chunk-the-rest-${{ matrix.ci_node_index }}
path: coverage/*.*
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
if-no-files-found: ignore
include-hidden-files: true
non_knapsack_jest_karma:
runs-on: ubuntu-22.04
services:
@@ -485,3 +530,39 @@ jobs:
- name: Run jest tests
run: yarn jest
collate_simplecov_results:
runs-on: ubuntu-22.04
needs:
- controllers
- models
- engines
- system_admin
- system_consumer
- test_the_rest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Download individual results from individual runners
uses: actions/download-artifact@v4
with:
pattern: simplecov-chunk-*
path: tmp/simplecov
merge-multiple: true
- name: collate results from each of the workers
run: bundle exec rake 'simplecov:collate_results[tmp/simplecov]'
- name: Upload collated results
uses: actions/upload-artifact@v4
with:
name: combined-simplecov-report
path: coverage/**/*.*
retention-days: 7
if-no-files-found: ignore
include-hidden-files: true

View File

@@ -14,4 +14,6 @@ SimpleCov.start 'rails' do
add_filter '/log'
add_filter '/db'
add_filter '/lib/tasks/sample_data/'
formatter SimpleCov::Formatter::SimpleFormatter
end

View File

@@ -16,7 +16,6 @@ gem "image_processing"
gem 'activemerchant', '>= 1.78.0'
gem 'angular-rails-templates', '>= 0.3.0'
gem 'awesome_nested_set'
gem 'ransack', '~> 4.1.0'
gem 'responders'
gem 'webpacker', '~> 5'
@@ -105,6 +104,7 @@ gem 'sidekiq-scheduler'
gem "cable_ready"
gem "stimulus_reflex"
gem "turbo_power"
gem "turbo-rails"
gem 'combine_pdf'

View File

@@ -161,8 +161,6 @@ GEM
activerecord (>= 3.1.0, < 8)
ast (2.4.2)
attr_required (1.0.2)
awesome_nested_set (3.6.0)
activerecord (>= 4.0.0, < 7.2)
aws-eventstream (1.3.0)
aws-partitions (1.929.0)
aws-sdk-core (3.196.1)
@@ -787,6 +785,8 @@ GEM
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
turbo_power (0.6.2)
turbo-rails (>= 1.3.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
@@ -837,7 +837,7 @@ GEM
websocket-extensions (0.1.5)
whenever (1.0.0)
chronic (>= 0.6.3)
wicked_pdf (2.6.3)
wicked_pdf (2.8.1)
activesupport
wkhtmltopdf-binary (0.12.6.7)
xml-simple (1.1.8)
@@ -863,7 +863,6 @@ DEPENDENCIES
angularjs-file-upload-rails (~> 2.4.1)
angularjs-rails (= 1.8.0)
arel-helpers (~> 2.12)
awesome_nested_set
aws-sdk-s3
bigdecimal (= 3.0.2)
bootsnap
@@ -976,6 +975,7 @@ DEPENDENCIES
stripe
timecop
turbo-rails
turbo_power
valid_email2
validates_lengths_from_database
vcr

View File

@@ -10,7 +10,6 @@
//= require jquery.ui.all
//= require jquery.powertip
//= require jquery.cookie
//= require jquery.jstree/jquery.jstree
//= require jquery.vAlign
//= require angular
//= require angular-resource

View File

@@ -19,6 +19,8 @@ angular.module('admin.orderCycles')
$scope.submit = ($event, destination) ->
$event.preventDefault()
$scope.order_cycle?.trigger_action = $($event.target).data('trigger-action');
$scope.order_cycle?.confirm = $($event.target).data('confirm');
StatusMessage.display 'progress', t('js.saving')
OrderCycle.update(destination, $scope.order_cycle_form)

View File

@@ -1,7 +1,11 @@
angular.module("admin.orderCycles").controller "OrderCyclesCtrl", ($scope, $q, Columns, StatusMessage, RequestMonitor, OrderCycles, Enterprises, Schedules, Dereferencer) ->
$scope.RequestMonitor = RequestMonitor
$scope.columns = Columns.columns
$scope.saveAll = -> OrderCycles.saveChanges($scope.order_cycles_form)
$scope.saveAll = ($event) ->
trigger_action = $($event.target).data('trigger-action')
confirm = $($event.target).data('confirm')
OrderCycles.saveChanges($scope.order_cycles_form, { trigger_action, confirm })
$scope.ordersCloseAtLimit = -31 # days
$scope.resetSelectFilters = ->

View File

@@ -22,6 +22,8 @@ angular.module('admin.orderCycles').controller "AdminSimpleEditOrderCycleCtrl",
$scope.submit = ($event, destination) ->
$event.preventDefault()
$scope.order_cycle?.trigger_action = $($event.target).data('trigger-action');
$scope.order_cycle?.confirm = $($event.target).data('confirm');
StatusMessage.display 'progress', t('js.saving')
OrderCycle.mirrorIncomingToOutgoingProducts()
OrderCycle.update(destination, $scope.order_cycle_form) if OrderCycle.confirmNoDistributors()

View File

@@ -161,7 +161,11 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, $
StatusMessage.display('failure', t('js.order_cycles.create_failure'))
update: (destination, form) ->
oc = new OrderCycleResource({order_cycle: this.dataForSubmit()})
oc = new OrderCycleResource({
order_cycle: this.dataForSubmit(),
confirm: this.order_cycle.confirm,
trigger_action: this.order_cycle.trigger_action
})
oc.$update {order_cycle_id: this.order_cycle.id, reloading: (if destination? then 1 else 0)}, (data) =>
form.$setPristine() if form
if destination?
@@ -171,6 +175,8 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, $
, (response) ->
if response.data.errors?
StatusMessage.display('failure', response.data.errors[0])
else if (response.data.trigger_action)
StatusMessage.display('notice', t('js.order_cycles.unsaved_changes'), response.data.trigger_action)
else
StatusMessage.display('failure', t('js.order_cycles.update_failure'))

View File

@@ -29,13 +29,13 @@ angular.module("admin.resources").factory 'OrderCycles', ($q, $injector, OrderCy
deferred.reject(response)
deferred.promise
saveChanges: (form) ->
saveChanges: (form, params = {}) ->
changed = {}
for id, orderCycle of @byID when not @saved(orderCycle)
changed[Object.keys(changed).length] = @changesFor(orderCycle)
if Object.keys(changed).length > 0
StatusMessage.display('progress', "Saving...")
OrderCycleResource.bulkUpdate { order_cycle_set: { collection_attributes: changed } }, (data) =>
OrderCycleResource.bulkUpdate { order_cycle_set: { collection_attributes: changed }, confirm: params['confirm'], trigger_action: params['trigger_action'] }, (data) =>
for orderCycle in data
delete orderCycle.coordinator
delete orderCycle.producers
@@ -47,8 +47,10 @@ angular.module("admin.resources").factory 'OrderCycles', ($q, $injector, OrderCy
, (response) =>
if response.data.errors?
StatusMessage.display('failure', response.data.errors[0])
else if (response.data.trigger_action)
StatusMessage.display('notice', t('js.order_cycles.unsaved_changes'), response.data.trigger_action)
else
StatusMessage.display('failure', "Oh no! I was unable to save your changes.")
StatusMessage.display('failure', t('js.order_cycles.bulk_save_error'))
saved: (order_cycle) ->
@diff(order_cycle).length == 0

View File

@@ -1,21 +0,0 @@
root = exports ? this
root.taxon_tree_menu = (obj, context) ->
base_url = Spree.url(Spree.routes.taxonomy_taxons)
admin_base_url = Spree.url(Spree.routes.admin_taxonomy_taxons)
edit_url = Spree.url(Spree.routes.admin_taxonomy_taxons + '/' + obj.attr("id") + "/edit");
create:
label: "<i class='icon-plus'></i> " + Spree.translations.add,
action: (obj) -> context.create(obj)
rename:
label: "<i class='icon-pencil'></i> " + Spree.translations.rename,
action: (obj) -> context.rename(obj)
remove:
label: "<i class='icon-trash'></i> " + Spree.translations.remove,
action: (obj) -> context.remove(obj)
edit:
separator_before: true,
label: "<i class='icon-edit'></i> " + Spree.translations.edit,
action: (obj) -> window.location = edit_url.toString()

View File

@@ -1,139 +0,0 @@
handle_ajax_error = (XMLHttpRequest, textStatus, errorThrown) ->
$.jstree.rollback(last_rollback)
$("#ajax_error").show().html("<strong>" + server_error + "</strong><br />" + taxonomy_tree_error)
handle_move = (e, data) ->
last_rollback = data.rlbk
position = data.rslt.cp
node = data.rslt.o
new_parent = data.rslt.np
url = new URL(Spree.routes.admin_taxonomy_taxons)
url.pathname = url.pathname + '/' + node.attr("id")
data = {
_method: "put",
"taxon[position]": position,
"taxon[parent_id]": if !isNaN(new_parent.attr("id")) then new_parent.attr("id") else undefined
}
$.ajax
type: "POST",
dataType: "json",
url: url.toString(),
data: data,
error: handle_ajax_error
true
handle_create = (e, data) ->
last_rollback = data.rlbk
node = data.rslt.obj
name = data.rslt.name
position = data.rslt.position
new_parent = data.rslt.parent
data = {
"taxon[name]": name,
"taxon[position]": position
"taxon[parent_id]": if !isNaN(new_parent.attr("id")) then new_parent.attr("id") else undefined
}
$.ajax
type: "POST",
dataType: "json",
url: base_url.toString(),
data: data,
error: handle_ajax_error,
success: (data,result) ->
node.attr('id', data.id)
handle_rename = (e, data) ->
last_rollback = data.rlbk
node = data.rslt.obj
name = data.rslt.new_name
# change the name inside the main input field as well if taxon is the root one
document.getElementById("taxonomy_name").value = name if node.parents("[id]").attr("id") == "taxonomy_tree"
url = new URL(base_url)
url.pathname = url.pathname + '/' + node.attr("id")
$.ajax
type: "POST",
dataType: "json",
url: url.toString(),
data: {_method: "put", "taxon[name]": name },
error: handle_ajax_error
handle_delete = (e, data) ->
last_rollback = data.rlbk
node = data.rslt.obj
delete_url = new URL(base_url)
delete_url.pathname = delete_url.pathname + '/' + node.attr("id")
if confirm(Spree.translations.are_you_sure_delete)
$.ajax
type: "POST",
dataType: "json",
url: delete_url.toString(),
data: {_method: "delete"},
error: handle_ajax_error
else
$.jstree.rollback(last_rollback)
last_rollback = null
root = exports ? this
root.setup_taxonomy_tree = (taxonomy_id) ->
if taxonomy_id != undefined
# this is defined within admin/taxonomies/edit
root.base_url = Spree.url(Spree.routes.taxonomy_taxons)
$.ajax
url: base_url.pathname.replace("/taxons", "/jstree"),
success: (taxonomy) ->
last_rollback = null
conf =
json_data:
data: taxonomy,
ajax:
url: (e) ->
base_url.pathname + '/' + e.attr('id') + '/jstree'
themes:
theme: "apple",
url: "/assets/jquery.jstree/themes/apple/style.css"
strings:
new_node: new_taxon,
loading: Spree.translations.loading + "..."
crrm:
move:
check_move: (m) ->
position = m.cp
node = m.o
new_parent = m.np
# no parent or cant drag and drop
if !new_parent || node.attr("rel") == "root"
return false
# can't drop before root
if new_parent.attr("id") == "taxonomy_tree" && position == 0
return false
true
contextmenu:
items: (obj) ->
taxon_tree_menu(obj, this)
plugins: ["themes", "json_data", "dnd", "crrm", "contextmenu"]
$("#taxonomy_tree").jstree(conf)
.bind("move_node.jstree", handle_move)
.bind("remove.jstree", handle_delete)
.bind("create.jstree", handle_create)
.bind("rename.jstree", handle_rename)
.bind "loaded.jstree", ->
$(this).jstree("core").toggle_node($('.jstree-icon').first())
$("#taxonomy_tree a").on "dblclick", (e) ->
$("#taxonomy_tree").jstree("rename", this)
# surpress form submit on enter/return
$(document).keypress (e) ->
if e.keyCode == 13
e.preventDefault()

View File

@@ -9,6 +9,7 @@ angular.module("admin.utils")
$window.onbeforeunload = @onBeforeUnloadHandler
$rootScope.$on "$locationChangeStart", @locationChangeStartHandler
$window.onBeforeUnloadHandler = @onBeforeUnloadHandler
# Action for regular browser navigation.
onBeforeUnloadHandler: ($event) =>

View File

@@ -10,7 +10,9 @@ angular.module("admin.utils").factory "StatusMessage", ->
statusMessage:
text: ""
style: {}
style: {},
type: null,
actionName: null
invalidMessage: ""
@@ -23,11 +25,15 @@ angular.module("admin.utils").factory "StatusMessage", ->
active: ->
@statusMessage.text != ''
display: (type, text) ->
display: (type, text, actionName = null) ->
@statusMessage.text = text
@statusMessage.type = type
@statusMessage.actionName = actionName
@statusMessage.style = @types[type].style
null
clear: ->
@statusMessage.text = ''
@statusMessage.style = {}
@statusMessage.type = null
@statusMessage.actionName = null

View File

@@ -1,2 +1,3 @@
- # NOTE: make sure that any changes in this template are reflected in app/views/admin/products_v3/product_preview.turbo_stream.haml
%li{ "ng-class": "{active: selector.active}" }
%a{ tooltip: "{{selector.object.value}}", "tooltip-placement": "bottom", "ng-transclude": true, "ng-class": "{active: selector.active, 'has-tip': selector.object.value}" }

View File

@@ -1,7 +1,7 @@
#save-bar.animate-show{ "ng-show": 'dirty || persist || StatusMessage.active()' }
.container
.seven.columns.alpha
%h5#status-message{ "ng-show": "StatusMessage.invalidMessage == ''", "ng-style": 'StatusMessage.statusMessage.style' }
%h5#status-message{ "ng-show": "StatusMessage.invalidMessage == ''", "ng-style": 'StatusMessage.statusMessage.style', data: { 'order-cycle-form-target': 'statusMessage' }, "ng-attr-data-type": "{{StatusMessage.statusMessage.type}}", "ng-attr-data-action-name": "{{StatusMessage.statusMessage.actionName}}" }
{{ StatusMessage.statusMessage.text || "&nbsp;" }}
%h5#status-message{ style: 'color: #C85136', "ng-show": "StatusMessage.invalidMessage !== ''" }
{{ StatusMessage.invalidMessage || "&nbsp;" }}

View File

@@ -1,3 +1,4 @@
- # NOTE: make sure that any changes in this template are reflected in app/views/admin/products_v3/product_preview.turbo_stream.haml
%ul
%active-selector{ "ng-repeat": "selector in allSelectors", "ng-show": "ifDefined(selector.fits, true)" }
%span{"ng-bind" => "::selector.object.name"}

View File

@@ -1,3 +1,4 @@
- # NOTE: make sure that any changes in this template are reflected in app/views/admin/products_v3/product_preview.turbo_stream.haml
.row
.columns.small-12.medium-6.large-6.product-header
%h3{"ng-bind" => "::product.name"}

View File

@@ -0,0 +1 @@
@import './mail/all.scss';

View File

@@ -0,0 +1,3 @@
@import '../../../webpacker/css/admin/globals/palette.scss';
@import 'email';
@import 'payments_list';

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
class AdminTooltipComponent < ViewComponent::Base
def initialize(text:, link_text:, placement: "top", link: "", link_class: "")
@text = text
@link_text = link_text
@placement = placement
@link = link
@link_class = link_class
end
end

View File

@@ -0,0 +1,8 @@
%div{"data-controller": "tooltip", "data-tooltip-placement-value": @placement }
%a{"data-tooltip-target": "element", href: @link, class: @link_class}
= @link_text
.tooltip-container
.tooltip{"data-tooltip-target": "tooltip"}
= sanitize @text
.arrow{"data-tooltip-target": "arrow"}

View File

@@ -1,11 +1,15 @@
# frozen_string_literal: true
class ModalComponent < ViewComponent::Base
def initialize(id:, close_button: true, instant: false, modal_class: :small)
def initialize(id:, close_button: true, instant: false, modal_class: :small, **options)
@id = id
@close_button = close_button
@instant = instant
@modal_class = modal_class
@options = options
@data_controller = "modal #{@options.delete(:'data-controller')}".squish
@data_action =
"keyup@document->modal#closeIfEscapeKey #{@options.delete(:'data-action')}".squish
end
private

View File

@@ -1,4 +1,4 @@
%div{ id: @id, "data-controller": "modal", "data-action": "keyup@document->modal#closeIfEscapeKey", "data-modal-instant-value": @instant }
%div{ id: @id, "data-controller": @data_controller, "data-action": @data_action, "data-modal-instant-value": @instant, **@options }
.reveal-modal-bg.fade{ "data-modal-target": "background", "data-action": "click->modal#close" }
.reveal-modal.fade.modal-component{ "data-modal-target": "modal", class: @modal_class }
= content

View File

@@ -24,6 +24,19 @@
max-width: 100%;
height: auto;
}
.flex-column {
display: flex;
flex-direction: column;
}
.gap-1 {
gap: 1rem;
}
.gap-2 {
gap: 2rem;
}
}
/* prevent arrow on selected admin menu item appearing above modal */

View File

@@ -11,7 +11,8 @@ class SearchableDropdownComponent < ViewComponent::Base
selected_option:,
placeholder_value:,
include_blank: false,
aria_label: ''
aria_label: '',
other_attrs: {}
)
@f = form
@name = name
@@ -20,11 +21,13 @@ class SearchableDropdownComponent < ViewComponent::Base
@placeholder_value = placeholder_value
@include_blank = include_blank
@aria_label = aria_label
@other_attrs = other_attrs
end
private
attr_reader :f, :name, :options, :selected_option, :placeholder_value, :include_blank, :aria_label
attr_reader :f, :name, :options, :selected_option, :placeholder_value, :include_blank,
:aria_label, :other_attrs
def classes
"fullwidth #{remove_search_plugin? ? 'no-input' : ''}"

View File

@@ -1 +1 @@
= f.select name, options_for_select(options, selected_option), { include_blank: }, class: classes, data:, 'aria-label': aria_label
= f.select name, options_for_select(options, selected_option), { include_blank: }, class: classes, data:, 'aria-label': aria_label, **other_attrs

View File

@@ -8,6 +8,10 @@ export default class extends Controller {
window.addEventListener("click", this.#hideIfClickedOutside);
}
disconnect() {
window.removeEventListener("click", this.#hideIfClickedOutside);
}
toggle() {
this.contentTarget.classList.toggle("show");
}

View File

@@ -35,13 +35,7 @@ module Admin
private
def fetch_catalog(url)
if url =~ /food-data-collaboration/
fdc_json = FdcRequest.new(spree_current_user).call(url)
fdc_message = JSON.parse(fdc_json)
fdc_message["products"]
else
DfcRequest.new(spree_current_user).call(url)
end
DfcRequest.new(spree_current_user).call(url)
end
# Most of this code is the same as in the DfcProvider::SuppliedProductsController.

View File

@@ -2,6 +2,8 @@
module Admin
class OrderCyclesController < Admin::ResourceController
class DateTimeChangeError < StandardError; end
include ::OrderCyclesHelper
include PaperTrailLogging
@@ -62,9 +64,7 @@ module Admin
end
def update
@order_cycle_form = OrderCycles::FormService.new(@order_cycle, order_cycle_params,
spree_current_user)
@order_cycle_form = set_order_cycle_form
if @order_cycle_form.save
update_nil_subscription_line_items_price_estimate(@order_cycle)
respond_to do |format|
@@ -77,6 +77,9 @@ module Admin
elsif request.format.json?
render json: { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity
end
rescue DateTimeChangeError
render json: { trigger_action: params[:trigger_action] },
status: :unprocessable_entity
end
def bulk_update
@@ -90,6 +93,9 @@ module Admin
order_cycle = order_cycle_set.collection.find{ |oc| oc.errors.present? }
render json: { errors: order_cycle.errors.full_messages }, status: :unprocessable_entity
end
rescue DateTimeChangeError
render json: { trigger_action: params[:trigger_action] },
status: :unprocessable_entity
end
def bulk_update_nil_subscription_line_items_price_estimate
@@ -235,7 +241,7 @@ module Admin
else
begin
yield
rescue ActiveRecord::InvalidForeignKey
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::DeleteRestrictionError
redirect_to main_app.admin_order_cycles_url
flash[:error] = I18n.t('admin.order_cycles.destroy_errors.orders_present')
end
@@ -270,7 +276,10 @@ module Admin
end
def order_cycle_set
@order_cycle_set ||= Sets::OrderCycleSet.new(@order_cycles, order_cycle_bulk_params)
@order_cycle_set ||= Sets::OrderCycleSet.new(
@order_cycles, { **order_cycle_bulk_params,
confirm_datetime_change: params[:confirm], error_class: DateTimeChangeError }
)
end
def require_order_cycle_set_params
@@ -294,5 +303,14 @@ module Admin
collection_attributes: [:id] + PermittedAttributes::OrderCycle.basic_attributes
).to_h.with_indifferent_access
end
def set_order_cycle_form
OrderCycles::FormService.new(
@order_cycle, order_cycle_params.merge(
{ confirm_datetime_change: params[:order_cycle][:confirm],
error_class: DateTimeChangeError }
), spree_current_user
)
end
end
end

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
module Admin
class ProductPreviewController < Spree::Admin::BaseController
def show
@product = Spree::Product.find(params[:id])
authorize! :show, @product
respond_with do |format|
format.turbo_stream {
render "admin/products_v3/product_preview", status: :ok
}
end
end
private
def model_class
Spree::Product
end
end
end

View File

@@ -11,6 +11,8 @@ module Admin
def index
fetch_products
render "index", locals: { producers:, categories:, tax_category_options:, flash: }
session[:products_return_to_url] = request.url
end
def bulk_update
@@ -38,6 +40,8 @@ module Admin
{ id: params[:id] }
).find_product
authorize! :delete, @record
@record.destroyed_by = spree_current_user
status = :ok
@@ -72,6 +76,8 @@ module Admin
def clone
@product = Spree::Product.find(params[:id])
authorize! :clone, @product
status = :ok
begin

View File

@@ -6,7 +6,7 @@ module Admin
include ReportsActions
helper ReportsHelper
before_action :authorize_report, only: [:show]
before_action :authorize_report, only: [:show, :create]
# Define model class for Can? permissions
def model_class
@@ -20,14 +20,17 @@ module Admin
end
def show
@report = report_class.new(spree_current_user, params, render: render_data?)
@rendering_options = rendering_options # also stores user preferences
@report = report_class.new(spree_current_user, params, render: false)
@rendering_options = rendering_options
if render_data?
render_in_background
else
show_report
end
show_report
end
def create
@report = report_class.new(spree_current_user, params, render: true)
update_rendering_options
render_in_background
end
private
@@ -54,31 +57,15 @@ module Admin
@variant_serialized = Api::Admin::VariantSerializer.new(variant)
end
def render_data?
request.post?
end
def render_in_background
cable_ready[ScopedChannel.for_id(params[:uuid])]
.inner_html(
selector: "#report-go",
html: helpers.button(t(:go), "report__submit-btn", "submit", disabled: true)
).inner_html(
selector: "#report-table",
html: render_to_string(partial: "admin/reports/loading")
).scroll_into_view(
selector: "#report-table",
block: "start"
).broadcast
@blob = ReportBlob.create_for_upload_later!(report_filename)
ReportJob.perform_later(
report_class:, user: spree_current_user, params:,
format: report_format,
filename: report_filename,
blob: @blob,
channel: ScopedChannel.for_id(params[:uuid]),
)
head :no_content
end
end
end

View File

@@ -1,16 +0,0 @@
# frozen_string_literal: true
module Api
module V0
class TaxonomiesController < Api::V0::BaseController
respond_to :json
skip_authorization_check only: :jstree
def jstree
@taxonomy = Spree::Taxonomy.find(params[:id])
render json: @taxonomy.root, serializer: Api::TaxonJstreeSerializer
end
end
end
end

View File

@@ -5,12 +5,10 @@ module Api
class TaxonsController < Api::V0::BaseController
respond_to :json
skip_authorization_check only: [:index, :show, :jstree]
skip_authorization_check only: [:index, :show]
def index
@taxons = if taxonomy
taxonomy.root.children
elsif params[:ids]
@taxons = if params[:ids]
Spree::Taxon.where(id: raw_params[:ids].split(","))
else
Spree::Taxon.ransack(raw_params[:q]).result
@@ -18,23 +16,9 @@ module Api
render json: @taxons, each_serializer: Api::TaxonSerializer
end
def jstree
@taxon = taxon
render json: @taxon.children, each_serializer: Api::TaxonJstreeSerializer
end
def create
authorize! :create, Spree::Taxon
@taxon = Spree::Taxon.new(taxon_params)
@taxon.taxonomy_id = params[:taxonomy_id]
taxonomy = Spree::Taxonomy.find_by(id: params[:taxonomy_id])
if taxonomy.nil?
@taxon.errors.add(:taxonomy_id, I18n.t(:invalid_taxonomy_id, scope: 'spree.api'))
invalid_resource!(@taxon) && return
end
@taxon.parent_id = taxonomy.root.id unless params.dig(:taxon, :parent_id)
if @taxon.save
render json: @taxon, serializer: Api::TaxonSerializer, status: :created
@@ -60,20 +44,14 @@ module Api
private
def taxonomy
return if params[:taxonomy_id].blank?
@taxonomy ||= Spree::Taxonomy.find(params[:taxonomy_id])
end
def taxon
@taxon ||= taxonomy.taxons.find(params[:id])
@taxon = Spree::Taxon.find(params[:id])
end
def taxon_params
return if params[:taxon].blank?
params.require(:taxon).permit([:name, :parent_id, :position])
params.require(:taxon).permit([:name, :position])
end
end
end

View File

@@ -88,14 +88,10 @@ module ReportsActions
display_header_row: false
}
end
update_rendering_options
@rendering_options
end
def update_rendering_options
return unless request.post?
@rendering_options.update(
rendering_options.update(
options: {
fields_to_show: params[:fields_to_show],
display_summary_row: params[:display_summary_row].present?,

View File

@@ -19,15 +19,18 @@ module Spree
before_action :authorize_admin
before_action :set_locale
before_action :warn_invalid_order_cycles, if: :html_request?
before_action :warn_invalid_order_cycles, if: :page_load_request?
# Warn the user when they have an active order cycle with hubs that are not ready
# for checkout (ie. does not have valid shipping and payment methods).
def warn_invalid_order_cycles
return if flash[:notice].present?
return if flash[:notice].present? || session[:displayed_order_cycle_warning]
warning = OrderCycles::WarningService.new(spree_current_user).call
flash[:notice] = warning if warning.present?
return if warning.blank?
flash.now[:notice] = warning
session[:displayed_order_cycle_warning] = true
end
protected
@@ -81,6 +84,12 @@ module Spree
private
def page_load_request?
return false if request.format.include?('turbo')
html_request?
end
def html_request?
request.format.html?
end

View File

@@ -10,6 +10,7 @@ module Spree
include OpenFoodNetwork::SpreeApiKeyLoader
include OrderCyclesHelper
include EnterprisesHelper
helper ::Admin::ProductsHelper
before_action :load_data
before_action :load_producers, only: [:index, :new]

View File

@@ -1,27 +0,0 @@
# frozen_string_literal: true
module Spree
module Admin
class TaxonomiesController < ::Admin::ResourceController
respond_to :json, only: [:get_children]
def get_children
@taxons = Taxon.find(params[:parent_id]).children
end
private
def location_after_save
if @taxonomy.created_at == @taxonomy.updated_at
spree.edit_admin_taxonomy_url(@taxonomy)
else
spree.admin_taxonomies_url
end
end
def permitted_resource_params
params.require(:taxonomy).permit(:name)
end
end
end
end

View File

@@ -2,122 +2,70 @@
module Spree
module Admin
class TaxonsController < Spree::Admin::BaseController
respond_to :html, :json, :js
class TaxonsController < ::Admin::ResourceController
before_action :set_taxon, except: %i[create index new]
def edit
@taxonomy = Taxonomy.find(params[:taxonomy_id])
@taxon = @taxonomy.taxons.find(params[:id])
@permalink_part = @taxon.permalink.split("/").last
def index
@taxons = Taxon.order(:name)
end
def new
@taxon = Taxon.new
end
def edit; end
def create
@taxonomy = Taxonomy.find(params[:taxonomy_id])
@taxon = @taxonomy.taxons.build(params[:taxon])
@taxon = Spree::Taxon.new(taxon_params)
if @taxon.save
respond_with(@taxon) do |format|
format.json { render json: @taxon.to_json }
end
flash[:success] = flash_message_for(@taxon, :successfully_created)
redirect_to edit_admin_taxon_path(@taxon.id)
else
flash[:error] = Spree.t('errors.messages.could_not_create_taxon')
respond_with(@taxon) do |format|
format.html do
if redirect_to @taxonomy
spree.edit_admin_taxonomy_url(@taxonomy)
else
spree.admin_taxonomies_url
end
end
end
render :new, status: :unprocessable_entity
end
end
def update
@taxonomy = Taxonomy.find(params[:taxonomy_id])
@taxon = @taxonomy.taxons.find(params[:id])
parent_id = params[:taxon][:parent_id]
new_position = params[:taxon][:position]
if parent_id || new_position # taxon is being moved
new_parent = parent_id.nil? ? @taxon.parent : Taxon.find(parent_id.to_i)
new_position = new_position.nil? ? -1 : new_position.to_i
# Bellow is a very complicated way of finding where in nested set we
# should actually move the taxon to achieve sane results,
# JS is giving us the desired position, which was awesome for previous setup,
# but now it's quite complicated to find where we should put it as we have
# to differenciate between moving to the same branch, up down and into
# first position.
new_siblings = new_parent.children
if new_position <= 0 && new_siblings.empty?
@taxon.move_to_child_of(new_parent)
elsif new_parent.id != @taxon.parent_id
if new_position.zero?
@taxon.move_to_left_of(new_siblings.first)
else
@taxon.move_to_right_of(new_siblings[new_position - 1])
end
elsif new_position < new_siblings.index(@taxon)
@taxon.move_to_left_of(new_siblings[new_position]) # we move up
else
@taxon.move_to_right_of(new_siblings[new_position - 1]) # we move down
end
# Reset legacy position, if any extensions still rely on it
new_parent.children.reload.each do |t|
t.update_columns(
position: t.position,
updated_at: Time.zone.now
)
end
if parent_id
@taxon.reload
@taxon.set_permalink
@taxon.save!
@update_children = true
end
end
if params.key? "permalink_part"
parent_permalink = @taxon.permalink.split("/")[0...-1].join("/")
parent_permalink += "/" if parent_permalink.present?
params[:taxon][:permalink] = parent_permalink + params[:permalink_part]
end
# check if we need to rename child taxons if parent name or permalink changes
if params[:taxon][:name] != @taxon.name || params[:taxon][:permalink] != @taxon.permalink
@update_children = true
end
if @taxon.update(taxon_params)
flash[:success] = flash_message_for(@taxon, :successfully_updated)
end
# rename child taxons
if @update_children
@taxon.descendants.each do |taxon|
taxon.reload
taxon.set_permalink
taxon.save!
end
end
respond_with(@taxon) do |format|
format.html { redirect_to spree.edit_admin_taxonomy_url(@taxonomy) }
format.json { render json: @taxon.to_json }
redirect_to edit_admin_taxon_path(@taxon.id)
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@taxon = Taxon.find(params[:id])
@taxon.destroy
respond_with(@taxon) { |format| format.json { render json: '' } }
status = if @taxon.destroy
flash_message = t('.delete_taxon.success')
status = :ok
else
flash_message = t('.delete_taxon.error')
status = :unprocessable_entity
end
respond_to do |format|
format.html {
flash[:success] = flash_message if status == :ok
flash[:error] = flash_message if status == :unprocessable_entity
redirect_to admin_taxons_path
}
format.turbo_stream {
flash[:success] = flash_message if status == :ok
flash[:error] = flash_message if status == :unprocessable_entity
render :destroy_taxon, status:
}
end
end
private
def set_taxon
@taxon = Taxon.find(params[:id])
end
def taxon_params
params.require(:taxon).permit(
:name, :parent_id, :position, :icon, :description, :permalink, :taxonomy_id,
:name, :position, :icon, :description, :permalink,
:meta_description, :meta_keywords, :meta_title, :dfc_id
)
end

View File

@@ -10,8 +10,11 @@ module Admin
end
end
def prepare_new_variant(product)
product.variants.build
def prepare_new_variant(product, producer_options)
# e.g producer_options = [['producer name', id]]
product.variants.build do |new_variant|
new_variant.supplier_id = producer_options.first.second if producer_options.one?
end
end
def unit_value_with_description(variant)
@@ -29,5 +32,19 @@ module Admin
[precised_unit_value, variant.unit_description].compact_blank.join(" ")
end
def products_return_to_url(url_filters)
if feature?(:admin_style_v3, spree_current_user)
return session[:products_return_to_url] || admin_products_url
end
"#{admin_products_path}#{url_filters.empty? ? '' : "#?#{url_filters.to_query}"}"
end
# if user hasn't saved any preferences on products page and there's only one producer;
# we need to hide producer column
def hide_producer_column?(producer_options)
spree_current_user.column_preferences.bulk_edit_product.empty? && producer_options.one?
end
end
end

View File

@@ -61,15 +61,6 @@ module ApplicationHelper
classes << shopfront_layout
end
def pdf_stylesheet_pack_tag(source)
if running_in_development?
options = { media: "all", host: "#{Webpacker.dev_server.host}:#{Webpacker.dev_server.port}" }
stylesheet_pack_tag(source, **options)
else
wicked_pdf_stylesheet_pack_tag(source)
end
end
def cache_with_locale(key = nil, options = {}, &block)
cache(cache_key_with_locale(key, I18n.locale), options) do
yield(block)

View File

@@ -8,11 +8,13 @@ module InjectionHelper
include OrderCyclesHelper
def inject_enterprises(enterprises = nil)
enterprises ||= default_enterprise_query
inject_json_array(
"enterprises",
enterprises || default_enterprise_query,
enterprises,
Api::EnterpriseSerializer,
enterprise_injection_data,
enterprise_injection_data(enterprises.map(&:id)),
)
end
@@ -57,15 +59,16 @@ module InjectionHelper
inject_json_array "enterprises",
enterprises_and_relatives,
Api::EnterpriseSerializer,
enterprise_injection_data
enterprise_injection_data(enterprises_and_relatives.map(&:id))
end
def inject_group_enterprises(group)
enterprises = group.enterprises.activated.visible.all
inject_json_array(
"enterprises",
group.enterprises.activated.visible.all,
enterprises,
Api::EnterpriseSerializer,
enterprise_injection_data,
enterprise_injection_data(enterprises.map(&:id)),
)
end
@@ -73,7 +76,7 @@ module InjectionHelper
inject_json "currentHub",
current_distributor,
Api::EnterpriseSerializer,
enterprise_injection_data
enterprise_injection_data(current_distributor ? [current_distributor.id] : nil)
end
def inject_current_order
@@ -153,7 +156,9 @@ module InjectionHelper
Enterprise.activated.includes(address: [:state, :country]).all
end
def enterprise_injection_data
@enterprise_injection_data ||= { data: OpenFoodNetwork::EnterpriseInjectionData.new }
def enterprise_injection_data(enterprise_ids)
{
data: OpenFoodNetwork::EnterpriseInjectionData.new(enterprise_ids)
}
end
end

View File

@@ -1,11 +0,0 @@
# frozen_string_literal: true
module Spree
module Admin
module TaxonsHelper
def taxon_path(taxon)
taxon.ancestors.reverse.collect(&:name).join( " >> ")
end
end
end
end

View File

@@ -9,12 +9,12 @@ class ReportJob < ApplicationJob
NOTIFICATION_TIME = 5.seconds
def perform(report_class:, user:, params:, format:, filename:, channel: nil)
def perform(report_class:, user:, params:, format:, blob:, channel: nil)
start_time = Time.zone.now
report = report_class.new(user, params, render: true)
result = report.render_as(format)
blob = ReportBlob.create!(filename, result)
blob.store(result)
execution_time = Time.zone.now - start_time

View File

@@ -15,10 +15,11 @@ class ColumnPreference < ApplicationRecord
validates :column_name, presence: true, inclusion: { in: proc { |p|
valid_columns_for(p.action_name)
} }
scope :bulk_edit_product, -> { where(action_name: 'products_v3_index') }
def self.for(user, action_name)
stored_preferences = where(user_id: user.id, action_name:)
default_preferences = __send__("#{action_name}_columns")
default_preferences = get_default_preferences(action_name, user)
filter(default_preferences, user, action_name)
default_preferences.each_with_object([]) do |(column_name, default_attributes), preferences|
stored_preference = stored_preferences.find_by(column_name:)
@@ -36,7 +37,7 @@ class ColumnPreference < ApplicationRecord
end
def self.valid_columns_for(action_name)
__send__("#{action_name}_columns").keys.map(&:to_s)
get_default_preferences(action_name, Spree::User.new).keys.map(&:to_s)
end
def self.known_actions
@@ -52,4 +53,13 @@ class ColumnPreference < ApplicationRecord
default_preferences.delete(:schedules)
end
def self.get_default_preferences(action_name, user)
case action_name
when 'products_v3_index'
products_v3_index_columns(user)
else
__send__("#{action_name}_columns")
end
end
end

View File

@@ -6,14 +6,14 @@
# `count_on_hand` can either be: nil or a number
#
# This means that a variant override can be in six different stock states
# but only three of them are valid.
# but only four of them are valid.
#
# | on_demand | count_on_hand | stock_overridden? | use_producer_stock_settings? | valid? |
# |-----------|---------------|-------------------|------------------------------|--------|
# | 1 | nil | false | false | true |
# | 1 | nil | true | false | true |
# | 0 | x | true | false | true |
# | nil | nil | false | true | true |
# | 1 | x | ? | ? | false |
# | 1 | x | true | false | true |
# | 0 | nil | ? | ? | false |
# | nil | x | ? | ? | false |
#
@@ -27,7 +27,6 @@ module StockSettingsOverrideValidation
def require_compatible_on_demand_and_count_on_hand
disallow_count_on_hand_if_using_producer_stock_settings
disallow_count_on_hand_if_on_demand
require_count_on_hand_if_limited_stock
end
@@ -39,14 +38,6 @@ module StockSettingsOverrideValidation
errors.add(:count_on_hand, error_message)
end
def disallow_count_on_hand_if_on_demand
return unless on_demand? && count_on_hand.present?
error_message = I18n.t("count_on_hand.on_demand_but_count_on_hand_set",
scope: i18n_scope_for_stock_settings_override_validation_error)
errors.add(:count_on_hand, error_message)
end
def require_count_on_hand_if_limited_stock
return unless on_demand == false && count_on_hand.blank?

View File

@@ -96,7 +96,7 @@ module VariantStock
# Here we depend only on variant.total_on_hand and variant.on_demand.
# This way, variant_overrides only need to override variant.total_on_hand and variant.on_demand.
def fill_status(quantity)
on_hand = if total_on_hand >= quantity || on_demand
on_hand = if total_on_hand.to_i >= quantity || on_demand
quantity
else
[0, total_on_hand].max
@@ -112,8 +112,7 @@ module VariantStock
#
# This enables us to override this behaviour for variant overrides
def move(quantity, originator = nil)
# Don't change variant stock if variant is on_demand or has been deleted
return if on_demand || deleted_at
return if deleted_at
raise_error_if_no_stock_item_available

View File

@@ -1,7 +1,5 @@
# frozen_string_literal: false
require "mini_magick"
class Enterprise < ApplicationRecord
SELLS = %w(unspecified none own any).freeze
ENTERPRISE_SEARCH_RADIUS = 100
@@ -249,12 +247,6 @@ class Enterprise < ApplicationRecord
count(distinct: true)
end
# Remove any unsupported HTML.
def long_description
HtmlSanitizer.sanitize_and_enforce_link_target_blank(super)
end
# Remove any unsupported HTML.
def long_description=(html)
super(HtmlSanitizer.sanitize_and_enforce_link_target_blank(html))
end
@@ -488,7 +480,7 @@ class Enterprise < ApplicationRecord
return unless image.variable?
image_variant_url_for(image.variant(name))
rescue ActiveStorage::Error, MiniMagick::Error, ActionView::Template::Error => e
rescue StandardError => e
Bugsnag.notify "Enterprise ##{id} #{image.try(:name)} error: #{e.message}"
Rails.logger.error(e.message)

View File

@@ -24,6 +24,7 @@ class OrderCycle < ApplicationRecord
where incoming: false
}, class_name: "Exchange", dependent: :destroy
has_many :orders, class_name: 'Spree::Order', dependent: :restrict_with_exception
has_many :suppliers, -> { distinct }, source: :sender, through: :cached_incoming_exchanges
has_many :distributors, -> { distinct }, source: :receiver, through: :cached_outgoing_exchanges
has_many :order_cycle_schedules, dependent: :destroy
@@ -147,17 +148,20 @@ class OrderCycle < ApplicationRecord
# Find the earliest closing times for each distributor in an active order cycle, and return
# them in the format {distributor_id => closing_time, ...}
def self.earliest_closing_times
Hash[
Exchange.
outgoing.
joins(:order_cycle).
merge(OrderCycle.active).
group('exchanges.receiver_id').
select("exchanges.receiver_id AS receiver_id,
MIN(order_cycles.orders_close_at) AS earliest_close_at").
map { |ex| [ex.receiver_id, ex.earliest_close_at.to_time] }
]
#
# Optionally, specify some distributor_ids as a parameter to scope the results
def self.earliest_closing_times(distributor_ids = nil)
cycles = Exchange.
outgoing.
joins(:order_cycle).
merge(OrderCycle.active).
group('exchanges.receiver_id')
cycles = cycles.where(receiver_id: distributor_ids) if distributor_ids.present?
cycles.pluck("exchanges.receiver_id AS receiver_id",
"MIN(order_cycles.orders_close_at) AS earliest_close_at")
.to_h
end
def attachable_distributor_payment_methods
@@ -313,6 +317,13 @@ class OrderCycle < ApplicationRecord
coordinator.sells == 'own'
end
def same_datetime_value(attribute, string)
return true if self[attribute].blank? && string.blank?
return false if self[attribute].blank? || string.blank?
DateTime.parse(string).to_fs(:short) == self[attribute]&.to_fs(:short)
end
private
def opening?

View File

@@ -5,7 +5,7 @@ class ReportBlob < ActiveStorage::Blob
# AWS S3 limits URL expiry to one week.
LIFETIME = 1.week
def self.create!(filename, content)
def self.create_locally!(filename, content)
create_and_upload!(
io: StringIO.new(content),
filename:,
@@ -15,11 +15,34 @@ class ReportBlob < ActiveStorage::Blob
)
end
def self.create_for_upload_later!(filename)
# ActiveStorage discourages modifying a blob later but we need a blob
# before we know anything about the report file. It enables us to use the
# same blob in the controller to read the result.
create_before_direct_upload!(
filename:,
byte_size: 0,
checksum: "0",
content_type: content_type(filename),
service_name: :local,
).tap do |blob|
ActiveStorage::PurgeJob.set(wait: LIFETIME).perform_later(blob)
end
end
def self.content_type(filename)
MIME::Types.of(filename).first&.to_s || "application/octet-stream"
end
def store(content)
io = StringIO.new(content)
upload(io, identify: false)
save!
end
def result
return if checksum == "0"
@result ||= download.force_encoding(Encoding::UTF_8)
end

View File

@@ -29,7 +29,6 @@ module Spree
can :update, Order do |order, token|
order.user == user || (order.token && token == order.token)
end
can [:index, :read], Product
can [:index, :read], ProductProperty
can [:index, :read], Property
can :create, Spree::User
@@ -39,7 +38,6 @@ module Spree
can [:index, :read], StockLocation
can [:index, :read], StockMovement
can [:index, :read], Taxon
can [:index, :read], Taxonomy
can [:index, :read], Variant
can [:index, :read], Zone
end
@@ -244,8 +242,8 @@ module Spree
can [:admin, :index], ::Admin::DfcProductImportsController
# Reports page
can [:admin, :index, :show], ::Admin::ReportsController
can [:admin, :show, :customers, :orders_and_distributors, :group_buys, :payments,
can [:admin, :index, :show, :create], ::Admin::ReportsController
can [:admin, :show, :create, :customers, :orders_and_distributors, :group_buys, :payments,
:orders_and_fulfillment, :products_and_inventory, :order_cycle_management,
:packing, :enterprise_fee_summary, :bulk_coop], :report
end
@@ -325,7 +323,7 @@ module Spree
end
# Reports page
can [:admin, :index, :show], ::Admin::ReportsController
can [:admin, :index, :show, :create], ::Admin::ReportsController
can [:admin, :customers, :group_buys, :sales_tax, :payments,
:orders_and_distributors, :orders_and_fulfillment, :products_and_inventory,
:order_cycle_management, :xero_invoices, :enterprise_fee_summary, :bulk_coop], :report

View File

@@ -1,7 +1,5 @@
# frozen_string_literal: true
require "mini_magick"
module Spree
class Image < Asset
has_one_attached :attachment, service: image_service do |attachment|
@@ -35,7 +33,7 @@ module Spree
return self.class.default_image_url(size) unless attachment.attached?
image_variant_url_for(variant(size))
rescue ActiveStorage::Error, MiniMagick::Error, ActionView::Template::Error => e
rescue StandardError => e
Bugsnag.notify "Product ##{viewable_id} Image ##{id} error: #{e.message}"
Rails.logger.error(e.message)

View File

@@ -263,10 +263,12 @@ module Spree
# Format as per WeightsAndMeasures (todo: re-orgnaise maybe after product/variant refactor)
def variant_unit_with_scale
# Our code is based upon English based number formatting with a period `.`
scale_clean = ActiveSupport::NumberHelper.number_to_rounded(variant_unit_scale,
precision: nil,
significant: false,
strip_insignificant_zeros: true)
strip_insignificant_zeros: true,
locale: :en)
[variant_unit, scale_clean].compact_blank.join("_")
end
@@ -295,14 +297,8 @@ module Spree
# eg clone product. Will raise error if clonning a product with no variant
return if variants.first&.valid?
unless Spree::Taxon.find_by(id: primary_taxon_id)
errors.add(:primary_taxon_id,
I18n.t('activerecord.errors.models.spree/product.must_exist'))
end
return if Enterprise.find_by(id: supplier_id)
errors.add(:supplier_id,
I18n.t('activerecord.errors.models.spree/product.must_exist'))
errors.add(:primary_taxon_id, :blank) unless Spree::Taxon.find_by(id: primary_taxon_id)
errors.add(:supplier_id, :blank) unless Enterprise.find_by(id: supplier_id)
end
def update_units

View File

@@ -87,16 +87,20 @@ module Spree
# Return the services (pickup, delivery) that different distributors provide, in the format:
# {distributor_id => {pickup: true, delivery: false}, ...}
def self.services
Hash[
Spree::ShippingMethod.
joins(:distributor_shipping_methods).
group('distributor_id').
select("distributor_id").
select("BOOL_OR(spree_shipping_methods.require_ship_address = 'f') AS pickup").
select("BOOL_OR(spree_shipping_methods.require_ship_address = 't') AS delivery").
map { |sm| [sm.distributor_id.to_i, { pickup: sm.pickup, delivery: sm.delivery }] }
]
#
# Optionally, specify some distributor_ids as a parameter to scope the results
def self.services(distributor_ids = nil)
methods = Spree::ShippingMethod.joins(:distributor_shipping_methods).group('distributor_id')
if distributor_ids.present?
methods = methods.where(distributor_shipping_methods: { distributor_id: distributor_ids })
end
methods.
pluck(Arel.sql("distributor_id"),
Arel.sql("BOOL_OR(spree_shipping_methods.require_ship_address = 'f') AS pickup"),
Arel.sql("BOOL_OR(spree_shipping_methods.require_ship_address = 't') AS delivery")).
to_h { |(distributor_id, pickup, delivery)| [distributor_id.to_i, { pickup:, delivery: }] }
end
def self.backend

View File

@@ -2,19 +2,11 @@
module Spree
class Taxon < ApplicationRecord
self.belongs_to_required_by_default = false
acts_as_nested_set dependent: :destroy
belongs_to :taxonomy, class_name: 'Spree::Taxonomy', touch: true
has_many :variants, class_name: "Spree::Variant", foreign_key: "primary_taxon_id",
inverse_of: :primary_taxon, dependent: :restrict_with_error
has_many :products, through: :variants, dependent: nil
before_create :set_permalink
validates :name, presence: true
# Indicate which filters should be used for this taxon
@@ -31,40 +23,21 @@ module Spree
end
end
def set_permalink
if parent.present?
self.permalink = [parent.permalink, permalink_end].join('/')
elsif permalink.blank?
self.permalink = UrlGenerator.to_url(name)
end
end
# For #2759
def to_param
permalink
end
def pretty_name
ancestor_chain = ancestors.inject("") do |name, ancestor|
name + "#{ancestor.name} -> "
end
ancestor_chain + name.to_s
end
# Find all the taxons of supplied products for each enterprise, indexed by enterprise.
# Format: {enterprise_id => [taxon_id, ...]}
def self.supplied_taxons
taxons = {}
#
# Optionally, specify some enterprise_ids to scope the results
def self.supplied_taxons(enterprise_ids = nil)
taxons = Spree::Taxon.joins(variants: :supplier)
Spree::Taxon.
joins(variants: :supplier).
select('spree_taxons.*, enterprises.id AS enterprise_id').
each do |t|
taxons[t.enterprise_id.to_i] ||= Set.new
taxons[t.enterprise_id.to_i] << t.id
end
taxons = taxons.where(enterprises: { id: enterprise_ids }) if enterprise_ids.present?
taxons
.pluck('spree_taxons.id, enterprises.id AS enterprise_id')
.each_with_object({}) do |(taxon_id, enterprise_id), collection|
collection[enterprise_id.to_i] ||= Set.new
collection[enterprise_id.to_i] << taxon_id
end
end
# Find all the taxons of distributed products for each enterprise, indexed by enterprise.
@@ -72,7 +45,9 @@ module Spree
# or :current taxons (distributed in an open order cycle).
#
# Format: {enterprise_id => [taxon_id, ...]}
def self.distributed_taxons(which_taxons = :all)
#
# Optionally, specify some enterprise_ids to scope the results
def self.distributed_taxons(which_taxons = :all, enterprise_ids = nil)
ents_and_vars = ExchangeVariant.joins(exchange: :order_cycle).merge(Exchange.outgoing)
.select("DISTINCT variant_id, receiver_id AS enterprise_id")
@@ -85,18 +60,14 @@ module Spree
INNER JOIN (#{ents_and_vars.to_sql}) AS ents_and_vars
ON spree_variants.id = ents_and_vars.variant_id")
if enterprise_ids.present?
taxons = taxons.where(ents_and_vars: { enterprise_id: enterprise_ids })
end
taxons.each_with_object({}) do |t, ts|
ts[t.enterprise_id.to_i] ||= Set.new
ts[t.enterprise_id.to_i] << t.id
end
end
private
def permalink_end
return UrlGenerator.to_url(name) if permalink.blank?
permalink.split('/').last
end
end
end

View File

@@ -1,27 +0,0 @@
# frozen_string_literal: true
module Spree
class Taxonomy < ApplicationRecord
validates :name, presence: true
has_many :taxons, dependent: :nullify
has_one :root, -> { where parent_id: nil }, class_name: "Spree::Taxon", dependent: :destroy
after_save :set_name
default_scope -> { order("#{table_name}.position") }
private
def set_name
if root
root.update_columns(
name:,
updated_at: Time.zone.now
)
else
self.root = Taxon.create!(taxonomy_id: id, name:)
end
end
end
end

View File

@@ -42,6 +42,7 @@ module Spree
has_many :credit_cards, dependent: :destroy
has_many :report_rendering_options, class_name: "::ReportRenderingOptions", dependent: :destroy
has_many :webhook_endpoints, dependent: :destroy
has_many :column_preferences, dependent: :destroy
has_one :oidc_account, dependent: :destroy
accepts_nested_attributes_for :enterprise_roles, allow_destroy: true

View File

@@ -15,7 +15,9 @@ class VariantOverride < ApplicationRecord
# Need to ensure this can be set by the user.
validates :default_stock, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
validates :price, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
validates :count_on_hand, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
validates :count_on_hand, numericality: {
greater_than_or_equal_to: 0, unless: :on_demand?
}, allow_nil: true
default_scope { where(permission_revoked_at: nil) }
@@ -36,9 +38,8 @@ class VariantOverride < ApplicationRecord
end
def stock_overridden?
# If count_on_hand is present, it means on_demand is false
# See StockSettingsOverrideValidation for details
count_on_hand.present?
# Testing for not nil because for a boolean `false.present?` is false.
!on_demand.nil? || !count_on_hand.nil?
end
def use_producer_stock_settings?

View File

@@ -3,7 +3,7 @@
module Api
module Admin
class TaxonSerializer < ActiveModel::Serializer
attributes :id, :name, :pretty_name
attributes :id, :name
end
end
end

View File

@@ -95,9 +95,7 @@ module Api
.merge(Exchange.to_enterprise(enterprise))
.select('DISTINCT spree_properties.*')
return properties.merge(OrderCycle.active) if active
properties
properties.merge(OrderCycle.active)
end
def distributed_producer_properties
@@ -106,16 +104,14 @@ module Api
properties = Spree::Property
.joins(
producer_properties: {
producer: { supplied_products: { variants: { exchanges: :order_cycle } } }
producer: { supplied_variants: { exchanges: :order_cycle } }
}
)
.merge(Exchange.outgoing)
.merge(Exchange.to_enterprise(enterprise))
.select('DISTINCT spree_properties.*')
return properties.merge(OrderCycle.active) if active
properties
properties.merge(OrderCycle.active)
end
def active

View File

@@ -4,5 +4,5 @@ class Api::TaxonSerializer < ActiveModel::Serializer
cached
delegate :cache_key, to: :object
attributes :id, :name, :permalink, :pretty_name, :position, :parent_id, :taxonomy_id
attributes :id, :name, :permalink, :position
end

View File

@@ -7,7 +7,7 @@ module Api
attributes :orders_close_at, :active
def orders_close_at
options[:data].earliest_closing_times[object.id]
options[:data].earliest_closing_times[object.id]&.to_time
end
def active

View File

@@ -7,6 +7,8 @@ module OrderCycles
class FormService
def initialize(order_cycle, order_cycle_params, user)
@order_cycle = order_cycle
@confirm_datetime_change = order_cycle_params.delete :confirm_datetime_change
@error_class = order_cycle_params.delete :error_class
@order_cycle_params = order_cycle_params
@specified_params = order_cycle_params.keys
@user = user
@@ -21,6 +23,9 @@ module OrderCycles
end
def save
# Check that order cycle datetime values changed if it has existing orders
verify_datetime_change!
schedule_ids = build_schedule_ids
order_cycle.assign_attributes(order_cycle_params)
return false unless order_cycle.valid?
@@ -229,5 +234,16 @@ module OrderCycles
DistributorShippingMethod.where(distributor_id: user_distributors_ids)
.pluck(:id)
end
def verify_datetime_change!
return unless @confirm_datetime_change
return unless @order_cycle.orders.exists?
return if @order_cycle.same_datetime_value(:orders_open_at,
@order_cycle_params[:orders_open_at]) &&
@order_cycle.same_datetime_value(:orders_close_at,
@order_cycle_params[:orders_close_at])
raise @error_class
end
end
end

View File

@@ -9,7 +9,7 @@ module PermittedAttributes
:unit_description, :variant_unit_name,
:display_as, :sku, :group_buy, :group_buy_unit_size,
:taxon_ids, :primary_taxon_id, :tax_category_id, :supplier_id,
:meta_keywords, :notes, :inherits_properties,
:meta_keywords, :notes, :inherits_properties, :shipping_category_id,
{ product_properties_attributes: [:id, :property_name, :value],
variants_attributes: [PermittedAttributes::Variant.attributes],
image_attributes: [:attachment] }

View File

@@ -3,7 +3,31 @@
module Sets
class OrderCycleSet < ModelSet
def initialize(collection, attributes = {})
@confirm_datetime_change = attributes.delete :confirm_datetime_change
@error_class = attributes.delete :error_class
super(OrderCycle, collection, attributes)
end
def process(order_cycle, attributes)
if @confirm_datetime_change &&
order_cycle.orders.exists? &&
datetime_value_changed(order_cycle, attributes)
raise @error_class
end
super
end
private
def datetime_value_changed(order_cycle, attributes)
# return true if either key is present in params and change in values detected
return true if attributes.key?(:orders_open_at) &&
!order_cycle.same_datetime_value(:orders_open_at, attributes[:orders_open_at])
attributes.key?(:orders_close_at) &&
!order_cycle.same_datetime_value(:orders_close_at, attributes[:orders_close_at])
end
end
end

View File

@@ -1,12 +1,17 @@
# frozen_string_literal: true
class ShopsListService
# shops that are ready for checkout, and have an order cycle that is currently open
def open_shops
shops_list.ready_for_checkout.all
shops_list.
ready_for_checkout.
distributors_with_active_order_cycles
end
# shops that are either not ready for checkout, or don't have an open order cycle; the inverse of
# #open_shops
def closed_shops
shops_list.not_ready_for_checkout.all
shops_list.where.not(id: open_shops.reselect("enterprises.id"))
end
private

View File

@@ -26,9 +26,15 @@ class WeightsAndMeasures
def self.variant_unit_options
available_units_sorted.flat_map do |measurement, measurement_info|
measurement_info.filter_map do |scale, unit_info|
# Our code is based upon English based number formatting
# Some language locales like +hu+ uses a comma(,) for decimal separator
# While in English, decimal separator is represented by a period.
# e.g. en: 0.001, hu: 0,001
# Hence the results become "weight_0,001" for hu while or code recognizes "weight_0.001"
scale_clean =
ActiveSupport::NumberHelper.number_to_rounded(scale, precision: nil, significant: false,
strip_insignificant_zeros: true)
strip_insignificant_zeros: true,
locale: :en)
[
"#{I18n.t(measurement)} (#{unit_info['name']})", # Label (eg "Weight (g)")
"#{measurement}_#{scale_clean}", # Scale ID (eg "weight_1")

View File

@@ -0,0 +1,19 @@
.flex-column-gap-1
%h6
= t('.title')
%div{ style: 'font-size: 1rem;' }
= t('.content')
%p.modal-actions.justify-end.gap-1
- if action == 'simple_update'
%button.button.secondary{ "ng-click": "submit($event, null)", type: "button", style: "display: none;", data: { action: 'click->modal#close', 'trigger-action': 'save' } }
= t('.proceed')
%button.button.secondary{ "ng-click": "submit($event, '#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", type: "button", style: "display: none;", data: { action: 'click->modal#close', 'trigger-action': 'saveAndNext' } }
= t('.proceed')
%button.button.secondary{ "ng-click": "submit($event, '#{main_app.admin_order_cycles_path}')", type: "button", style: "display: none;", data: { action: 'click->modal#close', 'trigger-action': 'saveAndBack' } }
= t('.proceed')
= link_to t('.cancel'), admin_order_cycles_path, id: 'cancel', class: 'button primary', data: { 'order-cycle-form-target': 'cancel' }
- if action == 'bulk_update'
%button.button.secondary{ "ng-click": "saveAll($event)", type: "button", style: "display: none;", data: { action: 'click->modal#close', trigger_action: 'bulk_save' } }
= t('.proceed')
%button.button.primary{ type: "button", 'data-action': 'click->modal#close' }
= t('.cancel')

View File

@@ -16,18 +16,25 @@
- ng_controller = @order_cycle.simple? ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl'
= admin_inject_order_cycle_instance(@order_cycle)
= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller, name: 'order_cycle_form'} do |f|
%div{ data: { controller: 'order-cycle-form' } }
= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller, name: 'order_cycle_form'} do |f|
%save-bar{ dirty: "order_cycle_form.$dirty", persist: "true" }
%input.red{ type: "button", value: t('.save'), "ng-click": "submit($event, null)", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid", data: { confirm: "true", 'trigger-action': 'save' } }
- if @order_cycle.simple?
%input.red{ type: "button", value: t('.save_and_back_to_list'), "ng-click": "submit($event, '#{main_app.admin_order_cycles_path}')", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid", data: { confirm: "true", 'trigger-action': 'saveAndBack' } }
- else
%input.red{ type: "button", value: t('.save_and_next'), "ng-click": "submit($event, '#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid", data: { confirm: "true", 'trigger-action': 'saveAndNext' } }
%input{ type: "button", value: t('.next'), "ng-click": "cancel('#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "order_cycle_form.$dirty" }
%input{ type: "button", "ng-value": "order_cycle_form.$dirty ? '#{t('.cancel')}' : '#{t('.back_to_list')}'", "ng-click": "cancel('#{main_app.admin_order_cycles_path}')" }
%save-bar{ dirty: "order_cycle_form.$dirty", persist: "true" }
%input.red{ type: "button", value: t('.save'), "ng-click": "submit($event, null)", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid" }
- if @order_cycle.simple?
%input.red{ type: "button", value: t('.save_and_back_to_list'), "ng-click": "submit($event, '#{main_app.admin_order_cycles_path}')", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid" }
= render 'simple_form', f: f
- else
%input.red{ type: "button", value: t('.save_and_next'), "ng-click": "submit($event, '#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid" }
%input{ type: "button", value: t('.next'), "ng-click": "cancel('#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "order_cycle_form.$dirty" }
%input{ type: "button", "ng-value": "order_cycle_form.$dirty ? '#{t('.cancel')}' : '#{t('.back_to_list')}'", "ng-click": "cancel('#{main_app.admin_order_cycles_path}')" }
= render 'form', f: f
- if @order_cycle.simple?
= render 'simple_form', f: f
- else
= render 'form', f: f
%div.warning-modal{ data: { controller: 'modal modal-link', 'modal-link-target-value': "linked-order-warning-modal" } }
%button.modal-target-trigger{ type: 'button', data: { 'action': 'modal-link#open' }, style: 'display: none;' }
= render ModalComponent.new(id: "linked-order-warning-modal", close_button: false) do
.content.flex-column.gap-2
= render 'date_time_warning_modal_content', action: 'simple_update'

View File

@@ -23,12 +23,20 @@
.thirteen.columns.alpha &nbsp;
%columns-dropdown{ action: "#{controller_name}_#{action_name}" }
%form{ name: 'order_cycles_form' }
%save-bar{ dirty: "order_cycles_form.$dirty", persist: "false" }
%input.red{ type: "button", value: t(:save_changes), "ng-click": "saveAll()", "ng-disabled": "!order_cycles_form.$dirty" }
%table.index#listing_order_cycles{ "ng-show": 'orderCycles.length > 0' }
= render 'admin/order_cycles/header' #, simple_index: simple_index
%tbody
= render 'admin/order_cycles/row' #, simple_index: simple_index
= render 'admin/order_cycles/loading_flash'
= render 'admin/order_cycles/show_more'
%div{ data: { controller: 'order-cycle-form' } }
%form{ name: 'order_cycles_form' }
%save-bar{ dirty: "order_cycles_form.$dirty", persist: "false" }
%input.red{ type: "button", value: t(:save_changes), "ng-click": "saveAll($event)", "ng-disabled": "!order_cycles_form.$dirty",
data: { confirm: "true", 'trigger-action': 'bulk_save' } }
%table.index#listing_order_cycles{ "ng-show": 'orderCycles.length > 0' }
= render 'admin/order_cycles/header' #, simple_index: simple_index
%tbody
= render 'admin/order_cycles/row' #, simple_index: simple_index
= render 'admin/order_cycles/loading_flash'
= render 'admin/order_cycles/show_more'
%div.warning-modal{ data: { controller: 'modal modal-link', 'modal-link-target-value': "linked-order-warning-modal" } }
%button.modal-target-trigger{ type: 'button', data: { 'action': 'modal-link#open' }, style: 'display: none;' }
= render ModalComponent.new(id: "linked-order-warning-modal", close_button: false) do
.content.flex-column.gap-2
= render 'date_time_warning_modal_content', action: 'bulk_update'

View File

@@ -25,7 +25,7 @@
-# empty
%td.col-on_hand.align-right
-# empty
%td.col-on_hand.align-right
%td.col-producer.align-right
-# empty
%td.col-category.align-left
-# empty
@@ -40,3 +40,4 @@
"data-modal-link-target-value": "product-delete-modal", "class": "delete",
"data-modal-link-modal-dataset-value": {'data-delete-path': admin_product_destroy_path(product)}.to_json }
= t('admin.products_page.actions.delete')
= link_to t('admin.products_page.actions.preview'), admin_product_preview_path(product), {"data-turbo-stream": "" }

View File

@@ -11,7 +11,7 @@
%tr.condensed{ id: dom_id(variant), 'data-controller': "variant", 'class': "nested-form-wrapper", 'data-new-record': variant.new_record? ? "true" : false }
= render partial: 'variant_row', locals: { variant:, f: variant_form, category_options:, tax_category_options:, producer_options: }
= form.fields_for("products][#{product_index}][variants_attributes][NEW_RECORD", prepare_new_variant(product)) do |new_variant_form|
= form.fields_for("products][#{product_index}][variants_attributes][NEW_RECORD", prepare_new_variant(product, producer_options)) do |new_variant_form|
%template{ 'data-nested-form-target': "template" }
%tr.condensed{ 'data-controller': "variant", 'class': "nested-form-wrapper", 'data-new-record': "true" }
= render partial: 'variant_row', locals: { variant: new_variant_form.object, f: new_variant_form, category_options:, tax_category_options:, producer_options: }

View File

@@ -1,7 +1,7 @@
#sort
%div.pagination-description
- if pagy.present?
= t(".pagination.total_html", total: pagy.count, from: pagy.from, to: pagy.to)
= t(".pagination.products_total_html", count: pagy.count, from: pagy.from, to: pagy.to)
- if search_term.present? || producer_id.present? || category_id.present?
%a{ href: url_for(page: 1), class: "button disruptive", data: { 'turbo-frame': "_self", 'turbo-action': "advance" } }

View File

@@ -13,16 +13,16 @@
= hidden_field_tag :producer_id, @producer_id
= hidden_field_tag :category_id, @category_id
%table.products{ 'data-column-preferences-target': "table" }
%table.products{ 'data-column-preferences-target': "table", class: (hide_producer_column?(producer_options) ? 'hide-producer' : '') }
%colgroup
-# The `min-width` property works in Chrome but not Firefox so is considered progressive enhancement.
%col.col-image{ width:"56px" }= # (image size + padding)
%col.col-image{ width:"44px" }= # (image size + padding)
%col.col-name{ style:"min-width: 6em" }= # (grow to fill)
%col.col-sku{ width:"8%", style:"min-width: 6em" }
%col.col-unit_scale{ width:"8%" }
%col.col-unit{ width:"8%" }
%col.col-price{ width:"5%", style:"min-width: 5em" }
%col.col-on_hand{ width:"10%"}
%col.col-price{ width:"10%", style:"min-width: 5em" }
%col.col-on_hand{ width:"8%" }
%col.col-producer{ style:"min-width: 6em" }= # (grow to fill)
%col.col-category{ width:"8%" }
%col.col-tax_category{ width:"8%" }

View File

@@ -20,3 +20,4 @@
= render partial: 'delete_modal', locals: { object_type: }
#modal-component
#edit_image_modal
#product-preview-modal-container

View File

@@ -0,0 +1,119 @@
= turbo_stream.update "product-preview-modal-container" do
= render ModalComponent.new(id: "product-preview-modal", instant: true, modal_class: "big") do
#product-preview{ "data-controller": "tabs" }
%h1
= t("admin.products_page.product_preview.product_preview")
%dl.admin-tabs
%dd
%a{ data: { "tabs-target": "tab", "action": "tabs#select" } }
= t("admin.products_page.product_preview.shop_tab")
%dd
%a{ data: { "tabs-target": "tab", "action": "tabs#select" } }
= t("admin.products_page.product_preview.product_details_tab")
.tabs-content
.content.active
%div{ data: { "tabs-target": "content" } }
.product-thumb
%a
- if @product.group_buy
%span.product-thumb__bulk-label
= t(".bulk")
= image_tag @product.image&.url(:small) || Spree::Image.default_image_url(:small)
.summary
.summary-header
%h3
%a
%span
= @product.name
- if @product.description
.product-description{ "data-controller": "add-blank-to-link" }
- # description is sanitized in Spree::Product#description method
= @product.description.html_safe
- if @product.variants.first.supplier.visible
%div
.product-producer
= t :products_from
%span
%a
= @product.variants.first.supplier.name
.product-properties.filter-shopfront.property-selectors
.filter-shopfront.property-selectors.inline-block
%ul
- @product.properties_including_inherited.each do |property|
%li
- if property[:value].present?
= render AdminTooltipComponent.new(text: property[:value], link_text: property[:name], placement: "bottom")
- else
%a
%span
= property[:name]
.shop-variants
- @product.variants.sort { |v1, v2| v1.name_to_display <=> v2.name_to_display }.sort { |v1, v2| v1.unit_value <=> v2.unit_value }.each do |variant|
.variants.row
.small-3.columns.variant-name
- if variant.display_name.present?
.inline
= variant.display_name
.variant-unit
= variant.unit_to_display
.small-4.medium-3.columns.variant-price
= number_to_currency(variant.price)
.unit-price.variant-unit-price
= render AdminTooltipComponent.new(text: t("js.shopfront.unit_price_tooltip"), link_text: "", placement: "top", link_class: "question-mark-icon")
- # TODO use an helper
- unit_price = UnitPrice.new(variant)
- price_per_unit = variant.price / (unit_price.denominator || 1)
= "#{number_to_currency(price_per_unit)}&nbsp;/&nbsp;#{unit_price.unit}".html_safe
.medium-3.columns.total-price
%span
= number_to_currency(0.00)
.small-5.medium-3.large-3.columns.variant-quantity-column.text-right
.variant-quantity-inputs
%button.add-variant
= t("js.shopfront.variant.add_to_cart")
- # TODO can't check the shop preferrence here, display by default ?
- if !variant.on_demand && variant.on_hand <= 3
.variant-remaining-stock
= t("js.shopfront.variant.remaining_in_stock", quantity: variant.on_hand)
%div{ data: { "tabs-target": "content" } }
.row
.columns.small-12.medium-6.large-6.product-header
%h3
= @product.name
%span
%em
= t("products_from")
%span
= @product.variants.first.supplier.name
%br
.filter-shopfront.property-selectors.inline-block
%ul
- @product.properties_including_inherited.each do |property|
%li
- if property[:value].present?
= render AdminTooltipComponent.new(text: property[:value], link_text: property[:name], placement: "bottom")
- else
%a
%span
= property[:name]
- if @product.description
.product-description{ 'data-controller': "add-blank-to-link" }
%p.text-small
- # description is sanitized in Spree::Product#description method
= @product.description.html_safe
.columns.small-12.medium-6.large-6.product-img
- if @product.image
%img{ src: @product.image.url(:large) }
-else
%img.placeholder{ src: Spree::Image.default_image_url(:large) }

View File

@@ -0,0 +1,47 @@
.download.hidden
= link_to t("admin.reports.download.button"), file_url, target: "_blank", class: "button icon icon-file"
:javascript
(function () {
const tryDownload = function() {
const link = document.querySelector(".download a");
// If the report was already rendered via web sockets:
if (link == null) return;
fetch(link.href).then((response) => {
if (response.ok) {
response.blob().then((blob) => blob.text()).then((text) => {
const loading = document.querySelector(".loading");
if (loading == null) return;
loading.remove();
document.querySelector("#report-go button").disabled = false;
if (link.href.endsWith(".html")) {
// This replaces the hidden download button with the report:
link.parentElement.outerHTML = text;
} else {
// Or just show the download button when it's ready:
document.querySelector(".download").classList.remove("hidden")
}
});
} else {
setTimeout(tryDownload, 2000);
}
});
}
/*
A lot of reports are rendered within 250ms. Others take at least
2.5 seconds. There's a big gap in between. Observed on:
https://openfoodnetwork.org.au/admin/sidekiq/metrics/ReportJob?period=8h
https://openfoodnetwork.org.uk/admin/sidekiq/metrics/ReportJob?period=8h
https://coopcircuits.fr/admin/sidekiq/metrics/ReportJob?period=8h
But let's leave the timed response to websockets for now and just poll
as a backup mechanism.
*/
setTimeout(tryDownload, 3000);
})();

View File

@@ -0,0 +1,6 @@
= turbo_stream.update "report-go" do
= button t(:go), "report__submit-btn", "submit", disabled: true
= turbo_stream.update "report-table" do
= render "admin/reports/loading"
= render "admin/reports/fallback_display", file_url: @blob.expiring_service_url
= turbo_stream.scroll_into_view("#report-table", behavior: "smooth")

View File

@@ -3,7 +3,7 @@
- content_for :minimal_js, true
= form_for @report.search, { url: url_for, data: { remote: "true" } } do |f|
= form_for @report.search, { url: url_for, data: { turbo: "true" } } do |f|
= hidden_field_tag "uuid", request.uuid
%fieldset.no-border-bottom.print-hidden

View File

@@ -1,7 +0,0 @@
%div{"data-controller": "tooltip"}
%a{"data-tooltip-target": "element", href: link, class: link_class}
= link_text
.tooltip-container
.tooltip{"data-tooltip-target": "tooltip"}
= sanitize tooltip_text
.arrow{"data-tooltip-target": "arrow"}

View File

@@ -1 +1 @@
= render partial: 'admin/shared/tooltip', locals: {link_class: "" ,link: nil, link_text: t('admin.whats_this'), tooltip_text: tooltip_text}
= render AdminTooltipComponent.new(text: tooltip_text, link_text: t('admin.whats_this'), link: nil)

View File

@@ -5,7 +5,7 @@
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
%title
= Spree::Config[:site_name]
= stylesheet_pack_tag 'mail'
= stylesheet_link_tag 'mail'
%body{:bgcolor => "#FFFFFF" }
- unless @hide_ofn_navigation
%table.head-wrap{:bgcolor => "#f2f2f2"}

View File

@@ -1,3 +1,4 @@
- # NOTE: make sure that any changes in this template are reflected in app/views/admin/products_v3/product_preview.turbo_stream.haml
= cache_with_locale do
%form{action: main_app.cart_path}
%products{"ng-init" => "refreshStaleData()", "ng-show" => "order_cycle.order_cycle_id != null", "ng-cloak" => true }

View File

@@ -1,8 +1,9 @@
- # NOTE: make sure that any changes in this template are reflected in app/views/admin/products_v3/product_preview.turbo_stream.haml
= cache_with_locale do
.small-4.medium-4.large-5.columns.variant-name
.small-3.columns.variant-name
.inline{"ng-if" => "::variant.display_name"} {{ ::variant.display_name }}
.variant-unit {{ ::variant.unit_to_display }}
.small-3.medium-3.large-2.columns.variant-price
.small-4.medium-3.columns.variant-price
%price-breakdown{"price-breakdown" => "_", variant: "variant",
"price-breakdown-append-to-body" => "true",
"price-breakdown-placement" => "bottom",
@@ -16,7 +17,7 @@
key: "'js.shopfront.unit_price_tooltip'"}
{{ variant.unit_price_price | localizeCurrency }}&nbsp;/&nbsp;{{ variant.unit_price_unit }}
.medium-2.large-2.columns.total-price
.medium-3.columns.total-price
%span{"ng-class" => "{filled: variant.line_item.total_price}"}
{{ variant.line_item.total_price | localizeCurrency }}
= render partial: "shop/products/shop_variant_no_group_buy"

View File

@@ -1,3 +1,4 @@
- # NOTE: make sure that any changes in this template are reflected in app/views/admin/products_v3/product_preview.turbo_stream.haml
= cache_with_locale do
.small-5.medium-3.large-3.columns.variant-quantity-column.text-right{"ng-if" => "::!variant.product.group_buy"}

View File

@@ -1,3 +1,4 @@
- # NOTE: make sure that any changes in this template are reflected in app/views/admin/products_v3/product_preview.turbo_stream.haml
= cache_with_locale do
.small-5.medium-3.large-3.columns.variant-quantity-column.text-right{"ng-if" => "::variant.product.group_buy"}

View File

@@ -1,3 +1,4 @@
- # NOTE: make sure that any changes in this template are reflected in app/views/admin/products_v3/product_preview.turbo_stream.haml
= cache_with_locale do
.product-thumb
%a{"ng-click" => "triggerProductModal()"}

View File

@@ -45,7 +45,7 @@
%div.row-loading-icons
- if local_assigns[:success]
%i.success.icon-ok-sign{"data-controller": "ephemeral"}
= render partial: 'admin/shared/tooltip', locals: {link_class: "icon_link with-tip icon-edit no-text" ,link: edit_admin_order_path(order), link_text: "", tooltip_text: t('spree.admin.orders.index.edit')}
= render AdminTooltipComponent.new(text: t('spree.admin.orders.index.edit'), link_text: "", link: edit_admin_order_path(order), link_class: "icon_link with-tip icon-edit no-text")
- if order.ready_to_ship?
%form
= render ShipOrderComponent.new(order: order)

View File

@@ -1,4 +1,4 @@
= pdf_stylesheet_pack_tag "mail"
= wicked_pdf_stylesheet_link_tag "mail"
%table{:width => "100%"}
%tbody

View File

@@ -1,4 +1,4 @@
= pdf_stylesheet_pack_tag "mail"
= wicked_pdf_stylesheet_link_tag "mail"
%table{:width => "100%"}
%tbody

View File

@@ -1,4 +1,4 @@
= pdf_stylesheet_pack_tag "mail"
= wicked_pdf_stylesheet_link_tag "mail"
%table{:width => "100%"}
%tbody

View File

@@ -1,6 +1,12 @@
= f.field_container :primary_taxon do
= f.field_container :primary_taxon_id do
= f.label :primary_taxon_id, t('.product_category')
%span.required *
%br
= f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
= render(SearchableDropdownComponent.new(form: f,
name: :primary_taxon_id,
aria_label: t('.product_category'),
options: Spree::Taxon.select(:name, :id).order(:name).pluck(:name, :id),
selected_option: @product.primary_taxon_id,
include_blank: true,
placeholder_value: t('.search_for_categories')))
= f.error_message_on :primary_taxon_id

View File

@@ -1,4 +1,4 @@
= f.field_container :shipping_categories do
= f.field_container :shipping_category_id do
= f.label :shipping_category_id, t(:shipping_category)
= f.collection_select(:shipping_category_id, Spree::ShippingCategory.all, :id, :name, {:include_blank => false}, {:class => 'select2 fullwidth'})
= f.error_message_on :shipping_category_id

View File

@@ -1,7 +1,7 @@
= admin_inject_available_units
- content_for :page_actions do
%li= button_link_to t('admin.products.back_to_products_list'), "#{admin_products_path}#{(@url_filters.empty? ? "" : "#?#{@url_filters.to_query}")}", :icon => 'icon-arrow-left'
%li= button_link_to t('admin.products.back_to_products_list'), products_return_to_url(@url_filters), :icon => 'icon-arrow-left'
%li#new_product_link
= button_link_to t(:new_product), new_object_url, { :icon => 'icon-plus', :id => 'admin_new_product' }
@@ -11,9 +11,13 @@
= render :partial => 'spree/shared/error_messages', :locals => { :target => @product }
= form_for [:admin, @product], :url => admin_product_path(@product, @url_filters), :method => :put, :html => { :multipart => true } do |f|
%fieldset.no-border-top{'ng-app' => 'admin.products'}
%fieldset.no-border-top{'ng-app': 'admin.products', 'data-turbo': true, 'data-controller': "product-preview"}
= render :partial => 'form', :locals => { :f => f }
.form-buttons.filter-actions.actions
= link_to t("admin.products_page.actions.preview"), Rails.application.routes.url_helpers.admin_product_preview_path(@product), {"data-turbo-stream": "" , class: "button secondary"}
= button t(:update), 'icon-refresh'
= button_link_to t(:cancel), "#{collection_url}#{(@url_filters.empty? ? "" : "#?#{@url_filters.to_query}")}", icon: 'icon-remove'
= button_link_to t(:cancel), products_return_to_url(@url_filters), icon: 'icon-remove'
#product-preview-modal-container

View File

@@ -8,10 +8,16 @@
%legend{align: "center"}= t(".new_product")
.sixteen.columns.alpha
.eight.columns.alpha
= f.field_container :supplier do
= f.label :supplier, t(".supplier")
= f.field_container :supplier_id do
= f.label :supplier_id, t(".supplier")
%span.required *
= f.select :supplier_id, options_from_collection_for_select(@producers, :id, :name, @product.supplier_id), { include_blank: t("spree.admin.products.new.supplier_select_placeholder") }, { "data-controller": "tom-select", class: "primary" }
= render(SearchableDropdownComponent.new(form: f,
name: :supplier_id,
aria_label: t('.supplier'),
options: @producers.select(:name, :id).order(:name).pluck(:name, :id),
selected_option: @product.supplier_id,
include_blank: true,
placeholder_value: t('.search_for_suppliers')))
= f.error_message_on :supplier_id
.eight.columns.omega
= f.field_container :name do
@@ -25,8 +31,16 @@
= f.field_container :variant_unit do
= f.label :variant_unit, t(".units")
%span.required *
%select{id: 'product_variant_unit_with_scale', 'ng-model' => 'product.variant_unit_with_scale', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options', "data-controller": "tom-select","data-tom-select-options-value": '{"allowEmptyOption":false}', class: "primary"}
%option{'value' => '', 'ng-hide' => "hasUnit(product)"}
= f.select 'variant_unit', [],
{ include_blank: true },
{ id: 'product_variant_unit_with_scale',
name: 'product_variant_unit_with_scale',
'ng-model' => 'product.variant_unit_with_scale',
'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options',
"data-controller": "tom-select",
"data-tom-select-options-value": '{"allowEmptyOption":false}',
class: "primary",
}
%input{ type: 'hidden', 'ng-value': 'product.variant_unit', "ng-init": "product.variant_unit='#{@product.variant_unit}'", name: 'product[variant_unit]' }
%input{ type: 'hidden', 'ng-value': 'product.variant_unit_scale', "ng-init": "product.variant_unit_scale='#{@product.variant_unit_scale}'", name: 'product[variant_unit_scale]' }
= f.error_message_on :variant_unit
@@ -34,16 +48,17 @@
= f.field_container :unit_value do
= f.label :unit_value, t(".value"), 'ng-disabled' => "!hasUnit(product)"
%span.required *
%input.fullwidth{ id: 'product_unit_value', 'ng-model' => 'product.master.unit_value_with_description', :type => 'text', placeholder: "eg. 2", 'ng-disabled' => "!hasUnit(product)" }
= f.text_field :unit_value, placeholder: "eg. 2", 'ng-model' => 'product.master.unit_value_with_description', class: 'fullwidth', 'ng-disabled' => "!hasUnit(product)"
%input{ type: 'hidden', 'ng-value': 'product.master.unit_value', "ng-init": "product.master.unit_value='#{@product.unit_value}'", name: 'product[unit_value]' }
%input{ type: 'hidden', 'ng-value': 'product.master.unit_description', "ng-init": "product.master.unit_description='#{@product.unit_description}'", name: 'product[unit_description]' }
= f.error_message_on :unit_value
= render 'display_as', f: f
.six.columns.omega{ 'ng-show' => "product.variant_unit_with_scale == 'items'" }
= f.field_container :unit_name do
= f.label :product_variant_unit_name, t(".unit_name")
= f.field_container :variant_unit_name do
= f.label :variant_unit_name, t(".unit_name")
%span.required *
%input.fullwidth{ id: 'product_variant_unit_name','ng-model' => 'product.variant_unit_name', :name => 'product[variant_unit_name]', :placeholder => t('admin.products.unit_name_placeholder'), :type => 'text' }
= f.text_field :variant_unit_name, :placeholder => t('admin.products.unit_name_placeholder'), 'ng-model' => 'product.variant_unit_name', class: 'fullwidth', 'ng-init': "product.variant_unit_name='#{@product.variant_unit_name}'"
= f.error_message_on :variant_unit_name
.sixteen.columns.alpha
.eight.columns.alpha
= render 'spree/admin/products/primary_taxon_form', f: f

View File

@@ -15,7 +15,7 @@
- if DefaultCountry.id
= configurations_sidebar_menu_item Spree.t(:states), admin_country_states_path(DefaultCountry.id)
= configurations_sidebar_menu_item Spree.t(:payment_methods), admin_payment_methods_path
= configurations_sidebar_menu_item Spree.t(:taxonomies), admin_taxonomies_path
= configurations_sidebar_menu_item Spree.t(:taxons), admin_taxons_path
= configurations_sidebar_menu_item Spree.t(:shipping_methods), admin_shipping_methods_path
= configurations_sidebar_menu_item Spree.t(:shipping_categories), admin_shipping_categories_path
= configurations_sidebar_menu_item t(:enterprise_fees), main_app.admin_enterprise_fees_path

Some files were not shown because too many files have changed in this diff Show More