Skip to content

Commit

Permalink
Merge pull request #44 from vtex-apps/feature/wishlist-download
Browse files Browse the repository at this point in the history
Wishlist Download
  • Loading branch information
tsheng1 authored May 6, 2021
2 parents 2655fd5 + 9cb6282 commit d941c36
Show file tree
Hide file tree
Showing 16 changed files with 415 additions and 35 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,28 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

## [1.6.2] - 2021-05-06

### Added

- Add ability to download all wishlists from the admin page as an xls file

## [1.6.1] - 2021-04-28

### Fixed

- After checking as wishlisted, you cannot uncheck

## [1.6.0] - 2021-04-23

### Added

- Add product after login

### Fixed

- Products recently added from the shelf don't show up as wishlisted at the PDP

## [1.5.2] - 2021-04-06

### Fixed
Expand Down
11 changes: 11 additions & 0 deletions admin/navigation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"section": "products",
"subSection": "catalog",
"subSectionItems": [
{
"labelId": "admin/wishlist.menu.label",
"searchKeyWordsHelpers": "",
"path": "/admin/wishlist"
}
]
}
6 changes: 6 additions & 0 deletions admin/routes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"admin.app.wishlist": {
"component": "WishlistAdmin",
"path": "/admin/app/wishlist"
}
}
20 changes: 19 additions & 1 deletion dotnet/Controlers/RoutesController.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
namespace service.Controllers
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Vtex.Api.Context;
using WishList.Data;
using WishList.Models;

public class RoutesController : Controller
{
private readonly IIOServiceContext _context;
private readonly IWishListRepository _wishListRepository;

public RoutesController(IIOServiceContext context)
public RoutesController(IIOServiceContext context, IWishListRepository wishListRepository)
{
this._context = context ?? throw new ArgumentNullException(nameof(context));
this._wishListRepository = wishListRepository ?? throw new ArgumentNullException(nameof(wishListRepository));
}

public async Task<IActionResult> ExportAllLists()
{
WishListsWrapper wishListsWrapper = await _wishListRepository.GetAllLists();
var queryString = HttpContext.Request.Query;
int from = int.Parse(queryString["from"]);
int to = int.Parse(queryString["to"]);
IList<WishListWrapper> wishListsWrappers = wishListsWrapper.WishLists;
wishListsWrapper.WishLists = wishListsWrappers.Skip(from - 1).Take(to - from).ToList();

return Json(wishListsWrapper);
}
}
}
2 changes: 2 additions & 0 deletions dotnet/Data/IWishListRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ public interface IWishListRepository
Task<ResponseListWrapper> GetWishList(string shopperId);
Task<bool> DeleteWishList(string documentId);
Task VerifySchema();

Task <WishListsWrapper> GetAllLists();
}
}
46 changes: 46 additions & 0 deletions dotnet/Data/WishListRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,5 +213,51 @@ public async Task VerifySchema()

Console.WriteLine($"Schema Response: {response.StatusCode}");
}

public async Task<WishListsWrapper> GetAllLists()
{
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri($"http://{this._httpContextAccessor.HttpContext.Request.Headers[WishListConstants.VTEX_ACCOUNT_HEADER_NAME]}.vtexcommercestable.com.br/api/dataentities/{WishListConstants.DATA_ENTITY}/search?_fields=email,ListItemsWrapper")
};

string authToken = this._httpContextAccessor.HttpContext.Request.Headers[WishListConstants.HEADER_VTEX_CREDENTIAL];
if (authToken != null)
{
request.Headers.Add(WishListConstants.AUTHORIZATION_HEADER_NAME, authToken);
request.Headers.Add(WishListConstants.VtexIdCookie, authToken);
request.Headers.Add(WishListConstants.PROXY_AUTHORIZATION_HEADER_NAME, authToken);
}

