From 10318b2fab9f658fada14381febb9f31c02d6cd5 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Tue, 19 Jan 2021 17:40:27 -0500 Subject: [PATCH] Add loading state for data hooks --- framework/bigcommerce/cart/use-cart.tsx | 15 ++--- framework/bigcommerce/lib/normalize.ts | 69 ++++++++++---------- framework/commerce/cart/use-cart.tsx | 10 ++- framework/commerce/utils/define-property.ts | 37 +++++++++++ framework/commerce/utils/use-data.tsx | 14 +++- framework/commerce/wishlist/use-wishlist.tsx | 8 +-- 6 files changed, 101 insertions(+), 52 deletions(-) create mode 100644 framework/commerce/utils/define-property.ts diff --git a/framework/bigcommerce/cart/use-cart.tsx b/framework/bigcommerce/cart/use-cart.tsx index 204ef0fa1..aab4c2e6f 100644 --- a/framework/bigcommerce/cart/use-cart.tsx +++ b/framework/bigcommerce/cart/use-cart.tsx @@ -1,10 +1,9 @@ - import { normalizeCart } from '../lib/normalize' import type { HookFetcher } from '@commerce/utils/types' import type { SwrOptions } from '@commerce/utils/use-data' import useCommerceCart, { CartInput } from '@commerce/cart/use-cart' import type { Cart as BigCommerceCart } from '../api/cart' -import update from "@framework/lib/immutability" +import update from '@framework/lib/immutability' const defaultOpts = { url: '/api/bigcommerce/cart', @@ -13,7 +12,7 @@ const defaultOpts = { type UseCartResponse = BigCommerceCart & Cart -export const fetcher: HookFetcher = ( +export const fetcher: HookFetcher = ( options, { cartId }, fetch @@ -42,11 +41,11 @@ export function extendHook( set: (x) => x, }) - - - return response.data ? update(response, { - data: { $set: normalizeCart(response.data ) } - }) : response + return response.data + ? update(response, { + data: { $set: normalizeCart(response.data) }, + }) + : response } useCart.extend = extendHook diff --git a/framework/bigcommerce/lib/normalize.ts b/framework/bigcommerce/lib/normalize.ts index 83ac0a2e6..bbce17214 100644 --- a/framework/bigcommerce/lib/normalize.ts +++ b/framework/bigcommerce/lib/normalize.ts @@ -1,14 +1,13 @@ -import update from "@framework/lib/immutability" -import { Cart, CartItem, Product } from '../../types' +import update from '@framework/lib/immutability' -function normalizeProductOption(productOption:any) { +function normalizeProductOption(productOption: any) { const { node: { entityId, values: { edges }, ...rest }, - } = productOption; + } = productOption return { id: entityId, @@ -30,52 +29,52 @@ export function normalizeProduct(productNode: any): Product { return update(productNode, { id: { $set: String(id) }, images: { - $apply: ({edges} : any) => edges?.map( - ({ node: { urlOriginal, altText, ...rest } }: any) => ({ + $apply: ({ edges }: any) => + edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({ url: urlOriginal, alt: altText, ...rest, - }) - ) - }, + })), + }, variants: { - $apply: ({edges} : any) => edges?.map( - ({ node: { entityId, productOptions, ...rest } }: any) => ({ + $apply: ({ edges }: any) => + edges?.map(({ node: { entityId, productOptions, ...rest } }: any) => ({ id: entityId, options: productOptions?.edges ? productOptions.edges.map(normalizeProductOption) : [], ...rest, - }) - ) + })), }, options: { - $set: productOptions.edges ? productOptions?.edges.map(normalizeProductOption) : [] + $set: productOptions.edges + ? productOptions?.edges.map(normalizeProductOption) + : [], }, brand: { - $apply:(brand : any) => brand?.entityId ? brand?.entityId : null, - }, - slug: { - $set: path?.replace(/^\/+|\/+$/g, '') + $apply: (brand: any) => (brand?.entityId ? brand?.entityId : null), + }, + slug: { + $set: path?.replace(/^\/+|\/+$/g, ''), }, price: { $set: { value: prices?.price.value, currencyCode: prices?.price.currencyCode, - } - }, - $unset: ['entityId'] + }, + }, + $unset: ['entityId'], }) } export function normalizeCart(data: any): Cart { return update(data, { $auto: { - items: { $set: data?.line_items?.physical_items?.map(itemsToProducts)}, + items: { $set: data?.line_items?.physical_items?.map(itemsToProducts) }, subTotal: { $set: data?.base_amount }, - total: { $set: data?.cart_amount } + total: { $set: data?.cart_amount }, }, - $unset: ['created_time', 'coupons', 'line_items', 'email'] + $unset: ['created_time', 'coupons', 'line_items', 'email'], }) } @@ -92,24 +91,28 @@ function itemsToProducts(item: any): CartItem { extended_list_price, extended_sale_price, ...rest - } = item; + } = item return update(item, { $auto: { prices: { $auto: { listPrice: { $set: list_price }, - salePrice: { $set: sale_price } , + salePrice: { $set: sale_price }, extendedListPrice: { $set: extended_list_price }, extendedSalePrice: { $set: extended_sale_price }, - } + }, + }, + images: { + $set: [ + { + alt: name, + url: image_url, + }, + ], }, - images: { $set: [{ - alt: name, - url: image_url - }]}, productId: { $set: product_id }, - variantId: { $set: variant_id } - } + variantId: { $set: variant_id }, + }, }) } diff --git a/framework/commerce/cart/use-cart.tsx b/framework/commerce/cart/use-cart.tsx index f280923a6..af472244d 100644 --- a/framework/commerce/cart/use-cart.tsx +++ b/framework/commerce/cart/use-cart.tsx @@ -1,12 +1,10 @@ import type { responseInterface } from 'swr' import Cookies from 'js-cookie' import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' -import useData, { SwrOptions } from '../utils/use-data' +import useData, { ResponseState, SwrOptions } from '../utils/use-data' import { useCommerce } from '..' -export type CartResponse = responseInterface & { - isEmpty: boolean -} +export type CartResponse = ResponseState & { isEmpty: boolean } export type CartInput = { cartId: Cart['id'] @@ -17,7 +15,7 @@ export default function useCart( input: HookInput, fetcherFn: HookFetcher, swrOptions?: SwrOptions -) { +): CartResponse { const { cartCookie } = useCommerce() const fetcher: typeof fetcherFn = (options, input, fetch) => { @@ -27,5 +25,5 @@ export default function useCart( const response = useData(options, input, fetcher, swrOptions) - return Object.assign(response, { isEmpty: true }) as CartResponse + return Object.assign(response, { isEmpty: true }) } diff --git a/framework/commerce/utils/define-property.ts b/framework/commerce/utils/define-property.ts new file mode 100644 index 000000000..e89735226 --- /dev/null +++ b/framework/commerce/utils/define-property.ts @@ -0,0 +1,37 @@ +// Taken from https://fettblog.eu/typescript-assertion-signatures/ + +type InferValue = Desc extends { + get(): any + value: any +} + ? never + : Desc extends { value: infer T } + ? Record + : Desc extends { get(): infer T } + ? Record + : never + +type DefineProperty< + Prop extends PropertyKey, + Desc extends PropertyDescriptor +> = Desc extends { writable: any; set(val: any): any } + ? never + : Desc extends { writable: any; get(): any } + ? never + : Desc extends { writable: false } + ? Readonly> + : Desc extends { writable: true } + ? InferValue + : Readonly> + +export default function defineProperty< + Obj extends object, + Key extends PropertyKey, + PDesc extends PropertyDescriptor +>( + obj: Obj, + prop: Key, + val: PDesc +): asserts obj is Obj & DefineProperty { + Object.defineProperty(obj, prop, val) +} diff --git a/framework/commerce/utils/use-data.tsx b/framework/commerce/utils/use-data.tsx index dd5917ccb..d7f8ac5d6 100644 --- a/framework/commerce/utils/use-data.tsx +++ b/framework/commerce/utils/use-data.tsx @@ -1,5 +1,6 @@ import useSWR, { ConfigInterface, responseInterface } from 'swr' import type { HookInput, HookFetcher, HookFetcherOptions } from './types' +import defineProperty from './define-property' import { CommerceError } from './errors' import { useCommerce } from '..' @@ -9,12 +10,16 @@ export type SwrOptions = ConfigInterface< HookFetcher > +export type ResponseState = responseInterface & { + isLoading: boolean +} + export type UseData = ( options: HookFetcherOptions | (() => HookFetcherOptions | null), input: HookInput, fetcherFn: HookFetcher, swrOptions?: SwrOptions -) => responseInterface +) => ResponseState const useData: UseData = (options, input, fetcherFn, swrOptions) => { const { fetcherRef } = useCommerce() @@ -54,6 +59,13 @@ const useData: UseData = (options, input, fetcherFn, swrOptions) => { swrOptions ) + defineProperty(response, 'isLoading', { + get() { + return response.data === undefined + }, + set: (x) => x, + }) + return response } diff --git a/framework/commerce/wishlist/use-wishlist.tsx b/framework/commerce/wishlist/use-wishlist.tsx index 7b2981412..66199e380 100644 --- a/framework/commerce/wishlist/use-wishlist.tsx +++ b/framework/commerce/wishlist/use-wishlist.tsx @@ -1,8 +1,8 @@ import type { responseInterface } from 'swr' import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' -import useData, { SwrOptions } from '../utils/use-data' +import useData, { ResponseState, SwrOptions } from '../utils/use-data' -export type WishlistResponse = responseInterface & { +export type WishlistResponse = ResponseState & { isEmpty: boolean } @@ -11,7 +11,7 @@ export default function useWishlist( input: HookInput, fetcherFn: HookFetcher, swrOptions?: SwrOptions -) { +): WishlistResponse { const response = useData(options, input, fetcherFn, swrOptions) - return Object.assign(response, { isEmpty: true }) as WishlistResponse + return Object.assign(response, { isEmpty: true }) }