diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dc7d0cc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Export all `Categories` translations in batch +- Export `Products` for a given category in batch - Limit of 1.600 products diff --git a/docs/README.md b/docs/README.md index 5d91411..9691374 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,7 +16,7 @@ There are two different pages for `Category` and `Product` translations. Both au From this binding list, the first one is always the `X-Vtex-Tenant` or the default language, for this option the details cannot be translated, to modify these values, the changes should be made inside your store's catalog. For all the others, it's possible to edit the content by category and by product. -It's also possible to export all current translations for `Categories`. Inside a binding different than the `X-Vtex-Tenant`, a button called `export` allows user to download the translations for that binding. +It's also possible to export all current translations for `Categories` and `Products`. Inside a binding different than the `X-Vtex-Tenant`, a button called `export` allows user to download the translations for that binding. Curently, it's only possible to export 1.600 products for a given category every 3 minutes. --- ## Usage diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 778263b..74053a9 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -1,3 +1,5 @@ type Query { categoryTranslations(locale: String!, active: Boolean): [Category] + getCategoriesName: [CategoryName] + productTranslations(locale: String!, categoryId: String!): [Product] } diff --git a/graphql/types/Category.graphql b/graphql/types/Category.graphql index d314a11..b5fa8f3 100644 --- a/graphql/types/Category.graphql +++ b/graphql/types/Category.graphql @@ -3,5 +3,11 @@ type Category { name: String title: String description: String + linkId: String locale: String } + +type CategoryName { + id: String + name: String +} diff --git a/graphql/types/Product.graphql b/graphql/types/Product.graphql new file mode 100644 index 0000000..a13c8fd --- /dev/null +++ b/graphql/types/Product.graphql @@ -0,0 +1,9 @@ +type Product { + id: String! + name: String + description: String + shortDescription: String + title: String + locale: String + linkId: String +} diff --git a/manifest.json b/manifest.json index b4a5421..267fcc0 100644 --- a/manifest.json +++ b/manifest.json @@ -24,6 +24,13 @@ "policies": [ { "name": "vtex.catalog-graphql:resolve-graphql" + }, + { + "name": "outbound-access", + "attrs": { + "host": "portal.vtexcommercestable.com.br", + "path": "/api/catalog_system/*" + } } ], "$schema": "https://raw.githubusercontent.com/vtex/node-vtex-api/master/gen/manifest.schema" diff --git a/node/clients/catalog.ts b/node/clients/catalog.ts index 071a705..db33049 100644 --- a/node/clients/catalog.ts +++ b/node/clients/catalog.ts @@ -1,85 +1,89 @@ -import { AppGraphQLClient, InstanceOptions, IOContext } from '@vtex/api' +import { + InstanceOptions, + IOContext, + JanusClient, + RequestConfig, +} from '@vtex/api' -import { statusToError } from '../utils' +import { + statusToError, + interations, + getInterationPairs, + extractProductId, + MAX_PRODUCTS_PER_CATEGORY, +} from '../utils' -const CATALOG_GRAPHQL_APP = 'vtex.catalog-graphql@1.x' +export class Catalog extends JanusClient { + constructor(ctx: IOContext, opts?: InstanceOptions) { + super(ctx, { + ...opts, + headers: { + ...opts?.headers, + ...(ctx.adminUserAuthToken + ? { VtexIdclientAutCookie: ctx.adminUserAuthToken } + : null), + }, + }) + } + + public getAllProducts = async (categoryId: string) => { + const { range, ...products } = await this.getProductIdsByCategory( + categoryId, + 1, + MAX_PRODUCTS_PER_CATEGORY + ) + const { total } = range + const remainingInterations = interations(total) + const productPerCategorypromises = [] -const CATEGORIES_QUERY = ` - query GetCategoriesId ($active: Boolean, $page: Int!) { - categories(term:"*", page: $page, pageSize: 50, active: $active) { - items { - id - } - paging { - pages - } + // /GetProductAndSkuIds returns max 50 responses. We loop over the remaining interations to get all products + for (let i = 1; i <= remainingInterations; i++) { + const [from, to] = getInterationPairs(i) + const productPerIdPromise = this.getProductIdsByCategory( + categoryId, + from, + to + ) + productPerCategorypromises.push(productPerIdPromise) } - } -` -const GET_TRANSLATION_QUERY = ` - query getTranslation($id:ID!) { - category(id: $id) { - id - name - title - description + const productPerCategoryCollection = await Promise.all( + productPerCategorypromises + ) + + const finalProducts = [] + + // we plug together the first response and all the others. Then we extract only the product ids from responses + for (const product of [products, ...productPerCategoryCollection]) { + const productIds = extractProductId(product.data) + finalProducts.push(...productIds) } + return finalProducts } -` -export class Catalog extends AppGraphQLClient { - constructor(ctx: IOContext, opts?: InstanceOptions) { - super(CATALOG_GRAPHQL_APP, ctx, opts) + private getProductIdsByCategory = ( + categoryId: string, + _from: number, + _to: number + ) => { + return this.get(this.routes.getProductAndSkuIds(), { + params: { categoryId, _from, _to }, + }) } - public getCategoriesId = async (active = true) => { + protected get = (url: string, config: RequestConfig = {}) => { try { - const response = await this.getCategoriesIdPerPage({ active, page: 1 }) - const { - items, - paging: { pages }, - } = (response.data as CategoryIdsResponse).categories - const collectItems = items - const responsePromises = [] - - for (let i = 2; i <= pages; i++) { - const promise = this.getCategoriesIdPerPage({ active, page: i }) - responsePromises.push(promise) - } - - const resolvedPromises = await Promise.all(responsePromises) - - const flattenResponse = resolvedPromises.reduce((acc, curr) => { - return [...acc, ...(curr.data as CategoryIdsResponse).categories.items] - }, collectItems) - - return flattenResponse - } catch (error) { - return statusToError(error) + return this.http.get(url, config) + } catch (e) { + return statusToError(e) } } - private getCategoriesIdPerPage = ({ - active = true, - page, - }: { - active: boolean - page: number - }) => - this.graphql.query({ - query: CATEGORIES_QUERY, - variables: { - active, - page, - }, - }) + private get routes() { + const basePath = '/api/catalog_system' - public getTranslation = (id: string) => - this.graphql.query({ - query: GET_TRANSLATION_QUERY, - variables: { - id, - }, - }) + return { + getProductAndSkuIds: () => `${basePath}/pvt/products/GetProductAndSkuIds`, + } + } } diff --git a/node/clients/catalogGQL.ts b/node/clients/catalogGQL.ts new file mode 100644 index 0000000..9232143 --- /dev/null +++ b/node/clients/catalogGQL.ts @@ -0,0 +1,129 @@ +import { AppGraphQLClient, InstanceOptions, IOContext } from '@vtex/api' + +import { statusToError } from '../utils' + +const CATALOG_GRAPHQL_APP = 'vtex.catalog-graphql@1.x' + +const CATEGORIES_QUERY = ` + query GetCategories ($active: Boolean, $page: Int!) { + categories(term:"*", page: $page, pageSize: 50, active: $active) { + items { + id + name + } + paging { + pages + } + } + } +` + +const GET_CATEGORY_TRANSLATION_QUERY = ` + query getTranslation($id:ID!) { + category(id: $id) { + id + name + title + description + linkId + } + } +` + +const GET_PRODUCT_TRANSLATION_QUERY = ` + query getProductTranslation($identifier: ProductUniqueIdentifier) { + product(identifier: $identifier) { + id + name + description + shortDescription + title + linkId + } + } +` + +export class CatalogGQL extends AppGraphQLClient { + constructor(ctx: IOContext, opts?: InstanceOptions) { + super(CATALOG_GRAPHQL_APP, ctx, opts) + } + + public getCategories = async (active = true) => { + try { + const response = await this.getCategoriesPerPage({ active, page: 1 }) + const { + items, + paging: { pages }, + } = (response.data as CategoryResponse).categories + const collectItems = items + const responsePromises = [] + + for (let i = 2; i <= pages; i++) { + const promise = this.getCategoriesPerPage({ active, page: i }) + responsePromises.push(promise) + } + + const resolvedPromises = await Promise.all(responsePromises) + + const flattenResponse = resolvedPromises.reduce((acc, curr) => { + return [...acc, ...(curr.data as CategoryResponse).categories.items] + }, collectItems) + + return flattenResponse + } catch (error) { + return statusToError(error) + } + } + + private getCategoriesPerPage = ({ + active = true, + page, + }: { + active: boolean + page: number + }) => + this.graphql.query({ + query: CATEGORIES_QUERY, + variables: { + active, + page, + }, + }) + + public getCategoryTranslation = (id: string, locale: string) => { + return this.graphql.query( + { + query: GET_CATEGORY_TRANSLATION_QUERY, + variables: { + id, + }, + }, + { + headers: { + 'x-vtex-locale': `${locale}`, + }, + } + ) + } + + public getProductTranslation = (id: string, locale: string) => + this.graphql.query< + ProductTranslationResponse, + { identifier: { value: string; field: 'id' } } + >( + { + query: GET_PRODUCT_TRANSLATION_QUERY, + variables: { + identifier: { + field: 'id', + value: id, + }, + }, + }, + { + headers: { + 'x-vtex-locale': `${locale}`, + }, + } + ) +} diff --git a/node/clients/index.ts b/node/clients/index.ts index c769ac8..02d7601 100644 --- a/node/clients/index.ts +++ b/node/clients/index.ts @@ -1,8 +1,13 @@ import { IOClients } from '@vtex/api' +import { CatalogGQL } from './catalogGQL' import { Catalog } from './catalog' export class Clients extends IOClients { + public get catalogGQL() { + return this.getOrSet('catalogGQL', CatalogGQL) + } + public get catalog() { return this.getOrSet('catalog', Catalog) } diff --git a/node/index.ts b/node/index.ts index 16b8b87..40eca34 100644 --- a/node/index.ts +++ b/node/index.ts @@ -22,6 +22,7 @@ export default new Service({ options: { default: { retries: 2, + timeout: 2 * 60000, }, }, }, diff --git a/node/package.json b/node/package.json index b4b0cc7..9337241 100644 --- a/node/package.json +++ b/node/package.json @@ -8,6 +8,9 @@ }, "devDependencies": { "@vtex/tsconfig": "^0.5.6", - "typescript": "3.9.7" + "typescript": "3.9.7", + "vtex.catalog-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.catalog-graphql@1.96.0/public/@types/vtex.catalog-graphql", + "vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.138.0/public/@types/vtex.styleguide", + "vtex.tenant-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.tenant-graphql@0.1.2/public/@types/vtex.tenant-graphql" } } diff --git a/node/resolvers/category.ts b/node/resolvers/category.ts index 9616556..46a565d 100644 --- a/node/resolvers/category.ts +++ b/node/resolvers/category.ts @@ -1,19 +1,21 @@ -import { statusToError } from '../utils' - export const Category = { locale: ( - _root: ResolvedPromise, + _root: ResolvedPromise, _args: unknown, ctx: Context ) => { return ctx.state.locale }, - name: (root: ResolvedPromise) => root.data.category.name, - title: (root: ResolvedPromise) => + name: (root: ResolvedPromise) => + root.data.category.name, + title: (root: ResolvedPromise) => root.data.category.title, - description: (root: ResolvedPromise) => + description: (root: ResolvedPromise) => root.data.category.description, - id: (root: ResolvedPromise) => root.data.category.id, + id: (root: ResolvedPromise) => + root.data.category.id, + linkId: (root: ResolvedPromise) => + root.data.category.linkId, } const categoryTranslations = async ( @@ -22,27 +24,40 @@ const categoryTranslations = async ( ctx: Context ) => { const { - clients: { catalog }, + clients: { catalogGQL }, } = ctx - ctx.state.locale = args.locale + const { active, locale } = args - try { - const ids = await catalog.getCategoriesId() + ctx.state.locale = locale - const translationsP = [] + const ids = await catalogGQL.getCategories(active) - for (const { id } of ids) { - const promise = catalog.getTranslation(id) - translationsP.push(promise) - } + const translationsP = [] - return translationsP - } catch (error) { - return statusToError(error) + for (const { id } of ids) { + const promise = catalogGQL.getCategoryTranslation(id, locale) + translationsP.push(promise) } + + const translations = await Promise.all(translationsP) + + return translations +} + +const getCategoriesName = async ( + _root: unknown, + _args: unknown, + ctx: Context +) => { + const { + clients: { catalogGQL }, + } = ctx + + return catalogGQL.getCategories(false) } export const queries = { categoryTranslations, + getCategoriesName, } diff --git a/node/resolvers/index.ts b/node/resolvers/index.ts index 11f38f1..87f1fc5 100644 --- a/node/resolvers/index.ts +++ b/node/resolvers/index.ts @@ -1,9 +1,12 @@ import { Category, queries as categoryQueries } from './category' +import { Product, queries as productQueries } from './product' export const queries = { ...categoryQueries, + ...productQueries, } export const resolvers = { Category, + Product, } diff --git a/node/resolvers/product.ts b/node/resolvers/product.ts new file mode 100644 index 0000000..74c70b2 --- /dev/null +++ b/node/resolvers/product.ts @@ -0,0 +1,64 @@ +export const Product = { + locale: ( + _root: ResolvedPromise, + _args: unknown, + ctx: Context + ) => { + return ctx.state.locale + }, + id: (root: ResolvedPromise) => + root.data.product.id, + name: (root: ResolvedPromise) => + root.data.product.name, + description: (root: ResolvedPromise) => + root.data.product.description, + shortDescription: (root: ResolvedPromise) => + root.data.product.shortDescription, + title: (root: ResolvedPromise) => + root.data.product.title, + linkId: (root: ResolvedPromise) => + root.data.product.linkId, +} + +const PRODUCT_LIMIT = 1600 + +const productTranslations = async ( + _root: unknown, + args: { locale: string; categoryId: string }, + ctx: Context +) => { + const { + clients: { catalog, catalogGQL }, + } = ctx + + const { locale, categoryId } = args + + ctx.state.locale = locale + + const productIdCollection = await catalog.getAllProducts(categoryId) + + const productTranslationPromises = [] + + let counter = 0 + + for (const productId of productIdCollection) { + // Getting a 429 when products list > 2k. Setting threshold a little below it to ensure it works + if (counter === PRODUCT_LIMIT) { + break + } + const translationPromise = catalogGQL.getProductTranslation( + productId, + locale + ) + productTranslationPromises.push(translationPromise) + counter++ + } + + const translations = await Promise.all(productTranslationPromises) + + return translations +} + +export const queries = { + productTranslations, +} diff --git a/node/service.json b/node/service.json index 21b58b2..9254e2c 100644 --- a/node/service.json +++ b/node/service.json @@ -1,8 +1,8 @@ { - "memory": 256, + "memory": 1024, "ttl": 10, - "timeout": 5, + "timeout": 60, "minReplicas": 2, - "maxReplicas": 4, + "maxReplicas": 10, "workers": 2 } diff --git a/node/typings/category.d.ts b/node/typings/category.d.ts index 53094c2..5d094a8 100644 --- a/node/typings/category.d.ts +++ b/node/typings/category.d.ts @@ -1,18 +1,19 @@ -interface CategoryIdsResponse { +interface CategoryResponse { categories: { - items: Array<{ id: string }> + items: Array<{ id: string; name: string }> paging: { pages: number } } } -interface TranslationResponse { +interface CategoryTranslationResponse { category: { id: string name: string title: string description: string + linkId: string } } diff --git a/node/typings/product.d.ts b/node/typings/product.d.ts new file mode 100644 index 0000000..e6aeb53 --- /dev/null +++ b/node/typings/product.d.ts @@ -0,0 +1,21 @@ +interface GetProductAndSkuIds { + data: { + [productId: string]: number[] + } + range: { + total: number + from: number + to: number + } +} + +interface ProductTranslationResponse { + product: { + id: string + name: string + description: string + shortDescription: string + title: string + linkId: string + } +} diff --git a/node/utils.ts b/node/utils.ts index cf6bb73..4915a19 100644 --- a/node/utils.ts +++ b/node/utils.ts @@ -27,3 +27,21 @@ export const statusToError = (e: AxiosError) => { throw new TypeError(e.message) } } + +export const MAX_PRODUCTS_PER_CATEGORY = 50 + +export const interations = (total: number) => { + return ( + Math.floor(total / MAX_PRODUCTS_PER_CATEGORY) - + (total % MAX_PRODUCTS_PER_CATEGORY ? 0 : 1) + ) +} + +export const getInterationPairs = (currentStep: number): number[] => [ + MAX_PRODUCTS_PER_CATEGORY * currentStep + 1, + MAX_PRODUCTS_PER_CATEGORY * currentStep + 50, +] + +export const extractProductId = (productResponse: Record) => { + return Object.keys(productResponse) +} diff --git a/node/yarn.lock b/node/yarn.lock index 2a8e9bd..11c9a0b 100644 --- a/node/yarn.lock +++ b/node/yarn.lock @@ -1334,6 +1334,18 @@ vary@^1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= +"vtex.catalog-graphql@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.catalog-graphql@1.96.0/public/@types/vtex.catalog-graphql": + version "1.96.0" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.catalog-graphql@1.96.0/public/@types/vtex.catalog-graphql#20fd7dc4f24848cb3e227a3ae369fcfb71861008" + +"vtex.styleguide@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.138.0/public/@types/vtex.styleguide": + version "9.138.0" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.138.0/public/@types/vtex.styleguide#ce25c45f1827df0a0f5024a40c9a93875f29e15c" + +"vtex.tenant-graphql@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.tenant-graphql@0.1.2/public/@types/vtex.tenant-graphql": + version "0.1.2" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.tenant-graphql@0.1.2/public/@types/vtex.tenant-graphql#74673fe86baefe74f21a6d2615993ea0ccb5e79e" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" diff --git a/react/components/ProductTranslation/ProductTranslation.tsx b/react/components/ProductTranslation/ProductTranslation.tsx index 6e946ac..3163a58 100644 --- a/react/components/ProductTranslation/ProductTranslation.tsx +++ b/react/components/ProductTranslation/ProductTranslation.tsx @@ -1,13 +1,42 @@ -import React, { FC, SyntheticEvent } from 'react' -import { InputSearch, PageBlock, Spinner } from 'vtex.styleguide' +import React, { FC, SyntheticEvent, useEffect, useState, useRef } from 'react' +import { + InputSearch, + PageBlock, + Spinner, + ButtonWithIcon, + IconDownload, + ModalDialog, + AutocompleteInput, + Alert, +} from 'vtex.styleguide' +import { useLazyQuery, useQuery } from 'react-apollo' import { useLocaleSelector } from '../LocaleSelector' import getProductQuery from '../../graphql/getProduct.gql' import ProductForm from './ProductForm' import ErrorHandler from '../ErrorHandler' import useCatalogQuery from '../../hooks/useCatalogQuery' +import GET_CATEGORIES_NAME from '../../graphql/getCategoriesName.gql' +import { filterSearchCategories, parseJSONToXLS } from '../../utils' +import GET_PRODUCT_TRANSLATION from '../../graphql/getProductTranslations.gql' + +const AUTOCOMPLETE_LIST_SIZE = 6 + +interface AutocompleteValue { + label: string + value: string +} const ProductTranslation: FC = () => { + const [isExportOpen, setIsExportOpen] = useState(false) + const [searchTerm, setSearchTerm] = useState('') + const [selectedCategory, setSelectedCategory] = useState( + {} as AutocompleteValue + ) + const [downloading, setDownloading] = useState(false) + const [showMissingCatId, setShowMissingCatId] = useState(false) + const [hasError, setHasError] = useState(false) + const { entryInfo, isLoadingOrRefetching, @@ -22,7 +51,32 @@ const ProductTranslation: FC = () => { { identifier: { value: string; field: 'id' } } >(getProductQuery) - const { selectedLocale } = useLocaleSelector() + const { selectedLocale, isXVtexTenant } = useLocaleSelector() + + const [ + fetchProductTranslations, + { data: productTranslations, error: prodTranslationError }, + ] = useLazyQuery( + GET_PRODUCT_TRANSLATION, + { + context: { + headers: { + 'x-vtex-locale': `${selectedLocale}`, + }, + }, + } + ) + + const { + data: categoryInfo, + loading: loadingCategoryInfo, + error: categoryError, + } = useQuery(GET_CATEGORIES_NAME) + + const listOfOptions = filterSearchCategories({ + categoryList: categoryInfo?.getCategoriesName ?? [], + term: searchTerm, + }) const handleSubmitProductId = (e: SyntheticEvent) => { e.preventDefault() @@ -34,41 +88,175 @@ const ProductTranslation: FC = () => { variables: { identifier: { field: 'id', value: entryId } }, }) } + + const downloadProducts = () => { + if (!selectedCategory.value) { + setShowMissingCatId(true) + return + } + setDownloading(true) + fetchProductTranslations({ + variables: { locale: selectedLocale, categoryId: selectedCategory.value }, + }) + } + + const handleClose = () => { + setSelectedCategory({} as AutocompleteValue) + setIsExportOpen(false) + setSearchTerm('') + setHasError(false) + } + + useEffect(() => { + // eslint-disable-next-line vtex/prefer-early-return + if (productTranslations && downloading) { + parseJSONToXLS(productTranslations.productTranslations, { + fileName: `category-${selectedCategory.value}-product-data-${selectedLocale}`, + sheetName: 'product_data', + }) + + setDownloading(false) + handleClose() + } + }, [productTranslations, selectedLocale, downloading, selectedCategory]) + + const alertRef = useRef() + + useEffect(() => { + clearTimeout(alertRef.current) + if (showMissingCatId) { + alertRef.current = setTimeout(() => { + setShowMissingCatId(false) + }, 5000) + } + }, [showMissingCatId]) + + useEffect(() => { + // eslint-disable-next-line vtex/prefer-early-return + if (prodTranslationError || categoryError) { + setDownloading(false) + setHasError(true) + } + }, [prodTranslationError, categoryError]) + const { id, ...productInfo } = entryInfo?.product || ({} as Product) return ( -
-
- -
- {id || isLoadingOrRefetching || errorMessage ? ( - - {errorMessage ? ( - +
+
+
+ - ) : isLoadingOrRefetching ? ( +
+ {isXVtexTenant ? null : ( +
+ } + variation="primary" + onClick={() => setIsExportOpen(true)} + > + Export + +
+ )} +
+ {id || isLoadingOrRefetching || errorMessage ? ( + + {errorMessage ? ( + + ) : isLoadingOrRefetching ? ( + + ) : ( + + )} + + ) : null} +
+ + {showMissingCatId ? ( +
+
+ setShowMissingCatId(false)}> + Please select a Category Id + +
+
+ ) : null} +
+

Export Product Data for {selectedLocale}

+ {loadingCategoryInfo ? ( ) : ( - +
+

Select category

+ setSearchTerm(term), + onClear: () => { + setSearchTerm('') + setSelectedCategory({} as AutocompleteValue) + }, + value: searchTerm, + }} + options={{ + onSelect: (selectedItem: AutocompleteValue) => + setSelectedCategory(selectedItem), + value: !searchTerm.length + ? [] + : listOfOptions.slice(0, AUTOCOMPLETE_LIST_SIZE), + loading: listOfOptions.length > AUTOCOMPLETE_LIST_SIZE, + }} + /> +

+ Currently, the app allows to export 1.600 products every 3 + minutes +

+ {hasError ? ( +

+ There was an error exporting products. Please try again in a + few minutes. +

+ ) : null} +
)} - - ) : null} -
+ + + ) } diff --git a/react/graphql/getAllCategories.gql b/react/graphql/getAllCategories.gql index 4e95008..e37e51c 100644 --- a/react/graphql/getAllCategories.gql +++ b/react/graphql/getAllCategories.gql @@ -6,5 +6,6 @@ query getAllCategories($locale: String!, $active: Boolean) { title description locale + linkId } } diff --git a/react/graphql/getCategoriesName.gql b/react/graphql/getCategoriesName.gql new file mode 100644 index 0000000..fbe8c39 --- /dev/null +++ b/react/graphql/getCategoriesName.gql @@ -0,0 +1,6 @@ +query getCategoriesName { + getCategoriesName @context(provider: "vtex.admin-catalog-translation") { + id + name + } +} diff --git a/react/graphql/getProductTranslations.gql b/react/graphql/getProductTranslations.gql new file mode 100644 index 0000000..7306a79 --- /dev/null +++ b/react/graphql/getProductTranslations.gql @@ -0,0 +1,12 @@ +query getProductTranslations($locale: String!, $categoryId: String!) { + productTranslations(locale: $locale, categoryId: $categoryId) + @context(provider: "vtex.admin-catalog-translation") { + id + name + description + shortDescription + title + linkId + locale + } +} diff --git a/react/package.json b/react/package.json index 7aa7c49..275d082 100644 --- a/react/package.json +++ b/react/package.json @@ -15,7 +15,11 @@ "react": "^16.8.6", "react-dom": "^16.8.6", "react-intl": "^2.7.2", - "typescript": "3.9.7" + "typescript": "3.9.7", + "vtex.catalog-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.catalog-graphql@1.96.0/public/@types/vtex.catalog-graphql", + "vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.128.2/public/@types/vtex.render-runtime", + "vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.138.0/public/@types/vtex.styleguide", + "vtex.tenant-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.tenant-graphql@0.1.2/public/@types/vtex.tenant-graphql" }, "dependencies": { "@types/faker": "^4.1.5", diff --git a/react/typings/catalog.d.ts b/react/typings/catalog.d.ts index a6c700c..3affa54 100644 --- a/react/typings/catalog.d.ts +++ b/react/typings/catalog.d.ts @@ -49,3 +49,18 @@ interface ProductInputTranslation { title: string linkId: string } + +interface CategoriesNameAndId { + getCategoriesName: Array<{ id: string; name: string }> +} + +interface ProductTranslations { + productTranslations: Array<{ + id: string + name: string + description: string + shortDescription: string + title: string + locale: string + }> +} diff --git a/react/utils/index.ts b/react/utils/index.ts index 8642e14..1c3ad7e 100644 --- a/react/utils/index.ts +++ b/react/utils/index.ts @@ -60,3 +60,21 @@ export function parseJSONToXLS( const exportFileName = `${fileName}.xlsx` XLSX.writeFile(workBook, exportFileName) } + +interface FilterSearchCategoriesArgs { + categoryList: Array<{ id: string; name: string }> + term: string +} + +export const filterSearchCategories = ({ + categoryList, + term, +}: FilterSearchCategoriesArgs): Array<{ label: string; value: string }> => { + return ( + categoryList + .map(({ id, name }) => ({ label: `${id} - ${name}`, value: id })) + .filter(({ label }) => + label.toLowerCase().includes(term.toLowerCase()) + ) ?? [] + ) +} diff --git a/react/yarn.lock b/react/yarn.lock index 079bff6..bdc73bc 100644 --- a/react/yarn.lock +++ b/react/yarn.lock @@ -420,6 +420,22 @@ typescript@3.9.7: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== +"vtex.catalog-graphql@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.catalog-graphql@1.96.0/public/@types/vtex.catalog-graphql": + version "1.96.0" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.catalog-graphql@1.96.0/public/@types/vtex.catalog-graphql#20fd7dc4f24848cb3e227a3ae369fcfb71861008" + +"vtex.render-runtime@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.128.2/public/@types/vtex.render-runtime": + version "8.128.2" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.128.2/public/@types/vtex.render-runtime#67f5975f7edd73c9afa7bee57734540c0ead5428" + +"vtex.styleguide@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.138.0/public/@types/vtex.styleguide": + version "9.138.0" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.138.0/public/@types/vtex.styleguide#ce25c45f1827df0a0f5024a40c9a93875f29e15c" + +"vtex.tenant-graphql@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.tenant-graphql@0.1.2/public/@types/vtex.tenant-graphql": + version "0.1.2" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.tenant-graphql@0.1.2/public/@types/vtex.tenant-graphql#74673fe86baefe74f21a6d2615993ea0ccb5e79e" + wmf@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da"