var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);
string responseContent = await response.Content.ReadAsStringAsync();
WishListsWrapper wishListsWrapper = new WishListsWrapper();
wishListsWrapper.WishLists = new List<WishListWrapper>();
WishListWrapper responseListWrapper = new WishListWrapper();
try
{
JArray searchResult = JArray.Parse(responseContent);
for (int l = 0; l < searchResult.Count; l++)
{
JToken listWrapper = searchResult[l];
if (listWrapper != null)
{
responseListWrapper = JsonConvert.DeserializeObject<WishListWrapper>(listWrapper.ToString());
if (responseListWrapper != null)
{
wishListsWrapper.WishLists.Add(responseListWrapper);
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error:{ex.Message}: Rsp = {responseContent} ");
}

return wishListsWrapper;
}
}
}
5 changes: 4 additions & 1 deletion dotnet/service.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
"runtimeArgs": [
],
"routes": {

"ExportAllLists": {
"path": "/_v/wishlist/export-lists",
"public": true
}
},
"events": {
"onAppsLinked": {
Expand Down
5 changes: 2 additions & 3 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"docs": "0.x",
"dotnet": "2.x",
"graphql": "1.x",
"admin": "0.x",
"messages": "1.x",
"react": "3.x",
"store": "0.x"
Expand Down Expand Up @@ -74,9 +75,7 @@
},
"free": true,
"type": "free",
"availableCountries": [
"*"
]
"availableCountries": ["*"]
},
"$schema": "https://raw.githubusercontent.com/vtex/node-vtex-api/master/gen/manifest.schema"
}
5 changes: 4 additions & 1 deletion messages/context.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
"store/myaccount-menu": "store/myaccount-menu",
"store/myaccount-empty-list": "store/myaccount-empty-list",
"store/wishlist-login": "store/wishlist-login",
"store/wishlist-not-logged": "store/wishlist-not-logged"
"store/wishlist-not-logged": "store/wishlist-not-logged",
"admin/wishlist.menu.label": "admin/wishlist.menu.label",
"admin/settings.title": "admin/settings.title",
"admin/settings.download": "admin/settings.download"
}
5 changes: 4 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
"store/myaccount-menu": "Wishlist",
"store/myaccount-empty-list": "This list is empty",
"store/wishlist-login": "Login",
"store/wishlist-not-logged": "You need to login before adding it to the list"
"store/wishlist-not-logged": "You need to login before adding it to the list",
"admin/wishlist.menu.label": "Wishlist",
"admin/settings.title": "Wishlist Export",
"admin/settings.download": "Download Wishlists"
}
4 changes: 3 additions & 1 deletion messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
"store/myaccount-menu": "Lista de deseos",
"store/myaccount-empty-list": "La tua wishlist è vuota",
"store/wishlist-login": "Iniciar sesión",
"store/wishlist-not-logged": "Debe iniciar sesión antes de agregarlo a la lista"
"store/wishlist-not-logged": "Debe iniciar sesión antes de agregarlo a la lista",
"admin/settings.title": "",
"admin/settings.download": ""
}
4 changes: 3 additions & 1 deletion messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
"store/myaccount-menu": "Wishlist",
"store/myaccount-empty-list": "Esta lista está vazia",
"store/wishlist-login": "Autenticar",
"store/wishlist-not-logged": "Você precisa se autenticar antes de adicionar um produto em sua lista"
"store/wishlist-not-logged": "Você precisa se autenticar antes de adicionar um produto em sua lista",
"admin/settings.title": "",
"admin/settings.download": ""
}
119 changes: 119 additions & 0 deletions react/WishlistAdmin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* eslint-disable no-console */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-loop-func */
import React, { FC, useState } from 'react'
import { injectIntl, defineMessages } from 'react-intl'
import {
Layout,
PageBlock,
PageHeader,
ButtonWithIcon,
IconDownload,
} from 'vtex.styleguide'
import XLSX from 'xlsx'

