mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-16 19:16:49 +00:00
Compare commits
219 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6e4b107b0 | ||
|
|
a5d17b4da9 | ||
|
|
83ab9594f6 | ||
|
|
562a24524b | ||
|
|
2809194b42 | ||
|
|
7d3eff2abb | ||
|
|
f8bb33a9e8 | ||
|
|
24a25d31a0 | ||
|
|
4822a9ebcd | ||
|
|
68fa903d61 | ||
|
|
c2e0c94f2e | ||
|
|
296997d558 | ||
|
|
a9ad6a2851 | ||
|
|
1078e7cd36 | ||
|
|
40c4d38e45 | ||
|
|
a25937321a | ||
|
|
a8db288425 | ||
|
|
a106eb10b6 | ||
|
|
a6d71f8dd1 | ||
|
|
5c300d6d41 | ||
|
|
bb4ff5adc2 | ||
|
|
be548c506d | ||
|
|
955f8ba5ae | ||
|
|
ad94da975a | ||
|
|
f33eb23909 | ||
|
|
9ead14b8a0 | ||
|
|
38721d9f36 | ||
|
|
3f6aaa74cc | ||
|
|
243a4a55b4 | ||
|
|
5be53a40a9 | ||
|
|
76fdf3725a | ||
|
|
67f037280a | ||
|
|
776b9fcdab | ||
|
|
7e84d41e8c | ||
|
|
68491559f3 | ||
|
|
f8d3467d46 | ||
|
|
1580d539df | ||
|
|
e2aac8ca1d | ||
|
|
15a2513815 | ||
|
|
00768f6ba0 | ||
|
|
908caa984b | ||
|
|
6993750757 | ||
|
|
379e5acfe5 | ||
|
|
5bf6bdf7f0 | ||
|
|
8de7c304fe | ||
|
|
b6695ba9a2 | ||
|
|
e8de76dc46 | ||
|
|
55733555bf | ||
|
|
f59ee96011 | ||
|
|
2b74bbd45d | ||
|
|
d56ab9257b | ||
|
|
f24a4edc68 | ||
|
|
27dd5def57 | ||
|
|
561f4648d2 | ||
|
|
64d3091db9 | ||
|
|
0a9b858f2a | ||
|
|
4756ab47c2 | ||
|
|
0a04342712 | ||
|
|
556539d1b1 | ||
|
|
b7aaab204c | ||
|
|
632184b0a8 | ||
|
|
8500f6c198 | ||
|
|
ec4dba71c2 | ||
|
|
6117d70fae | ||
|
|
2e5c526170 | ||
|
|
32e32117e3 | ||
|
|
2a1d494301 | ||
|
|
fd45dea9f7 | ||
|
|
9073f0e5a8 | ||
|
|
c4f2c1c3ca | ||
|
|
a23bbf8537 | ||
|
|
6fac32b446 | ||
|
|
cf21c03619 | ||
|
|
0f7f1130f1 | ||
|
|
009d033e4c | ||
|
|
983addff0d | ||
|
|
d061fe8ad9 | ||
|
|
53286c22ba | ||
|
|
0cf8f079e4 | ||
|
|
f2163a42c4 | ||
|
|
05b25c78bb | ||
|
|
cc3181c820 | ||
|
|
9cd39d5c91 | ||
|
|
7d2f3bfa2f | ||
|
|
6df0b24bcf | ||
|
|
cf5e182cf7 | ||
|
|
74bbc7c3c0 | ||
|
|
4773d1c82e | ||
|
|
fde18ebf24 | ||
|
|
fd2cbb67db | ||
|
|
3f1d99d77c | ||
|
|
9cfcab4f02 | ||
|
|
703ad26773 | ||
|
|
627c9eede2 | ||
|
|
f9a76342f8 | ||
|
|
d52134dad8 | ||
|
|
1016656781 | ||
|
|
bd1611630f | ||
|
|
ce28c10c7e | ||
|
|
4342d3b912 | ||
|
|
af3aed827a | ||
|
|
f73be6447e | ||
|
|
98eabc9d0f | ||
|
|
169cbbe1a1 | ||
|
|
72a503c3c1 | ||
|
|
afc4c1e967 | ||
|
|
d76c4bddb0 | ||
|
|
ae993784d8 | ||
|
|
d1abe22c32 | ||
|
|
2817b8891e | ||
|
|
ab87610d91 | ||
|
|
54252f5444 | ||
|
|
7b6b0dbb78 | ||
|
|
b2e15f52cf | ||
|
|
5d18c48b6c | ||
|
|
0c2dcbc50d | ||
|
|
4a028b2238 | ||
|
|
43a366005c | ||
|
|
64470e977a | ||
|
|
a696c66857 | ||
|
|
cfeb0afbd4 | ||
|
|
4968e3dc8d | ||
|
|
5324747f89 | ||
|
|
ef2856d169 | ||
|
|
d9c79ee49c | ||
|
|
d1f9b0855d | ||
|
|
b82726e7ba | ||
|
|
7e7ab2e36d | ||
|
|
e35a5179bb | ||
|
|
85385a1989 | ||
|
|
a816814819 | ||
|
|
94b98867d8 | ||
|
|
35ef1b9c7f | ||
|
|
8badfb2505 | ||
|
|
d61acd2cc1 | ||
|
|
7417cee20a | ||
|
|
d489c77efe | ||
|
|
e2423ad612 | ||
|
|
8b036113d9 | ||
|
|
7f09044ae1 | ||
|
|
e9c7e1778c | ||
|
|
32cd14ef54 | ||
|
|
ad585f1eab | ||
|
|
d9368c1bfc | ||
|
|
b6bfb4e866 | ||
|
|
4d222c61c6 | ||
|
|
d599cf77a2 | ||
|
|
8f7505d53d | ||
|
|
867e17301f | ||
|
|
95135ca526 | ||
|
|
de063fecb1 | ||
|
|
ef9ca33913 | ||
|
|
2710eafc33 | ||
|
|
4718fdb0be | ||
|
|
ce6ae04147 | ||
|
|
1621f97fdb | ||
|
|
96f9894f41 | ||
|
|
66b519bd1c | ||
|
|
1b8e256e8a | ||
|
|
b73e529bfc | ||
|
|
25b1620707 | ||
|
|
5f9b14df9f | ||
|
|
922b853e3a | ||
|
|
8d747a2508 | ||
|
|
a50be52cde | ||
|
|
d62d002bc5 | ||
|
|
0c7448ba43 | ||
|
|
24afd40414 | ||
|
|
524aec7868 | ||
|
|
f2eb4b05f4 | ||
|
|
ffaf1b4ea0 | ||
|
|
eb547f4861 | ||
|
|
c9daca22d5 | ||
|
|
7b22740289 | ||
|
|
66c8a5c424 | ||
|
|
cfeac651b6 | ||
|
|
05315ff8e0 | ||
|
|
c4ee6b14ff | ||
|
|
a78f46259c | ||
|
|
1e79fde236 | ||
|
|
a9fe52a4ff | ||
|
|
075f5499f8 | ||
|
|
ed61f7e7bc | ||
|
|
bd6019036e | ||
|
|
0bbc3d2758 | ||
|
|
ae6182579b | ||
|
|
1e05811917 | ||
|
|
5f86a26f42 | ||
|
|
3f1b907ef2 | ||
|
|
d9c296cdb3 | ||
|
|
23aa762be2 | ||
|
|
61f2954973 | ||
|
|
d354317c73 | ||
|
|
19ef047193 | ||
|
|
037eb456c0 | ||
|
|
aed78f3138 | ||
|
|
c31416c536 | ||
|
|
f154de66f9 | ||
|
|
4a30493716 | ||
|
|
f325857e1f | ||
|
|
58872a7017 | ||
|
|
7392079d4d | ||
|
|
506126c1d3 | ||
|
|
fa004d0897 | ||
|
|
db7add88fe | ||
|
|
f21aca234c | ||
|
|
93a6ff4b50 | ||
|
|
97a72dfde7 | ||
|
|
5ca7f40a4e | ||
|
|
a2f4df191a | ||
|
|
b49da46842 | ||
|
|
2d24593403 | ||
|
|
2201d2e8c2 | ||
|
|
b6c407971d | ||
|
|
cd8dc41b15 | ||
|
|
a1887bdc76 | ||
|
|
e9f89362f4 | ||
|
|
675b7febdf | ||
|
|
90fdf59415 |
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
|
||||
|
||||
118
.github/workflows/build.yml
vendored
118
.github/workflows/build.yml
vendored
@@ -3,7 +3,7 @@ name: Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches-ignore:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
pull_request:
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
- name: Setup redis
|
||||
uses: supercharge/redis-github-action@1.4.0
|
||||
with:
|
||||
with:
|
||||
redis-version: 6
|
||||
|
||||
- name: Set up Ruby
|
||||
@@ -81,11 +81,20 @@ jobs:
|
||||
# RSpec split test files by test examples feature - it's optional
|
||||
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
|
||||
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
|
||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/controllers/**/*_spec.rb}"
|
||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/controllers/**/*_spec.rb}"
|
||||
run: |
|
||||
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:
|
||||
@@ -116,7 +125,7 @@ jobs:
|
||||
|
||||
- name: Setup redis
|
||||
uses: supercharge/redis-github-action@1.4.0
|
||||
with:
|
||||
with:
|
||||
redis-version: 6
|
||||
|
||||
- name: Set up Ruby
|
||||
@@ -141,10 +150,19 @@ jobs:
|
||||
# RSpec split test files by test examples feature - it's optional
|
||||
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
|
||||
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
|
||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/models/**/*_spec.rb}"
|
||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/models/**/*_spec.rb}"
|
||||
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:
|
||||
@@ -175,7 +193,7 @@ jobs:
|
||||
|
||||
- name: Setup redis
|
||||
uses: supercharge/redis-github-action@1.4.0
|
||||
with:
|
||||
with:
|
||||
redis-version: 6
|
||||
|
||||
- name: Set up Ruby
|
||||
@@ -209,16 +227,25 @@ jobs:
|
||||
# RSpec split test files by test examples feature - it's optional
|
||||
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
|
||||
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
|
||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/admin/**/*_spec.rb}"
|
||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/admin/**/*_spec.rb}"
|
||||
|
||||
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@v4
|
||||
with:
|
||||
name: failed-admin-tests-screenshots
|
||||
name: failed-admin_${{ matrix.ci_node_index }}-tests-screenshots
|
||||
path: tmp/capybara/screenshots/*.png
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
@@ -247,13 +274,13 @@ jobs:
|
||||
ci_node_total: [12]
|
||||
# Indexes for parallel jobs (starting from zero).
|
||||
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
||||
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
||||
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup redis
|
||||
uses: supercharge/redis-github-action@1.4.0
|
||||
with:
|
||||
with:
|
||||
redis-version: 6
|
||||
|
||||
- name: Set up Ruby
|
||||
@@ -287,16 +314,25 @@ jobs:
|
||||
# RSpec split test files by test examples feature - it's optional
|
||||
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
|
||||
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
|
||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/consumer/**/*_spec.rb}"
|
||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/consumer/**/*_spec.rb}"
|
||||
|
||||
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@v4
|
||||
with:
|
||||
name: failed-consumer-tests-screenshots
|
||||
name: failed-consumer_${{ matrix.ci_node_index }}-tests-screenshots
|
||||
path: tmp/capybara/screenshots/*.png
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
@@ -331,7 +367,7 @@ jobs:
|
||||
|
||||
- name: Setup redis
|
||||
uses: supercharge/redis-github-action@1.4.0
|
||||
with:
|
||||
with:
|
||||
redis-version: 6
|
||||
|
||||
- name: Set up Ruby
|
||||
@@ -371,6 +407,15 @@ jobs:
|
||||
run: |
|
||||
bin/rake knapsack_pro:rspec
|
||||
|
||||
- name: Save SimpleCov file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
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
|
||||
services:
|
||||
@@ -401,7 +446,7 @@ jobs:
|
||||
|
||||
- name: Setup redis
|
||||
uses: supercharge/redis-github-action@1.4.0
|
||||
with:
|
||||
with:
|
||||
redis-version: 6
|
||||
|
||||
- name: Set up Ruby
|
||||
@@ -439,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:
|
||||
@@ -476,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
|
||||
|
||||
1
Gemfile
1
Gemfile
@@ -104,6 +104,7 @@ gem 'sidekiq-scheduler'
|
||||
gem "cable_ready"
|
||||
gem "stimulus_reflex"
|
||||
|
||||
gem "turbo_power"
|
||||
gem "turbo-rails"
|
||||
|
||||
gem 'combine_pdf'
|
||||
|
||||
@@ -785,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)
|
||||
@@ -835,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)
|
||||
@@ -973,6 +975,7 @@ DEPENDENCIES
|
||||
stripe
|
||||
timecop
|
||||
turbo-rails
|
||||
turbo_power
|
||||
valid_email2
|
||||
validates_lengths_from_database
|
||||
vcr
|
||||
|
||||
@@ -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 = ->
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
module Admin
|
||||
class OrderCyclesController < Admin::ResourceController
|
||||
class DateTimeChangeError < StandardError; end
|
||||
|
||||
include ::OrderCyclesHelper
|
||||
include PaperTrailLogging
|
||||
|
||||
@@ -11,7 +13,6 @@ module Admin
|
||||
before_action :remove_protected_attrs, only: [:update]
|
||||
before_action :require_order_cycle_set_params, only: [:bulk_update]
|
||||
around_action :protect_invalid_destroy, only: :destroy
|
||||
before_action :verify_datetime_change, only: :update
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
@@ -63,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|
|
||||
@@ -78,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
|
||||
@@ -91,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
|
||||
@@ -271,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
|
||||
@@ -296,21 +304,13 @@ module Admin
|
||||
).to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
# Check that order cycle datetime values changed if it has existing orders
|
||||
def verify_datetime_change
|
||||
return unless params[:order_cycle][:confirm]
|
||||
return unless @order_cycle.orders.exists?
|
||||
return if same_dates(@order_cycle.orders_open_at, order_cycle_params[:orders_open_at]) &&
|
||||
same_dates(@order_cycle.orders_close_at, order_cycle_params[:orders_close_at])
|
||||
|
||||
render json: { trigger_action: params[:order_cycle][:trigger_action] },
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def same_dates(date, string)
|
||||
false unless date && string
|
||||
|
||||
DateTime.parse(string).to_fs(:short) == date.to_fs(:short)
|
||||
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
|
||||
@@ -40,6 +40,8 @@ module Admin
|
||||
{ id: params[:id] }
|
||||
).find_product
|
||||
|
||||
authorize! :delete, @record
|
||||
|
||||
@record.destroyed_by = spree_current_user
|
||||
status = :ok
|
||||
|
||||
@@ -74,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
|
||||
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
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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)
|
||||
@@ -37,5 +40,11 @@ module Admin
|
||||
|
||||
"#{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
|
||||
|
||||
@@ -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,11 +247,6 @@ class Enterprise < ApplicationRecord
|
||||
count(distinct: true)
|
||||
end
|
||||
|
||||
# Remove any unsupported HTML.
|
||||
def long_description
|
||||
HtmlSanitizer.sanitize(super)
|
||||
end
|
||||
|
||||
# Remove any unsupported HTML.
|
||||
def long_description=(html)
|
||||
super(HtmlSanitizer.sanitize(html))
|
||||
@@ -479,7 +472,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)
|
||||
|
||||
|
||||
@@ -148,17 +148,20 @@ class OrderCycle < ApplicationRecord
|
||||
|
||||
# Find the earliest closing times for each distributor in an active order cycle, and return
|
||||
# them in the format {distributor_id => closing_time, ...}
|
||||
def self.earliest_closing_times
|
||||
Hash[
|
||||
Exchange.
|
||||
outgoing.
|
||||
joins(:order_cycle).
|
||||
merge(OrderCycle.active).
|
||||
group('exchanges.receiver_id').
|
||||
select("exchanges.receiver_id AS receiver_id,
|
||||
MIN(order_cycles.orders_close_at) AS earliest_close_at").
|
||||
map { |ex| [ex.receiver_id, ex.earliest_close_at.to_time] }
|
||||
]
|
||||
#
|
||||
# Optionally, specify some distributor_ids as a parameter to scope the results
|
||||
def self.earliest_closing_times(distributor_ids = nil)
|
||||
cycles = Exchange.
|
||||
outgoing.
|
||||
joins(:order_cycle).
|
||||
merge(OrderCycle.active).
|
||||
group('exchanges.receiver_id')
|
||||
|
||||
cycles = cycles.where(receiver_id: distributor_ids) if distributor_ids.present?
|
||||
|
||||
cycles.pluck("exchanges.receiver_id AS receiver_id",
|
||||
"MIN(order_cycles.orders_close_at) AS earliest_close_at")
|
||||
.to_h
|
||||
end
|
||||
|
||||
def attachable_distributor_payment_methods
|
||||
@@ -314,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
|
||||
@@ -243,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
|
||||
@@ -324,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
|
||||
|
||||
@@ -25,18 +25,19 @@ module Spree
|
||||
|
||||
# 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.
|
||||
@@ -44,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")
|
||||
|
||||
@@ -57,6 +60,10 @@ 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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
%div{ style: 'font-size: 1rem;' }
|
||||
= t('.content')
|
||||
%p.modal-actions.justify-end.gap-1
|
||||
%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')
|
||||
%button.button.primary{ type: "button", 'data-action': 'click->modal#close' }
|
||||
= t('.cancel')
|
||||
- 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')
|
||||
%button.button.primary{ type: "button", 'data-action': 'click->modal#close' }
|
||||
= t('.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')
|
||||
@@ -37,4 +37,4 @@
|
||||
%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'
|
||||
= render 'date_time_warning_modal_content', action: 'simple_update'
|
||||
@@ -23,12 +23,20 @@
|
||||
.thirteen.columns.alpha
|
||||
%columns-dropdown{ action: "#{controller_name}_#{action_name}" }
|
||||
|
||||
%form{ name: 'order_cycles_form' }
|
||||
%save-bar{ dirty: "order_cycles_form.$dirty", persist: "false" }
|
||||
%input.red{ type: "button", value: t(:save_changes), "ng-click": "saveAll()", "ng-disabled": "!order_cycles_form.$dirty" }
|
||||
%table.index#listing_order_cycles{ "ng-show": 'orderCycles.length > 0' }
|
||||
= render 'admin/order_cycles/header' #, simple_index: simple_index
|
||||
%tbody
|
||||
= render 'admin/order_cycles/row' #, simple_index: simple_index
|
||||
= render 'admin/order_cycles/loading_flash'
|
||||
= render 'admin/order_cycles/show_more'
|
||||
%div{ data: { controller: 'order-cycle-form' } }
|
||||
%form{ name: 'order_cycles_form' }
|
||||
%save-bar{ dirty: "order_cycles_form.$dirty", persist: "false" }
|
||||
%input.red{ type: "button", value: t(:save_changes), "ng-click": "saveAll($event)", "ng-disabled": "!order_cycles_form.$dirty",
|
||||
data: { confirm: "true", 'trigger-action': 'bulk_save' } }
|
||||
%table.index#listing_order_cycles{ "ng-show": 'orderCycles.length > 0' }
|
||||
= render 'admin/order_cycles/header' #, simple_index: simple_index
|
||||
%tbody
|
||||
= render 'admin/order_cycles/row' #, simple_index: simple_index
|
||||
= render 'admin/order_cycles/loading_flash'
|
||||
= render 'admin/order_cycles/show_more'
|
||||
|
||||
%div.warning-modal{ data: { controller: 'modal modal-link', 'modal-link-target-value': "linked-order-warning-modal" } }
|
||||
%button.modal-target-trigger{ type: 'button', data: { 'action': 'modal-link#open' }, style: 'display: none;' }
|
||||
= render ModalComponent.new(id: "linked-order-warning-modal", close_button: false) do
|
||||
.content.flex-column.gap-2
|
||||
= render 'date_time_warning_modal_content', action: 'bulk_update'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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), 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
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
= "#{line_item.product.name}"
|
||||
- unless line_item.product.name.include? line_item.name_to_display
|
||||
%span= "- #{line_item.name_to_display}"
|
||||
- if line_item.options_text
|
||||
= "(#{line_item.options_text})"
|
||||
- if line_item.unit_to_display
|
||||
= "(#{line_item.unit_to_display})"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
- separator = messages.values.any? ? ": " : ", "
|
||||
- orders.each_with_index do |order, i|
|
||||
%a{ href: order_url(order) }>= order.number
|
||||
%a{ href: spree.edit_admin_order_url(order) }>= order.number
|
||||
= separator if messages.values.any? || i < orders.count - 1
|
||||
- if messages.values.any?
|
||||
= messages[order.id] || t(".no_message_provided")
|
||||
|
||||
@@ -6,7 +6,7 @@ export default class ColumnPreferencesController extends Controller {
|
||||
connect() {
|
||||
this.table = document.querySelector('table[data-column-preferences-target="table"]');
|
||||
this.cols = Array.from(this.table.querySelectorAll('col'));
|
||||
this.colSpanCells = this.table.querySelectorAll('th[colspan],td[colspan]');
|
||||
this.colSpanCells = Array.from(this.table.querySelectorAll('th[colspan],td[colspan]'));
|
||||
// Initialise data-default-col-span
|
||||
this.colSpanCells.forEach((cell)=> {
|
||||
cell.dataset.defaultColSpan ||= cell.colSpan;
|
||||
@@ -19,6 +19,8 @@ export default class ColumnPreferencesController extends Controller {
|
||||
// On checkbox changed
|
||||
element.addEventListener("change", this.#showHideColumn.bind(this));
|
||||
}
|
||||
|
||||
this.#observeProductsTableRows();
|
||||
}
|
||||
|
||||
// private
|
||||
@@ -30,14 +32,39 @@ export default class ColumnPreferencesController extends Controller {
|
||||
this.table.classList.toggle(`hide-${name}`, !element.checked);
|
||||
|
||||
// Reset cell colspans
|
||||
const hiddenColCount = this.checkboxes.filter((checkbox)=> !checkbox.checked).length;
|
||||
for(const cell of this.colSpanCells) {
|
||||
const span = parseInt(cell.dataset.defaultColSpan, 10) - hiddenColCount;
|
||||
cell.colSpan = span;
|
||||
this.#updateColSpanCell(cell);
|
||||
};
|
||||
}
|
||||
|
||||
#showHideElement(element, show) {
|
||||
element.style.display = show ? "" : "none";
|
||||
}
|
||||
|
||||
#observeProductsTableRows(){
|
||||
this.productsTableObserver = new MutationObserver((mutations, _observer) => {
|
||||
const mutationRecord = mutations[0];
|
||||
|
||||
if(mutationRecord){
|
||||
const productRowElement = mutationRecord.addedNodes[0];
|
||||
|
||||
if(productRowElement){
|
||||
const newColSpanCell = productRowElement.querySelector('td[colspan]');
|
||||
newColSpanCell.dataset.defaultColSpan ||= newColSpanCell.colSpan;
|
||||
this.#updateColSpanCell(newColSpanCell);
|
||||
this.colSpanCells.push(newColSpanCell);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.productsTableObserver.observe(this.table, { childList: true });
|
||||
}
|
||||
|
||||
#hiddenColCount(){
|
||||
return this.checkboxes.filter((checkbox)=> !checkbox.checked).length;
|
||||
}
|
||||
|
||||
#updateColSpanCell(cell){
|
||||
cell.colSpan = parseInt(cell.dataset.defaultColSpan, 10) - this.#hiddenColCount();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,14 @@ import { Controller } from "stimulus";
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
window.addEventListener("trix-change", this.#trixChange);
|
||||
this.element.addEventListener("trix-change", this.#trixChange);
|
||||
this.#trixInitialize();
|
||||
window.addEventListener("trix-initialize", this.#trixInitialize);
|
||||
this.element.addEventListener("trix-initialize", this.#trixInitialize);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.element.removeEventListener("trix-change", this.#trixChange);
|
||||
this.element.removeEventListener("trix-initialize", this.#trixInitialize);
|
||||
}
|
||||
|
||||
#trixChange = (event) => {
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
border-bottom: 2px solid $color-tbl-bg;
|
||||
|
||||
&.with-image {
|
||||
padding: 8px;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,3 +132,5 @@
|
||||
@import "app/webpacker/css/admin/trix.scss";
|
||||
|
||||
@import "terms_of_service_banner"; // admin_v3
|
||||
|
||||
@import "pages/product_preview"; // admin_v3
|
||||
|
||||
@@ -1,11 +1,60 @@
|
||||
// Navigation
|
||||
//---------------------------------------------------
|
||||
|
||||
@mixin menu-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@mixin menu-link {
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 16px 20px;
|
||||
color: $color-9 !important;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
color: $red !important;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
height: 3px;
|
||||
background: $red;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
@extend :hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline-menu {
|
||||
margin: 0;
|
||||
-webkit-margin-before: 0;
|
||||
-webkit-padding-start: 0;
|
||||
}
|
||||
|
||||
// tabs
|
||||
/// use the same styling as #admin-menu via menu-display and menu-link mixins
|
||||
dl.admin-tabs {
|
||||
box-shadow: $box-shadow;
|
||||
@include menu-display;
|
||||
|
||||
dd {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
@include menu-link;
|
||||
}
|
||||
}
|
||||
|
||||
nav.menu {
|
||||
ul {
|
||||
list-style: none;
|
||||
@@ -95,33 +144,10 @@ nav.menu {
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@include menu-display;
|
||||
|
||||
li {
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 16px 20px;
|
||||
color: $color-9 !important;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
color: $red !important;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
height: 3px;
|
||||
background: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
@include menu-link;
|
||||
|
||||
&.selected a {
|
||||
@extend a, :hover;
|
||||
|
||||
@@ -40,8 +40,8 @@ $color-tbl-thead-txt: $color-headers !default;
|
||||
$color-tbl-thead-bg: $light-grey !default;
|
||||
$color-tbl-border: $pale-blue !default;
|
||||
$padding-tbl-cell: 12px;
|
||||
$padding-tbl-cell-condensed: 4px 12px;
|
||||
$padding-tbl-cell-relaxed: 12px 12px;
|
||||
$padding-tbl-cell-condensed: 4px 3px;
|
||||
$padding-tbl-cell-relaxed: 8px 3px;
|
||||
|
||||
// Button colors
|
||||
$color-btn-bg: $teal !default;
|
||||
|
||||
176
app/webpacker/css/admin_v3/pages/product_preview.scss
Normal file
176
app/webpacker/css/admin_v3/pages/product_preview.scss
Normal file
@@ -0,0 +1,176 @@
|
||||
@import "../../darkswarm/branding";
|
||||
@import "../../darkswarm/mixins";
|
||||
|
||||
#product-preview {
|
||||
// The frontend css is base on foundation-sites https://github.com/foundation/foundation-sites
|
||||
// Below we copied the sections that are relevant to the product preview modal
|
||||
|
||||
// from foundation-sites/scss/foundations/components/_types.scss
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: #222222;
|
||||
font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.2rem;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.6875rem;
|
||||
}
|
||||
|
||||
em,
|
||||
i {
|
||||
font-style: italic;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol,
|
||||
dl {
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
list-style-position: outside;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
// from foundation-sites/scss/foundations/components/_buttons.scss
|
||||
button,
|
||||
.button {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
border-radius: 0;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
cursor: pointer;
|
||||
font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
line-height: normal;
|
||||
margin: 0 0 1.25rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
padding: 1rem 2rem 1.0625rem 2rem;
|
||||
font-size: 1rem;
|
||||
background-color: #008cba;
|
||||
border-color: #007095;
|
||||
color: #ffffff;
|
||||
transition: background-color 300ms ease-out;
|
||||
}
|
||||
|
||||
// from foundation-sites/scss/foundations/components/_grid.scss
|
||||
@media only screen and (min-width: 64.0625em) {
|
||||
.column,
|
||||
.columns {
|
||||
position: relative;
|
||||
padding-left: 0.9375rem;
|
||||
padding-right: 0.9375rem;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.column + .column:last-child,
|
||||
.column + .columns:last-child,
|
||||
.columns + .column:last-child,
|
||||
.columns + .columns:last-child {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.small-3 {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 40.0625em) {
|
||||
.medium-3 {
|
||||
width: 20%; // original value 25%
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 64.0625em) {
|
||||
.large-3 {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 64.0625em) {
|
||||
.large-6 {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
// from foundation-sites/scss/foundations/components/_global.scss
|
||||
img {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// Import frontend partials
|
||||
|
||||
// Product details
|
||||
@import "../../darkswarm/shop_partials/typography";
|
||||
@import "../../darkswarm/overrides";
|
||||
|
||||
.row {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@import "../../darkswarm/shop_partials/animations";
|
||||
@import "../../darkswarm/shop_partials/shop-filters";
|
||||
@import "../../darkswarm/shop-modals";
|
||||
@import "../../darkswarm/shop_partials/images";
|
||||
|
||||
// Shop
|
||||
@import "../../darkswarm/shop_partials/shop-product-thumb";
|
||||
@import "../../darkswarm/shop_partials/shop-product-rows";
|
||||
@import "../../darkswarm/shop_partials/shop-inputs";
|
||||
@import "../../shared/question-mark-icon";
|
||||
|
||||
button.add-variant {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.variant-remaining-stock {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.question-mark-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@include joyride-content;
|
||||
width: $joyride-width;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
background-color: $dynamic-blue;
|
||||
}
|
||||
|
||||
.columns {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,102 +1,4 @@
|
||||
|
||||
@mixin filter-selector($base-clr, $border-clr, $hover-clr) {
|
||||
&.inline-block, ul.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
|
||||
@include border-radius(0);
|
||||
|
||||
padding: 0;
|
||||
margin: 0 0.5rem 0.5rem 0;
|
||||
|
||||
&:hover, &:focus {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
a, a.button {
|
||||
display: block;
|
||||
|
||||
@include border-radius(0.5em);
|
||||
|
||||
border: 1px solid $border-clr;
|
||||
padding: 0.5em 0.625em;
|
||||
color: $base-clr;
|
||||
font-size: 0.75em;
|
||||
background: white;
|
||||
margin: 0;
|
||||
|
||||
i {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
render-svg {
|
||||
&, & svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
float: left;
|
||||
padding-right: 0.25rem;
|
||||
|
||||
path {
|
||||
@include csstrans;
|
||||
|
||||
fill: $base-clr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: $hover-clr;
|
||||
color: $hover-clr;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
path {
|
||||
fill: $hover-clr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: $border-clr;
|
||||
color: $base-clr;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
path {
|
||||
fill: $base-clr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active, &.active:hover, &.active:focus {
|
||||
border: 1px solid $base-clr;
|
||||
background: $base-clr;
|
||||
color: white;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
path {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@import "shop_partials/shop-filters";
|
||||
|
||||
// Alert when search, taxon, filter is triggered
|
||||
|
||||
@@ -167,23 +69,3 @@
|
||||
max-height: calc(100vh - #{$topbar-height});
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-shopfront {
|
||||
&.taxon-selectors, &.property-selectors {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// Shopfront taxons
|
||||
&.taxon-selectors {
|
||||
@include filter-selector($clr-blue, $clr-blue-light, $clr-blue-bright);
|
||||
}
|
||||
|
||||
// Shopfront properties
|
||||
&.property-selectors {
|
||||
@include filter-selector(#666, #ccc, #777);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,81 +15,7 @@
|
||||
//
|
||||
// They are not nested so that they can be used in modals.
|
||||
|
||||
.variant-quantity-inputs {
|
||||
height: 2.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
button.add-variant, button.variant-quantity {
|
||||
height: 2.5rem;
|
||||
border-radius: 0;
|
||||
background-color: $orange-500;
|
||||
color: white;
|
||||
// Override foundation button styles:
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
background-color: $orange-600;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background-color: $grey-400;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: $grey-400;
|
||||
}
|
||||
}
|
||||
&:nth-of-type(1) {
|
||||
border-bottom-left-radius: 0.25em;
|
||||
border-top-left-radius: 0.25em;
|
||||
}
|
||||
&:nth-last-of-type(1) {
|
||||
border-top-right-radius: 0.25em;
|
||||
border-bottom-right-radius: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
button.add-variant {
|
||||
min-width: 7rem;
|
||||
padding: 0 1em;
|
||||
|
||||
&[disabled] {
|
||||
&:hover, &:focus {
|
||||
background-color: $orange-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.variant-quantity {
|
||||
width: 2.25rem;
|
||||
|
||||
&:nth-of-type(1):not(.bulk-buy):not(.bulk-buy-add) {
|
||||
border-right: .1em solid $orange-400;
|
||||
}
|
||||
}
|
||||
|
||||
.variant-quantity-display, .variant-remaining-stock {
|
||||
font-size: 0.875em;
|
||||
margin-top: 0.25em;
|
||||
text-align: center;
|
||||
width: 7rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.variant-quantity-display {
|
||||
visibility: hidden;
|
||||
|
||||
&.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.variant-remaining-stock {
|
||||
color: $red-500;
|
||||
}
|
||||
@import "shop_partials/shop-inputs";
|
||||
|
||||
button.bulk-buy.variant-quantity {
|
||||
background-color: transparent;
|
||||
|
||||
@@ -1,201 +1,7 @@
|
||||
.darkswarm {
|
||||
products {
|
||||
product {
|
||||
// GENERAL LAYOUT
|
||||
.row {
|
||||
.columns {
|
||||
padding-top: 0em;
|
||||
padding-bottom: 0em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
.shop-variants {
|
||||
// product-thumb width + 1rem
|
||||
padding-left: calc(22.222% + 1rem);
|
||||
|
||||
@include breakpoint(phablet) {
|
||||
padding-left: 0;
|
||||
clear: left;
|
||||
}
|
||||
}
|
||||
|
||||
// ROW VARIANTS
|
||||
.row.variants {
|
||||
margin: 0 0 1em 0;
|
||||
|
||||
&.out-of-stock {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.variant-name,
|
||||
.total-price {
|
||||
padding-top: .74em;
|
||||
}
|
||||
.variant-price {
|
||||
padding-top: .65em;
|
||||
}
|
||||
|
||||
// Variant name
|
||||
.variant-name {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
@include breakpoint(phablet) {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
& > *:nth-child(n + 2) {
|
||||
color: $grey-550;
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
// Variant price
|
||||
.variant-price {
|
||||
white-space: nowrap;
|
||||
@include breakpoint(phablet) {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.variant-unit-price {
|
||||
color: $grey-700;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 15px;
|
||||
position: relative;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
// Total price
|
||||
.total-price {
|
||||
padding-left: 0rem;
|
||||
color: $disabled-med;
|
||||
|
||||
.filled {
|
||||
color: $med-drk-grey;
|
||||
}
|
||||
|
||||
@include breakpoint(phablet) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ROW SUMMARY
|
||||
.summary {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 1.25em;
|
||||
background: #fff;
|
||||
|
||||
.columns {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
line-height: 1;
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
padding-top: 0.65rem;
|
||||
padding-bottom: 0.65rem;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
// product-thumb width + 1rem
|
||||
padding-left: calc(22.222% + 1rem);
|
||||
padding-right: 1rem;
|
||||
|
||||
@include breakpoint(phablet) {
|
||||
padding-left: calc(33.333% + 1rem);
|
||||
}
|
||||
|
||||
.product-producer {
|
||||
color: $grey-550;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-style: italic;
|
||||
|
||||
a {
|
||||
color: $teal-500;
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
color: $teal-600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
h3 a {
|
||||
color: $orange-500;
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
color: $orange-600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.product-description {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 0.75rem;
|
||||
cursor: pointer;
|
||||
// Force product description to be on one line
|
||||
// and truncate with ellipsis
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
// line-clamp is not supported in Safari
|
||||
// Trick to get overflow: hidden to work in old Safari
|
||||
line-height: 1rem;
|
||||
height: 1.75rem;
|
||||
|
||||
> div {
|
||||
margin-bottom: 1.5rem; // Equivalent to p (trix doesn't use p as separator by default, so emulate div as p to be backward compatible)
|
||||
}
|
||||
|
||||
@include trix-styles;
|
||||
}
|
||||
|
||||
.product-properties {
|
||||
margin: .5em 0;
|
||||
|
||||
li {
|
||||
margin: 0 0.25rem 0.25rem 0;
|
||||
|
||||
a {
|
||||
padding: 0.1em 0.625em;
|
||||
|
||||
cursor: auto;
|
||||
|
||||
&.has-tip {
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
&:hover, &:focus {
|
||||
border-color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
// Foundation doesn't show the nub on mobile.
|
||||
// Repeating the style to show it here.
|
||||
.nub {
|
||||
border-color: transparent transparent #333333 transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@import "shop_partials/shop-product-rows";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,7 @@
|
||||
.darkswarm {
|
||||
products {
|
||||
product {
|
||||
.product-thumb {
|
||||
// Desktop: the product summary is nine columns wide. Use two
|
||||
// for the image. 100% / 9 * 2 = 22.222% <= 192px
|
||||
width: calc(22.222%);
|
||||
float: left;
|
||||
|
||||
// Mobile: the summary has full twelve columns and the image
|
||||
// should take four of them. 100% / 12 * 4 = 33.333% <= 227px
|
||||
@include breakpoint(phablet) {
|
||||
width: calc(33.333%);
|
||||
}
|
||||
|
||||
// Make this an anchor for the bulk label.
|
||||
position: relative;
|
||||
|
||||
.product-thumb__bulk-label {
|
||||
background-color: $grey-700;
|
||||
color: white;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: .8em;
|
||||
padding: .25em .5em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(96%);
|
||||
}
|
||||
}
|
||||
@import "shop_partials/shop-product-thumb";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,11 +275,4 @@ product.animate-repeat {
|
||||
}
|
||||
}
|
||||
|
||||
@mixin csstrans {
|
||||
-webkit-transition: all 300ms ease;
|
||||
-moz-transition: all 300ms ease;
|
||||
-ms-transition: all 300ms ease;
|
||||
-o-transition: all 300ms ease;
|
||||
transition: all 300ms ease;
|
||||
-webkit-transform-style: preserve-3d;
|
||||
}
|
||||
@import "shop_partials/animations";
|
||||
|
||||
@@ -1,23 +1,4 @@
|
||||
.product-img {
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
padding: 0.3rem;
|
||||
|
||||
// placeholder for when no product images
|
||||
&.placeholder {
|
||||
opacity: 0.35;
|
||||
|
||||
@include breakpoint(desktop) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@import "shop_partials/images";
|
||||
|
||||
.hero-img {
|
||||
outline: 1px solid $disabled-bright;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
@mixin csstrans {
|
||||
-webkit-transition: all 300ms ease;
|
||||
-moz-transition: all 300ms ease;
|
||||
-ms-transition: all 300ms ease;
|
||||
-o-transition: all 300ms ease;
|
||||
transition: all 300ms ease;
|
||||
-webkit-transform-style: preserve-3d;
|
||||
}
|
||||
21
app/webpacker/css/darkswarm/shop_partials/_images.scss
Normal file
21
app/webpacker/css/darkswarm/shop_partials/_images.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
.product-img {
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
padding: 0.3rem;
|
||||
|
||||
// placeholder for when no product images
|
||||
&.placeholder {
|
||||
opacity: 0.35;
|
||||
|
||||
@include breakpoint(desktop) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
119
app/webpacker/css/darkswarm/shop_partials/_shop-filters.scss
Normal file
119
app/webpacker/css/darkswarm/shop_partials/_shop-filters.scss
Normal file
@@ -0,0 +1,119 @@
|
||||
@mixin filter-selector($base-clr, $border-clr, $hover-clr) {
|
||||
&.inline-block, ul.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
|
||||
@include border-radius(0);
|
||||
|
||||
padding: 0;
|
||||
margin: 0 0.5rem 0.5rem 0;
|
||||
|
||||
&:hover, &:focus {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
a, a.button {
|
||||
display: block;
|
||||
|
||||
@include border-radius(0.5em);
|
||||
|
||||
border: 1px solid $border-clr;
|
||||
padding: 0.5em 0.625em;
|
||||
color: $base-clr;
|
||||
font-size: 0.75em;
|
||||
background: white;
|
||||
margin: 0;
|
||||
|
||||
i {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
render-svg {
|
||||
&, & svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
float: left;
|
||||
padding-right: 0.25rem;
|
||||
|
||||
path {
|
||||
@include csstrans;
|
||||
|
||||
fill: $base-clr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: $hover-clr;
|
||||
color: $hover-clr;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
path {
|
||||
fill: $hover-clr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: $border-clr;
|
||||
color: $base-clr;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
path {
|
||||
fill: $base-clr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active, &.active:hover, &.active:focus {
|
||||
border: 1px solid $base-clr;
|
||||
background: $base-clr;
|
||||
color: white;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
path {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-shopfront {
|
||||
&.taxon-selectors, &.property-selectors {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// Shopfront taxons
|
||||
&.taxon-selectors {
|
||||
@include filter-selector($clr-blue, $clr-blue-light, $clr-blue-bright);
|
||||
}
|
||||
|
||||
// Shopfront properties
|
||||
&.property-selectors {
|
||||
@include filter-selector(#666, #ccc, #777);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
76
app/webpacker/css/darkswarm/shop_partials/_shop-inputs.scss
Normal file
76
app/webpacker/css/darkswarm/shop_partials/_shop-inputs.scss
Normal file
@@ -0,0 +1,76 @@
|
||||
.variant-quantity-inputs {
|
||||
height: 2.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
button.add-variant, button.variant-quantity {
|
||||
height: 2.5rem;
|
||||
border-radius: 0;
|
||||
background-color: $orange-500;
|
||||
color: white;
|
||||
// Override foundation button styles:
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
background-color: $orange-600;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background-color: $grey-400;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: $grey-400;
|
||||
}
|
||||
}
|
||||
&:nth-of-type(1) {
|
||||
border-bottom-left-radius: 0.25em;
|
||||
border-top-left-radius: 0.25em;
|
||||
}
|
||||
&:nth-last-of-type(1) {
|
||||
border-top-right-radius: 0.25em;
|
||||
border-bottom-right-radius: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
button.add-variant {
|
||||
min-width: 7rem;
|
||||
padding: 0 1em;
|
||||
|
||||
&[disabled] {
|
||||
&:hover, &:focus {
|
||||
background-color: $orange-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.variant-quantity {
|
||||
width: 2.25rem;
|
||||
|
||||
&:nth-of-type(1):not(.bulk-buy):not(.bulk-buy-add) {
|
||||
border-right: .1em solid $orange-400;
|
||||
}
|
||||
}
|
||||
|
||||
.variant-quantity-display, .variant-remaining-stock {
|
||||
font-size: 0.875em;
|
||||
margin-top: 0.25em;
|
||||
text-align: center;
|
||||
width: 7rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.variant-quantity-display {
|
||||
visibility: hidden;
|
||||
|
||||
&.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.variant-remaining-stock {
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
// GENERAL LAYOUT
|
||||
.row {
|
||||
.columns {
|
||||
padding-top: 0em;
|
||||
padding-bottom: 0em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
.shop-variants {
|
||||
// product-thumb width + 1rem
|
||||
padding-left: calc(22.222% + 1rem);
|
||||
|
||||
@include breakpoint(phablet) {
|
||||
padding-left: 0;
|
||||
clear: left;
|
||||
}
|
||||
}
|
||||
|
||||
// ROW VARIANTS
|
||||
.row.variants {
|
||||
margin: 0 0 1em 0;
|
||||
|
||||
&.out-of-stock {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.variant-name,
|
||||
.total-price {
|
||||
padding-top: .74em;
|
||||
}
|
||||
.variant-price {
|
||||
padding-top: .65em;
|
||||
}
|
||||
|
||||
// Variant name
|
||||
.variant-name {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
@include breakpoint(phablet) {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
& > *:nth-child(n + 2) {
|
||||
color: $grey-550;
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
// Variant price
|
||||
.variant-price {
|
||||
white-space: nowrap;
|
||||
@include breakpoint(phablet) {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.variant-unit-price {
|
||||
color: $grey-700;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 15px;
|
||||
position: relative;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
// Total price
|
||||
.total-price {
|
||||
padding-left: 0rem;
|
||||
color: $disabled-med;
|
||||
|
||||
.filled {
|
||||
color: $med-drk-grey;
|
||||
}
|
||||
|
||||
@include breakpoint(phablet) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ROW SUMMARY
|
||||
.summary {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 1.25em;
|
||||
background: #fff;
|
||||
|
||||
.columns {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
line-height: 1;
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
padding-top: 0.65rem;
|
||||
padding-bottom: 0.65rem;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
// product-thumb width + 1rem
|
||||
padding-left: calc(22.222% + 1rem);
|
||||
padding-right: 1rem;
|
||||
|
||||
@include breakpoint(phablet) {
|
||||
padding-left: calc(33.333% + 1rem);
|
||||
}
|
||||
|
||||
.product-producer {
|
||||
color: $grey-550;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-style: italic;
|
||||
|
||||
a {
|
||||
color: $teal-500;
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
color: $teal-600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
h3 a {
|
||||
color: $orange-500;
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
color: $orange-600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.product-description {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 0.75rem;
|
||||
cursor: pointer;
|
||||
// Force product description to be on one line
|
||||
// and truncate with ellipsis
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
// line-clamp is not supported in Safari
|
||||
// Trick to get overflow: hidden to work in old Safari
|
||||
line-height: 1rem;
|
||||
height: 1.75rem;
|
||||
|
||||
> div {
|
||||
margin-bottom: 1.5rem; // Equivalent to p (trix doesn't use p as separator by default, so emulate div as p to be backward compatible)
|
||||
}
|
||||
|
||||
@include trix-styles;
|
||||
}
|
||||
|
||||
.product-properties {
|
||||
margin: .5em 0;
|
||||
|
||||
li {
|
||||
margin: 0 0.25rem 0.25rem 0;
|
||||
|
||||
a {
|
||||
padding: 0.1em 0.625em;
|
||||
|
||||
cursor: auto;
|
||||
|
||||
&.has-tip {
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
&:hover, &:focus {
|
||||
border-color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
// Foundation doesn't show the nub on mobile.
|
||||
// Repeating the style to show it here.
|
||||
.nub {
|
||||
border-color: transparent transparent #333333 transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
.product-thumb {
|
||||
// Desktop: the product summary is nine columns wide. Use two
|
||||
// for the image. 100% / 9 * 2 = 22.222% <= 192px
|
||||
width: calc(22.222%);
|
||||
float: left;
|
||||
|
||||
// Mobile: the summary has full twelve columns and the image
|
||||
// should take four of them. 100% / 12 * 4 = 33.333% <= 227px
|
||||
@include breakpoint(phablet) {
|
||||
width: calc(33.333%);
|
||||
}
|
||||
|
||||
// Make this an anchor for the bulk label.
|
||||
position: relative;
|
||||
|
||||
.product-thumb__bulk-label {
|
||||
background-color: $grey-700;
|
||||
color: white;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: .8em;
|
||||
padding: .25em .5em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(96%);
|
||||
}
|
||||
}
|
||||
31
app/webpacker/css/darkswarm/shop_partials/_typography.scss
Normal file
31
app/webpacker/css/darkswarm/shop_partials/_typography.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
@mixin headingFont {
|
||||
font-family: "Oswald", sans-serif;
|
||||
}
|
||||
|
||||
// TODO should probably move that to a variables.scss
|
||||
$body-font: "Roboto", Arial, sans-serif;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@include headingFont;
|
||||
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $clr-brick;
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
text-decoration: none;
|
||||
color: $clr-brick-bright;
|
||||
}
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: $body-font;
|
||||
|
||||
&, & * {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
@@ -347,7 +347,6 @@
|
||||
margin-top: 40px;
|
||||
|
||||
.button.primary {
|
||||
background-color: $clr-turquoise;
|
||||
&:hover {
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user