mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-28 01:53:25 +00:00
Compare commits
1 Commits
v4.4.48
...
v4.4.5-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0262dcd11b |
16
.env
16
.env
@@ -42,10 +42,16 @@ SMTP_PASSWORD="f00d"
|
||||
# Javascript error reporting via Bugsnag.
|
||||
# BUGSNAG_JS_KEY=""
|
||||
|
||||
# SingleSignOn login for Discourse
|
||||
#
|
||||
# DISCOURSE_SSO_SECRET should be a random string. It must be the same as provided to your Discourse instance.
|
||||
# DISCOURSE_SSO_SECRET=""
|
||||
#
|
||||
# DISCOURSE_URL must be the URL of your Discourse instance.
|
||||
# DISCOURSE_URL="https://noticeboard.openfoodnetwork.org.au"
|
||||
|
||||
# see="https://developers.google.com/maps/documentation/javascript/get-api-key
|
||||
# GOOGLE_MAPS_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
# see https://developers.google.com/maps/documentation/javascript/localization#Region
|
||||
# GOOGLE_MAPS_REGION="XX"
|
||||
|
||||
# Stripe details for instance account
|
||||
# Find these under 'Developers' -> 'API keys' in your Stripe account dashboard.
|
||||
@@ -55,9 +61,3 @@ SMTP_PASSWORD="f00d"
|
||||
# STRIPE_INSTANCE_PUBLISHABLE_KEY="pk_test_xxxx" # This can be a test key or a live key
|
||||
# STRIPE_CLIENT_ID="ca_xxxx" # This can be a development ID or a production ID
|
||||
# STRIPE_ENDPOINT_SECRET="whsec_xxxx"
|
||||
|
||||
# New relic settings
|
||||
# see: https://one.eu.newrelic.com/admin-portal/, Administration > API keys to get the license key
|
||||
# NEW_RELIC_AGENT_ENABLED=true
|
||||
# NEW_RELIC_APP_NAME="Open Food Network"
|
||||
# NEW_RELIC_LICENSE_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
# ENV vars for the test environment
|
||||
# Override locally with `.env.test.local`
|
||||
|
||||
OFN_REDIS_JOBS_URL="redis://localhost:6379/2"
|
||||
|
||||
SECRET_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
STRIPE_INSTANCE_SECRET_KEY="bogus_key"
|
||||
STRIPE_SECRET_TEST_API_KEY="bogus_key"
|
||||
STRIPE_CUSTOMER="bogus_customer"
|
||||
STRIPE_ACCOUNT="bogus_account"
|
||||
STRIPE_CLIENT_ID="bogus_client_id"
|
||||
STRIPE_PUBLIC_TEST_API_KEY="bogus_stripe_publishable_key"
|
||||
|
||||
SITE_URL="test.host"
|
||||
|
||||
OPENID_APP_ID="test-provider"
|
||||
OPENID_APP_SECRET="12345"
|
||||
OPENID_REFRESH_TOKEN="dummy-refresh-token"
|
||||
|
||||
25
.github/ISSUE_TEMPLATE/release.md
vendored
25
.github/ISSUE_TEMPLATE/release.md
vendored
@@ -10,21 +10,16 @@ assignees: ''
|
||||
## 1. Preparation on Thursday
|
||||
|
||||
- [ ] Merge pull requests in the [Ready To Go] column
|
||||
- [ ] Include translations: `script/release/update_locales`
|
||||
- [ ] 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)
|
||||
- Patch: all others. Shortcut: `script/release/tag`
|
||||
- [ ] Include translations: `tx pull --force`
|
||||
- [ ] [Draft new release]. Look at previous [releases] for inspiration.
|
||||
- Select new release tag
|
||||
- _Generate release notes_ and check to ensure all items are arranged in the right category.
|
||||
- [ ] Notify [#instance-managers] of user-facing :eyes:, API :warning: and experimental :construction: changes.
|
||||
- [ ] Notify [#instance-managers] of user-facing changes.
|
||||
|
||||
## 2. Testing
|
||||
|
||||
- [ ] [Find build] of the release commit and copy it below.
|
||||
- [ ] Move this issue to Test Ready.
|
||||
- [ ] Notify `@testers` in [#testing].
|
||||
- [ ] Test build: [Deploy to Staging] with release tag.
|
||||
- [ ] Test build: <!-- paste build link here, e.g. https://semaphore...builds/1234 -->
|
||||
|
||||
## 3. Finish on Tuesday
|
||||
|
||||
@@ -34,22 +29,20 @@ assignees: ''
|
||||
<pre>
|
||||
cd ofn-install
|
||||
git pull
|
||||
ansible-playbook --limit all_prod --extra-vars "git_version=vX.Y.Z" playbooks/deploy.yml
|
||||
ansible-playbook --limit all-prod --extra-vars "git_version=vx.y.z" playbooks/deploy.yml
|
||||
</pre>
|
||||
</details>
|
||||
- [ ] Notify [#instance-managers]:
|
||||
> @instance_managers The new release has been deployed.
|
||||
- [ ] [Create issue] for next release and confirm with next release manager in [#core-devs].
|
||||
- [ ] Nudge next release manager
|
||||
|
||||
The full process is described at https://github.com/openfoodfoundation/openfoodnetwork/wiki/Releasing.
|
||||
|
||||
[Ready To Go]: https://github.com/orgs/openfoodfoundation/projects/8?filterQuery=status%3A%22Ready+to+go+%F0%9F%9A%80%22
|
||||
[Ready To Go]: #zenhub
|
||||
[Transifex pull request]: https://github.com/openfoodfoundation/openfoodnetwork/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aopen+head%3Atransifex
|
||||
[Draft new release]: https://github.com/openfoodfoundation/openfoodnetwork/releases/new?title=v+Code+Name&body=Congrats%0A%0ADescription%0A%0A
|
||||
[Draft new release]: https://github.com/openfoodfoundation/openfoodnetwork/releases/new?tag=v&title=v+Code+Name&body=Congrats%0A%0ADescription%0A%0A%23%23+User+facing+changes+:eyes:%0A%0A%0A%23%23%23+Experimental+features+for+testing+:sunglasses:%0A%0A%0A%23%23+Technical+changes+:wrench:%0A%0A
|
||||
[releases]: https://github.com/openfoodfoundation/openfoodnetwork/releases
|
||||
[#instance-managers]: https://app.slack.com/client/T02G54U79/CG7NJ966B
|
||||
[#testing]: https://openfoodnetwork.slack.com/app_redirect?channel=C02TZ6X00
|
||||
[Deploy to Staging]: https://github.com/openfoodfoundation/openfoodnetwork/actions/workflows/stage.yml
|
||||
[Find build]: https://semaphoreci.com/openfoodfoundation/openfoodnetwork-2/branches/master
|
||||
[#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
|
||||
|
||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -20,12 +20,7 @@
|
||||
|
||||
<!-- Please select one for your PR and delete the other. -->
|
||||
|
||||
Changelog Category (reviewers may add a label for the release notes):
|
||||
|
||||
- [ ] User facing changes
|
||||
- [ ] API changes (V0, V1, DFC or Webhook)
|
||||
- [ ] Technical changes only
|
||||
- [ ] Feature toggled
|
||||
Changelog Category: User facing changes | Technical changes
|
||||
|
||||
<!-- Choose a pull request title above which explains your change to a
|
||||
a user of the Open Food Network app. -->
|
||||
|
||||
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@@ -1,8 +1,3 @@
|
||||
# Dependabot configuration
|
||||
#
|
||||
# The `directory` and `schedule.interval` options are mandatory.
|
||||
# Most of the configuration here is not used for security updates though.
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
|
||||
@@ -10,7 +5,7 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
open-pull-requests-limit: 10
|
||||
# Only specific requirements are specified in Gemfile, so don't touch it.
|
||||
versioning-strategy: lockfile-only
|
||||
|
||||
@@ -18,6 +13,5 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
# All versions are specified in package.json, so please update them.
|
||||
versioning-strategy: increase
|
||||
|
||||
37
.github/release.yml
vendored
37
.github/release.yml
vendored
@@ -1,37 +0,0 @@
|
||||
changelog:
|
||||
# Categorise according to what an instance manager needs to know
|
||||
categories:
|
||||
# Add the right label if anything appears in here.
|
||||
# Then re-generate the release notes.
|
||||
- title: "❓❓❓ Uncategorised ❓❓❓"
|
||||
labels:
|
||||
- '*'
|
||||
exclude:
|
||||
labels:
|
||||
- api changes
|
||||
- dependencies
|
||||
- feature toggled
|
||||
- technical changes only
|
||||
- user facing changes
|
||||
|
||||
# Posted in advance for #instance-managers
|
||||
- title: "User-facing changes 👀"
|
||||
labels:
|
||||
- user facing changes
|
||||
|
||||
- title: "API changes ⚠️"
|
||||
labels:
|
||||
- api changes
|
||||
|
||||
- title: "Experimental features for testing 🚧" # may be tested by instance managers
|
||||
labels:
|
||||
- feature toggled
|
||||
|
||||
# Instance managers ignore below
|
||||
- title: "Technical changes 🛠️"
|
||||
labels:
|
||||
- technical changes only
|
||||
|
||||
- title: "Dependencies 📦"
|
||||
labels:
|
||||
- dependencies
|
||||
337
.github/workflows/build.yml
vendored
337
.github/workflows/build.yml
vendored
@@ -17,7 +17,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
controllers:
|
||||
knapsack_rspec_controllers:
|
||||
runs-on: ubuntu-22.04
|
||||
services:
|
||||
postgres:
|
||||
@@ -55,7 +55,6 @@ jobs:
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
# JS is required in order for webpacker to compile, in order to render templates containing image urls
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
@@ -65,7 +64,8 @@ jobs:
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
bin/rake db:create db:schema:load
|
||||
bundle exec rake db:create
|
||||
bundle exec rake db:schema:load
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
@@ -83,9 +83,9 @@ jobs:
|
||||
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
|
||||
bundle exec rake knapsack_pro:rspec
|
||||
|
||||
models:
|
||||
knapsack_rspec_models:
|
||||
runs-on: ubuntu-22.04
|
||||
services:
|
||||
postgres:
|
||||
@@ -106,69 +106,10 @@ jobs:
|
||||
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
||||
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
||||
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
||||
ci_node_total: [4]
|
||||
ci_node_total: [7]
|
||||
# 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]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup redis
|
||||
uses: supercharge/redis-github-action@1.4.0
|
||||
with:
|
||||
redis-version: 6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
bin/rake db:create db:schema:load
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: 09476e2ce491c12083df62768667c674
|
||||
KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
|
||||
KNAPSACK_PRO_CI_NODE_INDEX: ${{ matrix.ci_node_index }}
|
||||
KNAPSACK_PRO_LOG_LEVEL: info
|
||||
# if you use Knapsack Pro Queue Mode you must set below env variable
|
||||
# to be able to retry CI build and run previously recorded tests
|
||||
# https://github.com/KnapsackPro/knapsack_pro-ruby#knapsack_pro_fixed_queue_split-remember-queue-split-on-retry-ci-node
|
||||
# KNAPSACK_PRO_FIXED_QUEUE_SPLIT: false
|
||||
# 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}"
|
||||
run: |
|
||||
bin/rake knapsack_pro:rspec
|
||||
|
||||
system_admin:
|
||||
runs-on: ubuntu-22.04
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:10
|
||||
ports: ["5432:5432"]
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
env:
|
||||
POSTGRES_DB: open_food_network_test
|
||||
POSTGRES_USER: ofn
|
||||
POSTGRES_PASSWORD: f00d
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
||||
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
||||
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
||||
ci_node_total: [14]
|
||||
# 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, 12, 13]
|
||||
ci_node_index: [0, 1, 2, 3, 4, 5, 6]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -191,7 +132,77 @@ jobs:
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
bin/rake db:create db:schema:load
|
||||
bundle exec rake db:create
|
||||
bundle exec rake db:schema:load
|
||||
|
||||
- name: Run tests
|
||||
|
||||
env:
|
||||
KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: 09476e2ce491c12083df62768667c674
|
||||
KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
|
||||
KNAPSACK_PRO_CI_NODE_INDEX: ${{ matrix.ci_node_index }}
|
||||
KNAPSACK_PRO_LOG_LEVEL: info
|
||||
# if you use Knapsack Pro Queue Mode you must set below env variable
|
||||
# to be able to retry CI build and run previously recorded tests
|
||||
# https://github.com/KnapsackPro/knapsack_pro-ruby#knapsack_pro_fixed_queue_split-remember-queue-split-on-retry-ci-node
|
||||
# KNAPSACK_PRO_FIXED_QUEUE_SPLIT: false
|
||||
# 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}"
|
||||
|
||||
run: |
|
||||
bundle exec rake knapsack_pro:rspec
|
||||
|
||||
knapsack_rspec_system_admin:
|
||||
runs-on: ubuntu-22.04
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:10
|
||||
ports: ["5432:5432"]
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
env:
|
||||
POSTGRES_DB: open_food_network_test
|
||||
POSTGRES_USER: ofn
|
||||
POSTGRES_PASSWORD: f00d
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
||||
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
||||
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
||||
ci_node_total: [10]
|
||||
# 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]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup redis
|
||||
uses: supercharge/redis-github-action@1.4.0
|
||||
with:
|
||||
redis-version: 6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Install JS dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
bundle exec rake db:create
|
||||
bundle exec rake db:schema:load
|
||||
|
||||
- name: Run tests
|
||||
|
||||
@@ -210,7 +221,7 @@ jobs:
|
||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/admin/**/*_spec.rb}"
|
||||
|
||||
run: |
|
||||
bin/rake knapsack_pro:queue:rspec
|
||||
bundle exec rake knapsack_pro:queue:rspec
|
||||
|
||||
- name: Archive failed tests screenshots
|
||||
if: failure()
|
||||
@@ -221,7 +232,7 @@ jobs:
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
system_consumer:
|
||||
knapsack_rspec_system_consumer:
|
||||
runs-on: ubuntu-22.04
|
||||
services:
|
||||
postgres:
|
||||
@@ -242,10 +253,10 @@ jobs:
|
||||
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
||||
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
||||
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
||||
ci_node_total: [12]
|
||||
ci_node_total: [10]
|
||||
# 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]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -268,7 +279,8 @@ jobs:
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
bin/rake db:create db:schema:load
|
||||
bundle exec rake db:create
|
||||
bundle exec rake db:schema:load
|
||||
|
||||
- name: Run tests
|
||||
|
||||
@@ -287,7 +299,7 @@ jobs:
|
||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/consumer/**/*_spec.rb}"
|
||||
|
||||
run: |
|
||||
bin/rake knapsack_pro:queue:rspec
|
||||
bundle exec rake knapsack_pro:queue:rspec
|
||||
|
||||
- name: Archive failed tests screenshots
|
||||
if: failure()
|
||||
@@ -298,85 +310,7 @@ jobs:
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
engines:
|
||||
runs-on: ubuntu-22.04
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:10
|
||||
ports: ["5432:5432"]
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
env:
|
||||
POSTGRES_DB: open_food_network_test
|
||||
POSTGRES_USER: ofn
|
||||
POSTGRES_PASSWORD: f00d
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
||||
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
||||
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
||||
ci_node_total: [2]
|
||||
# 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]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup redis
|
||||
uses: supercharge/redis-github-action@1.4.0
|
||||
with:
|
||||
redis-version: 6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
# JS is required in order for webpacker to compile, in order to render templates linking to mail.css
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Install JS dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
bin/rake db:create db:schema:load
|
||||
|
||||
- name: Run tests
|
||||
|
||||
env:
|
||||
KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: d6ea7ceb766404ccd016c19aa2c81b1c
|
||||
KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
|
||||
KNAPSACK_PRO_CI_NODE_INDEX: ${{ matrix.ci_node_index }}
|
||||
KNAPSACK_PRO_LOG_LEVEL: info
|
||||
# if you use Knapsack Pro Queue Mode you must set below env variable
|
||||
# to be able to retry CI build and run previously recorded tests
|
||||
# https://github.com/KnapsackPro/knapsack_pro-ruby#knapsack_pro_fixed_queue_split-remember-queue-split-on-retry-ci-node
|
||||
# KNAPSACK_PRO_FIXED_QUEUE_SPLIT: false
|
||||
# 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/lib/**/*_spec.rb,spec/migrations/**/*_spec.rb,spec/serializers/**/*_spec.rb,engines/**/*_spec.rb}"
|
||||
|
||||
run: |
|
||||
bin/rake knapsack_pro:rspec
|
||||
|
||||
- name: Archive failed tests screenshots
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: failed-tests-screenshots
|
||||
path: tmp/capybara/screenshots/*.png
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
test_the_rest:
|
||||
knapsack_rspec_engines:
|
||||
runs-on: ubuntu-22.04
|
||||
services:
|
||||
postgres:
|
||||
@@ -414,7 +348,6 @@ jobs:
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
# JS is required in order for webpacker to compile, in order to render templates linking to mail.css
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
@@ -424,9 +357,89 @@ jobs:
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
bin/rake db:create db:schema:load
|
||||
bundle exec rake db:create
|
||||
bundle exec rake db:schema:load
|
||||
|
||||
- name: Run tests
|
||||
|
||||
env:
|
||||
KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: d6ea7ceb766404ccd016c19aa2c81b1c
|
||||
KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
|
||||
KNAPSACK_PRO_CI_NODE_INDEX: ${{ matrix.ci_node_index }}
|
||||
KNAPSACK_PRO_LOG_LEVEL: info
|
||||
# if you use Knapsack Pro Queue Mode you must set below env variable
|
||||
# to be able to retry CI build and run previously recorded tests
|
||||
# https://github.com/KnapsackPro/knapsack_pro-ruby#knapsack_pro_fixed_queue_split-remember-queue-split-on-retry-ci-node
|
||||
# KNAPSACK_PRO_FIXED_QUEUE_SPLIT: false
|
||||
# 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/lib/**/*_spec.rb,spec/migrations/**/*_spec.rb,spec/serializers/**/*_spec.rb,engines/**/*_spec.rb}"
|
||||
|
||||
run: |
|
||||
bundle exec rake knapsack_pro:rspec
|
||||
|
||||
- name: Archive failed tests screenshots
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: failed-tests-screenshots
|
||||
path: tmp/capybara/screenshots/*.png
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
knapsack_rspec_test_the_rest:
|
||||
runs-on: ubuntu-22.04
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:10
|
||||
ports: ["5432:5432"]
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
env:
|
||||
POSTGRES_DB: open_food_network_test
|
||||
POSTGRES_USER: ofn
|
||||
POSTGRES_PASSWORD: f00d
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
||||
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
||||
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
||||
ci_node_total: [5]
|
||||
# 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]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup redis
|
||||
uses: supercharge/redis-github-action@1.4.0
|
||||
with:
|
||||
redis-version: 6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Install JS dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
bundle exec rake db:create
|
||||
bundle exec rake db:schema:load
|
||||
|
||||
- name: Run tests
|
||||
|
||||
env:
|
||||
KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: e3b8800198d2d89b70c7edbdd85f8fd8
|
||||
KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
|
||||
@@ -440,8 +453,10 @@ jobs:
|
||||
# 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_EXCLUDE_PATTERN: "{engines/**/*_spec.rb,spec/models/**/*_spec.rb,spec/controllers/**/*_spec.rb,spec/serializers/**/*_spec.rb,spec/lib/**/*_spec.rb,spec/migrations/**/*_spec.rb,spec/system/**/*_spec.rb}"
|
||||
|
||||
|
||||
run: |
|
||||
bin/rake knapsack_pro:rspec
|
||||
bundle exec rake knapsack_pro:rspec
|
||||
|
||||
non_knapsack_jest_karma:
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -461,7 +476,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# Rails is required for the Karma rake script
|
||||
- name: Setup redis
|
||||
uses: supercharge/redis-github-action@1.4.0
|
||||
with:
|
||||
redis-version: 6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
@@ -474,8 +493,12 @@ jobs:
|
||||
- name: Install JS dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
bundle exec rake db:create
|
||||
bundle exec rake db:schema:load
|
||||
- name: Run JS tests
|
||||
run: bin/rake karma:run
|
||||
run: bundle exec rake karma:run
|
||||
|
||||
- name: Run jest tests
|
||||
run: yarn jest
|
||||
|
||||
9
.github/workflows/linters.yml
vendored
9
.github/workflows/linters.yml
vendored
@@ -7,11 +7,10 @@ jobs:
|
||||
name: runner / rubocop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- run: git show --no-patch # the commit being tested (which is often a merge due to actions/checkout@v3)
|
||||
|
||||
@@ -19,11 +18,9 @@ jobs:
|
||||
uses: reviewdog/action-rubocop@v2
|
||||
with:
|
||||
rubocop_version: gemfile
|
||||
rubocop_extensions: rubocop-rails:gemfile rubocop-rspec:gemfile
|
||||
rubocop_extensions: rubocop-rails:gemfile
|
||||
reporter: github-pr-check
|
||||
level: error
|
||||
filter_mode: nofilter
|
||||
use_bundler: true
|
||||
fail_on_error: true
|
||||
prettier:
|
||||
name: runner / prettier
|
||||
|
||||
2
.github/workflows/mapi.yml
vendored
2
.github/workflows/mapi.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
with:
|
||||
mapi-token: ${{ secrets.MAPI_TOKEN }}
|
||||
api-url: http://localhost:3000
|
||||
api-spec: swagger/v1.yaml
|
||||
api-spec: swagger/v1/swagger.yaml
|
||||
target: openfoodfoundation/openfoodnetwork
|
||||
duration: 1min
|
||||
sarif-report: mapi.sarif
|
||||
|
||||
6
.github/workflows/stage.yml
vendored
6
.github/workflows/stage.yml
vendored
@@ -13,10 +13,6 @@ on:
|
||||
- staging.openfoodnetwork.org.uk
|
||||
- staging.openfoodnetwork.org.au
|
||||
- staging.coopcircuits.fr
|
||||
commit_ref:
|
||||
description: "Commit Reference"
|
||||
type: string
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
deploy_pr:
|
||||
@@ -63,4 +59,4 @@ jobs:
|
||||
- name: Deploy to Staging
|
||||
if: success()
|
||||
run: |
|
||||
ssh ofn-deploy@${{ inputs.server }} -o LogLevel=ERROR "$GITHUB_REF_NAME ${{ inputs.commit_ref || github.sha }}"
|
||||
ssh ofn-deploy@${{ inputs.server }} -o LogLevel=ERROR "$GITHUB_REF_NAME $GITHUB_SHA"
|
||||
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
yarn pretty-quick --check --staged
|
||||
@@ -13,8 +13,7 @@ postcss.config.js
|
||||
|
||||
# SCSS
|
||||
# Enabled: most of admin
|
||||
/app/webpacker/css/admin/globals/mixins.scss
|
||||
/app/webpacker/css/admin/globals/variables.scss
|
||||
/app/webpacker/css/admin/globals/
|
||||
/app/webpacker/css/admin/shared/
|
||||
/app/webpacker/css/admin_v3/globals/variables.scss
|
||||
/app/webpacker/css/darkswarm/
|
||||
|
||||
11
.rubocop.yml
11
.rubocop.yml
@@ -4,13 +4,7 @@
|
||||
#
|
||||
# The configuration is split into three files. Look into those files for more details.
|
||||
#
|
||||
require:
|
||||
- rubocop-capybara
|
||||
- rubocop-factory_bot
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
- rubocop-rspec_rails
|
||||
|
||||
require: rubocop-rails
|
||||
inherit_from:
|
||||
|
||||
# The automatically generated todo list to ignore all current violations.
|
||||
@@ -19,10 +13,9 @@ inherit_from:
|
||||
# The relaxed style rules as a common starting point which we can refine.
|
||||
- .rubocop_relaxed_styleguide.yml
|
||||
|
||||
# Our Open Food Network style guides. If you want to see all violations,
|
||||
# Our Open Food Network style guide. If you want to see all violations,
|
||||
# then use only that configuration:
|
||||
#
|
||||
# bundle exec rubocop -c .rubocop_styleguide.yml
|
||||
#
|
||||
- .rubocop_styleguide.yml
|
||||
- .rubocop_rspec_styleguide.yml
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# OFN styleguide for rubocop-rspec
|
||||
# Because there are so many, we will disable by default, and enable rules as needed.
|
||||
|
||||
Capybara:
|
||||
Enabled: false
|
||||
|
||||
RSpec:
|
||||
Enabled: false
|
||||
|
||||
FactoryBot:
|
||||
Enabled: false
|
||||
|
||||
# Enabled rules
|
||||
|
||||
Capybara/NegationMatcher:
|
||||
Enabled: true
|
||||
EnforcedStyle: not_to
|
||||
|
||||
RSpec/ExpectChange:
|
||||
Enabled: true
|
||||
EnforcedStyle: block
|
||||
|
||||
RSpec/NotToNot:
|
||||
Enabled: true
|
||||
@@ -3,56 +3,24 @@
|
||||
# These are the rules we agreed upon and we work towards.
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
SuggestExtensions: false
|
||||
Exclude:
|
||||
- bin/**/*
|
||||
- db/**/*
|
||||
- config/**/*
|
||||
- script/**/*
|
||||
- vendor/**/*
|
||||
- node_modules/**/*
|
||||
- 'bin/**/*'
|
||||
- 'db/**/*'
|
||||
- 'config/**/*'
|
||||
- 'script/**/*'
|
||||
- 'vendor/**/*'
|
||||
- 'node_modules/**/*'
|
||||
# Excluding: inadequate Naming/FileName rule rejects GemFile name with camelcase
|
||||
- engines/web/Gemfile
|
||||
- 'engines/web/Gemfile'
|
||||
|
||||
Bundler/DuplicatedGem:
|
||||
Enabled: false
|
||||
|
||||
Layout/LineLength:
|
||||
Enabled: true
|
||||
Max: 100
|
||||
|
||||
Layout/MultilineMethodCallIndentation:
|
||||
Enabled: true
|
||||
EnforcedStyle: indented
|
||||
|
||||
# Don't think this is a big issue, mostly picking up RPSEC scope definitions
|
||||
# with lamdas and RSpec '.to change{}' blocks
|
||||
Lint/AmbiguousBlockAssociation:
|
||||
Enabled: false
|
||||
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- app/components/**/*
|
||||
|
||||
Lint/RaiseException:
|
||||
Enabled: true
|
||||
|
||||
Lint/StructNewOverride:
|
||||
Enabled: true
|
||||
|
||||
# Heaps of offences (> 100) in specs, mostly in situations where two or more
|
||||
# instances of a model are required, but only one is referenced. Difficult to
|
||||
# fix without making the spec look messy or rewriting it.
|
||||
# Should definitely fix at some point.
|
||||
Lint/UselessAssignment:
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
## OFN SETTINGS
|
||||
#
|
||||
# Cop settings that have been agreed upon by the OFN community
|
||||
|
||||
Metrics:
|
||||
Enabled: true
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 30 # default 17
|
||||
|
||||
Metrics/BlockLength:
|
||||
AllowedMethods: [
|
||||
"class_eval",
|
||||
@@ -71,66 +39,33 @@ Metrics/BlockLength:
|
||||
"put",
|
||||
"resource",
|
||||
"resources",
|
||||
"response",
|
||||
"scenario",
|
||||
"shared_examples",
|
||||
"shared_examples_for",
|
||||
"xdescribe",
|
||||
]
|
||||
|
||||
Metrics/MethodLength:
|
||||
Enabled: true
|
||||
Max: 25 # default 10
|
||||
|
||||
Metrics/ParameterLists:
|
||||
CountKeywordArgs: false
|
||||
|
||||
Metrics/PerceivedComplexity:
|
||||
Enabled: true
|
||||
Max: 14 # default 8
|
||||
|
||||
Naming/PredicateName:
|
||||
Enabled: false
|
||||
|
||||
Naming/VariableNumber:
|
||||
AllowedIdentifiers:
|
||||
- street_address_1
|
||||
- street_address_2
|
||||
AllowedPatterns:
|
||||
- _v[\d]+
|
||||
|
||||
Rails/ApplicationRecord:
|
||||
Exclude:
|
||||
# Migrations should not contain application code:
|
||||
- db/migrate/*.rb
|
||||
|
||||
# Allow many-to-many associations without explicit model.
|
||||
# - It avoids the additional code of a model class.
|
||||
# - It simplifies the declaration of the association.
|
||||
# - Rails may know that there are no callbacks associated.
|
||||
Rails/HasAndBelongsToMany:
|
||||
Enabled: false
|
||||
|
||||
Rails/OutputSafety:
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
- "db/migrate/*.rb"
|
||||
|
||||
Rails/SkipsModelValidations:
|
||||
AllowedMethods:
|
||||
- touch
|
||||
- touch_all
|
||||
- update_all
|
||||
- update_attribute
|
||||
- update_column
|
||||
- update_columns
|
||||
|
||||
Rails/WhereExists:
|
||||
EnforcedStyle: where # Cf. conversion https://github.com/openfoodfoundation/openfoodnetwork/pull/12363
|
||||
- "touch"
|
||||
- "touch_all"
|
||||
- "update_all"
|
||||
- "update_attribute"
|
||||
- "update_column"
|
||||
- "update_columns"
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
Style/FormatStringToken:
|
||||
Style/StringLiterals:
|
||||
Enabled: false
|
||||
|
||||
Style/HashSyntax:
|
||||
@@ -140,5 +75,62 @@ Style/HashSyntax:
|
||||
Style/Send:
|
||||
Enabled: true
|
||||
|
||||
Style/StringLiterals:
|
||||
Layout/MultilineMethodCallIndentation:
|
||||
Enabled: true
|
||||
EnforcedStyle: indented
|
||||
|
||||
Layout/LineLength:
|
||||
Enabled: true
|
||||
Max: 100
|
||||
|
||||
Lint/RaiseException:
|
||||
Enabled: true
|
||||
|
||||
Lint/StructNewOverride:
|
||||
Enabled: true
|
||||
|
||||
Naming/VariableNumber:
|
||||
AllowedIdentifiers:
|
||||
- street_address_1
|
||||
- street_address_2
|
||||
AllowedPatterns:
|
||||
- _v[\d]+
|
||||
|
||||
Bundler/DuplicatedGem:
|
||||
Enabled: false
|
||||
|
||||
## TEMPORARY/CONTESTED SETTINGS
|
||||
#
|
||||
# These are still to be decided upon, but recommended for inclusion by
|
||||
# oeoeaio after scrutinising offenses the codebase
|
||||
|
||||
# Don't think this is a big issue, mostly picking up RPSEC scope definitions
|
||||
# with lamdas and RSpec '.to change{}' blocks
|
||||
Lint/AmbiguousBlockAssociation:
|
||||
Enabled: false
|
||||
|
||||
# Heaps of offences (> 100) in specs, mostly in situations where two or more
|
||||
# instances of a model are required, but only one is referenced. Difficult to
|
||||
# fix without making the spec look messy or rewriting it.
|
||||
# Should definitely fix at some point.
|
||||
Lint/UselessAssignment:
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- 'app/components/**/*'
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 30 # default 17
|
||||
|
||||
Metrics/MethodLength:
|
||||
Enabled: true
|
||||
Max: 25 # default 10
|
||||
|
||||
Metrics/PerceivedComplexity:
|
||||
Enabled: true
|
||||
Max: 14 # default 8
|
||||
|
||||
Naming/PredicateName:
|
||||
Enabled: false
|
||||
|
||||
1280
.rubocop_todo.yml
1280
.rubocop_todo.yml
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,28 @@ This is a general guide to setting up an Open Food Network **development environ
|
||||
|
||||
Head to our wiki on [Learning Rails](https://github.com/openfoodfoundation/openfoodnetwork/wiki/Learning-Rails) to find some good starting points.
|
||||
|
||||
### Requirements
|
||||
|
||||
The fastest way to make it work locally is to use Docker, you only need to setup git, see the [Docker setup guide](docker/README.md).
|
||||
Otherwise, for a local setup you will need:
|
||||
* Ruby and bundler (check current Ruby version in [.ruby-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.ruby-version) file)
|
||||
- To manage versions, it's recommended to use [rbenv](https://github.com/rbenv/rbenv) or [RVM](https://rvm.io/)
|
||||
* Node and yarn (check current Node version in [.node-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.node-version) file)
|
||||
- [nodevn](https://github.com/nodenv/nodenv) is recommended.
|
||||
* PostgreSQL database
|
||||
* Redis (for background jobs)
|
||||
* Chrome (for testing)
|
||||
|
||||
The following guides will provide OS-specific step-by-step instructions to get these requirements installed:
|
||||
- [Ubuntu Setup Guide][ubuntu]
|
||||
- [Debian Setup Guide][debian]
|
||||
- [OSX Setup Guide][osx]
|
||||
|
||||
For those new to Rails, the following tutorial will help get you up to speed with configuring a [Rails environment](http://guides.rubyonrails.org/getting_started.html).
|
||||
|
||||
### Get it
|
||||
|
||||
If you're planning on contributing code to the project (which we [LOVE](CONTRIBUTING.md)), it is a good idea to begin by forking this repo using the `Fork` button in the top-right corner of this screen. You should then be able to use `git clone` to copy your fork onto your local machine:
|
||||
So you have set up your local environment according to the requirements listed above. If you're planning on contributing code to the project (which we [LOVE](CONTRIBUTING.md)), it is a good idea to begin by forking this repo using the `Fork` button in the top-right corner of this screen. You should then be able to use `git clone` to copy your fork onto your local machine:
|
||||
|
||||
git clone git@github.com:YOUR_GITHUB_USERNAME_HERE/openfoodnetwork.git
|
||||
|
||||
@@ -24,27 +43,6 @@ Fetch the latest version of `master` from `upstream` (ie. the main repo):
|
||||
|
||||
git fetch upstream master
|
||||
|
||||
### Installation
|
||||
|
||||
This project needs specific ruby/bundler versions as well as node/yarn specific versions. For a local setup you will need:
|
||||
|
||||
* Install or change your Ruby version according to the one specified at [.ruby-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.ruby-version) file.
|
||||
- To manage versions, it's recommended to use [rbenv](https://github.com/rbenv/rbenv) or [RVM](https://rvm.io/).
|
||||
* Install [nodenv](https://github.com/nodenv/nodenv) to ensure the correct [.node-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.node-version) is used.
|
||||
- [nodevn](https://github.com/nodenv/nodenv) is recommended as a node version manager.
|
||||
* PostgreSQL database
|
||||
* Redis (for background jobs)
|
||||
* Chrome (for testing)
|
||||
|
||||
The following guides will provide OS-specific step-by-step instructions to get these requirements installed:
|
||||
- [Ubuntu Setup Guide][ubuntu]
|
||||
- [Debian Setup Guide][debian]
|
||||
- [OSX Setup Guide][osx]
|
||||
|
||||
For those new to Rails, the following tutorial will help get you up to speed with configuring a [Rails environment](http://guides.rubyonrails.org/getting_started.html).
|
||||
|
||||
Another way to make it work locally would be using Docker. See the [Docker setup guide](docker/README.md).
|
||||
|
||||
### Get it running
|
||||
|
||||
First, you need to create the database user the app will use by manually typing the following in your terminal:
|
||||
@@ -55,8 +53,7 @@ sudo --login --user=postgres psql -c "CREATE USER ofn WITH SUPERUSER CREATEDB PA
|
||||
|
||||
This will create the "ofn" user as superuser and allowing it to create databases. If this command fails, check the [troubleshooting section](#creating-the-database) for an alternative.
|
||||
|
||||
Next, it is _strongly recommended_ to run the setup script:
|
||||
|
||||
Next, it is _strongly recommended_ to run the setup script.
|
||||
```sh
|
||||
./script/setup
|
||||
```
|
||||
|
||||
31
Gemfile
31
Gemfile
@@ -5,7 +5,7 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}.git" }
|
||||
|
||||
ruby File.read('.ruby-version').chomp
|
||||
|
||||
gem 'dotenv', require: 'dotenv/load' # Load ENV vars before other gems
|
||||
gem 'dotenv-rails', require: 'dotenv/rails-now' # Load ENV vars before other gems
|
||||
|
||||
gem 'rails'
|
||||
|
||||
@@ -17,8 +17,9 @@ gem "image_processing"
|
||||
gem 'activemerchant', '>= 1.78.0'
|
||||
gem 'angular-rails-templates', '>= 0.3.0'
|
||||
gem 'awesome_nested_set'
|
||||
gem 'ransack', '~> 4.1.0'
|
||||
gem 'ransack', '~> 2.6.0'
|
||||
gem 'responders'
|
||||
gem 'rexml'
|
||||
gem 'webpacker', '~> 5'
|
||||
|
||||
gem 'i18n'
|
||||
@@ -31,7 +32,6 @@ gem "db2fog", github: "openfoodfoundation/db2fog", branch: "rails-7"
|
||||
gem "fog-aws", "~> 2.0" # db2fog does not support v3
|
||||
gem "mime-types" # required by fog
|
||||
|
||||
gem "validates_lengths_from_database"
|
||||
gem "valid_email2"
|
||||
|
||||
gem "catalog", path: "./engines/catalog"
|
||||
@@ -73,7 +73,7 @@ gem 'rswag-ui'
|
||||
|
||||
gem 'omniauth_openid_connect'
|
||||
gem 'omniauth-rails_csrf_protection'
|
||||
gem 'openid_connect'
|
||||
gem 'openid_connect', '~> 1.3'
|
||||
|
||||
gem 'angularjs-rails', '1.8.0'
|
||||
gem 'bugsnag'
|
||||
@@ -92,20 +92,19 @@ gem 'bootsnap', require: false
|
||||
gem 'geocoder'
|
||||
gem 'gmaps4rails'
|
||||
gem 'mimemagic', '> 0.3.5'
|
||||
gem 'paper_trail'
|
||||
gem 'paper_trail', '~> 12.1'
|
||||
gem 'rack-rewrite'
|
||||
gem 'rack-timeout'
|
||||
gem 'roadie-rails'
|
||||
|
||||
gem 'hiredis'
|
||||
gem 'puma'
|
||||
gem 'redis'
|
||||
gem 'redis', '>= 4.0', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'sidekiq'
|
||||
gem 'sidekiq-scheduler'
|
||||
|
||||
gem "cable_ready"
|
||||
gem "stimulus_reflex"
|
||||
|
||||
gem "turbo-rails"
|
||||
gem "cable_ready", "5.0.0.rc2"
|
||||
gem "stimulus_reflex", "3.5.0.rc2"
|
||||
|
||||
gem 'combine_pdf'
|
||||
gem 'wicked_pdf'
|
||||
@@ -134,17 +133,11 @@ gem 'flipper-ui'
|
||||
gem "view_component"
|
||||
gem 'view_component_reflex', '3.1.14.pre9'
|
||||
|
||||
# mini_portile2 is needed when installing with Vargant
|
||||
# https://openfoodnetwork.slack.com/archives/CEBMTRCNS/p1668439152992899
|
||||
gem 'mini_portile2', '~> 2.8'
|
||||
|
||||
gem "faraday"
|
||||
gem "private_address_check"
|
||||
|
||||
gem 'newrelic_rpm'
|
||||
|
||||
gem 'invisible_captcha'
|
||||
|
||||
group :production, :staging do
|
||||
gem 'sd_notify' # For better Systemd process management. Used by Puma.
|
||||
end
|
||||
@@ -162,10 +155,8 @@ group :test, :development do
|
||||
gem 'letter_opener', '>= 1.4.1'
|
||||
gem 'rspec-rails', ">= 3.5.2"
|
||||
gem 'rspec-retry', require: false
|
||||
gem 'rspec-sql'
|
||||
gem 'rswag'
|
||||
gem 'rswag-specs'
|
||||
gem 'shoulda-matchers'
|
||||
gem 'stimulus_reflex_testing', github: "podia/stimulus_reflex_testing", branch: :main
|
||||
gem 'timecop'
|
||||
end
|
||||
|
||||
@@ -188,10 +179,8 @@ group :development do
|
||||
gem 'rails-erd'
|
||||
gem 'rubocop'
|
||||
gem 'rubocop-rails'
|
||||
gem 'rubocop-rspec'
|
||||
gem 'spring'
|
||||
gem 'spring-commands-rspec'
|
||||
gem 'spring-commands-rubocop'
|
||||
gem 'web-console'
|
||||
|
||||
gem 'rack-mini-profiler', '< 3.0.0'
|
||||
|
||||
647
Gemfile.lock
647
Gemfile.lock
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
[](https://github.com/openfoodfoundation/openfoodnetwork/actions/workflows/build.yml)
|
||||
[](https://codeclimate.com/github/openfoodfoundation/openfoodnetwork)
|
||||
|
||||
# Open Food Network
|
||||
|
||||
@@ -32,7 +33,7 @@ We also have a [Super Admin Guide][super-admin-guide] to help with configuration
|
||||
|
||||
## Testing
|
||||
|
||||
If you'd like to help out with testing, please introduce yourself on the #testing channel on [Slack][slack-invite]. Also, do have a look in our [Welcome New QAs board][welcome-qa] for some good first issues, both on manual and automated testing (RSpec/Capybara).
|
||||
If you'd like to help out with testing, please introduce yourself on the #testing channel on [Slack][slack-invite] and download the [ZenHub browser extension][zenhub] to view the development pipeline. Also, do have a look in our [Welcome New QAs board][welcome-qa] for some good first issues, both on manual and automated testing (RSpec/Capybara).
|
||||
|
||||
We use [BrowserStack](https://www.browserstack.com/) as a manual testing tool. BrowserStack provides open source projects with unlimited and free of charge accounts. A big thanks to them!
|
||||
|
||||
@@ -44,7 +45,7 @@ We use [KnapsackPro](https://knapsackpro.com/) for optimal parallelisation of ou
|
||||
|
||||
## Licence
|
||||
|
||||
Copyright (c) 2012 - 2024 Open Food Foundation, released under the AGPL licence.
|
||||
Copyright (c) 2012 - 2022 Open Food Foundation, released under the AGPL licence.
|
||||
|
||||
[survey]: https://docs.google.com/a/eaterprises.com.au/forms/d/1zxR5vSiU9CigJ9cEaC8-eJLgYid8CR8er7PPH9Mc-30/edit#
|
||||
[slack-invite]: https://join.slack.com/t/openfoodnetwork/shared_invite/zt-9sjkjdlu-r02kUMP1zbrTgUhZhYPF~A
|
||||
@@ -53,3 +54,4 @@ Copyright (c) 2012 - 2024 Open Food Foundation, released under the AGPL licence.
|
||||
[super-admin-guide]: https://ofn-user-guide.gitbook.io/ofn-super-admin-guide
|
||||
[welcome-dev]: https://github.com/orgs/openfoodfoundation/projects/5
|
||||
[welcome-qa]: https://github.com/orgs/openfoodfoundation/projects/6
|
||||
[zenhub]: https://www.zenhub.com/extension
|
||||
|
||||
@@ -48,7 +48,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
params = {
|
||||
'q[name_cont]': $scope.q.query,
|
||||
'q[supplier_id_eq]': $scope.q.producerFilter,
|
||||
'q[variants_primary_taxon_id_eq]': $scope.q.categoryFilter,
|
||||
'q[primary_taxon_id_eq]': $scope.q.categoryFilter,
|
||||
'q[s]': $scope.sorting,
|
||||
import_date: $scope.q.importDateFilter,
|
||||
page: $scope.page,
|
||||
@@ -135,8 +135,6 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
display_name: null
|
||||
on_hand: null
|
||||
price: null
|
||||
tax_category_id: null
|
||||
category_id: null
|
||||
DisplayProperties.setShowVariants product.id, true
|
||||
|
||||
|
||||
@@ -218,7 +216,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
filters:
|
||||
'q[name_cont]': $scope.q.query
|
||||
'q[supplier_id_eq]': $scope.q.producerFilter
|
||||
'q[variants_primary_taxon_id_eq]': $scope.q.categoryFilter
|
||||
'q[primary_taxon_id_eq]': $scope.q.categoryFilter
|
||||
'q[s]': $scope.sorting
|
||||
import_date: $scope.q.importDateFilter
|
||||
page: $scope.page
|
||||
@@ -249,6 +247,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
else
|
||||
product.variant_unit = product.variant_unit_scale = null
|
||||
|
||||
$scope.packVariant product, product.master if product.master
|
||||
|
||||
if product.variants
|
||||
for id, variant of product.variants
|
||||
@@ -299,6 +298,7 @@ filterSubmitProducts = (productsToFilter) ->
|
||||
if product.hasOwnProperty("id")
|
||||
filteredProduct = {id: product.id}
|
||||
filteredVariants = []
|
||||
filteredMaster = null
|
||||
hasUpdatableProperty = false
|
||||
|
||||
if product.hasOwnProperty("variants")
|
||||
@@ -308,6 +308,16 @@ filterSubmitProducts = (productsToFilter) ->
|
||||
variantHasUpdatableProperty = result.hasUpdatableProperty
|
||||
filteredVariants.push filteredVariant if variantHasUpdatableProperty
|
||||
|
||||
if product.master?.hasOwnProperty("unit_value")
|
||||
filteredMaster ?= { id: product.master.id }
|
||||
filteredMaster.unit_value = product.master.unit_value
|
||||
if product.master?.hasOwnProperty("unit_description")
|
||||
filteredMaster ?= { id: product.master.id }
|
||||
filteredMaster.unit_description = product.master.unit_description
|
||||
if product.master?.hasOwnProperty("display_as")
|
||||
filteredMaster ?= { id: product.master.id }
|
||||
filteredMaster.display_as = product.master.display_as
|
||||
|
||||
if product.hasOwnProperty("sku")
|
||||
filteredProduct.sku = product.sku
|
||||
hasUpdatableProperty = true
|
||||
@@ -333,9 +343,18 @@ filterSubmitProducts = (productsToFilter) ->
|
||||
if product.hasOwnProperty("on_demand") and filteredVariants.length == 0 #only update if no variants present
|
||||
filteredProduct.on_demand = product.on_demand
|
||||
hasUpdatableProperty = true
|
||||
if product.hasOwnProperty("category_id")
|
||||
filteredProduct.primary_taxon_id = product.category_id
|
||||
hasUpdatableProperty = true
|
||||
if product.hasOwnProperty("tax_category_id")
|
||||
filteredProduct.tax_category_id = product.tax_category_id
|
||||
hasUpdatableProperty = true
|
||||
if product.hasOwnProperty("inherits_properties")
|
||||
filteredProduct.inherits_properties = product.inherits_properties
|
||||
hasUpdatableProperty = true
|
||||
if filteredMaster?
|
||||
filteredProduct.master_attributes = filteredMaster
|
||||
hasUpdatableProperty = true
|
||||
if filteredVariants.length > 0 # Note that the name of the property changes to enable mass assignment of variants attributes with rails
|
||||
filteredProduct.variants_attributes = filteredVariants
|
||||
hasUpdatableProperty = true
|
||||
@@ -370,12 +389,6 @@ filterSubmitVariant = (variant) ->
|
||||
if variant.hasOwnProperty("display_name")
|
||||
filteredVariant.display_name = variant.display_name
|
||||
hasUpdatableProperty = true
|
||||
if variant.hasOwnProperty("tax_category_id")
|
||||
filteredVariant.tax_category_id = variant.tax_category_id
|
||||
hasUpdatableProperty = true
|
||||
if variant.hasOwnProperty("category_id")
|
||||
filteredVariant.primary_taxon_id = variant.category_id
|
||||
hasUpdatableProperty = true
|
||||
if variant.hasOwnProperty("display_as")
|
||||
filteredVariant.display_as = variant.display_as
|
||||
hasUpdatableProperty = true
|
||||
|
||||
@@ -11,8 +11,14 @@ angular.module("admin.indexUtils").directive "objForUpdate", (switchClass, pendi
|
||||
pendingChanges.remove(scope.object().id, scope.attr)
|
||||
scope.clear()
|
||||
else
|
||||
change =
|
||||
object: scope.object()
|
||||
type: scope.type
|
||||
attr: scope.attr
|
||||
value: if value? then value else ""
|
||||
scope: scope
|
||||
scope.pending()
|
||||
addPendingChange(scope.attr, value ? "")
|
||||
pendingChanges.add(scope.object().id, scope.attr, change)
|
||||
|
||||
scope.reset = (value) ->
|
||||
scope.savedValue = value
|
||||
@@ -28,33 +34,3 @@ angular.module("admin.indexUtils").directive "objForUpdate", (switchClass, pendi
|
||||
|
||||
scope.clear = ->
|
||||
switchClass( element, "", ["update-pending", "update-error", "update-success"], false )
|
||||
|
||||
# When a list of customer is filtered and we removed the "filtered value" from a customer, we
|
||||
# want to make sure the customer is updated. IE. filtering by tag, and removing said tag.
|
||||
# Deleting the "filtered value" from a customer will remove the customer entry, thus
|
||||
# removing "objForUpdate" directive from the active scope. That means $watch won't pick up
|
||||
# the attribute changed.
|
||||
# To ensure the customer is still updated, we check on the $destroy event to see if
|
||||
# the attribute has changed, if so we queue up the change.
|
||||
scope.$on '$destroy', (value) ->
|
||||
# No update
|
||||
return if scope.object()[scope.attr] is scope.savedValue
|
||||
|
||||
# For some reason the code attribute is removed from the object when cleared, so we add
|
||||
# an emptyvalue so it gets updated properly
|
||||
if scope.attr is "code" and scope.object()[scope.attr] is undefined
|
||||
scope.object()["code"] = ""
|
||||
|
||||
# Queuing up change
|
||||
addPendingChange(scope.attr, scope.object()[scope.attr])
|
||||
|
||||
# private
|
||||
|
||||
addPendingChange = (attr, value) ->
|
||||
change =
|
||||
object: scope.object()
|
||||
type: scope.type
|
||||
attr: attr
|
||||
value: value
|
||||
scope: scope
|
||||
pendingChanges.add(scope.object().id, attr, change)
|
||||
|
||||
@@ -24,9 +24,6 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
|
||||
"order_bill_address_firstname",
|
||||
"order_bill_address_lastname",
|
||||
"order_bill_address_full_name",
|
||||
"order_bill_address_full_name_reversed",
|
||||
"order_bill_address_full_name_with_comma",
|
||||
"order_bill_address_full_name_with_comma_reversed",
|
||||
"variant_product_supplier_name",
|
||||
"order_email",
|
||||
"order_number",
|
||||
|
||||
@@ -42,10 +42,8 @@ angular.module('admin.payments').factory 'Payment', (AdminStripeElements, curren
|
||||
|
||||
submit: =>
|
||||
munged = @preprocess()
|
||||
PaymentResource.create({order_id: munged.order_id}, munged, (response, headers, status) ->
|
||||
rawHtml = Object.values(response).join('').replace('[object Object]true', '')
|
||||
document.body.innerHTML = rawHtml
|
||||
$window.history.pushState({}, '', "/admin/orders/" + munged.order_id + "/payments")
|
||||
PaymentResource.create({order_id: munged.order_id}, munged, (response, headers, status)=>
|
||||
$window.location.pathname = "/admin/orders/" + munged.order_id + "/payments"
|
||||
, (response) ->
|
||||
StatusMessage.display 'error', t("spree.admin.payments.source_forms.stripe.error_saving_payment")
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# Controller for "New Products" form (spree/admin/products/new)
|
||||
angular.module("admin.products")
|
||||
.controller "unitsCtrl", ($scope, VariantUnitManager, OptionValueNamer, UnitPrices, PriceParser) ->
|
||||
$scope.product = { master: {} }
|
||||
@@ -13,15 +12,13 @@ angular.module("admin.products")
|
||||
|
||||
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
|
||||
|
||||
# Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
|
||||
# and update hidden product fields
|
||||
$scope.processVariantUnitWithScale = ->
|
||||
if $scope.product.variant_unit_with_scale
|
||||
match = $scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/) # matches string like "weight_1000"
|
||||
match = $scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
|
||||
if match
|
||||
$scope.product.variant_unit = match[1]
|
||||
$scope.product.variant_unit_scale = parseFloat(match[2])
|
||||
else # "items"
|
||||
else
|
||||
$scope.product.variant_unit = $scope.product.variant_unit_with_scale
|
||||
$scope.product.variant_unit_scale = null
|
||||
else if $scope.product.variant_unit
|
||||
@@ -35,8 +32,6 @@ angular.module("admin.products")
|
||||
else
|
||||
$scope.product.variant_unit = $scope.product.variant_unit_scale = null
|
||||
|
||||
# Extract unit_value and unit_description from text field unit_value_with_description,
|
||||
# and update hidden variant fields
|
||||
$scope.processUnitValueWithDescription = ->
|
||||
if $scope.product.master.hasOwnProperty("unit_value_with_description")
|
||||
match = $scope.product.master.unit_value_with_description.match(/^([\d\.,]+(?= *|$)|)( *)(.*)$/)
|
||||
@@ -50,7 +45,6 @@ angular.module("admin.products")
|
||||
value = window.bigDecimal.divide(value, $scope.product.variant_unit_scale, 2) if $scope.product.master.unit_value && $scope.product.variant_unit_scale
|
||||
$scope.product.master.unit_value_with_description = value + " " + $scope.product.master.unit_description
|
||||
|
||||
# Calculate unit price based on product price and variant_unit_scale
|
||||
$scope.processUnitPrice = ->
|
||||
price = $scope.product.price
|
||||
scale = $scope.product.variant_unit_scale
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager) ->
|
||||
# Javascript clone of VariantUnits::OptionValueNamer, for bulk product editing.
|
||||
class OptionValueNamer
|
||||
constructor: (@variant) ->
|
||||
|
||||
|
||||
@@ -2,9 +2,6 @@ angular.module("admin.products").factory "VariantUnitManager", (availableUnits)
|
||||
class VariantUnitManager
|
||||
@units:
|
||||
'weight':
|
||||
0.001:
|
||||
name: 'mg'
|
||||
system: 'metric'
|
||||
1.0:
|
||||
name: 'g'
|
||||
system: 'metric'
|
||||
@@ -24,21 +21,12 @@ angular.module("admin.products").factory "VariantUnitManager", (availableUnits)
|
||||
0.001:
|
||||
name: 'mL'
|
||||
system: 'metric'
|
||||
0.01:
|
||||
name: 'cL'
|
||||
system: 'metric'
|
||||
0.1:
|
||||
name: 'dL'
|
||||
system: 'metric'
|
||||
1.0:
|
||||
name: 'L'
|
||||
system: 'metric'
|
||||
1000.0:
|
||||
name: 'kL'
|
||||
system: 'metric'
|
||||
4.54609:
|
||||
name: 'gal'
|
||||
system: 'metric'
|
||||
'items':
|
||||
1:
|
||||
name: 'items'
|
||||
@@ -72,13 +60,8 @@ angular.module("admin.products").factory "VariantUnitManager", (availableUnits)
|
||||
|
||||
@compatibleUnitScales: (scale, unitType) ->
|
||||
scaleSystem = @units[unitType][scale]['system']
|
||||
if availableUnits
|
||||
available = availableUnits.split(",")
|
||||
(parseFloat(scale) for scale, scaleInfo of @units[unitType] when scaleInfo['system'] == scaleSystem and available.includes(scaleInfo['name'])).sort (a, b) ->
|
||||
a - b
|
||||
else
|
||||
(parseFloat(scale) for scale, scaleInfo of @units[unitType] when scaleInfo['system'] == scaleSystem).sort (a, b) ->
|
||||
a - b
|
||||
(parseFloat(scale) for scale, scaleInfo of @units[unitType] when scaleInfo['system'] == scaleSystem).sort (a, b) ->
|
||||
a - b
|
||||
|
||||
@systemOfMeasurement: (scale, unitType) ->
|
||||
if @units[unitType][scale]
|
||||
|
||||
@@ -32,6 +32,9 @@ jQuery(function($) {
|
||||
});
|
||||
}
|
||||
|
||||
// Make flash messages dissapear
|
||||
setTimeout('$(".flash").fadeOut()', 5000);
|
||||
|
||||
// Highlight hovered table column
|
||||
$('table tbody tr td.actions a').hover(function(){
|
||||
var tr = $(this).closest('tr');
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
// Shipments AJAX API
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
handle_ship_click = function(){
|
||||
var link = $(this);
|
||||
var shipment_number = link.data('shipment-number');
|
||||
var url = Spree.url( Spree.routes.orders_api + "/" + order_number + "/shipments/" + shipment_number + "/ship.json");
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: url
|
||||
}).done(function( msg ) {
|
||||
window.location.reload();
|
||||
}).error(function( msg ) {
|
||||
console.log(msg);
|
||||
});
|
||||
}
|
||||
$('.admin-order-edit-form a.ship').click(handle_ship_click);
|
||||
|
||||
//handle shipping method edit click
|
||||
$('a.edit-method').click(toggleMethodEdit);
|
||||
$('a.cancel-method').click(toggleMethodEdit);
|
||||
|
||||
@@ -50,11 +50,11 @@ $(document).ready(function() {
|
||||
if (quantity > maxQuantity) {
|
||||
quantity = maxQuantity;
|
||||
save.parents('tr').find('input.line_item_quantity').val(maxQuantity);
|
||||
ofnAlert(t("js.admin.orders.quantity_unavailable"));
|
||||
} else {
|
||||
adjustItems(shipment_number, variant_id, quantity, true);
|
||||
ofnAlert(t("js.admin.orders.quantity_adjusted"));
|
||||
}
|
||||
toggleItemEdit();
|
||||
|
||||
adjustItems(shipment_number, variant_id, quantity, true);
|
||||
return false;
|
||||
}
|
||||
$('a.save-item').click(handle_save_click);
|
||||
|
||||
@@ -68,7 +68,7 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
|
||||
per_page: $scope.per_page,
|
||||
'q[name_or_meta_keywords_or_variants_display_as_or_variants_display_name_or_supplier_name_cont]': $scope.query,
|
||||
'q[with_properties][]': $scope.activeProperties,
|
||||
'q[variants_primary_taxon_id_in_any][]': $scope.activeTaxons
|
||||
'q[primary_taxon_id_in_any][]': $scope.activeTaxons
|
||||
}
|
||||
|
||||
$scope.searchKeypress = (e)->
|
||||
|
||||
@@ -9,9 +9,10 @@ angular.module('Darkswarm').controller "RegistrationFormCtrl", ($scope, Registra
|
||||
$scope.create = (form) ->
|
||||
if ($scope.valid(form))
|
||||
$scope.disableButton()
|
||||
EnterpriseRegistrationService.create($scope.enableButton).then(() ->
|
||||
EnterpriseRegistrationService.create().then(() ->
|
||||
$scope.enableButton()
|
||||
)
|
||||
end
|
||||
|
||||
$scope.update = (nextStep, form) ->
|
||||
EnterpriseRegistrationService.update(nextStep) if $scope.valid(form)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
%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}" }
|
||||
%li{ ng: { class: "{active: selector.active}" } }
|
||||
%a{ "tooltip" => "{{selector.object.value}}", "tooltip-placement" => "bottom",
|
||||
ng: { transclude: true, class: "{active: selector.active, 'has-tip': selector.object.value}" } }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.sixteen.columns.alpha.omega.alert-row{ "ng-show": '!dismissed' }
|
||||
.sixteen.columns.alpha.omega.alert-row{ ng: { show: '!dismissed' } }
|
||||
.fifteen.columns.pad.alpha
|
||||
%span.message.text-big{ "ng-bind": 'message' }
|
||||
%span.message.text-big{ ng: { bind: 'message'} }
|
||||
|
||||
%input{ type: 'button', "ng-value": "buttonText", "ng-show": 'buttonText && buttonAction', "ng-click": "buttonAction()" }
|
||||
%input{ type: 'button', ng: { value: "buttonText", show: 'buttonText && buttonAction', click: "buttonAction()" } }
|
||||
.one.column.omega.pad.text-center
|
||||
%a.close{ href: "#", "ng-click": "dismiss()" }
|
||||
%a.close{ href: "#", ng: { click: "dismiss()" } }
|
||||
×
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
.ofn-drop-down.ofn-drop-down-v2.right#columns-dropdown{ "ng-controller": 'ColumnsDropdownCtrl' }
|
||||
.ofn-drop-down.ofn-drop-down-v2.right#columns-dropdown{ ng: { controller: 'ColumnsDropdownCtrl' } }
|
||||
.ofn-drop-down-label
|
||||
= " #{t('admin.columns')}".html_safe
|
||||
%span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" }
|
||||
%div.menu{ 'ng-show' => "expanded" }
|
||||
.menu_items
|
||||
.menu_item{ "ng-repeat": "column in columns", "ng-click": "toggle(column);" }
|
||||
%input.redesigned-input{ type: "checkbox", "ng-checked": "column.visible" }
|
||||
.menu_item{ ng: { repeat: "column in columns", click: "toggle(column);" } }
|
||||
%input.redesigned-input{ type: "checkbox", ng: { checked: "column.visible" } }
|
||||
{{ column.name }}
|
||||
%hr
|
||||
%div.menu_item.text-center
|
||||
%input.fullwidth.orange{ type: "button", "ng-value": "saved() ? 'Saved': 'Saving'", "ng-show": "saved() || saving", "ng-disabled": "saved()" }
|
||||
%input.fullwidth.red{ type: "button", value: t('admin.column_save_as_default').html_safe, "ng-show": "!saved() && !saving", "ng-click": "saveColumnPreferences(action)" }
|
||||
%input.fullwidth.orange{ type: "button", ng: { value: "saved() ? 'Saved': 'Saving'", show: "saved() || saving", disabled: "saved()" } }
|
||||
%input.fullwidth.red{ type: "button", :value => t('admin.column_save_as_default').html_safe, ng: { show: "!saved() && !saving", click: "saveColumnPreferences(action)"} }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#confirm-dialog{ "ng-class": "dialog_class" }
|
||||
#confirm-dialog{ ng: { class: "dialog_class" } }
|
||||
.message.clearfix.margin-bottom-30
|
||||
.icon.text-center
|
||||
%i.icon-question-sign
|
||||
.text{ "ng-bind": "::message" }
|
||||
.text{ ng: { bind: "::message" } }
|
||||
.action-buttons.text-center
|
||||
%button.cancel{ "ng-click": "close()", "ng-bind": "::cancelText" }
|
||||
%button.confirm.red{ "ng-click": "confirm()", "ng-bind": "::confirmText" }
|
||||
%button.cancel{ ng: { click: "close()", bind: "::cancelText" } }
|
||||
%button.confirm.red{ ng: { click: "confirm()", bind: "::confirmText" } }
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#edit-address-dialog
|
||||
%h2 {{ addressType === 'bill_address' ? "#{t('admin.customers.index.edit_bill_address')}" : "#{t('admin.customers.index.edit_ship_address')}" }}
|
||||
%form{ name: 'edit_address_form', novalidate: true, "ng-submit": 'updateAddress()' }
|
||||
%form{ name: 'edit_address_form', novalidate: true, ng: { submit: 'updateAddress()'}}
|
||||
.row
|
||||
{{ 'admin.customers.index.required_fileds' | t }}
|
||||
(
|
||||
%span.required *
|
||||
)
|
||||
.error{ "ng-repeat": "error in errors", "ng-bind": "error" }
|
||||
.error{ ng: { repeat: "error in errors", bind: "error" } }
|
||||
|
||||
%table.no-borders
|
||||
%tr
|
||||
@@ -14,55 +14,61 @@
|
||||
{{ 'first_name' | t }}
|
||||
%span.required *
|
||||
%td
|
||||
%input{ type: 'text', name: 'firstname', required: true, "ng-model": 'address.firstname' }
|
||||
%input{ type: 'text', name: 'firstname', required: true, ng: { model: 'address.firstname'} }
|
||||
%tr
|
||||
%td
|
||||
{{ 'last_name' | t }}
|
||||
%span.required *
|
||||
%td
|
||||
%input{ type: 'text', name: 'lastname', required: true, "ng-model": 'address.lastname' }
|
||||
%input{ type: 'text', name: 'lastname', required: true, ng: { model: 'address.lastname'} }
|
||||
%tr
|
||||
%td
|
||||
{{ 'address' | t }}
|
||||
%span.required *
|
||||
%td
|
||||
%input{ type: 'text', name: 'address1', required: true, "ng-model": 'address.address1' }
|
||||
%input{ type: 'text', name: 'address1', required: true, ng: { model: 'address.address1'} }
|
||||
%tr
|
||||
%td
|
||||
{{ 'address2' | t }}
|
||||
%td
|
||||
%input{ type: 'text', name: 'address2', "ng-model": 'address.address2' }
|
||||
%input{ type: 'text', name: 'address2', ng: { model: 'address.address2'} }
|
||||
%tr
|
||||
%td
|
||||
{{ 'city' | t }}
|
||||
%span.required *
|
||||
%td
|
||||
%input{ type: 'text', name: 'city', required: true, "ng-model": 'address.city' }
|
||||
%input{ type: 'text', name: 'city', required: true, ng: { model: 'address.city'} }
|
||||
%tr
|
||||
%td
|
||||
{{ 'postcode' | t }}
|
||||
%span.required *
|
||||
%td
|
||||
%input{ type: 'text', name: 'zipcode', required: true, "ng-model": 'address.zipcode' }
|
||||
%input{ type: 'text', name: 'zipcode', required: true, ng: { model: 'address.zipcode'} }
|
||||
%tr
|
||||
%td
|
||||
{{ 'country' | t }}
|
||||
%span.required *
|
||||
%td
|
||||
%input.ofn-select2.fullwidth#country_id{ type: 'number', name: 'country_id', required: true, placeholder: "{{ 'admin.customers.index.select_country' | t }}", data: 'availableCountries', "ng-model": 'address.country_id' }
|
||||
%input.ofn-select2.fullwidth#country_id{ type: 'number',
|
||||
name: 'country_id', required: true,
|
||||
placeholder: "{{ 'admin.customers.index.select_country' | t }}",
|
||||
data: 'availableCountries', ng: { model: 'address.country_id' } }
|
||||
%tr
|
||||
%td
|
||||
{{ 'state' | t }}
|
||||
%span.required *
|
||||
%td
|
||||
%input.ofn-select2.fullwidth#state_id{ type: 'number', name: 'state_id', required: true, placeholder: "{{ 'admin.customers.index.select_state' | t }}", data: 'states', "ng-model": 'address.state_id' }
|
||||
%input.ofn-select2.fullwidth#state_id{ type: 'number',
|
||||
name: 'state_id', required: true,
|
||||
placeholder: "{{ 'admin.customers.index.select_state' | t }}",
|
||||
data: 'states', ng: { model: 'address.state_id' } }
|
||||
|
||||
%tr
|
||||
%td
|
||||
{{ 'phone' | t }}
|
||||
%span.required *
|
||||
%td
|
||||
%input{ type: 'text', name: 'phone', required: true, "ng-model": 'address.phone' }
|
||||
%input{ type: 'text', name: 'phone', required: true, ng: { model: 'address.phone'} }
|
||||
|
||||
.text-center
|
||||
%input.button.red.icon-plus{ type: 'submit', value: t('admin.customers.index.update_address')}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#info-dialog{ "ng-class": "dialog_class" }
|
||||
#info-dialog{ ng: { class: "dialog_class" } }
|
||||
.message.clearfix.margin-bottom-30
|
||||
.icon.text-center
|
||||
%i{ "ng-class": "icon_class" }
|
||||
%i{ ng: { class: "icon_class" } }
|
||||
.text
|
||||
{{ message }}
|
||||
.action-buttons.text-center
|
||||
%button{ "ng-click": "close()" }
|
||||
%button{ ng: { click: "close()" } }
|
||||
= t(:ok)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
%h4.modal-title
|
||||
= t('js.admin.orders.index.compiling_invoices')
|
||||
|
||||
%p.message{ "ng-show": 'message' }
|
||||
%p.message{ ng: { show: 'message' } }
|
||||
{{message}}
|
||||
%p.error{ "ng-show": 'error' }
|
||||
%p.error{ ng: { show: 'error' } }
|
||||
{{error}}
|
||||
|
||||
%img.spinner{ src: image_path("/spinning-circles.svg"), "ng-show": "loading" }
|
||||
%p{ "ng-show": "loading" }
|
||||
%img.spinner{ src: image_path("/spinning-circles.svg"), ng: { show: "loading" } }
|
||||
%p{ ng: { show: "loading" } }
|
||||
= t('js.admin.orders.index.please_wait')
|
||||
|
||||
%a.button{ target: '_blank', "ng-click": 'showBulkInvoice()', "ng-href": '/admin/orders/invoices/{{invoice_id}}', "ng-show": "!loading && !error" }
|
||||
%a.button{ target: '_blank', ng: { click: 'showBulkInvoice()', href: '/admin/orders/invoices/{{invoice_id}}', show: "!loading && !error" } }
|
||||
= t('js.admin.orders.index.view_file')
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
%a.close-reveal-modal{"ng-click" => "$close()"}
|
||||
%i.fa.fa-times-circle{'aria-hidden' => "true"}
|
||||
|
||||
%form#image_upload{ name: 'form', novalidate: true, enctype: 'multipart/form-data', multipart: true, "ng-controller": "ProductImageCtrl" }
|
||||
%form#image_upload{ name: 'form', novalidate: true, enctype: 'multipart/form-data', multipart: true, ng: { controller: "ProductImageCtrl" } }
|
||||
%div.image-preview
|
||||
%img.spinner{ src: image_path("/spinning-circles.svg"), "ng-hide": "!imageUploader.isUploading" }
|
||||
%img.preview{ "ng-src": "{{imagePreview}}", "ng-class": "{'faded': imageUploader.isUploading}" }
|
||||
%img.spinner{ src: image_path("/spinning-circles.svg"), ng: { hide: "!imageUploader.isUploading" }}
|
||||
%img.preview{ng: {src: "{{imagePreview}}", class: "{'faded': imageUploader.isUploading}"}}
|
||||
|
||||
%label{for: 'image-upload', class: 'button'} {{ 'admin.products.index.upload_an_image' | t }}
|
||||
%input#image-upload{hidden: true, type: 'file', 'nv-file-select' => true, uploader: "imageUploader"}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
.text-normal.margin-bottom-30.text-center
|
||||
{{ 'js.admin.customers.index.add_a_new_customer_for' | t:{ shop_name: CurrentShop.shop.name } }}
|
||||
|
||||
%form{ name: 'new_customer_form', novalidate: true, "ng-submit": "addCustomer()" }
|
||||
%form{ name: 'new_customer_form', novalidate: true, ng: { submit: "addCustomer()" }}
|
||||
|
||||
.text-center.margin-bottom-30
|
||||
%input.fullwidth{ type: 'email', name: 'email', required: true, placeholder: "{{ 'js.admin.customers.index.customer_placeholder' | t }}", "ng-model": "email" }
|
||||
%div{ "ng-show": "submitted && new_customer_form.$pristine" }
|
||||
.error{ "ng-show": "(new_customer_form.email.$error.email || new_customer_form.email.$error.required)" }
|
||||
%input.fullwidth{ type: 'email', name: 'email', required: true, placeholder: "{{ 'js.admin.customers.index.customer_placeholder' | t }}", ng: { model: "email" } }
|
||||
%div{ ng: { show: "submitted && new_customer_form.$pristine" } }
|
||||
.error{ ng: { show: "(new_customer_form.email.$error.email || new_customer_form.email.$error.required)" } }
|
||||
{{ 'js.admin.customers.index.valid_email_error' | t }}
|
||||
.error{ "ng-repeat": "error in errors", "ng-bind": "error" }
|
||||
.error{ ng: { repeat: "error in errors", bind: "error" } }
|
||||
|
||||
.text-center
|
||||
%input.button.red.icon-plus{ type: 'submit', value: "{{ 'js.admin.customers.index.add_customer' | t }}" }
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
.text-center.margin-bottom-30
|
||||
-# %select.fullwidth{ 'select2-min-search' => 5, 'ng-model' => 'newRuleType', 'ng-options' => 'ruleType.id as ruleType.name for ruleType in availableRuleTypes' }
|
||||
%input.ofn-select2.fullwidth{ id: 'rule_type_selector', data: "ruleTypes", "min-search": "5", "ng-model": "ruleType" }
|
||||
%input.ofn-select2.fullwidth{ :id => 'rule_type_selector', ng: { model: "ruleType" }, data: "ruleTypes", 'min-search' => "5" }
|
||||
|
||||
.text-center
|
||||
%input.button.red.icon-plus{ type: 'button', value: "{{ 'js.admin.new_tag_rule_dialog.add_rule' | t }}", "ng-click": 'addRule(tagGroup, ruleType)' }
|
||||
%input.button.red.icon-plus{ type: 'button', value: "{{ 'js.admin.new_tag_rule_dialog.add_rule' | t }}", ng: { click: 'addRule(tagGroup, ruleType)' } }
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
%td#available-order-cycles
|
||||
{{ 'js.admin.order_cycles.schedules.available' | t }}
|
||||
.order-cycles
|
||||
.order-cycle{ "ng-repeat": 'orderCycle in orderCycles | available:selectedOrderCycles as availableOrderCycles', "ng-click": 'selections.available = orderCycle', "ng-dblclick": 'add(orderCycle)', "ng-class": '{selected: selections.available == orderCycle}' }
|
||||
.order-cycle{ ng: { repeat: 'orderCycle in orderCycles | available:selectedOrderCycles as availableOrderCycles', click: 'selections.available = orderCycle', dblclick: 'add(orderCycle)', class: '{selected: selections.available == orderCycle}' } }
|
||||
{{ orderCycle.name }}
|
||||
%td#add-remove-buttons
|
||||
%a.add.button{ href: 'javascript:void(0)', "ng-click": 'add()' }
|
||||
%a.add.button{ href: 'javascript:void(0)', ng: { click: 'add()' } }
|
||||
%i.icon-chevron-right
|
||||
%a.remove.button{ href: 'javascript:void(0)', "ng-click": 'remove()' }
|
||||
%a.remove.button{ href: 'javascript:void(0)', ng: { click: 'remove()' } }
|
||||
%i.icon-chevron-left
|
||||
%td#selected-order-cycles
|
||||
{{ 'js.admin.order_cycles.schedules.selected' | t }}
|
||||
.order-cycles
|
||||
.order-cycle{ "ng-repeat": 'orderCycle in selectedOrderCycles', "ng-click": 'selections.selected = orderCycle', "ng-dblclick": 'remove(orderCycle)', "ng-class": '{selected: selections.selected == orderCycle}' }
|
||||
.order-cycle{ ng: { repeat: 'orderCycle in selectedOrderCycles', click: 'selections.selected = orderCycle', dblclick: 'remove(orderCycle)', class: '{selected: selections.selected == orderCycle}' } }
|
||||
{{ orderCycle.name }}
|
||||
.error{ "ng-repeat": "error in errors", "ng-bind": "error" }
|
||||
.error{ ng: { repeat: "error in errors", bind: "error" } }
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
%td{ colspan: "{{columnCount}}", "ng-if": "template" }
|
||||
.panel{ "ng-include": "template" }
|
||||
%td{ colspan: "{{columnCount}}", ng: { if: "template" } }
|
||||
.panel{ ng: { include: "template" } }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.row.enterprise_package_panel{ "ng-controller": 'indexPackagePanelCtrl' }
|
||||
.row.enterprise_package_panel{ ng: { controller: 'indexPackagePanelCtrl' } }
|
||||
.alpha.eight.columns
|
||||
%div{ "ng-if": "!enterprise.is_primary_producer", "ng-switch": "enterprise.sells" }
|
||||
.info{ "ng-switch-when": "none" }
|
||||
%div{ ng: { if: "!enterprise.is_primary_producer", switch: "enterprise.sells" } }
|
||||
.info{ ng: { switch: { when: "none" } } }
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_package.hub_profile' | t }}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
%p
|
||||
{{ 'js.admin.panels.enterprise_package.hub_profile_text2' | t }}
|
||||
|
||||
.info{ "ng-switch-when": "any" }
|
||||
.info{ ng: { switch: { when: "any" } } }
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_package.hub_shop' | t }}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
%p
|
||||
{{ 'js.admin.panels.enterprise_package.hub_shop_text3' | t }}
|
||||
|
||||
.info{ "ng-switch-default": true }
|
||||
.info{ ng: { switch: { default: true } } }
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_package.choose_package' | t }}
|
||||
%i.icon-arrow-right
|
||||
@@ -40,8 +40,8 @@
|
||||
%p
|
||||
{{ 'js.admin.panels.enterprise_package.choose_package_text2' | t }}
|
||||
|
||||
%div{ "ng-if": "enterprise.is_primary_producer", "ng-switch": "enterprise.sells" }
|
||||
.info{ "ng-switch-when": "none" }
|
||||
%div{ ng: { if: "enterprise.is_primary_producer", switch: "enterprise.sells" } }
|
||||
.info{ ng: { switch: { when: "none" } } }
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_package.profile_only' | t }}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
%p
|
||||
{{ 'js.admin.panels.enterprise_package.profile_only_text3' | t }}
|
||||
|
||||
.info{ "ng-switch-when": "own" }
|
||||
.info{ ng: { switch: { when: "own" } } }
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_package.producer_shop' | t }}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
%p
|
||||
{{ 'js.admin.panels.enterprise_package.producer_shop_text2' | t }}
|
||||
|
||||
.info{ "ng-switch-when": "any" }
|
||||
.info{ ng: { switch: { when: "any" } } }
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_package.producer_hub' | t }}
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
%p
|
||||
{{ 'js.admin.panels.enterprise_package.producer_hub_text3' | t }}
|
||||
|
||||
.info{ "ng-switch-default": true }
|
||||
.info{ ng: { switch: { default: true } } }
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_package.choose_package' | t }}
|
||||
%i.icon-arrow-right
|
||||
@@ -93,9 +93,9 @@
|
||||
%p
|
||||
{{ 'js.admin.panels.enterprise_package.choose_package_text2' | t }}
|
||||
|
||||
.omega.eight.columns{ "ng-switch": "enterprise.is_primary_producer" }
|
||||
%div{ "ng-switch-when": "false" }
|
||||
%a.button.selector.hub-profile{ "ng-click": "enterprise.owned && (enterprise.sells='none')", "ng-class": "{selected: enterprise.sells=='none', disabled: !enterprise.owned}" }
|
||||
.omega.eight.columns{ ng: { switch: "enterprise.is_primary_producer" } }
|
||||
%div{ ng: { switch: { when: "false" } } }
|
||||
%a.button.selector.hub-profile{ ng: { click: "enterprise.owned && (enterprise.sells='none')", class: "{selected: enterprise.sells=='none', disabled: !enterprise.owned}" } }
|
||||
.top
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_package.profile_only' | t }}
|
||||
@@ -103,15 +103,15 @@
|
||||
{{ 'js.admin.panels.enterprise_package.get_listing' | t }}
|
||||
.bottom
|
||||
{{ 'js.admin.panels.enterprise_package.always_free' | t }}
|
||||
%a.button.selector.hub{ "ng-click": "enterprise.owned && (enterprise.sells='any')", "ng-class": "{selected: enterprise.sells=='any', disabled: !enterprise.owned}" }
|
||||
%a.button.selector.hub{ ng: { click: "enterprise.owned && (enterprise.sells='any')", class: "{selected: enterprise.sells=='any', disabled: !enterprise.owned}" } }
|
||||
.top
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_package.hub_shop' | t }}
|
||||
%p
|
||||
{{ 'js.admin.panels.enterprise_package.sell_produce_others' | t }}
|
||||
|
||||
%div{ "ng-switch-when": "true" }
|
||||
%a.button.selector.producer-profile{ "ng-click": "enterprise.owned && (enterprise.sells='none')", "ng-class": "{selected: enterprise.sells=='none', disabled: !enterprise.owned}" }
|
||||
%div{ ng: { switch: { when: "true" } } }
|
||||
%a.button.selector.producer-profile{ ng: { click: "enterprise.owned && (enterprise.sells='none')", class: "{selected: enterprise.sells=='none', disabled: !enterprise.owned}" } }
|
||||
.top
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_package.profile_only' | t }}
|
||||
@@ -119,27 +119,27 @@
|
||||
{{ 'js.admin.panels.enterprise_package.get_listing' | t }}
|
||||
.bottom
|
||||
{{ 'js.admin.panels.enterprise_package.always_free' | t }}
|
||||
%a.button.selector.producer-shop{ "ng-click": "enterprise.owned && (enterprise.sells='own')", "ng-class": "{selected: enterprise.sells=='own', disabled: !enterprise.owned}" }
|
||||
%a.button.selector.producer-shop{ ng: { click: "enterprise.owned && (enterprise.sells='own')", class: "{selected: enterprise.sells=='own', disabled: !enterprise.owned}" } }
|
||||
.top
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_package.producer_shop' | t }}
|
||||
%p
|
||||
{{ 'js.admin.panels.enterprise_package.sell_own_produce' | t }}
|
||||
|
||||
%a.button.selector.producer-hub{ "ng-click": "enterprise.owned && (enterprise.sells='any')", "ng-class": "{selected: enterprise.sells=='any', disabled: !enterprise.owned}" }
|
||||
%a.button.selector.producer-hub{ ng: { click: "enterprise.owned && (enterprise.sells='any')", class: "{selected: enterprise.sells=='any', disabled: !enterprise.owned}" } }
|
||||
.top
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_package.producer_hub' | t }}
|
||||
%p
|
||||
{{ 'js.admin.panels.enterprise_package.sell_both' | t }}
|
||||
|
||||
%a.button.update.fullwidth{ "ng-show": "enterprise.owned", "ng-class": "{disabled: saved() && !saving, saving: saving}", "ng-click": "save()" }
|
||||
%span{ "ng-hide": "saved() || saving" }
|
||||
%a.button.update.fullwidth{ ng: { show: "enterprise.owned", class: "{disabled: saved() && !saving, saving: saving}", click: "save()" } }
|
||||
%span{ ng: {hide: "saved() || saving" } }
|
||||
{{ 'js.admin.panels.save' | t }}
|
||||
%i.icon-save
|
||||
%span{ "ng-show": "saved() && !saving" }
|
||||
%span{ ng: {show: "saved() && !saving" } }
|
||||
{{ 'js.admin.panels.saved' | t }}
|
||||
%i.icon-ok-sign
|
||||
%span{ "ng-show": "saving" }
|
||||
%span{ ng: {show: "saving" } }
|
||||
{{ 'js.admin.panels.saving' | t }}
|
||||
%i.icon-refresh
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.row.enterprise_producer_panel{ "ng-controller": 'indexProducerPanelCtrl' }
|
||||
.row.enterprise_producer_panel{ ng: { controller: 'indexProducerPanelCtrl' } }
|
||||
.alpha.eight.columns
|
||||
.info{ "ng-show": "enterprise.is_primary_producer==true" }
|
||||
.info{ ng: { show: "enterprise.is_primary_producer==true" } }
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_producer.producer' | t }}
|
||||
%p
|
||||
@@ -8,7 +8,7 @@
|
||||
%p
|
||||
{{ 'js.admin.panels.enterprise_producer.producer_text2' | t }}
|
||||
|
||||
.info{ "ng-show": "enterprise.is_primary_producer==false" }
|
||||
.info{ ng: { show: "enterprise.is_primary_producer==false" } }
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_producer.non_producer' | t }}
|
||||
%p
|
||||
@@ -17,7 +17,7 @@
|
||||
{{ 'js.admin.panels.enterprise_producer.non_producer_text2' | t }}
|
||||
|
||||
.omega.eight.columns
|
||||
%a.button.selector.producer{ "ng-click": 'enterprise.owned && changeToProducer()', "ng-class": "{selected: enterprise.is_primary_producer==true, disabled: !enterprise.owned}" }
|
||||
%a.button.selector.producer{ ng: { click: 'enterprise.owned && changeToProducer()', class: "{selected: enterprise.is_primary_producer==true, disabled: !enterprise.owned}" } }
|
||||
.top
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_producer.producer' | t }}
|
||||
@@ -26,7 +26,7 @@
|
||||
.bottom
|
||||
{{ 'js.admin.panels.enterprise_producer.producer_example' | t }}
|
||||
|
||||
%a.button.selector.non-producer{ "ng-click": 'enterprise.owned && changeToNonProducer()', "ng-class": "{selected: enterprise.is_primary_producer==false, disabled: !enterprise.owned}" }
|
||||
%a.button.selector.non-producer{ ng: { click: 'enterprise.owned && changeToNonProducer()', class: "{selected: enterprise.is_primary_producer==false, disabled: !enterprise.owned}" } }
|
||||
.top
|
||||
%h3
|
||||
{{ 'js.admin.panels.enterprise_producer.non_producer' | t }}
|
||||
@@ -35,13 +35,13 @@
|
||||
.bottom
|
||||
{{ 'js.admin.panels.enterprise_producer.non_producer_example' | t }}
|
||||
|
||||
%a.button.update.fullwidth{ "ng-show": "enterprise.owned", "ng-class": "{disabled: saved() && !saving, saving: saving}", "ng-click": "save()" }
|
||||
%span{ "ng-hide": "saved() || saving" }
|
||||
%a.button.update.fullwidth{ ng: { show: "enterprise.owned", class: "{disabled: saved() && !saving, saving: saving}", click: "save()" } }
|
||||
%span{ ng: {hide: "saved() || saving" } }
|
||||
{{ 'js.admin.panels.save' | t }}
|
||||
%i.icon-save
|
||||
%span{ "ng-show": "saved() && !saving" }
|
||||
%span{ ng: {show: "saved() && !saving" } }
|
||||
{{ 'js.admin.panels.saved' | t }}
|
||||
%i.icon-ok-sign
|
||||
%span{ "ng-show": "saving" }
|
||||
%span{ ng: {show: "saving" } }
|
||||
{{ 'js.admin.panels.saving' | t }}
|
||||
%i.icon-refresh
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.row.enterprise_status_panel{ "ng-controller": 'indexStatusPanelCtrl' }
|
||||
.row.enterprise_status_panel{ ng: { controller: 'indexStatusPanelCtrl' } }
|
||||
.alpha.omega.sixteen.columns
|
||||
%h4.status-ok.text-center{ "ng-show": "issues.length == 0 && warnings.length == 0" }
|
||||
%h4.status-ok.text-center{ ng: { show: "issues.length == 0 && warnings.length == 0" } }
|
||||
%i.icon-ok-sign
|
||||
{{ 'js.admin.panels.enterprise_status.status_title' | t:{ name: object.name } }}
|
||||
|
||||
%table{ "ng-show": "issues.length > 0 || warnings.length > 0" }
|
||||
%table{ ng: { show: "issues.length > 0 || warnings.length > 0" } }
|
||||
%thead
|
||||
%th.severity
|
||||
{{ 'js.admin.panels.enterprise_status.severity' | t }}
|
||||
@@ -12,17 +12,17 @@
|
||||
{{ 'js.admin.panels.enterprise_status.description' | t }}
|
||||
%th.resolve
|
||||
{{ 'js.admin.panels.enterprise_status.resolve' | t }}
|
||||
%tr{ "ng-repeat": "issue in issues" }
|
||||
%tr{ ng: { repeat: "issue in issues"} }
|
||||
%td.severity
|
||||
%i.icon-warning-sign.issue
|
||||
%td.description
|
||||
%span{ "ng-bind": "::issue.description" }
|
||||
%span{ ng: { bind: "::issue.description" } }
|
||||
%td.resolve
|
||||
%div{ "ng-bind-html": "issue.link" }
|
||||
%tr{ "ng-repeat": "warning in warnings" }
|
||||
%div{ ng: { bind: { html: "issue.link" } } }
|
||||
%tr{ ng: { repeat: "warning in warnings"} }
|
||||
%td.severity
|
||||
%i.icon-warning-sign.warning
|
||||
%td.description
|
||||
%span{ "ng-bind": "::warning.description" }
|
||||
%span{ ng: { bind: "::warning.description" } }
|
||||
%td.resolve
|
||||
%div{ "ng-bind-html": "warning.link" }
|
||||
%div{ ng: { bind: { html: "warning.link" } } }
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
.name {{ product.name }}
|
||||
.supplier {{ product.supplier_name }}
|
||||
|
||||
.exchange-product-variant{'ng-repeat' => 'variant in product.variants | visibleVariants:exchange:order_cycle.visible_variants_for_outgoing_exchanges | filter:variantSuppliedToOrderCycle as filteredVariants'}
|
||||
.exchange-product-variant{'ng-repeat' => 'variant in product.variants | visibleVariants:exchange:order_cycle.visible_variants_for_outgoing_exchanges | filter:variantSuppliedToOrderCycle'}
|
||||
%label
|
||||
%input{ type: 'checkbox', name: 'order_cycle_outgoing_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}',
|
||||
value: 1,
|
||||
@@ -27,8 +27,5 @@
|
||||
'id' => 'order_cycle_outgoing_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}',
|
||||
'ng-disabled' => '!order_cycle.editable_variants_for_outgoing_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_outgoing_exchanges[exchange.enterprise_id].indexOf(variant.id) < 0' }
|
||||
{{ variant.label }}
|
||||
|
||||
%em{ 'ng-if' => 'filteredVariants.length === 0' }
|
||||
{{ 'js.admin.panels.exchange_products.no_variants' | t }}
|
||||
|
||||
%div{ 'ng-include' => "'admin/panels/exchange_products_panel_footer.html'" }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
.exchange-load-all-variants
|
||||
%div
|
||||
{{ 'js.admin.panels.exchange_products.variants_loaded' | t:{ num_of_variants_loaded: enterprises[exchange.enterprise_id].loaded_variants, total_number_of_variants: exchangeTotalVariants(exchange) } }}
|
||||
%em{ 'ng-if': 'enterprises[exchange.enterprise_id].loaded_variants > exchangeTotalVariants(exchange)' }
|
||||
{{ 'js.admin.panels.exchange_products.some_variants_hidden' | t }}
|
||||
%a{ 'ng-click' => 'loadAllExchangeProducts(exchange)', 'ng-show' => 'enterprises[exchange.enterprise_id].last_page_loaded < enterprises[exchange.enterprise_id].num_of_pages' }
|
||||
{{ 'js.admin.panels.exchange_products.load_all_variants' | t }}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#save-bar.animate-show{ "ng-show": 'dirty || persist || StatusMessage.active()' }
|
||||
#save-bar.animate-show{ ng: { show: 'dirty || persist || StatusMessage.active()' } }
|
||||
.container
|
||||
.seven.columns.alpha
|
||||
%h5#status-message{ "ng-show": "StatusMessage.invalidMessage == ''", "ng-style": 'StatusMessage.statusMessage.style' }
|
||||
%h5#status-message{ ng: { show: "StatusMessage.invalidMessage == ''", style: 'StatusMessage.statusMessage.style' } }
|
||||
{{ StatusMessage.statusMessage.text || " " }}
|
||||
%h5#status-message{ style: 'color: #C85136', "ng-show": "StatusMessage.invalidMessage !== ''" }
|
||||
%h5#status-message{ ng: { show: "StatusMessage.invalidMessage !== ''" }, style: 'color: #C85136' }
|
||||
{{ StatusMessage.invalidMessage || " " }}
|
||||
.nine.columns.omega.text-right{ "ng-transclude": true }
|
||||
.nine.columns.omega.text-right{ ng: { transclude: true } }
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
#schedule-dialog
|
||||
.text-normal.margin-bottom-30.text-center
|
||||
%span{ "ng-hide": 'schedule.id' }
|
||||
%span{ ng: { hide: 'schedule.id' } }
|
||||
{{ 'js.admin.order_cycles.schedules.adding_a_new_schedule' | t }}
|
||||
%span{ "ng-show": 'schedule.id' }
|
||||
%span{ ng: { show: 'schedule.id' } }
|
||||
{{ 'js.admin.order_cycles.schedules.updating_a_schedule' | t }}
|
||||
|
||||
%form{ name: 'schedule_form', novalidate: true, "ng-submit": "submit()" }
|
||||
%form{ name: 'schedule_form', novalidate: true, ng: { submit: "submit()" }}
|
||||
|
||||
.text-center.margin-bottom-20
|
||||
%input.fullwidth{ type: 'text', name: 'name', required: true, placeholder: "{{ 'js.admin.order_cycles.schedules.schedule_name_placeholder' | t }}", "ng-model": "schedule.name" }
|
||||
%div{ "ng-show": "submitted && schedule_form.$pristine" }
|
||||
.error{ "ng-show": "(schedule_form.name.$error.required)" }
|
||||
%input.fullwidth{ type: 'text', name: 'name', required: true, placeholder: "{{ 'js.admin.order_cycles.schedules.schedule_name_placeholder' | t }}", ng: { model: "schedule.name" } }
|
||||
%div{ ng: { show: "submitted && schedule_form.$pristine" } }
|
||||
.error{ ng: { show: "(schedule_form.name.$error.required)" } }
|
||||
{{ 'js.admin.order_cycles.schedules.name_required_error' | t }}
|
||||
|
||||
.order-cycles-selector.text-center.margin-bottom-30
|
||||
|
||||
.text-center
|
||||
|
||||
%input.button{ type: 'submit', value: "{{ 'js.admin.order_cycles.schedules.create_schedule' | t }}", "ng-hide": 'schedule.id' }
|
||||
%input.button{ type: 'submit', value: "{{ 'js.admin.order_cycles.schedules.update_schedule' | t }}", "ng-show": 'schedule.id' }
|
||||
%span{ "ng-show": 'schedule.id' } or
|
||||
%input.button.red{ type: 'button', value: "{{ 'js.admin.order_cycles.schedules.delete_schedule' | t }}", "ng-show": 'schedule.id', "ng-click": 'delete()' }
|
||||
%input.button{ type: 'button', value: "{{ 'actions.cancel' | t }}", "ng-click": 'close()' }
|
||||
%input.button{ type: 'submit', value: "{{ 'js.admin.order_cycles.schedules.create_schedule' | t }}", ng: { hide: 'schedule.id' } }
|
||||
%input.button{ type: 'submit', value: "{{ 'js.admin.order_cycles.schedules.update_schedule' | t }}", ng: { show: 'schedule.id' } }
|
||||
%span{ ng: { show: 'schedule.id' } } or
|
||||
%input.button.red{ type: 'button', value: "{{ 'js.admin.order_cycles.schedules.delete_schedule' | t }}", ng: { show: 'schedule.id', click: 'delete()'} }
|
||||
%input.button{ type: 'button', value: "{{ 'actions.cancel' | t }}", ng: { click: 'close()' } }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.tag-template
|
||||
%div
|
||||
%span.tag-with-rules{ "ofn-with-tip": "{{ 'admin.tag_has_rules' | t:{num: data.rules} }}", "ng-if": "data.rules" }
|
||||
%span.tag-with-rules{ ng: { if: "data.rules" }, "ofn-with-tip" => "{{ 'admin.tag_has_rules' | t:{num: data.rules} }}" }
|
||||
{{$getDisplayText()}}
|
||||
%span{ "ng-if": "!data.rules" }
|
||||
%span{ ng: { if: "!data.rules" } }
|
||||
{{$getDisplayText()}}
|
||||
%a.remove-button{ "ng-click": "$removeTag()" }
|
||||
%a.remove-button{ ng: {click: "$removeTag()"} }
|
||||
✖
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.autocomplete-template
|
||||
%span.tag-with-rules{ "ng-if": "data.rules" }
|
||||
%span.tag-with-rules{ ng: { if: "data.rules" } }
|
||||
{{$getDisplayText()}}
|
||||
%span.tag-with-rules{ "ng-if": "data.rules == 1" }
|
||||
%span.tag-with-rules{ ng: { if: "data.rules == 1" } }
|
||||
—
|
||||
{{ 'admin.has_one_rule' | t }}
|
||||
%span.tag-with-rules{ "ng-if": "data.rules > 1" }
|
||||
%span.tag-with-rules{ ng: { if: "data.rules > 1" } }
|
||||
—
|
||||
{{ 'admin.has_n_rules' | t:{ num: data.rules } }}
|
||||
%span{ "ng-if": "!data.rules" }
|
||||
%span{ ng: { if: "!data.rules" } }
|
||||
{{$getDisplayText()}}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
%div
|
||||
%input{ type: "number", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_preferred_flat_percent", min: -100, max: 100, "invert-number": true, "ng-model": "rule.calculator.preferred_flat_percent" }
|
||||
%input{ type: "number",
|
||||
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_preferred_flat_percent",
|
||||
min: -100,
|
||||
max: 100,
|
||||
ng: { model: "rule.calculator.preferred_flat_percent" }, 'invert-number' => true }
|
||||
%span.text-normal %
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
%div
|
||||
%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_order_cycles_visibility", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_order_cycles_visibility]", data: 'visibilityOptions', "min-search": 5, "ng-model": "rule.preferred_matched_order_cycles_visibility", "ng-if": "!rule.is_default" }
|
||||
%input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_order_cycles_visibility", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_order_cycles_visibility]", "ng-value": "'hidden'", "ng-if": "rule.is_default" }
|
||||
%span.text-normal{ "ng-if": "rule.is_default" }
|
||||
%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_order_cycles_visibility",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_order_cycles_visibility]",
|
||||
ng: { model: "rule.preferred_matched_order_cycles_visibility", if: "!rule.is_default" },
|
||||
data: 'visibilityOptions', "min-search" => 5 }
|
||||
%input{ type: "hidden",
|
||||
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_order_cycles_visibility",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_order_cycles_visibility]",
|
||||
ng: { value: "'hidden'", if: "rule.is_default" } }
|
||||
%span.text-normal{ ng: { if: "rule.is_default" } }
|
||||
=t(:not_visible)
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
%div
|
||||
%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_payment_methods_visibility", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_payment_methods_visibility]", data: 'visibilityOptions', "min-search": 5, "ng-model": "rule.preferred_matched_payment_methods_visibility", "ng-if": "!rule.is_default" }
|
||||
%input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_payment_methods_visibility", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_payment_methods_visibility]", "ng-value": "'hidden'", "ng-if": "rule.is_default" }
|
||||
%span.text-normal{ "ng-if": "rule.is_default" }
|
||||
%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_payment_methods_visibility",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_payment_methods_visibility]",
|
||||
ng: { model: "rule.preferred_matched_payment_methods_visibility", if: "!rule.is_default" },
|
||||
data: 'visibilityOptions', "min-search" => 5 }
|
||||
%input{ type: "hidden",
|
||||
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_payment_methods_visibility",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_payment_methods_visibility]",
|
||||
ng: { value: "'hidden'", if: "rule.is_default" } }
|
||||
%span.text-normal{ ng: { if: "rule.is_default" } }
|
||||
= t(:not_visible)
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
%div
|
||||
%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_variants_visibility", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_variants_visibility]", data: 'visibilityOptions', "min-search": 5, "ng-model": "rule.preferred_matched_variants_visibility", "ng-if": "!rule.is_default" }
|
||||
%input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_variants_visibility", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_variants_visibility]", "ng-value": "'hidden'", "ng-if": "rule.is_default" }
|
||||
%span.text-normal{ "ng-if": "rule.is_default" }
|
||||
%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_variants_visibility",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_variants_visibility]",
|
||||
ng: { model: "rule.preferred_matched_variants_visibility", if: "!rule.is_default" },
|
||||
data: 'visibilityOptions', "min-search" => 5 }
|
||||
%input{ type: "hidden",
|
||||
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_variants_visibility",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_variants_visibility]",
|
||||
ng: { value: "'hidden'", if: "rule.is_default" } }
|
||||
%span.text-normal{ ng: { if: "rule.is_default" } }
|
||||
= t(:not_visible)
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
%div
|
||||
%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_shipping_methods_visibility", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_shipping_methods_visibility]", data: 'visibilityOptions', "min-search": 5, "ng-model": "rule.preferred_matched_shipping_methods_visibility", "ng-if": "!rule.is_default" }
|
||||
%input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_shipping_methods_visibility", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_shipping_methods_visibility]", "ng-value": "'hidden'", "ng-if": "rule.is_default" }
|
||||
%span.text-normal{ "ng-if": "rule.is_default" }
|
||||
%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_shipping_methods_visibility",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_shipping_methods_visibility]",
|
||||
ng: { model: "rule.preferred_matched_shipping_methods_visibility", if: "!rule.is_default" },
|
||||
data: 'visibilityOptions', "min-search" => 5 }
|
||||
%input{ type: "hidden",
|
||||
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_shipping_methods_visibility",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_shipping_methods_visibility]",
|
||||
ng: { value: "'hidden'", if: "rule.is_default" } }
|
||||
%span.text-normal{ ng: { if: "rule.is_default" } }
|
||||
= t(:not_visible)
|
||||
|
||||
|
||||
@@ -6,27 +6,45 @@
|
||||
%col.actions{ width: "10%" }
|
||||
%tr
|
||||
%td
|
||||
%input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", "ng-value": "rule.id" }
|
||||
%input{ type: "hidden",
|
||||
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]",
|
||||
ng: { value: "rule.id" } }
|
||||
|
||||
%input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", "ng-value": "rule.type" }
|
||||
%input{ type: "hidden",
|
||||
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]",
|
||||
ng: { value: "rule.type" } }
|
||||
|
||||
%input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_priority", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][priority]", "ng-value": "tagGroup.startIndex + $index" }
|
||||
%input{ type: "hidden",
|
||||
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_priority",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][priority]",
|
||||
ng: { value: "tagGroup.startIndex + $index" } }
|
||||
|
||||
%input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_is_default", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][is_default]", "ng-value": "rule.is_default" }
|
||||
%input{ type: "hidden",
|
||||
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_is_default",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][is_default]",
|
||||
ng: { value: "rule.is_default" } }
|
||||
|
||||
%input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", "ng-value": "rule.preferred_customer_tags" }
|
||||
%input{ type: "hidden",
|
||||
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]",
|
||||
ng: { value: "rule.preferred_customer_tags" } }
|
||||
|
||||
%input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_{{opt[rule.type].taggable}}_tags", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_{{opt[rule.type].taggable}}_tags]", "ng-value": "opt[rule.type].tagListFor(rule)" }
|
||||
%input{ type: "hidden",
|
||||
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_{{opt[rule.type].taggable}}_tags",
|
||||
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_{{opt[rule.type].taggable}}_tags]",
|
||||
ng: { value: "opt[rule.type].tagListFor(rule)" } }
|
||||
|
||||
%span.text-normal {{ opt[rule.type].textTop }}
|
||||
%td
|
||||
%tags-with-translation{ object: "rule", max: 1, "tags-attr" => "{{opt[rule.type].tagsAttr}}", "tag-list-attr" => "{{opt[rule.type].tagListAttr}}" }
|
||||
%td.actions{ rowspan: 2 }
|
||||
%a{ class: "delete-tag-rule icon-trash no-text", "ng-click": "deleteTagRule(tagGroup || defaultTagGroup, rule)" }
|
||||
%a{ ng: { click: "deleteTagRule(tagGroup || defaultTagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" }
|
||||
%tr
|
||||
%td
|
||||
%span.text-normal {{ opt[rule.type].textBottom }}
|
||||
%td
|
||||
%div{ "ng-include": "opt[rule.type].inputTemplate" }
|
||||
%div{ ng: { include: "opt[rule.type].inputTemplate"} }
|
||||
|
||||
%hr
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
%tags-input{ template: 'admin/tag.html', placeholder: t('admin.order_cycles.form.add_a_tag'), "ng-model": 'object[tagsAttr]', "ng-class": "{'limit-reached': limitReached}", "on-tag-added": 'tagAdded($tag)', "on-tag-removed": 'tagRemoved()' }
|
||||
%auto-complete{ source: "findTags({query: $query})", template: "admin/tag_autocomplete.html", "min-length": "0", "load-on-focus": "true", "load-on-empty": "true", "max-results-to-show": "32", "ng-if": "findTags" }
|
||||
%tags-input{ template: 'admin/tag.html',
|
||||
"placeholder" => t('admin.order_cycles.form.add_a_tag'),
|
||||
ng: { model: 'object[tagsAttr]', class: "{'limit-reached': limitReached}"},
|
||||
on: { tag: { added: 'tagAdded($tag)', removed:'tagRemoved()' } } }
|
||||
%auto-complete{ ng: { if: "findTags" }, source: "findTags({query: $query})",
|
||||
template: "admin/tag_autocomplete.html",
|
||||
"min-length" => "0",
|
||||
"load-on-focus" => "true",
|
||||
"load-on-empty" => "true",
|
||||
"max-results-to-show" => "32"}
|
||||
|
||||
@@ -22,22 +22,24 @@
|
||||
.variant-bulk-buy-quantity-label
|
||||
{{ "js.shopfront.bulk_buy_modal.min_quantity" | t }}
|
||||
%div
|
||||
%button.bulk-buy-add.variant-quantity{ type: "button", "ng-click": "add(-1)", "ng-disabled": "!canAdd(-1)" }>
|
||||
%button.bulk-buy-add.variant-quantity{type: "button", ng: {click: "add(-1)", disabled: "!canAdd(-1)"}}>
|
||||
-# U+FF0D Fullwidth Hyphen-Minus
|
||||
-
|
||||
%input.bulk-buy.variant-quantity{ type: "number", min: "0", max: "{{ available() }}", "ng-model": "variant.line_item.quantity", "ng-max": "Infinity" }>
|
||||
%button.bulk-buy-add.variant-quantity{ type: "button", "ng-click": "add(1)", "ng-disabled": "!canAdd(1)" }
|
||||
%input.bulk-buy.variant-quantity{type: "number", min: "0", max: "{{ available() }}",
|
||||
ng: {model: "variant.line_item.quantity", max: "Infinity"}}>
|
||||
%button.bulk-buy-add.variant-quantity{type: "button", ng: {click: "add(1)", disabled: "!canAdd(1)"}}
|
||||
-# U+FF0B Fullwidth Plus Sign
|
||||
+
|
||||
.columns.small-12.medium-6
|
||||
.variant-bulk-buy-quantity-label
|
||||
{{ "js.shopfront.bulk_buy_modal.max_quantity" | t }}
|
||||
%div
|
||||
%button.bulk-buy-add.variant-quantity{ type: "button", "ng-click": "addMax(-1)", "ng-disabled": "!canAddMax(-1)" }>
|
||||
%button.bulk-buy-add.variant-quantity{type: "button", ng: {click: "addMax(-1)", disabled: "!canAddMax(-1)"}}>
|
||||
-# U+FF0D Fullwidth Hyphen-Minus
|
||||
-
|
||||
%input.bulk-buy.variant-quantity{ type: "number", min: "0", max: "{{ available() }}", "ng-model": "variant.line_item.max_quantity", "ng-max": "Infinity" }>
|
||||
%button.bulk-buy-add.variant-quantity{ type: "button", "ng-click": "addMax(1)", "ng-disabled": "!canAddMax(1)" }
|
||||
%input.bulk-buy.variant-quantity{type: "number", min: "0", max: "{{ available() }}",
|
||||
ng: {model: "variant.line_item.max_quantity", max: "Infinity"}}>
|
||||
%button.bulk-buy-add.variant-quantity{type: "button", ng: {click: "addMax(1)", disabled: "!canAddMax(1)"}}
|
||||
-# U+FF0B Fullwidth Plus Sign
|
||||
+
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
%ul
|
||||
%active-selector{ "ng-repeat": "selector in allSelectors", "ng-show": "ifDefined(selector.fits, true)" }
|
||||
%active-selector{ ng: { repeat: "selector in allSelectors", show: "ifDefined(selector.fits, true)" } }
|
||||
%span{"ng-bind" => "::selector.object.name"}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
.small-12.columns.text-center
|
||||
{{ helpText }}
|
||||
.row.text-center
|
||||
%button.primary.small{ "ng-click": '$close()' }
|
||||
%button.primary.small{ ng: { click: '$close()' } }
|
||||
= t(:ok)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.row.pad-top{ "ng-if": 'enterprise.is_distributor' }
|
||||
.row.pad-top{ng: { if: 'enterprise.is_distributor' } }
|
||||
.cta-container.small-12.columns
|
||||
.row
|
||||
.small-4.columns
|
||||
@@ -14,7 +14,7 @@
|
||||
{{'hubs_delivery' | t}}
|
||||
.row
|
||||
.columns.small-12
|
||||
%a.cta-hub{"ng-href" => "{{::enterprise.path}}#/shop_panel", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}",
|
||||
%a.cta-hub{"ng-href" => "{{::enterprise.path}}#/shop", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}",
|
||||
"ng-class" => "{primary: enterprise.active, secondary: !enterprise.active}",
|
||||
"ng-click" => "$close()",
|
||||
"ofn-change-hub" => "enterprise"}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
.row
|
||||
.columns.small-12
|
||||
%a.cta-hub{"ng-repeat" => "hub in enterprise.hubs | filter:{id: '!'+enterprise.id} | orderBy:'-active'",
|
||||
"ng-href" => "{{::hub.path}}#/shop_panel", "ofn-empties-cart" => "hub",
|
||||
"ng-href" => "{{::hub.path}}#/shop", "ofn-empties-cart" => "hub",
|
||||
"ng-class" => "::{primary: hub.active, secondary: !hub.active}",
|
||||
"ng-click" => "$close()",
|
||||
"ofn-change-hub" => "hub"}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.joyride-tip-guide.price_breakdown{ "ng-class": "{ in: tt_isOpen, fade: tt_animation }", "ng-show": "tt_isOpen" }
|
||||
.joyride-tip-guide.price_breakdown{ng: {class: "{ in: tt_isOpen, fade: tt_animation }", show: "tt_isOpen"}}
|
||||
%span.joyride-nub.top
|
||||
.background{ "ng-click": "tt_isOpen = false" }
|
||||
.background{ng: {click: "tt_isOpen = false"}}
|
||||
.joyride-content-wrapper
|
||||
%h6 {{ "js.shopfront.price_breakdown" | t }}
|
||||
%ul
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
%filter-selector{ 'selector-set' => "productPropertySelectors", objects: "[product] | propertiesWithValuesOf" }
|
||||
|
||||
.product-description{"ng-if" => "product.description_html"}
|
||||
%p.text-small{"ng-bind-html" => "::product.description_html", "data-controller" => "add-blank-to-link"}
|
||||
%p.text-small{"ng-bind-html" => "::product.description_html"}
|
||||
|
||||
.columns.small-12.medium-6.large-6.product-img
|
||||
%img{"ng-src" => "{{::product.largeImage}}", "ng-if" => "::product.largeImage"}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.joyride-tip-guide.question-mark-tooltip{ class: "{{ context }}", "ng-class": "{ in: tt_isOpen, fade: tt_animation }", "ng-show": "tt_isOpen" }
|
||||
.background{ "ng-click": "tt_isOpen = false" }
|
||||
.joyride-tip-guide.question-mark-tooltip{class: "{{ context }}", ng: {class: "{ in: tt_isOpen, fade: tt_animation }", show: "tt_isOpen"}}
|
||||
.background{ng: {click: "tt_isOpen = false"}}
|
||||
.joyride-content-wrapper
|
||||
{{ key | t }}
|
||||
%span.joyride-nub.bottom
|
||||
|
||||
@@ -1 +1 @@
|
||||
.question-mark-icon{"ng-class" => "{open: tt_isOpen}", type: 'button'}
|
||||
%button.question-mark-icon{"ng-class" => "{open: tt_isOpen}", type: 'button'}
|
||||
|
||||
@@ -1,29 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ConfirmModalComponent < ModalComponent
|
||||
# @param actions_alignment_class [String] possible classes: 'justify-space-around', 'justify-end'
|
||||
def initialize(
|
||||
id:,
|
||||
reflex: nil,
|
||||
controller: nil,
|
||||
message: nil,
|
||||
confirm_actions: nil,
|
||||
confirm_reflexes: nil,
|
||||
confirm_button_class: :primary,
|
||||
confirm_button_text: I18n.t('js.admin.modals.confirm'),
|
||||
cancel_button_text: I18n.t('js.admin.modals.cancel'),
|
||||
actions_alignment_class: 'justify-space-around'
|
||||
)
|
||||
super(id:, close_button: true)
|
||||
def initialize(id:, confirm_actions: nil, reflex: nil, controller: nil, message: nil,
|
||||
confirm_reflexes: nil)
|
||||
super(id: id, close_button: true)
|
||||
@confirm_actions = confirm_actions
|
||||
@reflex = reflex
|
||||
@confirm_reflexes = confirm_reflexes
|
||||
@controller = controller
|
||||
@message = message
|
||||
@confirm_button_class = confirm_button_class
|
||||
@confirm_button_text = confirm_button_text
|
||||
@cancel_button_text = cancel_button_text
|
||||
@actions_alignment_class = actions_alignment_class
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
%div{ id: @id, "data-controller": "modal #{@controller}", "data-action": "keyup@document->modal#closeIfEscapeKey", "data-#{@controller}-reflex-value": @reflex }
|
||||
.reveal-modal-bg.fade{ "data-modal-target": "background", "data-action": "click->modal#close" }
|
||||
.reveal-modal.fade.tiny.modal-component{ "data-modal-target": "modal" }
|
||||
.reveal-modal.fade.tiny.help-modal{ "data-modal-target": "modal" }
|
||||
= content
|
||||
|
||||
= render @message if @message
|
||||
|
||||
%div{ class: "modal-actions #{@actions_alignment_class}" }
|
||||
%input{ class: "button icon-plus #{close_button_class}", type: 'button', value: @cancel_button_text, "data-action": "click->modal#close" }
|
||||
%input{ id: 'modal-confirm-button', class: "button icon-plus #{@confirm_button_class}", type: 'button', value: @confirm_button_text, "data-action": @confirm_actions, "data-reflex": @confirm_reflexes }
|
||||
.modal-actions
|
||||
%input{ class: "button icon-plus #{close_button_class}", type: 'button', value: t('js.admin.modals.cancel'), "data-action": "click->modal#close" }
|
||||
%input{ class: "button icon-plus primary", type: 'button', value: t('js.admin.modals.confirm'), "data-action": @confirm_actions, "data-reflex": @confirm_reflexes }
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
class HelpModalComponent < ModalComponent
|
||||
def initialize(id:, close_button: true)
|
||||
super(id:, close_button:)
|
||||
super(id: id, close_button: close_button)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
%div{ id: @id, "data-controller": "help-modal", "data-action": "keyup@document->help-modal#closeIfEscapeKey" }
|
||||
.reveal-modal-bg.fade{ "data-help-modal-target": "background", "data-action": "click->help-modal#close" }
|
||||
.reveal-modal.fade.small.modal-component{ "data-help-modal-target": "modal" }
|
||||
.reveal-modal.fade.small.help-modal{ "data-help-modal-target": "modal" }
|
||||
= content
|
||||
|
||||
- if close_button?
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.help-modal {
|
||||
visibility: visible;
|
||||
position: fixed;
|
||||
top: 3em;
|
||||
}
|
||||
|
||||
/* prevent arrow on selected admin menu item appearing above modal */
|
||||
body.modal-open #admin-menu li.selected a::after {
|
||||
z-index: 0;
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
# 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)
|
||||
@id = id
|
||||
@close_button = close_button
|
||||
@instant = instant
|
||||
@modal_class = modal_class
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
%div{ id: @id, "data-controller": "modal", "data-action": "keyup@document->modal#closeIfEscapeKey", "data-modal-instant-value": @instant }
|
||||
.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
|
||||
|
||||
- if close_button?
|
||||
.text-center
|
||||
%input{ class: "button icon-plus #{close_button_class}", type: 'button', value: t('js.admin.modals.close'), "data-action": "click->modal#close" }
|
||||
@@ -1,56 +0,0 @@
|
||||
// class name 'modal' is already taken by 'custom-alert' and 'custom-confirm'.
|
||||
.modal-component {
|
||||
visibility: visible;
|
||||
position: fixed;
|
||||
top: 3em;
|
||||
min-height: auto; // reset from reveal-modal
|
||||
|
||||
&.in {
|
||||
padding: 1.2rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
img {
|
||||
// Ensure image fits in container
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* prevent arrow on selected admin menu item appearing above modal */
|
||||
body.modal-open #admin-menu li.selected a::after {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
text-align: center; // Ensure text inside fullwidth buttons are centred on small screens
|
||||
|
||||
&.justify-space-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
&.justify-end {
|
||||
justify-content: flex-end;
|
||||
input[type="button"] {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
input[type="button"] {
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
app/components/pagination_component.rb
Normal file
15
app/components/pagination_component.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PaginationComponent < ViewComponentReflex::Component
|
||||
def initialize(pagy:, data:)
|
||||
super
|
||||
@count = pagy.count
|
||||
@page = pagy.page
|
||||
@per_page = pagy.items
|
||||
@pages = pagy.pages
|
||||
@next = pagy.next
|
||||
@prev = pagy.prev
|
||||
@data = data
|
||||
@series = pagy.series
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
= component_controller do
|
||||
%nav{"aria-label": "pagination"}
|
||||
.pagination
|
||||
.pagination-prev{data: @prev.nil? ? nil : @data, "data-page": @prev, class: "#{'inactive' if @prev.nil?}"}
|
||||
= I18n.t "components.pagination.previous"
|
||||
.pagination-pages
|
||||
- @series.each do |page|
|
||||
- if page == :gap
|
||||
.pagination-gap
|
||||
…
|
||||
- else
|
||||
.pagination-page{data: @data, "data-page": page, class: "#{'active' if page.to_i == @page}"}
|
||||
= page
|
||||
.pagination-next{data: @next.nil? ? nil : @data, "data-page": @next, class: "#{'inactive' if @next.nil?}"}
|
||||
= I18n.t "components.pagination.next"
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
nav {
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
font-size: 14px;
|
||||
|
||||
.pagination-prev, .pagination-next {
|
||||
cursor: pointer;
|
||||
|
||||
&:after, &:before {
|
||||
font-size: 2em;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
cursor: default;
|
||||
color: $disabled-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-prev {
|
||||
margin-left: 10px;
|
||||
|
||||
&:before {
|
||||
content: "‹";
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-next {
|
||||
margin-right: 10px;
|
||||
|
||||
&:after {
|
||||
content: "›";
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.pagination-pages {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
.pagination-gap, .pagination-page {
|
||||
padding: 0 0.5rem;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.pagination-gap {
|
||||
color: $disabled-dark;
|
||||
}
|
||||
|
||||
.pagination-page {
|
||||
color: $color-4;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
border-top: 3px solid $spree-blue;
|
||||
color: $spree-blue;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
app/components/product_component.rb
Normal file
57
app/components/product_component.rb
Normal file
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProductComponent < ViewComponentReflex::Component
|
||||
DATETIME_FORMAT = '%F %T'
|
||||
|
||||
def initialize(product:, columns:)
|
||||
super
|
||||
@product = product
|
||||
@image = @product.image if product.image.present?
|
||||
@columns = columns.map do |c|
|
||||
{
|
||||
id: c[:value],
|
||||
value: column_value(c[:value])
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# This must be define when using ProductComponent.with_collection()
|
||||
def collection_key
|
||||
@product.id
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def column_value(column)
|
||||
case column
|
||||
when 'name'
|
||||
@product.name
|
||||
when 'price'
|
||||
@product.price
|
||||
when 'unit'
|
||||
"#{@product.variants.first.unit_value} #{@product.variant_unit}"
|
||||
when 'producer'
|
||||
@product.supplier.name
|
||||
when 'category'
|
||||
@product.taxons.map(&:name).join(', ')
|
||||
when 'sku'
|
||||
@product.sku
|
||||
when 'on_hand'
|
||||
@product.on_hand || 0
|
||||
when 'on_demand'
|
||||
@product.on_demand
|
||||
when 'tax_category'
|
||||
@product.tax_category.name
|
||||
when 'inherits_properties'
|
||||
@product.inherits_properties
|
||||
when 'import_date'
|
||||
format_date(@product.import_date)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
private
|
||||
|
||||
def format_date(date)
|
||||
date&.strftime(DATETIME_FORMAT) || ''
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
%tr
|
||||
- @columns.each do |column|
|
||||
%td.products_column{class: column[:id]}
|
||||
- if column[:id] == "name" && @image&.attachment.present?
|
||||
= image_tag @image.url(:mini)
|
||||
= column[:value]
|
||||
179
app/components/products_table_component.rb
Normal file
179
app/components/products_table_component.rb
Normal file
@@ -0,0 +1,179 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProductsTableComponent < ViewComponentReflex::Component
|
||||
include Pagy::Backend
|
||||
|
||||
SORTABLE_COLUMNS = ['name', 'import_date'].freeze
|
||||
SELECTABLE_COLUMNS = [
|
||||
{ label: I18n.t("admin.products_page.columns_selector.price"), value: "price" },
|
||||
{ label: I18n.t("admin.products_page.columns_selector.unit"), value: "unit" },
|
||||
{ label: I18n.t("admin.products_page.columns_selector.producer"), value: "producer" },
|
||||
{ label: I18n.t("admin.products_page.columns_selector.category"), value: "category" },
|
||||
{ label: I18n.t("admin.products_page.columns_selector.sku"), value: "sku" },
|
||||
{ label: I18n.t("admin.products_page.columns_selector.on_hand"), value: "on_hand" },
|
||||
{ label: I18n.t("admin.products_page.columns_selector.on_demand"), value: "on_demand" },
|
||||
{ label: I18n.t("admin.products_page.columns_selector.tax_category"), value: "tax_category" },
|
||||
{
|
||||
label: I18n.t("admin.products_page.columns_selector.inherits_properties"),
|
||||
value: "inherits_properties"
|
||||
},
|
||||
{ label: I18n.t("admin.products_page.columns_selector.import_date"), value: "import_date" }
|
||||
].sort do |a, b|
|
||||
a[:label] <=> b[:label]
|
||||
end.freeze
|
||||
|
||||
PER_PAGE_VALUE = [10, 25, 50, 100].freeze
|
||||
PER_PAGE = PER_PAGE_VALUE.map { |value| { label: value, value: value } }
|
||||
NAME_COLUMN = {
|
||||
label: I18n.t("admin.products_page.columns.name"), value: "name", sortable: true
|
||||
}.freeze
|
||||
|
||||
def initialize(user:)
|
||||
super
|
||||
@user = user
|
||||
@selectable_columns = SELECTABLE_COLUMNS
|
||||
@columns_selected = ['unit', 'price', 'on_hand', 'category', 'import_date']
|
||||
@per_page = PER_PAGE
|
||||
@per_page_selected = [10]
|
||||
@categories = [{ label: "All", value: "all" }] +
|
||||
Spree::Taxon.order(:name)
|
||||
.map { |taxon| { label: taxon.name, value: taxon.id.to_s } }
|
||||
@categories_selected = ["all"]
|
||||
@producers = [{ label: "All", value: "all" }] +
|
||||
OpenFoodNetwork::Permissions.new(@user)
|
||||
.managed_product_enterprises.is_primary_producer.by_name
|
||||
.map { |producer| { label: producer.name, value: producer.id.to_s } }
|
||||
@producers_selected = ["all"]
|
||||
@page = 1
|
||||
@sort = { column: "name", direction: "asc" }
|
||||
@search_term = ""
|
||||
end
|
||||
|
||||
# any change on a "reflex_data_attributes" (defined in the template) will trigger a re render
|
||||
def before_render
|
||||
fetch_products
|
||||
refresh_columns
|
||||
end
|
||||
|
||||
# Element refers to the component the data is set on
|
||||
def search_term
|
||||
# Element is SearchInputComponent
|
||||
@search_term = element.dataset['value']
|
||||
end
|
||||
|
||||
def toggle_column
|
||||
# Element is SelectorComponent
|
||||
column = element.dataset['value']
|
||||
@columns_selected = if @columns_selected.include?(column)
|
||||
@columns_selected - [column]
|
||||
else
|
||||
@columns_selected + [column]
|
||||
end
|
||||
end
|
||||
|
||||
def click_sort
|
||||
# Element is TableHeaderComponent
|
||||
@sort = {
|
||||
column: element.dataset['sort-value'],
|
||||
direction: element.dataset['sort-direction'] == "asc" ? "desc" : "asc"
|
||||
}
|
||||
end
|
||||
|
||||
def toggle_per_page
|
||||
# Element is SelectorComponent
|
||||
selected = element.dataset['value'].to_i
|
||||
@per_page_selected = [selected] if PER_PAGE_VALUE.include?(selected)
|
||||
end
|
||||
|
||||
def toggle_category
|
||||
# Element is SelectorWithFilterComponent
|
||||
category_clicked = element.dataset['value']
|
||||
@categories_selected = toggle_selector_with_filter(category_clicked, @categories_selected)
|
||||
end
|
||||
|
||||
def toggle_producer
|
||||
# Element is SelectorWithFilterComponent
|
||||
producer_clicked = element.dataset['value']
|
||||
@producers_selected = toggle_selector_with_filter(producer_clicked, @producers_selected)
|
||||
end
|
||||
|
||||
def change_page
|
||||
# Element is PaginationComponent
|
||||
page = element.dataset['page'].to_i
|
||||
@page = page if page > 0
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def refresh_columns
|
||||
@columns = @columns_selected.map do |column|
|
||||
{
|
||||
label: I18n.t("admin.products_page.columns.#{column}"),
|
||||
value: column,
|
||||
sortable: SORTABLE_COLUMNS.include?(column)
|
||||
}
|
||||
end.sort! { |a, b| a[:label] <=> b[:label] }
|
||||
@columns.unshift(NAME_COLUMN)
|
||||
end
|
||||
|
||||
def toggle_selector_with_filter(clicked, selected)
|
||||
selected = if selected.include?(clicked)
|
||||
selected - [clicked]
|
||||
else
|
||||
selected + [clicked]
|
||||
end
|
||||
|
||||
if clicked == "all" || selected.empty?
|
||||
selected = ["all"]
|
||||
elsif selected.include?("all") && selected.length > 1
|
||||
selected -= ["all"]
|
||||
end
|
||||
selected
|
||||
end
|
||||
|
||||
def fetch_products
|
||||
product_query = OpenFoodNetwork::Permissions.new(@user).editable_products.merge(product_scope)
|
||||
@products = product_query.ransack(ransack_query).result
|
||||
@pagy, @products = pagy(@products, items: @per_page_selected.first, page: @page)
|
||||
end
|
||||
|
||||
def product_scope
|
||||
scope = if @user.has_spree_role?("admin") || @user.enterprises.present?
|
||||
Spree::Product
|
||||
else
|
||||
Spree::Product.active
|
||||
end
|
||||
|
||||
scope.includes(product_query_includes)
|
||||
end
|
||||
|
||||
def ransack_query
|
||||
query = { s: "#{@sort[:column]} #{@sort[:direction]}" }
|
||||
|
||||
query = if @producers_selected.include?("all")
|
||||
query.merge({ supplier_id_eq: "" })
|
||||
else
|
||||
query.merge({ supplier_id_in: @producers_selected })
|
||||
end
|
||||
|
||||
query = query.merge({ name_cont: @search_term }) if @search_term.present?
|
||||
|
||||
if @categories_selected.include?("all")
|
||||
query.merge({ primary_taxon_id_eq: "" })
|
||||
else
|
||||
query.merge({ primary_taxon_id_in: @categories_selected })
|
||||
end
|
||||
end
|
||||
|
||||
def product_query_includes
|
||||
[
|
||||
:image,
|
||||
variants: [
|
||||
:default_price,
|
||||
:stock_locations,
|
||||
:stock_items,
|
||||
:variant_overrides
|
||||
]
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
= component_controller(class: "products-table") do
|
||||
.products-table-form
|
||||
.products-table-form_filter_results
|
||||
= render(SearchInputComponent.new(value: @search_term, data: reflex_data_attributes(:search_term)))
|
||||
.products-table-form_categories_selector
|
||||
= render(SelectorWithFilterComponent.new(title: t("admin.products_page.filters.categories.title"), selected: @categories_selected, items: @categories, data: reflex_data_attributes(:toggle_category), selected_items_i18n_key: "admin.products_page.filters.categories.selected_categories"))
|
||||
.products-table-form_producers_selector
|
||||
= render(SelectorWithFilterComponent.new(title: t("admin.products_page.filters.producers.title"), selected: @producers_selected, items: @producers, data: reflex_data_attributes(:toggle_producer), selected_items_i18n_key: "admin.products_page.filters.producers.selected_producers"))
|
||||
.products-table-form_per-page_selector
|
||||
= render(SelectorComponent.new(title: t('admin.products_page.filters.per_page', count: @per_page_selected[0]), selected: @per_page_selected, items: @per_page, data: reflex_data_attributes(:toggle_per_page)))
|
||||
.products-table-form_columns_selector
|
||||
= render(SelectorComponent.new(title: t("admin.products_page.filters.columns"), selected: @columns_selected, items: @selectable_columns, data: reflex_data_attributes(:toggle_column)))
|
||||
|
||||
.products-table_table
|
||||
%table
|
||||
= render(TableHeaderComponent.new(columns: @columns, sort: @sort, data: reflex_data_attributes(:click_sort)))
|
||||
%tbody
|
||||
= render(ProductComponent.with_collection(@products, columns: @columns))
|
||||
|
||||
.products-table-form_pagination
|
||||
= render(PaginationComponent.new(pagy: @pagy, data: reflex_data_attributes(:change_page)))
|
||||
@@ -0,0 +1,47 @@
|
||||
.products-table {
|
||||
.products-table-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat( auto-fit, minmax(250px, 1fr) );
|
||||
grid-gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.products-table_table {
|
||||
box-shadow: 0 10px 10px -1px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.products-table-form_pagination {
|
||||
position: relative;
|
||||
top: -15px;
|
||||
|
||||
nav, .pagination {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.products-table.loading {
|
||||
.products-table-form_pagination, .products-table_table {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.products-table_table {
|
||||
&:before {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 50px 50px;
|
||||
background-image: url("../images/spinning-circles.svg");
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/components/search_input_component.rb
Normal file
9
app/components/search_input_component.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SearchInputComponent < ViewComponentReflex::Component
|
||||
def initialize(value: nil, data: {})
|
||||
super
|
||||
@value = value
|
||||
@data = data
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
= component_controller do
|
||||
%div.search-input
|
||||
%input{type: 'text', placeholder: t("components.search_input.placeholder"), id: 'search_query', data: {action: 'debounced:input->search-input#search'}, value: @value}
|
||||
.search-button{data: @data}
|
||||
%i.fa.fa-search
|
||||
@@ -0,0 +1,23 @@
|
||||
.search-input {
|
||||
border: 1px solid $disabled-light;
|
||||
height: 3em;
|
||||
display: flex;
|
||||
line-height: 3em;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
border: none;
|
||||
height: 3em;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
padding-right: 10px;
|
||||
padding-left: 5px;
|
||||
cursor: pointer;
|
||||
color: $color-4;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SearchableDropdownComponent < ViewComponent::Base
|
||||
REMOVED_SEARCH_PLUGIN = { 'tom-select-options-value': '{ "plugins": [] }' }.freeze
|
||||
MINIMUM_OPTIONS_FOR_SEARCH_FIELD = 11 # at least 11 options are required for the search field
|
||||
|
||||
def initialize(
|
||||
form:,
|
||||
name:,
|
||||
options:,
|
||||
selected_option:,
|
||||
placeholder_value:,
|
||||
include_blank: false,
|
||||
aria_label: ''
|
||||
)
|
||||
@f = form
|
||||
@name = name
|
||||
@options = options
|
||||
@selected_option = selected_option
|
||||
@placeholder_value = placeholder_value
|
||||
@include_blank = include_blank
|
||||
@aria_label = aria_label
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :f, :name, :options, :selected_option, :placeholder_value, :include_blank, :aria_label
|
||||
|
||||
def classes
|
||||
"fullwidth #{remove_search_plugin? ? 'no-input' : ''}"
|
||||
end
|
||||
|
||||
def data
|
||||
{
|
||||
controller: "tom-select",
|
||||
'tom-select-placeholder-value': placeholder_value
|
||||
}.merge(remove_search_plugin? ? REMOVED_SEARCH_PLUGIN : {})
|
||||
end
|
||||
|
||||
def remove_search_plugin?
|
||||
@remove_search_plugin ||= options.count < MINIMUM_OPTIONS_FOR_SEARCH_FIELD
|
||||
end
|
||||
end
|
||||
@@ -1 +0,0 @@
|
||||
= f.select name, options_for_select(options, selected_option), { include_blank: }, class: classes, data:, 'aria-label': aria_label
|
||||
17
app/components/selector_component.rb
Normal file
17
app/components/selector_component.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SelectorComponent < ViewComponentReflex::Component
|
||||
def initialize(title:, selected:, items:, data: {})
|
||||
super
|
||||
@title = title
|
||||
@items = items.map do |item|
|
||||
{
|
||||
label: item[:label],
|
||||
value: item[:value],
|
||||
selected: selected.include?(item[:value])
|
||||
}
|
||||
end
|
||||
@selected = selected
|
||||
@data = data
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
= component_controller do
|
||||
.selector.selector-close
|
||||
.selector-main{ data: { action: "click->selector#toggle" } }
|
||||
.selector-main-title
|
||||
= @title
|
||||
.selector-arrow
|
||||
.selector-wrapper
|
||||
.selector-items
|
||||
- @items.each do |item|
|
||||
.selector-item{ class: ("selected" if item[:selected]), data: @data, "data-value": item[:value] }
|
||||
= item[:label]
|
||||
86
app/components/selector_component/selector_component.scss
Normal file
86
app/components/selector_component/selector_component.scss
Normal file
@@ -0,0 +1,86 @@
|
||||
.selector {
|
||||
position: relative;
|
||||
|
||||
.selector-main {
|
||||
border: 1px solid $disabled-light;
|
||||
height: 3em;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.selector-main-title {
|
||||
line-height: 3em;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.selector-arrow {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
height: 3em;
|
||||
width: 1.5em;
|
||||
top: -1px;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 5px;
|
||||
margin-top: -5px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid $disabled-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selector-wrapper {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: 1;
|
||||
background-color: white;
|
||||
margin-top: -1px;
|
||||
border: 1px solid $disabled-light;
|
||||
|
||||
.selector-items {
|
||||
overflow-y: auto;
|
||||
min-height: 6em;
|
||||
|
||||
.selector-item {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-bottom: 1px solid $disabled-light;
|
||||
position: relative;
|
||||
height: 3em;
|
||||
line-height: 3em;
|
||||
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
&:after {
|
||||
content: "✓";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.selector-close {
|
||||
.selector-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
app/components/selector_with_filter_component.rb
Normal file
11
app/components/selector_with_filter_component.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SelectorWithFilterComponent < SelectorComponent
|
||||
def initialize(title:, selected:, items:, data: {},
|
||||
selected_items_i18n_key: 'components.selector_with_filter.selected_items')
|
||||
super(title: title, selected: selected, items: items, data: data)
|
||||
@selected_items = items.select { |item| @selected.include?(item[:value]) }
|
||||
@selected_items_i18n_key = selected_items_i18n_key
|
||||
@items = items
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
= component_controller do
|
||||
.super-selector.selector.selector-close
|
||||
.selector-main{ data: { action: "click->selector-with-filter#toggle" } }
|
||||
.super-selector-label
|
||||
= @title
|
||||
.super-selector-selected-items
|
||||
- case @selected_items.length
|
||||
- when 1, 2
|
||||
- @selected_items.each do |item|
|
||||
.super-selector-selected-item
|
||||
= item[:label]
|
||||
- else
|
||||
.super-selector-selected-item
|
||||
= t(@selected_items_i18n_key, count: @selected_items.length)
|
||||
.selector-arrow
|
||||
.selector-wrapper
|
||||
.super-selector-search
|
||||
%input{type: "text", placeholder: t("components.selector_with_filter.search_placeholder"), data: { action: "debounced:input->selector-with-filter#filter" } }
|
||||
.selector-items
|
||||
- @items.each do |item|
|
||||
.selector-item{ class: ("selected" if item[:selected]), data: @data.merge({ "selector-with-filter-target": "items" }), "data-value": item[:value] }
|
||||
= item[:label]
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
.super-selector {
|
||||
position: relative;
|
||||
|
||||
.selector-main {
|
||||
.super-selector-label {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
margin-left: 10px;
|
||||
position: absolute;
|
||||
top: -1em;
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.super-selector-selected-items {
|
||||
margin-left: 5px;
|
||||
margin-right: 2em;
|
||||
margin-top: 7px;
|
||||
display: flex;
|
||||
|
||||
.super-selector-selected-item {
|
||||
border: 1px solid $pale-blue;
|
||||
background-color: $spree-light-blue;
|
||||
border-radius: 20px;
|
||||
height: 2em;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
padding-top: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.selector-wrapper {
|
||||
.super-selector-search {
|
||||
border-bottom: 1px solid $disabled-light;
|
||||
padding: 10px 5px;
|
||||
|
||||
input {
|
||||
border: 1px solid $disabled-light;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
= render ConfirmModalComponent.new(id: dom_id(@order, :ship), confirm_reflexes: "click->Admin::OrdersReflex#ship", controller: "orders", reflex: "Admin::Orders#ship") do
|
||||
%div{class: "margin-bottom-30"}
|
||||
%p= t('spree.admin.orders.shipment.mark_as_shipped_message_html')
|
||||
%div{class: "margin-bottom-30"}
|
||||
= hidden_field_tag :id, @order.id
|
||||
= label_tag do
|
||||
= check_box_tag :send_shipment_email, "1", true
|
||||
= t('spree.admin.orders.shipment.mark_as_shipped_label_message')
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user