const WishlistAdmin: FC<any> = ({ intl }) => {
const [state, setState] = useState<any>({
loading: false,
})

const { loading } = state

const fetchWishlists = async (from: number, to: number) => {
const response: any = await fetch(
`/_v/wishlist/export-lists?from=${from}&to=${to}`,
{ mode: 'no-cors' }
)

return response.json()
}

const downloadWishlist = (allWishlists: any) => {
const header = ['Email', 'Product ID', 'SKU', 'Title']
const data: any = []

for (const shopper of allWishlists) {
const wishlists = shopper.listItemsWrapper
for (const wishlist of wishlists) {
for (const wishlistItem of wishlist.listItems) {
const shopperData = {
Email: shopper.email,
'Product ID': wishlistItem.productId,
SKU: wishlistItem.sku,
Title: wishlistItem.title,
}

data.push(shopperData)
}
}
}

const ws = XLSX.utils.json_to_sheet(data, { header })
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1')
const exportFileName = `wishlists.xls`
XLSX.writeFile(wb, exportFileName)
}

let allWishlists: any = []

const getAllWishlists = async () => {
let status = true
let i = 0
const chunkLength = 100
setState({ ...state, loading: true })

while (status) {
await fetchWishlists(i, i + chunkLength).then(data => {
const wishlistArr = data.wishLists

if (!wishlistArr.length) {
status = false
}
allWishlists = [...allWishlists, ...wishlistArr]
})

i += 100
}

downloadWishlist(allWishlists)
setState({ ...state, loading: false })
}

const messages = defineMessages({
title: {
id: 'admin/wishlist.menu.label',
defaultMessage: 'Wishlist',
},
exportLabel: {
id: 'admin/settings.title',
defaultMessage: 'Wishlist Export',
},
download: {
id: 'admin/settings.download',
defaultMessage: 'Download Wishlists',
},
})

const download = <IconDownload />

return (
<Layout
pageHeader={<PageHeader title={intl.formatMessage(messages.title)} />}
>
<PageBlock variation="full">
<ButtonWithIcon
icon={download}
isLoading={loading}
onClick={() => {
getAllWishlists()
}}
>
{intl.formatMessage(messages.download)}
</ButtonWithIcon>
</PageBlock>
</Layout>
)
}

export default injectIntl(WishlistAdmin)
28 changes: 21 additions & 7 deletions react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"lodash": "^4.17.15",
"react-apollo": "^3.1.3",
"react-dom": "^16.9.2",
"react-intl": "^5.10.17"
"react-intl": "^5.10.17",
"xlsx": "^0.15.6"
},
"devDependencies": {
"@types/classnames": "^2.2.7",
Expand All @@ -23,13 +24,26 @@
"react": "^16.9.2",
"react-apollo": "^3.1.3",
"typescript": "3.9.7",
"vtex.add-to-cart-button": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.add-to-cart-button",
"vtex.css-handles": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.css-handles",
"vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.render-runtime",
"vtex.rich-text": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.rich-text",
"vtex.search-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.search-graphql",
"vtex.store-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.store-graphql",
"vtex.store-resources": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.store-resources",
"vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.styleguide"
"vtex.flex-layout": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.flex-layout",
"vtex.list-context": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.list-context",
"vtex.my-account": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.my-account",
"vtex.my-account-commons": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.my-account-commons",
"vtex.product-context": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.product-context",
"vtex.product-list-context": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.product-list-context",
"vtex.product-quantity": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.product-quantity",
"vtex.product-specification-badges": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.product-specification-badges",
"vtex.product-summary": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.product-summary",
"vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.render-runtime",
"vtex.rich-text": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.rich-text",
"vtex.search-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.search-graphql",
"vtex.slider-layout": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.slider-layout",
"vtex.store": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.store",
"vtex.store-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.store-graphql",
"vtex.store-icons": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.store-icons",
"vtex.store-resources": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.store-resources",
"vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public/@types/vtex.styleguide"
},
"version": "1.6.1"
}
6 changes: 5 additions & 1 deletion react/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
{
"extends": "@vtex/tsconfig",
"compilerOptions": {
"jsx": "react",
"esModuleInterop": true,
"lib": ["es2017", "dom", "es2018.promise"]
"lib": ["es2017", "dom", "es2018.promise"],
"module": "esnext",
"moduleResolution": "node",
"target": "es2017"
},
"include": ["./typings/*.d.ts", "./**/*.tsx", "./**/*.ts"]
}
Loading

0 comments on commit d941c36

Please sign in to comment.