mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-11 18:26:50 +00:00
Merge in latest :master and resolve conflict in app/models/enterprise.rb
This commit is contained in:
2
.github/ISSUE_TEMPLATE/release.md
vendored
2
.github/ISSUE_TEMPLATE/release.md
vendored
@@ -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
|
||||
|
||||
101
.github/workflows/build.yml
vendored
101
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 = ->
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}" }
|
||||
|
||||
@@ -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 || " " }}
|
||||
%h5#status-message{ style: 'color: #C85136', "ng-show": "StatusMessage.invalidMessage !== ''" }
|
||||
{{ StatusMessage.invalidMessage || " " }}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
1
app/assets/stylesheets/mail.scss
Normal file
1
app/assets/stylesheets/mail.scss
Normal file
@@ -0,0 +1 @@
|
||||
@import './mail/all.scss';
|
||||
3
app/assets/stylesheets/mail/all.scss
Normal file
3
app/assets/stylesheets/mail/all.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
@import '../../../webpacker/css/admin/globals/palette.scss';
|
||||
@import 'email';
|
||||
@import 'payments_list';
|
||||
11
app/components/admin_tooltip_component.rb
Normal file
11
app/components/admin_tooltip_component.rb
Normal 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
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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' : ''}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -35,14 +35,8 @@ 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
|
||||
end
|
||||
|
||||
# Most of this code is the same as in the DfcProvider::SuppliedProductsController.
|
||||
def import_product(subject, enterprise)
|
||||
|
||||
@@ -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
|
||||
|
||||
22
app/controllers/admin/product_preview_controller.rb
Normal file
22
app/controllers/admin/product_preview_controller.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
# 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').
|
||||
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] }
|
||||
]
|
||||
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?
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
module Api
|
||||
module Admin
|
||||
class TaxonSerializer < ActiveModel::Serializer
|
||||
attributes :id, :name, :pretty_name
|
||||
attributes :id, :name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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')
|
||||
@@ -16,14 +16,15 @@
|
||||
|
||||
- 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" }
|
||||
%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" }
|
||||
%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" }
|
||||
%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}')" }
|
||||
|
||||
@@ -31,3 +32,9 @@
|
||||
= 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'
|
||||
@@ -23,12 +23,20 @@
|
||||
.thirteen.columns.alpha
|
||||
%columns-dropdown{ action: "#{controller_name}_#{action_name}" }
|
||||
|
||||
%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()", "ng-disabled": "!order_cycles_form.$dirty" }
|
||||
%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'
|
||||
|
||||
@@ -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": "" }
|
||||
|
||||
@@ -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: }
|
||||
|
||||
@@ -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" } }
|
||||
|
||||
@@ -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%" }
|
||||
|
||||
@@ -20,3 +20,4 @@
|
||||
= render partial: 'delete_modal', locals: { object_type: }
|
||||
#modal-component
|
||||
#edit_image_modal
|
||||
#product-preview-modal-container
|
||||
|
||||
119
app/views/admin/products_v3/product_preview.turbo_stream.haml
Normal file
119
app/views/admin/products_v3/product_preview.turbo_stream.haml
Normal 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)} / #{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) }
|
||||
47
app/views/admin/reports/_fallback_display.html.haml
Normal file
47
app/views/admin/reports/_fallback_display.html.haml
Normal 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);
|
||||
})();
|
||||
6
app/views/admin/reports/create.turbo_stream.haml
Normal file
6
app/views/admin/reports/create.turbo_stream.haml
Normal 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")
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
@@ -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)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }} / {{ 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"
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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()"}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
= pdf_stylesheet_pack_tag "mail"
|
||||
= wicked_pdf_stylesheet_link_tag "mail"
|
||||
|
||||
%table{:width => "100%"}
|
||||
%tbody
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
= pdf_stylesheet_pack_tag "mail"
|
||||
= wicked_pdf_stylesheet_link_tag "mail"
|
||||
|
||||
%table{:width => "100%"}
|
||||
%tbody
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
= pdf_stylesheet_pack_tag "mail"
|
||||
= wicked_pdf_stylesheet_link_tag "mail"
|
||||
|
||||
%table{:width => "100%"}
|
||||
%tbody
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user