workarea-save_for_later 1.0.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.eslintrc.json +35 -0
- data/.github/workflows/ci.yml +60 -0
- data/.gitignore +1 -2
- data/.rubocop.yml +3 -0
- data/.stylelintrc.json +8 -0
- data/CHANGELOG.md +23 -4
- data/Gemfile +2 -9
- data/README.md +47 -1
- data/app/assets/javscripts/workarea/storefront/save_for_later/modules/saved_list_analytics.js +31 -0
- data/app/controllers/workarea/admin/reports_controller.decorator +10 -0
- data/app/controllers/workarea/storefront/analytics_controller.decorator +17 -0
- data/app/controllers/workarea/storefront/current_saved_list.rb +1 -1
- data/app/helpers/workarea/storefront/saved_lists_helper.rb +11 -3
- data/app/models/workarea/insights/most_saved_products.rb +32 -0
- data/app/models/workarea/metrics/product_by_day.decorator +10 -0
- data/app/models/workarea/saved_list.rb +12 -2
- data/app/queries/workarea/order_metrics.decorator +26 -0
- data/app/queries/workarea/reports/save_for_later_products.rb +51 -0
- data/app/view_models/workarea/admin/dashboards/reports_view_model.decorator +11 -0
- data/app/view_models/workarea/admin/reports/save_for_later_products_view_model.rb +20 -0
- data/app/view_models/workarea/storefront/saved_list_view_model.rb +0 -1
- data/app/views/workarea/admin/dashboards/_save_for_later_products_card.html.haml +17 -0
- data/app/views/workarea/admin/insights/_most_saved_products.html.haml +22 -0
- data/app/views/workarea/admin/reports/save_for_later_products.html.haml +38 -0
- data/app/views/workarea/storefront/carts/_save_for_later_button.html.haml +1 -1
- data/app/views/workarea/storefront/carts/_saved_for_later.html.haml +4 -1
- data/app/workers/workarea/save_order_metrics.decorator +21 -0
- data/config/initializers/appends.rb +10 -0
- data/config/initializers/config.rb +5 -1
- data/config/locales/en.yml +21 -0
- data/config/routes.rb +11 -0
- data/lib/workarea/save_for_later/version.rb +1 -1
- data/package.json +9 -0
- data/test/integration/workarea/storefront/current_saved_list_integration_test.rb +1 -1
- data/test/models/workarea/insights/most_saved_products_test.rb +63 -0
- data/test/queries/workarea/reports/save_for_later_products_test.rb +122 -0
- data/test/queries/workarea/save_for_later_order_metrics_test.rb +48 -0
- data/test/system/workarea/admin/save_for_later_products_report_system_test.rb +44 -0
- data/test/system/workarea/storefront/save_for_later_system_test.rb +1 -1
- data/test/system/workarea/storefront/saved_list_analytics_system_test.rb +76 -0
- data/workarea-save_for_later.gemspec +1 -3
- data/yarn.lock +3265 -0
- metadata +33 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3569051f8611040d8cc0eebad53a149062b5613136c7ccd35dec1d912e7fc055
|
4
|
+
data.tar.gz: 16cca8449ab063bb6b54c83f03d6da4d738768afaeaf6f0e0a35ff01838efaf2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 419afeabf7b6908b740aa826349789aabe9f35ecdd53ac8f3f2242564b4799a21c396538818dfaf2d561e03930b39a887f0492bf18a429832276f7ef83b0c1cd
|
7
|
+
data.tar.gz: 9ed1b62784231a7983bd681bf4c08b44e3189830caf155921e10baf226de8ecdf130b6962e85ffb728a1f3cd5663da3f547d45b40f936a89915f41255aa3dca5
|
data/.eslintrc.json
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
{
|
2
|
+
"extends": "eslint:recommended",
|
3
|
+
"rules": {
|
4
|
+
"semi": ["error", "always"],
|
5
|
+
"eqeqeq": ["error", "always"]
|
6
|
+
},
|
7
|
+
"globals": {
|
8
|
+
"window": true,
|
9
|
+
"document": true,
|
10
|
+
"WORKAREA": true,
|
11
|
+
"$": true,
|
12
|
+
"jQuery": true,
|
13
|
+
"_": true,
|
14
|
+
"feature": true,
|
15
|
+
"JST": true,
|
16
|
+
"Turbolinks": true,
|
17
|
+
"I18n": true,
|
18
|
+
"Chart": true,
|
19
|
+
"Dropzone": true,
|
20
|
+
"strftime": true,
|
21
|
+
"Waypoint": true,
|
22
|
+
"wysihtml": true,
|
23
|
+
"LocalTime": true,
|
24
|
+
"describe": true,
|
25
|
+
"after": true,
|
26
|
+
"afterEach": true,
|
27
|
+
"before": true,
|
28
|
+
"beforeEach": true,
|
29
|
+
"it": true,
|
30
|
+
"expect": true,
|
31
|
+
"sinon": true,
|
32
|
+
"fixture": true,
|
33
|
+
"chai": true
|
34
|
+
}
|
35
|
+
}
|
@@ -0,0 +1,60 @@
|
|
1
|
+
name: CI
|
2
|
+
on: [push]
|
3
|
+
|
4
|
+
jobs:
|
5
|
+
static_analysis:
|
6
|
+
runs-on: ubuntu-latest
|
7
|
+
steps:
|
8
|
+
- uses: actions/checkout@v1
|
9
|
+
- uses: workarea-commerce/ci/bundler-audit@v1
|
10
|
+
- uses: workarea-commerce/ci/rubocop@v1
|
11
|
+
- uses: workarea-commerce/ci/eslint@v1
|
12
|
+
with:
|
13
|
+
args: '**/*.js'
|
14
|
+
- uses: workarea-commerce/ci/stylelint@v1
|
15
|
+
with:
|
16
|
+
args: '**/*.scss'
|
17
|
+
|
18
|
+
admin_tests:
|
19
|
+
runs-on: ubuntu-latest
|
20
|
+
steps:
|
21
|
+
- uses: actions/checkout@v1
|
22
|
+
- uses: actions/setup-ruby@v1
|
23
|
+
with:
|
24
|
+
ruby-version: 2.6.x
|
25
|
+
- uses: workarea-commerce/ci/test@v1
|
26
|
+
with:
|
27
|
+
command: bin/rails app:workarea:test:admin
|
28
|
+
|
29
|
+
core_tests:
|
30
|
+
runs-on: ubuntu-latest
|
31
|
+
steps:
|
32
|
+
- uses: actions/checkout@v1
|
33
|
+
- uses: actions/setup-ruby@v1
|
34
|
+
with:
|
35
|
+
ruby-version: 2.6.x
|
36
|
+
- uses: workarea-commerce/ci/test@v1
|
37
|
+
with:
|
38
|
+
command: bin/rails app:workarea:test:core
|
39
|
+
|
40
|
+
storefront_tests:
|
41
|
+
runs-on: ubuntu-latest
|
42
|
+
steps:
|
43
|
+
- uses: actions/checkout@v1
|
44
|
+
- uses: actions/setup-ruby@v1
|
45
|
+
with:
|
46
|
+
ruby-version: 2.6.x
|
47
|
+
- uses: workarea-commerce/ci/test@v1
|
48
|
+
with:
|
49
|
+
command: bin/rails app:workarea:test:storefront
|
50
|
+
|
51
|
+
plugins_tests:
|
52
|
+
runs-on: ubuntu-latest
|
53
|
+
steps:
|
54
|
+
- uses: actions/checkout@v1
|
55
|
+
- uses: actions/setup-ruby@v1
|
56
|
+
with:
|
57
|
+
ruby-version: 2.6.x
|
58
|
+
- uses: workarea-commerce/ci/test@v1
|
59
|
+
with:
|
60
|
+
command: bin/rails app:workarea:test:plugins
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
data/.stylelintrc.json
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,9 +1,28 @@
|
|
1
|
-
Workarea Save For Later 1.0
|
1
|
+
Workarea Save For Later 1.1.0 (2019-03-13)
|
2
2
|
--------------------------------------------------------------------------------
|
3
3
|
|
4
|
-
*
|
5
|
-
|
6
|
-
|
4
|
+
* Clean Saved Lists After Predetermined Time Range
|
5
|
+
|
6
|
+
Ensure that `Workarea::SavedList` records are removed after a
|
7
|
+
predetermined length of time (default: 3 months) by way of the [TTL
|
8
|
+
Indexes](https://docs.mongodb.com/manual/core/index-ttl/) feature of
|
9
|
+
MongoDB. Only Saved Lists that do not belong to a User will be cleaned
|
10
|
+
in this manner.
|
11
|
+
|
12
|
+
SAVE4LATER-4
|
13
|
+
Tom Scott
|
14
|
+
|
15
|
+
* Update for workarea v3.4 compatibility
|
16
|
+
|
17
|
+
SAVE4LATER-3
|
18
|
+
Matt Duffy
|
19
|
+
|
20
|
+
* Add details to README
|
21
|
+
|
22
|
+
SAVE4LATER-2
|
23
|
+
Matt Duffy
|
24
|
+
|
25
|
+
|
7
26
|
|
8
27
|
Workarea Save For Later 1.0.0 (2018-08-08)
|
9
28
|
--------------------------------------------------------------------------------
|
data/Gemfile
CHANGED
@@ -1,17 +1,10 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
|
-
git_source(:github) { |repo| "
|
2
|
+
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
3
3
|
|
4
|
-
# Declare your gem's dependencies in save_for_later.gemspec.
|
5
|
-
# Bundler will treat runtime dependencies like base dependencies, and
|
6
|
-
# development dependencies will be added by default to the :development group.
|
7
4
|
gemspec
|
8
5
|
|
9
|
-
# Declare any dependencies that are still in development here instead of in
|
10
|
-
# your gemspec. These might include edge Rails or gems from your path or
|
11
|
-
# Git. Remember to move these dependencies to your gemspec before releasing
|
12
|
-
# your gem to rubygems.org.
|
13
|
-
|
14
6
|
# To use a debugger
|
15
7
|
# gem 'byebug', group: [:development, :test]
|
16
8
|
|
9
|
+
# gem 'workarea', '>= 3.4.0'
|
17
10
|
gem 'workarea', github: 'workarea-commerce/workarea'
|
data/README.md
CHANGED
@@ -1,7 +1,53 @@
|
|
1
1
|
Workarea Save For Later
|
2
2
|
================================================================================
|
3
3
|
|
4
|
-
|
4
|
+
Increase average order values and help retail customers save items for later without having to create an account.
|
5
|
+
|
6
|
+
This plugin allows a consumer with or without an account to move items from their cart to a "Saved For Later" list that displays on the cart page. For users without an account, this list is persisted through a cookie that will retain their list on their browser. For users with accounts, this list is tied to their account directly and will appear in any browser they log into allow quick access to items they may be considering for purchase.
|
7
|
+
|
8
|
+
Features
|
9
|
+
--------------------------------------------------------------------------------
|
10
|
+
|
11
|
+
* Each product in cart will display a “Save for later” link
|
12
|
+
* Moving a product from the regular cart to the saved cart will remove it from the cart
|
13
|
+
* Saved cart product list item will maintain SKU, quantity, and customizations
|
14
|
+
* Each product listed in the saved cart will display a “Move to cart” link
|
15
|
+
* Moving a product from the saved cart to the regular cart will remove it from the saved cart
|
16
|
+
* If a consumer has not saved any products to a saved cart the saved cart will not display
|
17
|
+
* Cookie-based list for guests, tied to account's for signed in users.
|
18
|
+
* Lists created while logged out will merge into a consumer's existing list if they log in after adding items to their "Saved for Later" list
|
19
|
+
* Utilizes existing cart HTML/CSS to add no additional components that need to be styled.
|
20
|
+
* Lists created by guest users are pruned from the database after a
|
21
|
+
predetermined length of time (**default:** 3 months)
|
22
|
+
|
23
|
+
Configuration
|
24
|
+
--------------------------------------------------------------------------------
|
25
|
+
|
26
|
+
`Workarea.config.saved_lists_expiration` configures the expiration time for `Workarea::SavedList` records. Using [MongoDB's TTL Indexes](https://docs.mongodb.com/manual/core/index-ttl/), we can leverage the database to prune these documents in a background thread when they are no longer relevant. This is used to define the `expireAfterSeconds` parameter on the TTL index, which means if this setting is changed, you'll have to rebuild indexes for the `SavedList` model like so:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
Workarea::SavedList.remove_indexes
|
30
|
+
Workarea::SavedList.create_indexes
|
31
|
+
```
|
32
|
+
|
33
|
+
To make sure your changes were taken up, view the specification for the TTL index like so:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
Workarea::SavedList.index_specifications
|
37
|
+
|
38
|
+
=> [#<Mongoid::Indexable::Specification:0x00007fd52a3b9078
|
39
|
+
@fields=[:user_id],
|
40
|
+
@key={:user_id=>1},
|
41
|
+
@klass=Workarea::SavedList,
|
42
|
+
@options={}>,
|
43
|
+
#<Mongoid::Indexable::Specification:0x00007fd52a3acc10
|
44
|
+
@fields=[:updated_at],
|
45
|
+
@key={:updated_at=>1},
|
46
|
+
@klass=Workarea::SavedList,
|
47
|
+
@options=
|
48
|
+
{:expire_after=>1 month, # <<< this is what should change
|
49
|
+
:partial_filter_expression=>{:user_id=>{"$eq"=>nil}}}>]
|
50
|
+
```
|
5
51
|
|
6
52
|
Getting Started
|
7
53
|
--------------------------------------------------------------------------------
|
@@ -0,0 +1,31 @@
|
|
1
|
+
/**
|
2
|
+
* @method
|
3
|
+
* @name registerAdapter
|
4
|
+
* @memberof WORKAREA.analytics
|
5
|
+
*/
|
6
|
+
WORKAREA.analytics.registerAdapter('saved_lists', function () {
|
7
|
+
'use strict';
|
8
|
+
|
9
|
+
return {
|
10
|
+
'addToSavedList': function (payload) {
|
11
|
+
if (payload.id) {
|
12
|
+
$.ajax({
|
13
|
+
type: 'POST',
|
14
|
+
url: WORKAREA.routes.storefront.analyticsSavedListAddPath(
|
15
|
+
{ product_id: payload.id }
|
16
|
+
),
|
17
|
+
});
|
18
|
+
}
|
19
|
+
},
|
20
|
+
'removeFromSavedList': function (payload) {
|
21
|
+
if (payload.id) {
|
22
|
+
$.ajax({
|
23
|
+
type: 'POST',
|
24
|
+
url: WORKAREA.routes.storefront.analyticsSavedListRemovePath(
|
25
|
+
{ product_id: payload.id }
|
26
|
+
),
|
27
|
+
});
|
28
|
+
}
|
29
|
+
}
|
30
|
+
};
|
31
|
+
});
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Admin::ReportsController, with: :save_for_later do
|
3
|
+
def save_for_later_products
|
4
|
+
@report = Admin::Reports::SaveForLaterProductsViewModel.wrap(
|
5
|
+
Workarea::Reports::SaveForLaterProducts.new(params),
|
6
|
+
view_model_options
|
7
|
+
)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Storefront::AnalyticsController, with: :save_for_later do
|
3
|
+
def saved_list_add
|
4
|
+
Metrics::ProductByDay.inc(
|
5
|
+
key: { product_id: params[:product_id] },
|
6
|
+
saved_list_adds: 1
|
7
|
+
)
|
8
|
+
end
|
9
|
+
|
10
|
+
def saved_list_remove
|
11
|
+
Metrics::ProductByDay.inc(
|
12
|
+
key: { product_id: params[:product_id] },
|
13
|
+
saved_list_deletes: 1
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -14,7 +14,7 @@ module Workarea
|
|
14
14
|
def current_saved_list
|
15
15
|
@current_saved_list ||= SavedListViewModel.wrap(
|
16
16
|
if current_user.present?
|
17
|
-
SavedList.find_or_create_by(
|
17
|
+
SavedList.find_or_create_by(user_id: current_user.id)
|
18
18
|
elsif cookies[:saved_list_id].present?
|
19
19
|
SavedList.find_or_create_by(id: cookies[:saved_list_id])
|
20
20
|
else
|
@@ -1,11 +1,19 @@
|
|
1
1
|
module Workarea
|
2
2
|
module Storefront
|
3
3
|
module SavedListsHelper
|
4
|
-
def
|
4
|
+
def add_to_saved_list_analytics_data(product)
|
5
5
|
{
|
6
|
-
event: '
|
6
|
+
event: 'addToSavedList',
|
7
7
|
domEvent: 'submit',
|
8
|
-
payload:
|
8
|
+
payload: product_analytics_data(product)
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove_from_saved_list_analytics_data(product)
|
13
|
+
{
|
14
|
+
event: 'removeFromSavedList',
|
15
|
+
domEvent: 'submit',
|
16
|
+
payload: product_analytics_data(product)
|
9
17
|
}
|
10
18
|
end
|
11
19
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Workarea
|
2
|
+
module Insights
|
3
|
+
class MostSavedProducts < Base
|
4
|
+
class << self
|
5
|
+
def dashboards
|
6
|
+
%w(catalog)
|
7
|
+
end
|
8
|
+
|
9
|
+
def generate_monthly!
|
10
|
+
results = generate_results
|
11
|
+
create!(results: results) if results.present?
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate_results
|
15
|
+
report
|
16
|
+
.results
|
17
|
+
.take(Workarea.config.insights_products_list_max_results)
|
18
|
+
.map { |result| result.merge(product_id: result['_id']) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def report
|
22
|
+
Reports::SaveForLaterProducts.new(
|
23
|
+
starts_at: beginning_of_last_month,
|
24
|
+
ends_at: end_of_last_month,
|
25
|
+
sort_by: 'adds',
|
26
|
+
sort_direction: 'desc'
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Metrics::ProductByDay, with: :save_for_later do
|
3
|
+
decorated do
|
4
|
+
field :saved_list_adds, type: Integer, default: 0
|
5
|
+
field :saved_list_deletes, type: Integer, default: 0
|
6
|
+
field :saved_list_units_sold, type: Integer, default: 0
|
7
|
+
field :saved_list_revenue, type: Float, default: 0.0
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -2,11 +2,21 @@ module Workarea
|
|
2
2
|
class SavedList
|
3
3
|
include ApplicationDocument
|
4
4
|
|
5
|
-
|
6
|
-
field :_id, type: String, default: -> { BSON::ObjectId.new.to_s }
|
5
|
+
field :user_id, type: String
|
7
6
|
|
8
7
|
embeds_many :items, class_name: 'Workarea::SavedList::Item'
|
9
8
|
|
9
|
+
index({ user_id: 1 })
|
10
|
+
index(
|
11
|
+
{ updated_at: 1 },
|
12
|
+
{
|
13
|
+
expire_after_seconds: Workarea.config.saved_lists_expiration,
|
14
|
+
partial_filter_expression: {
|
15
|
+
user_id: { '$eq' => nil }
|
16
|
+
}
|
17
|
+
}
|
18
|
+
)
|
19
|
+
|
10
20
|
def add_item(item_attributes = {})
|
11
21
|
item_attributes = item_attributes.with_indifferent_access
|
12
22
|
existing = items.where(item_attributes.slice(:sku, :customizations)).first
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate OrderMetrics, with: :save_for_later do
|
3
|
+
def products_from_saved_lists
|
4
|
+
@products_from_saved_lists ||=
|
5
|
+
items_from_saved_list
|
6
|
+
.group_by(&:product_id)
|
7
|
+
.transform_values do |items|
|
8
|
+
values = calculate_based_on_items(items)
|
9
|
+
|
10
|
+
{
|
11
|
+
saved_list_units_sold: values[:units_sold],
|
12
|
+
saved_list_revenue: values[:revenue]
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def items_from_saved_list
|
20
|
+
@items_from_saved_list ||= items.select do |item|
|
21
|
+
via = GlobalID.parse(item.via)
|
22
|
+
via.present? && via.model_class.name == 'Workarea::SavedList'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Workarea
|
2
|
+
module Reports
|
3
|
+
class SaveForLaterProducts
|
4
|
+
include Report
|
5
|
+
|
6
|
+
self.reporting_class = Metrics::ProductByDay
|
7
|
+
self.sort_fields = %w(revenue units_sold adds deletes)
|
8
|
+
|
9
|
+
def aggregation
|
10
|
+
[filter_results, project_used_fields, group_by_product]
|
11
|
+
end
|
12
|
+
|
13
|
+
def filter_results
|
14
|
+
{
|
15
|
+
'$match' => {
|
16
|
+
'reporting_on' => { '$gte' => starts_at, '$lte' => ends_at },
|
17
|
+
'$or' => [
|
18
|
+
{ 'saved_list_adds' => { '$gt' => 0 } },
|
19
|
+
{ 'saved_list_deletes' => { '$gt' => 0 } },
|
20
|
+
{ 'saved_list_units_sold' => { '$gt' => 0 } }
|
21
|
+
]
|
22
|
+
}
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def project_used_fields
|
27
|
+
{
|
28
|
+
'$project' => {
|
29
|
+
'product_id' => 1,
|
30
|
+
'saved_list_units_sold' => 1,
|
31
|
+
'saved_list_adds' => 1,
|
32
|
+
'saved_list_deletes' => 1,
|
33
|
+
'saved_list_revenue' => 1
|
34
|
+
}
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def group_by_product
|
39
|
+
{
|
40
|
+
'$group' => {
|
41
|
+
'_id' => '$product_id',
|
42
|
+
'units_sold' => { '$sum' => '$saved_list_units_sold' },
|
43
|
+
'adds' => { '$sum' => '$saved_list_adds' },
|
44
|
+
'deletes' => { '$sum' => '$saved_list_deletes' },
|
45
|
+
'revenue' => { '$sum' => '$saved_list_revenue' }
|
46
|
+
}
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|