From 8d59a24d9a0b179ba0d78a8b6dec3a53c38668a5 Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Thu, 22 Feb 2024 11:27:12 +0100 Subject: [PATCH 01/13] feat: add create and edit pages --- packages/app/src/App.tsx | 4 + .../app/src/components/ListEmptyState.tsx | 60 +++++++ packages/app/src/components/ListItemSku.tsx | 11 +- .../src/components/ListItemSkuListItem.tsx | 38 ++++- packages/app/src/components/SkuListForm.tsx | 151 ++++++++++++++++++ packages/app/src/hooks/useAddItemOverlay.tsx | 120 ++++++++++++++ packages/app/src/pages/SkuListDetails.tsx | 25 ++- packages/app/src/pages/SkuListEdit.tsx | 111 +++++++++++++ packages/app/src/pages/SkuListNew.tsx | 94 +++++++++++ 9 files changed, 600 insertions(+), 14 deletions(-) create mode 100644 packages/app/src/components/ListEmptyState.tsx create mode 100644 packages/app/src/components/SkuListForm.tsx create mode 100644 packages/app/src/hooks/useAddItemOverlay.tsx create mode 100644 packages/app/src/pages/SkuListEdit.tsx create mode 100644 packages/app/src/pages/SkuListNew.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 8cd55f7..2309631 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -1,5 +1,7 @@ import { ErrorNotFound } from '#pages/ErrorNotFound' import { SkuListDetails } from '#pages/SkuListDetails' +import { SkuListEdit } from '#pages/SkuListEdit' +import { SkuListNew } from '#pages/SkuListNew' import { SkuListsList } from '#pages/SkuListsList' import { CoreSdkProvider, @@ -46,6 +48,8 @@ export function App(): JSX.Element { path={appRoutes.details.path} component={SkuListDetails} /> + + diff --git a/packages/app/src/components/ListEmptyState.tsx b/packages/app/src/components/ListEmptyState.tsx new file mode 100644 index 0000000..9787bf2 --- /dev/null +++ b/packages/app/src/components/ListEmptyState.tsx @@ -0,0 +1,60 @@ +import { appRoutes } from '#data/routes' +import { A, Button, EmptyState } from '@commercelayer/app-elements' +import { Link, useRoute } from 'wouter' + +interface Props { + scope?: 'noSKUListItems' | 'noSKUs' | 'noSKUsFiltered' +} + +export function ListEmptyState({ + scope = 'noSKUListItems' +}: Props): JSX.Element { + if (scope === 'noSKUsFiltered') { + return ( + +

+ We didn't find any SKU matching the current filters selection. +

+ + } + className='bg-white' + /> + ) + } + if (scope === 'noSKUs') { + return ( + +

Add a SKU with the API, or use the CLI.

+ + View API reference. + + + } + /> + ) + } + + const [, params] = useRoute<{ skuListId: string }>(appRoutes.edit.path) + const skuListId = params?.skuListId ?? '' + + return ( + + + + } + /> + ) +} diff --git a/packages/app/src/components/ListItemSku.tsx b/packages/app/src/components/ListItemSku.tsx index 2f16d01..4c60f35 100644 --- a/packages/app/src/components/ListItemSku.tsx +++ b/packages/app/src/components/ListItemSku.tsx @@ -1,22 +1,19 @@ import { Avatar, - Icon, ListItem, Text, - withSkeletonTemplate, - type ListItemProps + withSkeletonTemplate } from '@commercelayer/app-elements' import type { Sku } from '@commercelayer/sdk' import { makeSku } from 'src/mocks/resources/skus' interface Props { resource?: Sku - variant: ListItemProps['variant'] onSelect?: (resource: Sku) => void } export const ListItemSku = withSkeletonTemplate( - ({ resource = makeSku(), variant, onSelect }) => { + ({ resource = makeSku(), onSelect }) => { return ( ( /> } className='bg-white' - variant={variant} >
@@ -43,9 +39,6 @@ export const ListItemSku = withSkeletonTemplate( {resource.name}
- {variant === 'card' && ( - - )}
) } diff --git a/packages/app/src/components/ListItemSkuListItem.tsx b/packages/app/src/components/ListItemSkuListItem.tsx index 04f5346..d6813f6 100644 --- a/packages/app/src/components/ListItemSkuListItem.tsx +++ b/packages/app/src/components/ListItemSkuListItem.tsx @@ -1,20 +1,29 @@ import { makeSkuListItem } from '#mocks' import { Avatar, + Icon, + InputSpinner, ListItem, Text, - withSkeletonTemplate + withSkeletonTemplate, + type ListItemProps } from '@commercelayer/app-elements' import type { SkuListItem } from '@commercelayer/sdk' interface Props { resource?: SkuListItem + variant?: ListItemProps['variant'] + onQuantityChange?: (resource: SkuListItem, quantity: number) => void isLoading?: boolean delayMs?: number } export const ListItemSkuListItem = withSkeletonTemplate( - ({ resource = makeSkuListItem() }): JSX.Element | null => { + ({ + resource = makeSkuListItem(), + variant = 'list', + onQuantityChange + }): JSX.Element | null => { return ( ( src={resource.sku?.image_url as `https://${string}`} /> } - alignItems='bottom' + variant={variant} + alignItems={variant === 'list' ? 'bottom' : 'center'} + className='bg-white' >
@@ -34,7 +45,26 @@ export const ListItemSkuListItem = withSkeletonTemplate( {resource.sku?.name}
- x {resource.quantity} + {variant === 'list' && ( + x {resource.quantity} + )} + {variant === 'card' && ( +
+ { + if (onQuantityChange != null) { + onQuantityChange(resource, newQuantity) + } + }} + /> + +
+ )}
) } diff --git a/packages/app/src/components/SkuListForm.tsx b/packages/app/src/components/SkuListForm.tsx new file mode 100644 index 0000000..a997e5e --- /dev/null +++ b/packages/app/src/components/SkuListForm.tsx @@ -0,0 +1,151 @@ +import { useAddItemOverlay } from '#hooks/useAddItemOverlay' +import { makeSkuListItem } from '#mocks' +import { + Button, + ButtonCard, + HookedForm, + HookedInput, + HookedValidationApiError, + HookedValidationError, + Section, + Spacer, + Text +} from '@commercelayer/app-elements' +import type { SkuList, SkuListItem } from '@commercelayer/sdk' +import { zodResolver } from '@hookform/resolvers/zod' +import { useEffect, useMemo, useState } from 'react' +import { useForm, type UseFormSetError } from 'react-hook-form' +import { z } from 'zod' +import { ListItemSkuListItem } from './ListItemSkuListItem' + +const skuListFormSchema = z.object({ + id: z.string().optional(), + name: z.string().min(1), + items: z.array( + z.object({ + id: z.string().min(1), + quantity: z.string().min(1) + }) + ) +}) + +export type SkuListFormValues = z.infer + +interface Props { + resource?: SkuList + defaultValues?: Partial + isSubmitting: boolean + onSubmit: ( + formValues: SkuListFormValues, + setError: UseFormSetError + ) => void + apiError?: any +} + +export function SkuListForm({ + resource, + defaultValues, + onSubmit, + apiError, + isSubmitting +}: Props): JSX.Element { + const skuListItemFormMethods = useForm({ + defaultValues, + resolver: zodResolver(skuListFormSchema) + }) + + const { show: showAddItemOverlay, Overlay: AddItemOverlay } = + useAddItemOverlay() + + const [selectedItems, setSelectedItems] = useState([]) + const selectedItemsIds = useMemo(() => { + console.log(selectedItems) + const excludedIds = [] as string[] + selectedItems.forEach((sku) => { + excludedIds.push(sku.id) + }) + return excludedIds.join(',') ?? '' + }, [selectedItems]) + + useEffect(() => { + if (resource != null) { + resource.sku_list_items?.forEach((item) => { + setSelectedItems([...selectedItems, item]) + }) + } + }, [resource]) + + return ( + <> + { + onSubmit(formValues, skuListItemFormMethods.setError) + }} + > +
+ + + + + SKUs + {selectedItems.map((item, idx) => ( + + { + const updatedSelectedItems: SkuListItem[] = [] + selectedItems.forEach((item) => { + if (item.id === resource.id) { + item.quantity = quantity + } + updatedSelectedItems.push(item) + }) + setSelectedItems(updatedSelectedItems) + }} + /> + + ))} + + { + showAddItemOverlay(selectedItemsIds) + }} + /> + + + + + { + const newSkuListItem = makeSkuListItem() + newSkuListItem.quantity = 1 + newSkuListItem.sku = selectedSku + newSkuListItem.sku_code = selectedSku.code + // delete (newSkuListItem as any).id + setSelectedItems([...selectedItems, newSkuListItem]) + }} + /> + +
+ + + + + + +
+ + ) +} diff --git a/packages/app/src/hooks/useAddItemOverlay.tsx b/packages/app/src/hooks/useAddItemOverlay.tsx new file mode 100644 index 0000000..4c1c5b8 --- /dev/null +++ b/packages/app/src/hooks/useAddItemOverlay.tsx @@ -0,0 +1,120 @@ +import { ListEmptyState } from '#components/ListEmptyState' +import { ListItemSku } from '#components/ListItemSku' +import { + Card, + PageLayout, + useOverlay, + useResourceFilters +} from '@commercelayer/app-elements' +import type { FiltersInstructions } from '@commercelayer/app-elements/dist/ui/resources/useResourceFilters/types' +import type { Sku } from '@commercelayer/sdk' +import { useState } from 'react' +import { navigate, useSearch } from 'wouter/use-location' + +interface OverlayHook { + show: (excludedIds?: string) => void + Overlay: React.FC<{ onConfirm: (resource: Sku) => void }> +} + +export function useAddItemOverlay(): OverlayHook { + const { Overlay: OverlayElement, open, close } = useOverlay() + const [excludedIds, setExcludedIds] = useState('') + + const instructions: FiltersInstructions = [ + { + label: 'Search', + type: 'textSearch', + sdk: { + predicate: ['name', 'code'].join('_or_') + '_cont' + }, + render: { + component: 'searchBar' + } + } + ] + + return { + show: (excludedIds) => { + console.log(excludedIds) + if (excludedIds != null) { + setExcludedIds(excludedIds) + } + open() + }, + Overlay: ({ onConfirm }) => { + const queryString = useSearch() + const { SearchWithNav, FilteredList, hasActiveFilter } = + useResourceFilters({ + instructions + }) + + return ( + + { + close() + }, + label: 'Back', + icon: 'arrowLeft' + }} + > +
+
+ {}} + onUpdate={(qs) => { + navigate(`?${qs}`, { + replace: true + }) + }} + queryString={queryString} + hideFiltersNav + searchBarPlaceholder='search...' + /> +
+
+ +
+
+ + ( + { + onConfirm(resource) + close() + navigate(`?`, { + replace: true + }) + }} + {...props} + /> + )} + emptyState={ + + } + hideTitle + /> + +
+
+ ) + } + } +} diff --git a/packages/app/src/pages/SkuListDetails.tsx b/packages/app/src/pages/SkuListDetails.tsx index fab309d..86b87d0 100644 --- a/packages/app/src/pages/SkuListDetails.tsx +++ b/packages/app/src/pages/SkuListDetails.tsx @@ -1,6 +1,7 @@ import { Button, Dropdown, + DropdownDivider, DropdownItem, EmptyState, InputReadonly, @@ -67,6 +68,18 @@ export const SkuListDetails = ( const pageTitle = skuList?.name + const contextMenuEdit = canUser('update', 'sku_lists') && ( + { + setLocation(appRoutes.edit.makePath({ skuListId })) + }} + /> + ) + + const contextMenuDivider = canUser('update', 'sku_lists') && + canUser('destroy', 'sku_lists') && + const contextMenuDelete = canUser('destroy', 'sku_lists') && ( ) - const contextMenu = {contextMenuDelete}} /> + const contextMenu = ( + + {contextMenuEdit} + {contextMenuDivider} + {contextMenuDelete} + + } + /> + ) return ( () + const [isSaving, setIsSaving] = useState(false) + + const [, params] = useRoute<{ skuListId: string }>(appRoutes.edit.path) + const skuListId = params?.skuListId ?? '' + + const { skuList, isLoading, mutateSkuList } = useSkuListDetails(skuListId) + + const goBackUrl = appRoutes.details.makePath({ skuListId }) + + if (!canUser('update', 'sku_lists')) { + return ( + { + setLocation(goBackUrl) + }, + label: 'Cancel', + icon: 'x' + }} + scrollToTop + > + + + + } + /> + + ) + } + + return ( + Edit SKU list + } + navigationButton={{ + onClick: () => { + setLocation(goBackUrl) + }, + label: 'Cancel', + icon: 'x' + }} + gap='only-top' + scrollToTop + overlay + > + + {!isLoading && skuList != null ? ( + { + setIsSaving(true) + const skuList = adaptFormValuesToSkuList(formValues) + void sdkClient.sku_lists + .update(skuList) + .then((updatedSkuList) => { + setLocation(goBackUrl) + void mutateSkuList({ ...updatedSkuList }) + }) + .catch((error) => { + setApiError(error) + setIsSaving(false) + }) + }} + /> + ) : null} + + + ) +} + +function adaptFormValuesToSkuList( + formValues: SkuListFormValues +): SkuListUpdate { + return { + id: formValues.id ?? '', + name: formValues.name + } +} diff --git a/packages/app/src/pages/SkuListNew.tsx b/packages/app/src/pages/SkuListNew.tsx new file mode 100644 index 0000000..db2f1d5 --- /dev/null +++ b/packages/app/src/pages/SkuListNew.tsx @@ -0,0 +1,94 @@ +import { SkuListForm, type SkuListFormValues } from '#components/SkuListForm' +import { appRoutes } from '#data/routes' +import { + Button, + EmptyState, + PageLayout, + Spacer, + useCoreSdkProvider, + useTokenProvider +} from '@commercelayer/app-elements' +import { type SkuListCreate } from '@commercelayer/sdk' +import { useState } from 'react' +import { Link, useLocation } from 'wouter' + +export function SkuListNew(): JSX.Element { + const { canUser } = useTokenProvider() + const { sdkClient } = useCoreSdkProvider() + const [, setLocation] = useLocation() + + const [apiError, setApiError] = useState() + const [isSaving, setIsSaving] = useState(false) + + const goBackUrl = appRoutes.list.makePath({}) + + if (!canUser('create', 'sku_lists')) { + return ( + { + setLocation(goBackUrl) + }, + label: 'Cancel', + icon: 'x' + }} + scrollToTop + > + + + + } + /> + + ) + } + + return ( + New SKU list} + navigationButton={{ + onClick: () => { + setLocation(goBackUrl) + }, + label: 'Cancel', + icon: 'x' + }} + gap='only-top' + scrollToTop + overlay + > + + { + setIsSaving(true) + const skuList = adaptFormValuesToSkuList(formValues) + void sdkClient.sku_lists + .create(skuList) + .then(() => { + setLocation(goBackUrl) + }) + .catch((error) => { + setApiError(error) + setIsSaving(false) + }) + }} + /> + + + ) +} + +function adaptFormValuesToSkuList( + formValues: SkuListFormValues +): SkuListCreate { + return { + name: formValues.name + } +} From 40ac4dba6886b54165e3f61767080d38aab5658b Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Fri, 23 Feb 2024 14:38:11 +0100 Subject: [PATCH 02/13] fix: fix top spacing of code regex in detail page --- packages/app/src/pages/SkuListDetails.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/app/src/pages/SkuListDetails.tsx b/packages/app/src/pages/SkuListDetails.tsx index 86b87d0..bad9ffc 100644 --- a/packages/app/src/pages/SkuListDetails.tsx +++ b/packages/app/src/pages/SkuListDetails.tsx @@ -138,12 +138,14 @@ export const SkuListDetails = ( ItemTemplate={ListItemSkuListItem} /> ) : ( - + + + )} From 8f6f4dc0e46b1dd3640942eed71ca2d7573cc927 Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Fri, 23 Feb 2024 15:40:21 +0100 Subject: [PATCH 03/13] feat: add remove feature on selected items --- .../src/components/ListItemSkuListItem.tsx | 12 +++- packages/app/src/components/SkuListForm.tsx | 71 ++++++++++--------- 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/packages/app/src/components/ListItemSkuListItem.tsx b/packages/app/src/components/ListItemSkuListItem.tsx index d6813f6..8614faa 100644 --- a/packages/app/src/components/ListItemSkuListItem.tsx +++ b/packages/app/src/components/ListItemSkuListItem.tsx @@ -14,6 +14,7 @@ interface Props { resource?: SkuListItem variant?: ListItemProps['variant'] onQuantityChange?: (resource: SkuListItem, quantity: number) => void + onRemoveClick?: (resource: SkuListItem) => void isLoading?: boolean delayMs?: number } @@ -22,7 +23,8 @@ export const ListItemSkuListItem = withSkeletonTemplate( ({ resource = makeSkuListItem(), variant = 'list', - onQuantityChange + onQuantityChange, + onRemoveClick }): JSX.Element | null => { return ( ( } }} /> - diff --git a/packages/app/src/components/SkuListForm.tsx b/packages/app/src/components/SkuListForm.tsx index a997e5e..4d285fd 100644 --- a/packages/app/src/components/SkuListForm.tsx +++ b/packages/app/src/components/SkuListForm.tsx @@ -8,8 +8,7 @@ import { HookedValidationApiError, HookedValidationError, Section, - Spacer, - Text + Spacer } from '@commercelayer/app-elements' import type { SkuList, SkuListItem } from '@commercelayer/sdk' import { zodResolver } from '@hookform/resolvers/zod' @@ -21,12 +20,9 @@ import { ListItemSkuListItem } from './ListItemSkuListItem' const skuListFormSchema = z.object({ id: z.string().optional(), name: z.string().min(1), - items: z.array( - z.object({ - id: z.string().min(1), - quantity: z.string().min(1) - }) - ) + manual: z.boolean().default(true), + items: z.array(z.custom()).optional(), + sku_code_regex: z.string().optional() }) export type SkuListFormValues = z.infer @@ -49,7 +45,7 @@ export function SkuListForm({ apiError, isSubmitting }: Props): JSX.Element { - const skuListItemFormMethods = useForm({ + const skuListFormMethods = useForm({ defaultValues, resolver: zodResolver(skuListFormSchema) }) @@ -75,12 +71,14 @@ export function SkuListForm({ } }, [resource]) + console.log(skuListFormMethods.watch('manual')); + return ( <> { - onSubmit(formValues, skuListItemFormMethods.setError) + onSubmit(formValues, skuListFormMethods.setError) }} >
@@ -93,25 +91,35 @@ export function SkuListForm({ /> - SKUs - {selectedItems.map((item, idx) => ( - - { - const updatedSelectedItems: SkuListItem[] = [] - selectedItems.forEach((item) => { - if (item.id === resource.id) { - item.quantity = quantity - } - updatedSelectedItems.push(item) - }) - setSelectedItems(updatedSelectedItems) - }} - /> - - ))} + <> + {selectedItems.map((item, idx) => ( + + { + const updatedSelectedItems: SkuListItem[] = [] + selectedItems.forEach((item) => { + if (item.id === resource.id) { + item.quantity = quantity + } + updatedSelectedItems.push(item) + }) + setSelectedItems(updatedSelectedItems) + }} + onRemoveClick={(resource) => { + const updatedSelectedItems: SkuListItem[] = [] + selectedItems.forEach((item) => { + if (item.id !== resource.id) { + updatedSelectedItems.push(item) + } + }) + setSelectedItems(updatedSelectedItems) + }} + /> + + ))} + - + { @@ -131,7 +139,6 @@ export function SkuListForm({ newSkuListItem.quantity = 1 newSkuListItem.sku = selectedSku newSkuListItem.sku_code = selectedSku.code - // delete (newSkuListItem as any).id setSelectedItems([...selectedItems, newSkuListItem]) }} /> From d6c1d02f0c1a60053c533f96888faae31c21e47d Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Mon, 26 Feb 2024 18:03:51 +0100 Subject: [PATCH 04/13] chore: add missing .config.mts include --- packages/app/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json index c9b58db..55d73ab 100644 --- a/packages/app/tsconfig.json +++ b/packages/app/tsconfig.json @@ -55,6 +55,7 @@ "global.d.ts", "src", "*.config.ts", + "*.config.mts", "*.config.js", "*.config.cjs", ".eslintrc.cjs", From 0c96ebdc071feaee92b33b3741242d60a4833d91 Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Tue, 27 Feb 2024 12:38:02 +0100 Subject: [PATCH 05/13] chore: simplify sku list item component --- .../src/components/ListItemSkuListItem.tsx | 45 ++----------------- 1 file changed, 4 insertions(+), 41 deletions(-) diff --git a/packages/app/src/components/ListItemSkuListItem.tsx b/packages/app/src/components/ListItemSkuListItem.tsx index 8614faa..6c57883 100644 --- a/packages/app/src/components/ListItemSkuListItem.tsx +++ b/packages/app/src/components/ListItemSkuListItem.tsx @@ -1,31 +1,20 @@ import { makeSkuListItem } from '#mocks' import { Avatar, - Icon, - InputSpinner, ListItem, Text, - withSkeletonTemplate, - type ListItemProps + withSkeletonTemplate } from '@commercelayer/app-elements' import type { SkuListItem } from '@commercelayer/sdk' interface Props { resource?: SkuListItem - variant?: ListItemProps['variant'] - onQuantityChange?: (resource: SkuListItem, quantity: number) => void - onRemoveClick?: (resource: SkuListItem) => void isLoading?: boolean delayMs?: number } export const ListItemSkuListItem = withSkeletonTemplate( - ({ - resource = makeSkuListItem(), - variant = 'list', - onQuantityChange, - onRemoveClick - }): JSX.Element | null => { + ({ resource = makeSkuListItem() }): JSX.Element | null => { return ( ( src={resource.sku?.image_url as `https://${string}`} /> } - variant={variant} - alignItems={variant === 'list' ? 'bottom' : 'center'} + alignItems='bottom' className='bg-white' >
@@ -47,32 +35,7 @@ export const ListItemSkuListItem = withSkeletonTemplate( {resource.sku?.name}
- {variant === 'list' && ( - x {resource.quantity} - )} - {variant === 'card' && ( -
- { - if (onQuantityChange != null) { - onQuantityChange(resource, newQuantity) - } - }} - /> - -
- )} + x {resource.quantity}
) } From 153279cd115fa018facc5da93b9a67bff797049f Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Tue, 27 Feb 2024 12:43:33 +0100 Subject: [PATCH 06/13] chore: update app-elements and other deps --- packages/app/package.json | 11 +- packages/app/src/hooks/useAddItemOverlay.tsx | 2 +- packages/app/src/pages/SkuListsList.tsx | 5 +- pnpm-lock.yaml | 113 +++++++++++-------- 4 files changed, 73 insertions(+), 58 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index de45b7b..0ace3cf 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -15,22 +15,23 @@ "prepare": "touch ./public/config.local.js" }, "dependencies": { - "@commercelayer/app-elements": "1.14.6", - "@commercelayer/sdk": "5.31.1", + "@commercelayer/app-elements": "1.15.1", + "@commercelayer/sdk": "5.32.0", "@hookform/resolvers": "^3.3.4", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-hook-form": "7.49.3", + "react-hook-form": "^7.50.1", "swr": "^2.2.4", - "type-fest": "^4.9.0", - "wouter": "^2.12.1", + "type-fest": "^4.10.2", + "wouter": "^3.0.0", "zod": "^3.22.4" }, "devDependencies": { "@commercelayer/eslint-config-ts-react": "^1.3.0", "@testing-library/react": "^14.1.2", "@types/lodash": "^4.14.202", + "@types/node": "20.11.16", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", diff --git a/packages/app/src/hooks/useAddItemOverlay.tsx b/packages/app/src/hooks/useAddItemOverlay.tsx index 4c1c5b8..ccf980f 100644 --- a/packages/app/src/hooks/useAddItemOverlay.tsx +++ b/packages/app/src/hooks/useAddItemOverlay.tsx @@ -9,7 +9,7 @@ import { import type { FiltersInstructions } from '@commercelayer/app-elements/dist/ui/resources/useResourceFilters/types' import type { Sku } from '@commercelayer/sdk' import { useState } from 'react' -import { navigate, useSearch } from 'wouter/use-location' +import { navigate, useSearch } from 'wouter/use-browser-location' interface OverlayHook { show: (excludedIds?: string) => void diff --git a/packages/app/src/pages/SkuListsList.tsx b/packages/app/src/pages/SkuListsList.tsx index f164bc9..630fa11 100644 --- a/packages/app/src/pages/SkuListsList.tsx +++ b/packages/app/src/pages/SkuListsList.tsx @@ -9,13 +9,12 @@ import { useTokenProvider } from '@commercelayer/app-elements' import { Link } from 'wouter' -import { navigate, useSearch } from 'wouter/use-location' +import { navigate, useSearch } from 'wouter/use-browser-location' export function SkuListsList(): JSX.Element { const { canUser, - dashboardUrl, - settings: { mode } + settings: { mode, dashboardUrl } } = useTokenProvider() const queryString = useSearch() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6bf860f..d536f68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,14 +24,14 @@ importers: packages/app: dependencies: '@commercelayer/app-elements': - specifier: 1.14.6 - version: 1.14.6(@commercelayer/sdk@5.31.1)(query-string@8.2.0)(react-dom@18.2.0)(react-gtm-module@2.0.11)(react-hook-form@7.49.3)(react@18.2.0)(wouter@2.12.1) + specifier: 1.15.1 + version: 1.15.1(@commercelayer/sdk@5.32.0)(query-string@8.2.0)(react-dom@18.2.0)(react-gtm-module@2.0.11)(react-hook-form@7.50.1)(react@18.2.0)(wouter@3.0.1) '@commercelayer/sdk': - specifier: 5.31.1 - version: 5.31.1 + specifier: 5.32.0 + version: 5.32.0 '@hookform/resolvers': specifier: ^3.3.4 - version: 3.3.4(react-hook-form@7.49.3) + version: 3.3.4(react-hook-form@7.50.1) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -42,17 +42,17 @@ importers: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) react-hook-form: - specifier: 7.49.3 - version: 7.49.3(react@18.2.0) + specifier: ^7.50.1 + version: 7.50.1(react@18.2.0) swr: specifier: ^2.2.4 version: 2.2.5(react@18.2.0) type-fest: - specifier: ^4.9.0 + specifier: ^4.10.2 version: 4.10.2 wouter: - specifier: ^2.12.1 - version: 2.12.1(react@18.2.0) + specifier: ^3.0.0 + version: 3.0.1(react@18.2.0) zod: specifier: ^3.22.4 version: 3.22.4 @@ -66,6 +66,9 @@ importers: '@types/lodash': specifier: ^4.14.202 version: 4.14.202 + '@types/node': + specifier: 20.11.16 + version: 20.11.16 '@types/react': specifier: ^18.2.48 version: 18.2.56 @@ -86,13 +89,13 @@ importers: version: 5.3.3 vite: specifier: ^5.0.12 - version: 5.1.3 + version: 5.1.3(@types/node@20.11.16) vite-tsconfig-paths: specifier: ^4.3.1 version: 4.3.1(typescript@5.3.3)(vite@5.1.3) vitest: specifier: ^1.2.1 - version: 1.3.0(jsdom@24.0.0) + version: 1.3.0(@types/node@20.11.16)(jsdom@24.0.0) packages: @@ -332,8 +335,8 @@ packages: dev: true optional: true - /@commercelayer/app-elements@1.14.6(@commercelayer/sdk@5.31.1)(query-string@8.2.0)(react-dom@18.2.0)(react-gtm-module@2.0.11)(react-hook-form@7.49.3)(react@18.2.0)(wouter@2.12.1): - resolution: {integrity: sha512-ESNnL6LuZVENEa58buYgabBXwHQKb57Qv/jtMdl2CxPLWMF/jNfZdHqjpqyKSKBKHQyftwAc43Yc/9ytnqLQjQ==} + /@commercelayer/app-elements@1.15.1(@commercelayer/sdk@5.32.0)(query-string@8.2.0)(react-dom@18.2.0)(react-gtm-module@2.0.11)(react-hook-form@7.50.1)(react@18.2.0)(wouter@3.0.1): + resolution: {integrity: sha512-nWIPTU2l7GWl5sRpviKSpobEiiM6LoiClk9neAXbqXm9UJ2AfDzeWZt9wRpRyWbd5LftrZkhK5FN+jRAsFi9sQ==} engines: {node: '>=18', pnpm: '>=7'} peerDependencies: '@commercelayer/sdk': ^5.x @@ -344,7 +347,7 @@ packages: react-hook-form: ^7.50.x wouter: ^3.x dependencies: - '@commercelayer/sdk': 5.31.1 + '@commercelayer/sdk': 5.32.0 '@types/lodash': 4.14.202 '@types/react': 18.2.56 '@types/react-datepicker': 6.0.1(react-dom@18.2.0)(react@18.2.0) @@ -354,17 +357,17 @@ packages: lodash: 4.17.21 query-string: 8.2.0 react: 18.2.0 - react-currency-input-field: 3.7.0(react@18.2.0) + react-currency-input-field: 3.8.0(react@18.2.0) react-datepicker: 6.1.0(react-dom@18.2.0)(react@18.2.0) react-dom: 18.2.0(react@18.2.0) react-gtm-module: 2.0.11 - react-hook-form: 7.49.3(react@18.2.0) + react-hook-form: 7.50.1(react@18.2.0) react-select: 5.8.0(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0) react-tooltip: 5.26.3(react-dom@18.2.0)(react@18.2.0) swr: 2.2.5(react@18.2.0) ts-invariant: 0.10.3 type-fest: 4.10.2 - wouter: 2.12.1(react@18.2.0) + wouter: 3.0.1(react@18.2.0) zod: 3.22.4 dev: false @@ -414,11 +417,11 @@ packages: - supports-color dev: true - /@commercelayer/sdk@5.31.1: - resolution: {integrity: sha512-V40RE6AhjKP7g4Xg68A5hn+P7pDoVo0oSmDmCuPn3PsN/Hot8Nxs8ougRyOox/WI57HbdFIP4qLh3GYufoienA==} + /@commercelayer/sdk@5.32.0: + resolution: {integrity: sha512-XZXmHN3fhyhOYMqhVEHSCo5LF6jD8QbPnDP/yFfusXsi5WH1Pq5hxrLYrjJmvE0FkbIxdfZUvmawzCfaJh05rg==} engines: {node: '>=16 || ^14.17'} dependencies: - axios: 1.6.5 + axios: 1.6.7 transitivePeerDependencies: - debug dev: false @@ -801,12 +804,12 @@ packages: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} dev: true - /@hookform/resolvers@3.3.4(react-hook-form@7.49.3): + /@hookform/resolvers@3.3.4(react-hook-form@7.50.1): resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==} peerDependencies: react-hook-form: ^7.0.0 dependencies: - react-hook-form: 7.49.3(react@18.2.0) + react-hook-form: 7.50.1(react@18.2.0) dev: false /@humanwhocodes/config-array@0.11.14: @@ -1621,6 +1624,12 @@ packages: resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} dev: true + /@types/node@20.11.16: + resolution: {integrity: sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==} + dependencies: + undici-types: 5.26.5 + dev: true + /@types/normalize-package-data@2.4.4: resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} dev: true @@ -1815,7 +1824,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.9) '@types/babel__core': 7.20.5 react-refresh: 0.14.0 - vite: 5.1.3 + vite: 5.1.3(@types/node@20.11.16) transitivePeerDependencies: - supports-color dev: true @@ -2172,16 +2181,6 @@ packages: engines: {node: '>= 0.4'} dev: true - /axios@1.6.5: - resolution: {integrity: sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==} - dependencies: - follow-redirects: 1.15.5 - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - dev: false - /axios@1.6.7: resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} dependencies: @@ -2190,7 +2189,6 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: true /babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} @@ -5610,6 +5608,10 @@ packages: yallist: 4.0.0 dev: true + /mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + dev: false + /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -6606,8 +6608,8 @@ packages: strip-json-comments: 2.0.1 dev: true - /react-currency-input-field@3.7.0(react@18.2.0): - resolution: {integrity: sha512-FW/VHnwuzu6BgODDRdvm2kLaigsn3s/vLUQA1RbpxpTx8TrtcFTdp5NzX17t6Hh6SHqlMEKrhCYiOMXFDVI/QA==} + /react-currency-input-field@3.8.0(react@18.2.0): + resolution: {integrity: sha512-DKSIjacrvgUDOpuB16b+OVDvp5pbCt+s+RHJgpRZCHNhzg1yBpRUoy4fbnXpeOj0kdbwf5BaXCr2mAtxEujfhg==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18.0.0 dependencies: @@ -6642,9 +6644,9 @@ packages: resolution: {integrity: sha512-8gyj4TTxeP7eEyc2QKawEuQoAZdjKvMY4pgWfycGmqGByhs17fR+zEBs0JUDq4US/l+vbTl+6zvUIx27iDo/Vw==} dev: false - /react-hook-form@7.49.3(react@18.2.0): - resolution: {integrity: sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==} - engines: {node: '>=18', pnpm: '8'} + /react-hook-form@7.50.1(react@18.2.0): + resolution: {integrity: sha512-3PCY82oE0WgeOgUtIr3nYNNtNvqtJ7BZjsbxh6TnYNbXButaD5WpjOmTjdxZfheuHKR68qfeFnEDVYoSSFPMTQ==} + engines: {node: '>=12.22.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 dependencies: @@ -6851,6 +6853,11 @@ packages: set-function-name: 2.0.1 dev: true + /regexparam@3.0.0: + resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} + engines: {node: '>=8'} + dev: false + /registry-auth-token@5.0.2: resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} engines: {node: '>=14'} @@ -7865,6 +7872,10 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + /unique-filename@2.0.1: resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -8022,7 +8033,7 @@ packages: builtins: 5.0.1 dev: true - /vite-node@1.3.0: + /vite-node@1.3.0(@types/node@20.11.16): resolution: {integrity: sha512-D/oiDVBw75XMnjAXne/4feCkCEwcbr2SU1bjAhCcfI5Bq3VoOHji8/wCPAfUkDIeohJ5nSZ39fNxM3dNZ6OBOA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -8031,7 +8042,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.1.3 + vite: 5.1.3(@types/node@20.11.16) transitivePeerDependencies: - '@types/node' - less @@ -8054,13 +8065,13 @@ packages: debug: 4.3.4 globrex: 0.1.2 tsconfck: 3.0.2(typescript@5.3.3) - vite: 5.1.3 + vite: 5.1.3(@types/node@20.11.16) transitivePeerDependencies: - supports-color - typescript dev: true - /vite@5.1.3: + /vite@5.1.3(@types/node@20.11.16): resolution: {integrity: sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -8088,6 +8099,7 @@ packages: terser: optional: true dependencies: + '@types/node': 20.11.16 esbuild: 0.19.12 postcss: 8.4.35 rollup: 4.12.0 @@ -8095,7 +8107,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.3.0(jsdom@24.0.0): + /vitest@1.3.0(@types/node@20.11.16)(jsdom@24.0.0): resolution: {integrity: sha512-V9qb276J1jjSx9xb75T2VoYXdO1UKi+qfflY7V7w93jzX7oA/+RtYE6TcifxksxsZvygSSMwu2Uw6di7yqDMwg==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -8120,6 +8132,7 @@ packages: jsdom: optional: true dependencies: + '@types/node': 20.11.16 '@vitest/expect': 1.3.0 '@vitest/runner': 1.3.0 '@vitest/snapshot': 1.3.0 @@ -8138,8 +8151,8 @@ packages: strip-literal: 2.0.0 tinybench: 2.6.0 tinypool: 0.8.2 - vite: 5.1.3 - vite-node: 1.3.0 + vite: 5.1.3(@types/node@20.11.16) + vite-node: 1.3.0(@types/node@20.11.16) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -8290,12 +8303,14 @@ packages: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} dev: true - /wouter@2.12.1(react@18.2.0): - resolution: {integrity: sha512-G7a6JMSLSNcu6o8gdOfIzqxuo8Qx1qs+9rpVnlurH69angsSFPZP5gESNuVNeJct/MGpQg191pDo4HUjTx7IIQ==} + /wouter@3.0.1(react@18.2.0): + resolution: {integrity: sha512-Wv2FSH56LchrKKoiGXn2F8ZA9gLgKiI3JVtI64DhZXXOtbgV/pFKSyMZ5ds94LoKxkGAyNEKK0LAnTMR0LMp3g==} peerDependencies: react: '>=16.8.0' dependencies: + mitt: 3.0.1 react: 18.2.0 + regexparam: 3.0.0 use-sync-external-store: 1.2.0(react@18.2.0) dev: false From 24db6f60e88e44f8ca6ef46c0073e696a52efc83 Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Wed, 28 Feb 2024 16:11:53 +0100 Subject: [PATCH 07/13] feat: add create and edit forms --- .../components/ListItemCardSkuListItem.tsx | 73 ++++++ packages/app/src/components/SkuListForm.tsx | 158 ------------- .../components/SkuListForm/SkuListForm.tsx | 222 ++++++++++++++++++ .../app/src/components/SkuListForm/index.tsx | 1 + .../app/src/components/SkuListForm/schema.ts | 34 +++ .../app/src/components/SkuListForm/utils.ts | 71 ++++++ packages/app/src/hooks/useCreateSkuList.tsx | 56 +++++ packages/app/src/hooks/useUpdateSkuList.tsx | 99 ++++++++ packages/app/src/pages/SkuListEdit.tsx | 47 ++-- packages/app/src/pages/SkuListNew.tsx | 38 +-- packages/app/src/pages/SkuListsList.tsx | 40 +++- 11 files changed, 611 insertions(+), 228 deletions(-) create mode 100644 packages/app/src/components/ListItemCardSkuListItem.tsx delete mode 100644 packages/app/src/components/SkuListForm.tsx create mode 100644 packages/app/src/components/SkuListForm/SkuListForm.tsx create mode 100644 packages/app/src/components/SkuListForm/index.tsx create mode 100644 packages/app/src/components/SkuListForm/schema.ts create mode 100644 packages/app/src/components/SkuListForm/utils.ts create mode 100644 packages/app/src/hooks/useCreateSkuList.tsx create mode 100644 packages/app/src/hooks/useUpdateSkuList.tsx diff --git a/packages/app/src/components/ListItemCardSkuListItem.tsx b/packages/app/src/components/ListItemCardSkuListItem.tsx new file mode 100644 index 0000000..30b92c2 --- /dev/null +++ b/packages/app/src/components/ListItemCardSkuListItem.tsx @@ -0,0 +1,73 @@ +import { + Avatar, + Icon, + InputSpinner, + ListItem, + Text, + withSkeletonTemplate +} from '@commercelayer/app-elements' +import type { FormSkuListItem } from './SkuListForm' + +interface Props { + resource?: FormSkuListItem + onQuantityChange?: (resource: FormSkuListItem, quantity: number) => void + onRemoveClick?: (resource: FormSkuListItem) => void + isLoading?: boolean + delayMs?: number +} + +export const ListItemCardSkuListItem = withSkeletonTemplate( + ({ resource, onQuantityChange, onRemoveClick }): JSX.Element | null => { + return ( + + } + alignItems='center' + variant='card' + className='bg-white' + > +
+ + {resource?.sku?.code} + + + {resource?.sku?.name} + +
+
+ { + if (onQuantityChange != null && resource != null) { + onQuantityChange(resource, newQuantity) + } + }} + /> + +
+
+ ) + } +) diff --git a/packages/app/src/components/SkuListForm.tsx b/packages/app/src/components/SkuListForm.tsx deleted file mode 100644 index 4d285fd..0000000 --- a/packages/app/src/components/SkuListForm.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { useAddItemOverlay } from '#hooks/useAddItemOverlay' -import { makeSkuListItem } from '#mocks' -import { - Button, - ButtonCard, - HookedForm, - HookedInput, - HookedValidationApiError, - HookedValidationError, - Section, - Spacer -} from '@commercelayer/app-elements' -import type { SkuList, SkuListItem } from '@commercelayer/sdk' -import { zodResolver } from '@hookform/resolvers/zod' -import { useEffect, useMemo, useState } from 'react' -import { useForm, type UseFormSetError } from 'react-hook-form' -import { z } from 'zod' -import { ListItemSkuListItem } from './ListItemSkuListItem' - -const skuListFormSchema = z.object({ - id: z.string().optional(), - name: z.string().min(1), - manual: z.boolean().default(true), - items: z.array(z.custom()).optional(), - sku_code_regex: z.string().optional() -}) - -export type SkuListFormValues = z.infer - -interface Props { - resource?: SkuList - defaultValues?: Partial - isSubmitting: boolean - onSubmit: ( - formValues: SkuListFormValues, - setError: UseFormSetError - ) => void - apiError?: any -} - -export function SkuListForm({ - resource, - defaultValues, - onSubmit, - apiError, - isSubmitting -}: Props): JSX.Element { - const skuListFormMethods = useForm({ - defaultValues, - resolver: zodResolver(skuListFormSchema) - }) - - const { show: showAddItemOverlay, Overlay: AddItemOverlay } = - useAddItemOverlay() - - const [selectedItems, setSelectedItems] = useState([]) - const selectedItemsIds = useMemo(() => { - console.log(selectedItems) - const excludedIds = [] as string[] - selectedItems.forEach((sku) => { - excludedIds.push(sku.id) - }) - return excludedIds.join(',') ?? '' - }, [selectedItems]) - - useEffect(() => { - if (resource != null) { - resource.sku_list_items?.forEach((item) => { - setSelectedItems([...selectedItems, item]) - }) - } - }, [resource]) - - console.log(skuListFormMethods.watch('manual')); - - return ( - <> - { - onSubmit(formValues, skuListFormMethods.setError) - }} - > -
- - - - - <> - {selectedItems.map((item, idx) => ( - - { - const updatedSelectedItems: SkuListItem[] = [] - selectedItems.forEach((item) => { - if (item.id === resource.id) { - item.quantity = quantity - } - updatedSelectedItems.push(item) - }) - setSelectedItems(updatedSelectedItems) - }} - onRemoveClick={(resource) => { - const updatedSelectedItems: SkuListItem[] = [] - selectedItems.forEach((item) => { - if (item.id !== resource.id) { - updatedSelectedItems.push(item) - } - }) - setSelectedItems(updatedSelectedItems) - }} - /> - - ))} - - - { - showAddItemOverlay(selectedItemsIds) - }} - /> - - - - - { - const newSkuListItem = makeSkuListItem() - newSkuListItem.quantity = 1 - newSkuListItem.sku = selectedSku - newSkuListItem.sku_code = selectedSku.code - setSelectedItems([...selectedItems, newSkuListItem]) - }} - /> - -
- - - - - - -
- - ) -} diff --git a/packages/app/src/components/SkuListForm/SkuListForm.tsx b/packages/app/src/components/SkuListForm/SkuListForm.tsx new file mode 100644 index 0000000..27c37ef --- /dev/null +++ b/packages/app/src/components/SkuListForm/SkuListForm.tsx @@ -0,0 +1,222 @@ +import { ListItemCardSkuListItem } from '#components/ListItemCardSkuListItem' +import { useAddItemOverlay } from '#hooks/useAddItemOverlay' +import { + Button, + ButtonCard, + HookedForm, + HookedInput, + HookedInputTextArea, + HookedValidationApiError, + HookedValidationError, + Section, + Spacer, + Tab, + Tabs, + type TabsProps +} from '@commercelayer/app-elements' +import type { SkuList } from '@commercelayer/sdk' +import { zodResolver } from '@hookform/resolvers/zod' +import { useCallback, useEffect, useMemo } from 'react' +import { useForm, type UseFormSetError } from 'react-hook-form' +import { type z } from 'zod' +import { skuListFormSchema } from './schema' +import { makeFormSkuListItem } from './utils' + +export type SkuListFormValues = z.infer + +export interface FormSkuListItem { + id: string + sku_code: string + quantity: number + sku: { id: string; code: string; name: string; image_url: string } +} + +interface Props { + resource?: SkuList + defaultValues?: Partial + isSubmitting: boolean + onSubmit: ( + formValues: SkuListFormValues, + setError: UseFormSetError + ) => void + apiError?: any +} + +export function SkuListForm({ + resource, + defaultValues, + onSubmit, + apiError, + isSubmitting +}: Props): JSX.Element { + const skuListFormMethods = useForm({ + defaultValues, + resolver: zodResolver(skuListFormSchema) + }) + + const { show: showAddItemOverlay, Overlay: AddItemOverlay } = + useAddItemOverlay() + + const watchedFormItems = skuListFormMethods.watch('items') + const watchedFormManual = skuListFormMethods.watch('manual') + const defaultTab = useMemo(() => { + return watchedFormManual ? 0 : 1 + }, [watchedFormManual]) + + // const selectedItemsCodes = useMemo(() => { + // const excludedCodes = [] as string[] + // watchedFormItems?.forEach((sku) => { + // excludedCodes.push(sku.sku_code) + // }) + // return excludedCodes.join(',') ?? '' + // }, [watchedFormItems]) + // console.log(selectedItemsCodes) + + useEffect(() => { + if (resource != null) { + const selectedItems = skuListFormMethods.getValues('items') ?? [] + resource.sku_list_items?.forEach((item) => { + if ( + selectedItems.filter( + (selectedItem) => selectedItem.sku_code === item.sku_code + ).length === 0 + ) { + selectedItems?.push(makeFormSkuListItem(item)) + } + }) + skuListFormMethods.setValue('items', selectedItems) + } + }, [resource]) + + const handleOnTabSwitch = useCallback>( + (activeTab) => { + if (activeTab === 0 && !watchedFormManual) { + skuListFormMethods.setValue('manual', true) + skuListFormMethods.setValue('sku_code_regex', '') + } + if (activeTab === 1 && watchedFormManual) { + skuListFormMethods.setValue('manual', false) + skuListFormMethods.setValue('items', []) + } + }, + [skuListFormMethods, watchedFormManual] + ) + + return ( + <> + { + onSubmit(formValues, skuListFormMethods.setError) + }} + > +
+ + + + + + + {watchedFormItems?.map((item, idx) => ( + + { + const updatedSelectedItems: FormSkuListItem[] = [] + watchedFormItems.forEach((item) => { + if (item.sku_code === resource.sku_code) { + item.quantity = quantity + } + updatedSelectedItems.push(item) + }) + skuListFormMethods.setValue( + 'items', + updatedSelectedItems + ) + }} + onRemoveClick={(resource) => { + const updatedSelectedItems: FormSkuListItem[] = [] + watchedFormItems.forEach((item) => { + if (item.sku_code !== resource.sku_code) { + updatedSelectedItems.push(item) + } + }) + skuListFormMethods.setValue( + 'items', + updatedSelectedItems + ) + }} + /> + + ))} + + { + showAddItemOverlay('') + }} + /> + + + + + { + const selectedItems = + skuListFormMethods.getValues('items') ?? [] + if ( + selectedItems.find( + (item) => item.sku_code === selectedSku.code + ) == null + ) { + const newSkuListItem = { + id: '', + sku_code: selectedSku.code, + quantity: 1, + sku: { + id: selectedSku.id, + code: selectedSku.code, + name: selectedSku.name, + image_url: selectedSku.image_url ?? '' + } + } + selectedItems?.push(newSkuListItem) + skuListFormMethods.setValue('items', selectedItems) + } + }} + /> + + + + + + +
+ + + + + + +
+ + ) +} diff --git a/packages/app/src/components/SkuListForm/index.tsx b/packages/app/src/components/SkuListForm/index.tsx new file mode 100644 index 0000000..463917d --- /dev/null +++ b/packages/app/src/components/SkuListForm/index.tsx @@ -0,0 +1 @@ +export * from './SkuListForm' diff --git a/packages/app/src/components/SkuListForm/schema.ts b/packages/app/src/components/SkuListForm/schema.ts new file mode 100644 index 0000000..698dc6d --- /dev/null +++ b/packages/app/src/components/SkuListForm/schema.ts @@ -0,0 +1,34 @@ +import { z } from 'zod' +import type { FormSkuListItem } from './SkuListForm' + +const formSkuListItemSchema: z.ZodType = z.object({ + id: z.string(), + sku_code: z.string().min(1), + quantity: z.number().min(1), + sku: z.object({ + id: z.string().min(1), + code: z.string().min(1), + name: z.string().min(1), + image_url: z.string().min(1) + }) +}) + +const formBaseSchema = z.object({ + id: z.string().optional(), + name: z.string().min(1) +}) + +const formWithManualItemsSchema = formBaseSchema.extend({ + manual: z.literal(true), + items: z.array(formSkuListItemSchema) +}) + +const formWithAutoItemsSchema = formBaseSchema.extend({ + manual: z.literal(false), + sku_code_regex: z.string().min(1) +}) + +export const skuListFormSchema = z.discriminatedUnion('manual', [ + formWithManualItemsSchema, + formWithAutoItemsSchema +]) diff --git a/packages/app/src/components/SkuListForm/utils.ts b/packages/app/src/components/SkuListForm/utils.ts new file mode 100644 index 0000000..6316c9e --- /dev/null +++ b/packages/app/src/components/SkuListForm/utils.ts @@ -0,0 +1,71 @@ +import type { + FormSkuListItem, + SkuListFormValues +} from '#components/SkuListForm' +import type { + CommerceLayerClient, + SkuList, + SkuListCreate, + SkuListItem, + SkuListItemCreate, + SkuListItemUpdate, + SkuListUpdate +} from '@commercelayer/sdk' + +export function makeFormSkuListItem(skuListItem: SkuListItem): FormSkuListItem { + return { + id: skuListItem.id, + quantity: skuListItem.quantity ?? 1, + sku_code: skuListItem.sku_code ?? '', + sku: { + id: skuListItem.sku?.id ?? '', + code: skuListItem.sku?.code ?? '', + name: skuListItem.sku?.name ?? '', + image_url: skuListItem.sku?.image_url ?? '' + } + } +} + +export function adaptFormValuesToSkuListCreate( + formValues: SkuListFormValues +): SkuListCreate { + return { + name: formValues.name, + manual: formValues.manual, + sku_code_regex: !formValues.manual ? formValues.sku_code_regex : null + } +} + +export function adaptFormValuesToSkuListUpdate( + formValues: SkuListFormValues +): SkuListUpdate { + return { + id: formValues.id ?? '', + name: formValues.name, + manual: formValues.manual, + sku_code_regex: !formValues.manual ? formValues.sku_code_regex : null + } +} + +export function adaptFormListItemToSkuListItemCreate( + item: FormSkuListItem, + listId: SkuList['id'], + sdkClient: CommerceLayerClient +): SkuListItemCreate { + return { + sku_code: item.sku_code, + quantity: item.quantity, + sku: sdkClient.skus.relationship(item.sku.id), + sku_list: sdkClient.sku_lists.relationship(listId) + } +} + +export function adaptFormListItemToSkuListItemUpdate( + item: FormSkuListItem +): SkuListItemUpdate { + return { + id: item.id, + sku_code: item.sku_code, + quantity: item.quantity + } +} diff --git a/packages/app/src/hooks/useCreateSkuList.tsx b/packages/app/src/hooks/useCreateSkuList.tsx new file mode 100644 index 0000000..336e926 --- /dev/null +++ b/packages/app/src/hooks/useCreateSkuList.tsx @@ -0,0 +1,56 @@ +import type { SkuListFormValues } from '#components/SkuListForm' +import { + adaptFormListItemToSkuListItemCreate, + adaptFormValuesToSkuListCreate +} from '#components/SkuListForm/utils' +import { useCoreSdkProvider } from '@commercelayer/app-elements' +import { useCallback, useState } from 'react' + +interface CreateSkuListHook { + isCreatingSkuList: boolean + createSkuListError?: any + createSkuList: (formValues: SkuListFormValues) => Promise +} + +export function useCreateSkuList(): CreateSkuListHook { + const { sdkClient } = useCoreSdkProvider() + + const [isCreatingSkuList, setIsCreatingSkuList] = useState(false) + const [createSkuListError, setCreateSkuListError] = + useState() + + const createSkuList: CreateSkuListHook['createSkuList'] = useCallback( + async (formValues) => { + setIsCreatingSkuList(true) + setCreateSkuListError(undefined) + + const skuList = adaptFormValuesToSkuListCreate(formValues) + try { + const createdSkuList = await sdkClient.sku_lists.create(skuList) + if (formValues.manual && createdSkuList.id != null) { + await Promise.all( + formValues.items.map(async (item) => { + const skuListItem = adaptFormListItemToSkuListItemCreate( + item, + createdSkuList.id, + sdkClient + ) + await sdkClient.sku_list_items.create(skuListItem) + }) + ) + } + } catch (err) { + setCreateSkuListError(err) + } finally { + setIsCreatingSkuList(false) + } + }, + [] + ) + + return { + isCreatingSkuList, + createSkuListError, + createSkuList + } +} diff --git a/packages/app/src/hooks/useUpdateSkuList.tsx b/packages/app/src/hooks/useUpdateSkuList.tsx new file mode 100644 index 0000000..679ef6d --- /dev/null +++ b/packages/app/src/hooks/useUpdateSkuList.tsx @@ -0,0 +1,99 @@ +import type { SkuListFormValues } from '#components/SkuListForm' +import { + adaptFormListItemToSkuListItemCreate, + adaptFormValuesToSkuListUpdate +} from '#components/SkuListForm/utils' +import { useCoreSdkProvider } from '@commercelayer/app-elements' +import type { SkuList } from '@commercelayer/sdk' +import { useCallback, useState } from 'react' +import type { KeyedMutator } from 'swr' + +interface UpdateSkuListHook { + isUpdatingSkuList: boolean + updateSkuListError?: any + updateSkuList: ( + formValues: SkuListFormValues, + mutateSkuList: KeyedMutator + ) => Promise +} + +export function useUpdateSkuList(): UpdateSkuListHook { + const { sdkClient } = useCoreSdkProvider() + + const [isUpdatingSkuList, setIsUpdatingSkuList] = useState(false) + const [updateSkuListError, setUpdateSkuListError] = + useState() + + const updateSkuList = useCallback( + async (formValues, mutateSkuList) => { + setIsUpdatingSkuList(true) + setUpdateSkuListError(undefined) + + try { + const skuList = adaptFormValuesToSkuListUpdate(formValues) + const updatedSkuList = await sdkClient.sku_lists.update(skuList, { + include: ['sku_list_items', 'sku_list_items.sku'] + }) + if (formValues.manual && updatedSkuList.id != null) { + void mutateSkuList({ ...updatedSkuList }) + // Create or update items + await Promise.all( + formValues.items.map(async (item) => { + const itemNeedsUpdate = updatedSkuList.sku_list_items?.find( + (skuListItem) => + skuListItem.sku_code === item.sku_code && + skuListItem.quantity !== item.quantity + ) + // Item needs to be updated + if (itemNeedsUpdate != null) { + await sdkClient.sku_list_items.update({ + id: itemNeedsUpdate.id, + quantity: item.quantity + }) + console.log('updated sku list item', item) + } + const itemIsAlreadyExisting = updatedSkuList.sku_list_items?.find( + (skuListItem) => skuListItem.sku_code === item.sku_code + ) + // Item needs to be created + if (itemIsAlreadyExisting == null) { + const skuListItem = adaptFormListItemToSkuListItemCreate( + item, + updatedSkuList.id, + sdkClient + ) + await sdkClient.sku_list_items.create(skuListItem) + console.log('created sku list item', skuListItem) + } + }) + ) + // Check if any of the old items needs to be removed + if (updatedSkuList.sku_list_items != null) { + await Promise.all( + updatedSkuList.sku_list_items?.map(async (oldItem) => { + const itemIsInNewListItems = formValues.items.find( + (item) => item.sku_code === oldItem.sku_code + ) + if (itemIsInNewListItems == null) { + await sdkClient.sku_list_items.delete(oldItem.id) + console.log('deleted sku list item', oldItem.id) + } + }) + ) + } + } + } catch (err) { + setUpdateSkuListError(err) + } finally { + setIsUpdatingSkuList(false) + } + }, + [] + ) + + return { + isUpdatingSkuList, + updateSkuListError, + updateSkuList + } +} diff --git a/packages/app/src/pages/SkuListEdit.tsx b/packages/app/src/pages/SkuListEdit.tsx index 8cb4d78..ad9dc80 100644 --- a/packages/app/src/pages/SkuListEdit.tsx +++ b/packages/app/src/pages/SkuListEdit.tsx @@ -1,31 +1,29 @@ -import { SkuListForm, type SkuListFormValues } from '#components/SkuListForm' +import { SkuListForm } from '#components/SkuListForm' import { appRoutes } from '#data/routes' import { useSkuListDetails } from '#hooks/useSkuListDetails' +import { useUpdateSkuList } from '#hooks/useUpdateSkuList' import { Button, EmptyState, PageLayout, SkeletonTemplate, Spacer, - useCoreSdkProvider, useTokenProvider } from '@commercelayer/app-elements' -import { type SkuListUpdate } from '@commercelayer/sdk' -import { useState } from 'react' import { Link, useLocation, useRoute } from 'wouter' export function SkuListEdit(): JSX.Element { const { canUser } = useTokenProvider() - const { sdkClient } = useCoreSdkProvider() const [, setLocation] = useLocation() - const [apiError, setApiError] = useState() - const [isSaving, setIsSaving] = useState(false) const [, params] = useRoute<{ skuListId: string }>(appRoutes.edit.path) const skuListId = params?.skuListId ?? '' const { skuList, isLoading, mutateSkuList } = useSkuListDetails(skuListId) + const { updateSkuListError, updateSkuList, isUpdatingSkuList } = + useUpdateSkuList() + const goBackUrl = appRoutes.details.makePath({ skuListId }) if (!canUser('update', 'sku_lists')) { @@ -76,23 +74,19 @@ export function SkuListEdit(): JSX.Element { resource={skuList} defaultValues={{ id: skuList.id, - name: skuList.name + name: skuList.name, + manual: Boolean(skuList.manual), + sku_code_regex: + skuList.manual === false + ? skuList.sku_code_regex ?? '' + : undefined }} - apiError={apiError} - isSubmitting={isSaving} + apiError={updateSkuListError} + isSubmitting={isUpdatingSkuList} onSubmit={(formValues) => { - setIsSaving(true) - const skuList = adaptFormValuesToSkuList(formValues) - void sdkClient.sku_lists - .update(skuList) - .then((updatedSkuList) => { - setLocation(goBackUrl) - void mutateSkuList({ ...updatedSkuList }) - }) - .catch((error) => { - setApiError(error) - setIsSaving(false) - }) + void updateSkuList(formValues, mutateSkuList).then(() => { + setLocation(goBackUrl) + }) }} /> ) : null} @@ -100,12 +94,3 @@ export function SkuListEdit(): JSX.Element { ) } - -function adaptFormValuesToSkuList( - formValues: SkuListFormValues -): SkuListUpdate { - return { - id: formValues.id ?? '', - name: formValues.name - } -} diff --git a/packages/app/src/pages/SkuListNew.tsx b/packages/app/src/pages/SkuListNew.tsx index db2f1d5..ec80339 100644 --- a/packages/app/src/pages/SkuListNew.tsx +++ b/packages/app/src/pages/SkuListNew.tsx @@ -1,24 +1,21 @@ -import { SkuListForm, type SkuListFormValues } from '#components/SkuListForm' +import { SkuListForm } from '#components/SkuListForm' import { appRoutes } from '#data/routes' +import { useCreateSkuList } from '#hooks/useCreateSkuList' import { Button, EmptyState, PageLayout, Spacer, - useCoreSdkProvider, useTokenProvider } from '@commercelayer/app-elements' -import { type SkuListCreate } from '@commercelayer/sdk' -import { useState } from 'react' import { Link, useLocation } from 'wouter' export function SkuListNew(): JSX.Element { const { canUser } = useTokenProvider() - const { sdkClient } = useCoreSdkProvider() const [, setLocation] = useLocation() - const [apiError, setApiError] = useState() - const [isSaving, setIsSaving] = useState(false) + const { createSkuListError, createSkuList, isCreatingSkuList } = + useCreateSkuList() const goBackUrl = appRoutes.list.makePath({}) @@ -64,31 +61,16 @@ export function SkuListNew(): JSX.Element { > { - setIsSaving(true) - const skuList = adaptFormValuesToSkuList(formValues) - void sdkClient.sku_lists - .create(skuList) - .then(() => { - setLocation(goBackUrl) - }) - .catch((error) => { - setApiError(error) - setIsSaving(false) - }) + void createSkuList(formValues).then(() => { + setLocation(goBackUrl) + }) }} /> ) } - -function adaptFormValuesToSkuList( - formValues: SkuListFormValues -): SkuListCreate { - return { - name: formValues.name - } -} diff --git a/packages/app/src/pages/SkuListsList.tsx b/packages/app/src/pages/SkuListsList.tsx index 630fa11..900aab1 100644 --- a/packages/app/src/pages/SkuListsList.tsx +++ b/packages/app/src/pages/SkuListsList.tsx @@ -19,7 +19,7 @@ export function SkuListsList(): JSX.Element { const queryString = useSearch() - const { SearchWithNav, FilteredList } = useResourceFilters({ + const { SearchWithNav, FilteredList, hasActiveFilter } = useResourceFilters({ instructions }) @@ -64,16 +64,34 @@ export function SkuListsList(): JSX.Element { }} ItemTemplate={ListItemSkuList} emptyState={ - - - - ) - } - /> + hasActiveFilter ? ( + +

We didn't find any SKU lists matching the search.

+ + } + action={ + canUser('create', 'sku_lists') && ( + + + + ) + } + /> + ) : ( + + + + ) + } + /> + ) } actionButton={ canUser('create', 'sku_lists') ? ( From 84644d08b29dfa8f348df07b6591d84ddc03d958 Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Wed, 28 Feb 2024 16:36:46 +0100 Subject: [PATCH 08/13] fix: avoid using resource list in detail page --- packages/app/src/hooks/useUpdateSkuList.tsx | 10 ++-------- packages/app/src/pages/SkuListDetails.tsx | 20 +++++++------------- packages/app/src/pages/SkuListEdit.tsx | 3 ++- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/packages/app/src/hooks/useUpdateSkuList.tsx b/packages/app/src/hooks/useUpdateSkuList.tsx index 679ef6d..3246b36 100644 --- a/packages/app/src/hooks/useUpdateSkuList.tsx +++ b/packages/app/src/hooks/useUpdateSkuList.tsx @@ -4,17 +4,12 @@ import { adaptFormValuesToSkuListUpdate } from '#components/SkuListForm/utils' import { useCoreSdkProvider } from '@commercelayer/app-elements' -import type { SkuList } from '@commercelayer/sdk' import { useCallback, useState } from 'react' -import type { KeyedMutator } from 'swr' interface UpdateSkuListHook { isUpdatingSkuList: boolean updateSkuListError?: any - updateSkuList: ( - formValues: SkuListFormValues, - mutateSkuList: KeyedMutator - ) => Promise + updateSkuList: (formValues: SkuListFormValues) => Promise } export function useUpdateSkuList(): UpdateSkuListHook { @@ -25,7 +20,7 @@ export function useUpdateSkuList(): UpdateSkuListHook { useState() const updateSkuList = useCallback( - async (formValues, mutateSkuList) => { + async (formValues) => { setIsUpdatingSkuList(true) setUpdateSkuListError(undefined) @@ -35,7 +30,6 @@ export function useUpdateSkuList(): UpdateSkuListHook { include: ['sku_list_items', 'sku_list_items.sku'] }) if (formValues.manual && updatedSkuList.id != null) { - void mutateSkuList({ ...updatedSkuList }) // Create or update items await Promise.all( formValues.items.map(async (item) => { diff --git a/packages/app/src/pages/SkuListDetails.tsx b/packages/app/src/pages/SkuListDetails.tsx index bad9ffc..511fbcc 100644 --- a/packages/app/src/pages/SkuListDetails.tsx +++ b/packages/app/src/pages/SkuListDetails.tsx @@ -6,7 +6,6 @@ import { EmptyState, InputReadonly, PageLayout, - ResourceList, Section, SkeletonTemplate, Spacer, @@ -125,18 +124,13 @@ export const SkuListDetails = (
{skuList.manual === true ? ( - } - ItemTemplate={ListItemSkuListItem} - /> + <> + {skuList.sku_list_items != null + ? skuList.sku_list_items.map((item, idx) => ( + + )) + : null} + ) : ( { - void updateSkuList(formValues, mutateSkuList).then(() => { + void updateSkuList(formValues).then(() => { + void mutateSkuList() setLocation(goBackUrl) }) }} From 4e085299f3f344630990758cea14e2c58b4317e3 Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Wed, 28 Feb 2024 19:09:57 +0100 Subject: [PATCH 09/13] feat: enforce sku selection page to hide already selected items --- .../components/SkuListForm/SkuListForm.tsx | 17 +++--- packages/app/src/hooks/useAddItemOverlay.tsx | 59 ++++++++++++------- packages/app/src/hooks/useUpdateSkuList.tsx | 3 - 3 files changed, 45 insertions(+), 34 deletions(-) diff --git a/packages/app/src/components/SkuListForm/SkuListForm.tsx b/packages/app/src/components/SkuListForm/SkuListForm.tsx index 27c37ef..8a52e7d 100644 --- a/packages/app/src/components/SkuListForm/SkuListForm.tsx +++ b/packages/app/src/components/SkuListForm/SkuListForm.tsx @@ -63,14 +63,13 @@ export function SkuListForm({ return watchedFormManual ? 0 : 1 }, [watchedFormManual]) - // const selectedItemsCodes = useMemo(() => { - // const excludedCodes = [] as string[] - // watchedFormItems?.forEach((sku) => { - // excludedCodes.push(sku.sku_code) - // }) - // return excludedCodes.join(',') ?? '' - // }, [watchedFormItems]) - // console.log(selectedItemsCodes) + const selectedItemsCodes = useMemo(() => { + const excludedCodes = [] as string[] + watchedFormItems?.forEach((sku) => { + excludedCodes.push(sku.sku_code) + }) + return excludedCodes + }, [watchedFormItems]) useEffect(() => { if (resource != null) { @@ -164,7 +163,7 @@ export function SkuListForm({ padding='4' fullWidth onClick={() => { - showAddItemOverlay('') + showAddItemOverlay(selectedItemsCodes) }} /> diff --git a/packages/app/src/hooks/useAddItemOverlay.tsx b/packages/app/src/hooks/useAddItemOverlay.tsx index ccf980f..443f4b2 100644 --- a/packages/app/src/hooks/useAddItemOverlay.tsx +++ b/packages/app/src/hooks/useAddItemOverlay.tsx @@ -12,36 +12,54 @@ import { useState } from 'react' import { navigate, useSearch } from 'wouter/use-browser-location' interface OverlayHook { - show: (excludedIds?: string) => void + show: (excludedCodes?: string[]) => void Overlay: React.FC<{ onConfirm: (resource: Sku) => void }> } export function useAddItemOverlay(): OverlayHook { const { Overlay: OverlayElement, open, close } = useOverlay() - const [excludedIds, setExcludedIds] = useState('') - - const instructions: FiltersInstructions = [ - { - label: 'Search', - type: 'textSearch', - sdk: { - predicate: ['name', 'code'].join('_or_') + '_cont' - }, - render: { - component: 'searchBar' - } - } - ] + const [excludedCodes, setExcludedCodes] = useState([]) return { - show: (excludedIds) => { - console.log(excludedIds) - if (excludedIds != null) { - setExcludedIds(excludedIds) + show: (excludedCodes) => { + if (excludedCodes != null) { + setExcludedCodes(excludedCodes) } open() }, Overlay: ({ onConfirm }) => { + // filters: { code_not_in: excludedCodes } + + const instructions: FiltersInstructions = [ + { + label: 'Already selected items', + type: 'options', + sdk: { + predicate: 'code_not_in', + defaultOptions: excludedCodes + }, + render: { + component: 'inputToggleButton', + props: { + mode: 'single', + options: excludedCodes.map((code) => { + return { value: code, label: code } + }) + } + } + }, + { + label: 'Search', + type: 'textSearch', + sdk: { + predicate: ['name', 'code'].join('_or_') + '_cont' + }, + render: { + component: 'searchBar' + } + } + ] + const queryString = useSearch() const { SearchWithNav, FilteredList, hasActiveFilter } = useResourceFilters({ @@ -89,9 +107,6 @@ export function useAddItemOverlay(): OverlayHook { ( { diff --git a/packages/app/src/hooks/useUpdateSkuList.tsx b/packages/app/src/hooks/useUpdateSkuList.tsx index 3246b36..fe863e9 100644 --- a/packages/app/src/hooks/useUpdateSkuList.tsx +++ b/packages/app/src/hooks/useUpdateSkuList.tsx @@ -44,7 +44,6 @@ export function useUpdateSkuList(): UpdateSkuListHook { id: itemNeedsUpdate.id, quantity: item.quantity }) - console.log('updated sku list item', item) } const itemIsAlreadyExisting = updatedSkuList.sku_list_items?.find( (skuListItem) => skuListItem.sku_code === item.sku_code @@ -57,7 +56,6 @@ export function useUpdateSkuList(): UpdateSkuListHook { sdkClient ) await sdkClient.sku_list_items.create(skuListItem) - console.log('created sku list item', skuListItem) } }) ) @@ -70,7 +68,6 @@ export function useUpdateSkuList(): UpdateSkuListHook { ) if (itemIsInNewListItems == null) { await sdkClient.sku_list_items.delete(oldItem.id) - console.log('deleted sku list item', oldItem.id) } }) ) From fae10da971f2be662f79a5ba950fc55bc54b5616 Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Thu, 29 Feb 2024 19:09:38 +0100 Subject: [PATCH 10/13] fix: use unique keys in maps --- packages/app/src/components/SkuListForm/SkuListForm.tsx | 4 ++-- packages/app/src/pages/SkuListDetails.tsx | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/SkuListForm/SkuListForm.tsx b/packages/app/src/components/SkuListForm/SkuListForm.tsx index 8a52e7d..f9f244c 100644 --- a/packages/app/src/components/SkuListForm/SkuListForm.tsx +++ b/packages/app/src/components/SkuListForm/SkuListForm.tsx @@ -125,8 +125,8 @@ export function SkuListForm({ defaultTab={defaultTab} > - {watchedFormItems?.map((item, idx) => ( - + {watchedFormItems?.map((item) => ( + { diff --git a/packages/app/src/pages/SkuListDetails.tsx b/packages/app/src/pages/SkuListDetails.tsx index 511fbcc..7c44fab 100644 --- a/packages/app/src/pages/SkuListDetails.tsx +++ b/packages/app/src/pages/SkuListDetails.tsx @@ -126,8 +126,11 @@ export const SkuListDetails = ( {skuList.manual === true ? ( <> {skuList.sku_list_items != null - ? skuList.sku_list_items.map((item, idx) => ( - + ? skuList.sku_list_items.map((item) => ( + )) : null} From 5a336be4a03a51c5d6dc4dedcdec986d285b1956 Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Thu, 29 Feb 2024 19:24:48 +0100 Subject: [PATCH 11/13] chore: add regex link in field hint --- .../app/src/components/SkuListForm/SkuListForm.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/SkuListForm/SkuListForm.tsx b/packages/app/src/components/SkuListForm/SkuListForm.tsx index f9f244c..ea62436 100644 --- a/packages/app/src/components/SkuListForm/SkuListForm.tsx +++ b/packages/app/src/components/SkuListForm/SkuListForm.tsx @@ -200,7 +200,19 @@ export function SkuListForm({ + Use{' '} + + regular expressions + {' '} + for matching SKU codes, such as "AT | BE". + + ) }} /> From a28683f78664180c804d36ba2e62895d24ecb588 Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Mon, 11 Mar 2024 11:44:45 +0100 Subject: [PATCH 12/13] chore: rename variables for better code readability --- packages/app/src/hooks/useUpdateSkuList.tsx | 27 ++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/app/src/hooks/useUpdateSkuList.tsx b/packages/app/src/hooks/useUpdateSkuList.tsx index fe863e9..5c96d6c 100644 --- a/packages/app/src/hooks/useUpdateSkuList.tsx +++ b/packages/app/src/hooks/useUpdateSkuList.tsx @@ -29,27 +29,30 @@ export function useUpdateSkuList(): UpdateSkuListHook { const updatedSkuList = await sdkClient.sku_lists.update(skuList, { include: ['sku_list_items', 'sku_list_items.sku'] }) + // SKU list resource is updated. Now it's time to update related SKU list items. if (formValues.manual && updatedSkuList.id != null) { // Create or update items await Promise.all( formValues.items.map(async (item) => { - const itemNeedsUpdate = updatedSkuList.sku_list_items?.find( + const itemToUpdate = updatedSkuList.sku_list_items?.find( (skuListItem) => skuListItem.sku_code === item.sku_code && skuListItem.quantity !== item.quantity ) - // Item needs to be updated - if (itemNeedsUpdate != null) { + // Item needs to be updated. Item exists but quantity needs to be updated. + if (itemToUpdate != null) { await sdkClient.sku_list_items.update({ - id: itemNeedsUpdate.id, + id: itemToUpdate.id, quantity: item.quantity }) } - const itemIsAlreadyExisting = updatedSkuList.sku_list_items?.find( - (skuListItem) => skuListItem.sku_code === item.sku_code + const isExistingItem = Boolean( + updatedSkuList.sku_list_items?.some( + (skuListItem) => skuListItem.sku_code === item.sku_code + ) ) - // Item needs to be created - if (itemIsAlreadyExisting == null) { + // Item needs to be created. + if (!isExistingItem) { const skuListItem = adaptFormListItemToSkuListItemCreate( item, updatedSkuList.id, @@ -63,10 +66,12 @@ export function useUpdateSkuList(): UpdateSkuListHook { if (updatedSkuList.sku_list_items != null) { await Promise.all( updatedSkuList.sku_list_items?.map(async (oldItem) => { - const itemIsInNewListItems = formValues.items.find( - (item) => item.sku_code === oldItem.sku_code + const itemIsInFormItems = Boolean( + formValues.items.some( + (item) => item.sku_code === oldItem.sku_code + ) ) - if (itemIsInNewListItems == null) { + if (!itemIsInFormItems) { await sdkClient.sku_list_items.delete(oldItem.id) } }) From ec19e9e5f458d8dbaab41526a9672bb561fcb975 Mon Sep 17 00:00:00 2001 From: Pier Francesco Ferrari Date: Mon, 11 Mar 2024 15:58:33 +0100 Subject: [PATCH 13/13] chore: redirect to detail page after sku list create --- packages/app/src/hooks/useCreateSkuList.tsx | 4 +++- packages/app/src/pages/SkuListNew.tsx | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/app/src/hooks/useCreateSkuList.tsx b/packages/app/src/hooks/useCreateSkuList.tsx index 336e926..c6651d0 100644 --- a/packages/app/src/hooks/useCreateSkuList.tsx +++ b/packages/app/src/hooks/useCreateSkuList.tsx @@ -4,12 +4,13 @@ import { adaptFormValuesToSkuListCreate } from '#components/SkuListForm/utils' import { useCoreSdkProvider } from '@commercelayer/app-elements' +import type { SkuList } from '@commercelayer/sdk' import { useCallback, useState } from 'react' interface CreateSkuListHook { isCreatingSkuList: boolean createSkuListError?: any - createSkuList: (formValues: SkuListFormValues) => Promise + createSkuList: (formValues: SkuListFormValues) => Promise } export function useCreateSkuList(): CreateSkuListHook { @@ -39,6 +40,7 @@ export function useCreateSkuList(): CreateSkuListHook { }) ) } + return createdSkuList } catch (err) { setCreateSkuListError(err) } finally { diff --git a/packages/app/src/pages/SkuListNew.tsx b/packages/app/src/pages/SkuListNew.tsx index ec80339..c6158ff 100644 --- a/packages/app/src/pages/SkuListNew.tsx +++ b/packages/app/src/pages/SkuListNew.tsx @@ -65,8 +65,12 @@ export function SkuListNew(): JSX.Element { apiError={createSkuListError} isSubmitting={isCreatingSkuList} onSubmit={(formValues) => { - void createSkuList(formValues).then(() => { - setLocation(goBackUrl) + void createSkuList(formValues).then((createdSkuList) => { + if (createdSkuList != null) { + setLocation( + appRoutes.details.makePath({ skuListId: createdSkuList.id }) + ) + } }) }} />