Skip to content

Commit

Permalink
feat(FX-4759): Display toast with actions summary for artwork lists f…
Browse files Browse the repository at this point in the history
…low (#8607)

* refactor: always call `onSave` handler

* refactor: add `types`

* refactor: add `ModifiedArtworkLists` interface

* refactor: add `ModifiedArtworkLists`

* feat: display toasts

* refactor: keep artwork entities

* refactor: displaying toast summary logic

* chore: use state data
  • Loading branch information
dimatretyak committed May 4, 2023
1 parent f513526 commit 2e66b3d
Show file tree
Hide file tree
Showing 12 changed files with 280 additions and 145 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { fireEvent } from "@testing-library/react-native"
import {
ArtworkEntity,
ArtworkListsProvider,
ArtworkListsProviderProps,
} from "app/Components/ArtworkLists/ArtworkListsContext"
import { ArtworkEntity } from "app/Components/ArtworkLists/types"
import { flushPromiseQueue } from "app/utils/tests/flushPromiseQueue"
import { renderWithHookWrappersTL } from "app/utils/tests/renderWithWrappers"
import { resolveMostRecentRelayOperation } from "app/utils/tests/resolveMostRecentRelayOperation"
Expand Down
196 changes: 106 additions & 90 deletions src/app/Components/ArtworkLists/ArtworkListsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,31 @@
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"
import { ArtworkListEntity } from "app/Components/ArtworkLists/types"
import { useArtworkListToast } from "app/Components/ArtworkLists/useArtworkListsToast"
import { CreateNewArtworkListView } from "app/Components/ArtworkLists/views/CreateNewArtworkListView/CreateNewArtworkListView"
import { SelectArtworkListsForArtworkView } from "app/Components/ArtworkLists/views/SelectArtworkListsForArtworkView/SelectArtworkListsForArtworkView"
import { createContext, Dispatch, FC, useCallback, useContext, useReducer, useState } from "react"

export enum ResultAction {
SavedToDefaultArtworkList,
RemovedFromDefaultArtworkList,
ModifiedCustomArtworkLists,
}

export interface RecentlyAddedArtworkList {
internalID: string
name: string
}

type State = {
createNewArtworkListViewVisible: boolean
artwork: ArtworkEntity | null
recentlyAddedArtworkList: RecentlyAddedArtworkList | null
selectedArtworkListIDs: string[]
addingArtworkListIDs: string[]
removingArtworkListIDs: string[]
}

export enum ArtworkListMode {
AddingArtworkListIDs = "addingArtworkListIDs",
RemovingArtworkListIDs = "removingArtworkListIDs",
}

type Action =
| { type: "SET_CREATE_NEW_ARTWORK_LIST_VIEW_VISIBLE"; payload: boolean }
| { type: "SET_ARTWORK"; payload: ArtworkEntity | null }
| { type: "SET_RECENTLY_ADDED_ARTWORK_LIST"; payload: RecentlyAddedArtworkList | null }
| { type: "RESET" }
| {
type: "ADD_OR_REMOVE_ARTWORK_LIST_ID"
payload: { mode: ArtworkListMode; artworkListID: string }
}
| { type: "SET_SELECTED_ARTWORK_LIST_IDS"; payload: string[] }

export interface ArtworkEntity {
id: string
internalID: string
title: string
year: string | null
artistNames: string | null
imageURL: string | null
}

export interface ResultArtworkListEntity {
id: string
name: string
}

export type DefaultArtworkListSaveResult = {
action: ResultAction.SavedToDefaultArtworkList | ResultAction.RemovedFromDefaultArtworkList
}

export type CustomArtworkListsSaveResult = {
action: ResultAction.ModifiedCustomArtworkLists
artworkLists: {
selected: ResultArtworkListEntity[]
added: ResultArtworkListEntity[]
removed: ResultArtworkListEntity[]
}
}

export type SaveResult = DefaultArtworkListSaveResult | CustomArtworkListsSaveResult

export interface ArtworkListsContextState {
state: State
artworkListId?: string
isSavedToArtworkList: boolean
dispatch: Dispatch<Action>
reset: () => void
onSave: (result: SaveResult) => void
}
import { createContext, FC, useCallback, useContext, useReducer, useState } from "react"
import {
ArtworkEntity,
ArtworkListAction,
ArtworkListsContextState,
ArtworkListState,
ResultAction,
SaveResult,
} from "./types"

export interface ArtworkListsProviderProps {
artworkListId?: string
// Needs for tests
artwork?: ArtworkEntity
}

export const ARTWORK_LISTS_CONTEXT_INITIAL_STATE: State = {
export const ARTWORK_LISTS_CONTEXT_INITIAL_STATE: ArtworkListState = {
createNewArtworkListViewVisible: false,
artwork: null,
recentlyAddedArtworkList: null,
selectedArtworkListIDs: [],
addingArtworkListIDs: [],
removingArtworkListIDs: [],
addingArtworkLists: [],
removingArtworkLists: [],
}

export const ArtworkListsContext = createContext<ArtworkListsContextState>(
Expand Down Expand Up @@ -118,20 +53,91 @@ export const ArtworkListsProvider: FC<ArtworkListsProviderProps> = ({
})
const toast = useArtworkListToast()

const showToastForAddedLists = (artworkLists: ArtworkListEntity[]) => {
if (artworkLists.length === 1) {
toast.addedToSingleArtworkList(artworkLists[0])
return
}

return toast.addedToMultipleArtworkLists(artworkLists)
}

const showToastForRemovedLists = (artworkLists: ArtworkListEntity[]) => {
if (artworkLists.length === 1) {
toast.removedFromSingleArtworkList(artworkLists[0])
return
}

return toast.removedFromMultipleArtworkLists(artworkLists)
}

const modifiedArtworkLists = (
addedArtworkLists: ArtworkListEntity[],
removedArtworkLists: ArtworkListEntity[]
) => {
if (addedArtworkLists.length > 0 && removedArtworkLists.length > 0) {
toast.changesSaved()
return
}

if (addedArtworkLists.length > 0) {
showToastForAddedLists(addedArtworkLists)
return
}

if (removedArtworkLists.length > 0) {
showToastForRemovedLists(removedArtworkLists)
return
}
}

const savedToDefaultArtworkList = (artwork: ArtworkEntity) => {
const openSelectArtworkListsForArtworkView = () => {
dispatch({
type: "SET_ARTWORK",
payload: artwork,
})
}

toast.savedToDefaultArtworkList(openSelectArtworkListsForArtworkView)
}

const onSave = (result: SaveResult) => {
if (artworkListId) {
if (result.action !== ResultAction.ModifiedCustomArtworkLists) {
throw new Error("You should pass `ModifiedCustomArtworkLists` action")
if (result.action !== ResultAction.ModifiedArtworkLists) {
throw new Error("You should pass `ModifiedArtworkLists` action")
}

const { selected } = result.artworkLists
const isSaved = selected.find((list) => list.id === artworkListId)
const isArtworkListAdded = isArtworkListsIncludes(artworkListId, state.addingArtworkLists)
const isArtworkListRemoved = isArtworkListsIncludes(artworkListId, state.removingArtworkLists)

if (isArtworkListAdded) {
setIsSavedToArtworkList(true)
} else if (isArtworkListRemoved) {
setIsSavedToArtworkList(false)
}

setIsSavedToArtworkList(!!isSaved)
toast.changesSaved()

return
}

if (result.action === ResultAction.SavedToDefaultArtworkList) {
savedToDefaultArtworkList(result.artwork)
return
}

if (result.action === ResultAction.RemovedFromDefaultArtworkList) {
toast.removedFromDefaultArtworkList()
return
}

if (result.action === ResultAction.ModifiedArtworkLists) {
modifiedArtworkLists(state.addingArtworkLists, state.removingArtworkLists)
return
}

throw new Error("Unexpected save result for artwork lists")
}

const reset = useCallback(() => {
Expand All @@ -140,10 +146,14 @@ export const ArtworkListsProvider: FC<ArtworkListsProviderProps> = ({
})
}, [dispatch])

const addingArtworkListIDs = state.addingArtworkLists.map((entity) => entity.internalID)
const removingArtworkListIDs = state.removingArtworkLists.map((entity) => entity.internalID)
const value: ArtworkListsContextState = {
state,
artworkListId,
isSavedToArtworkList,
addingArtworkListIDs,
removingArtworkListIDs,
dispatch,
reset,
onSave,
Expand All @@ -165,7 +175,7 @@ export const ArtworkListsProvider: FC<ArtworkListsProviderProps> = ({
)
}

const reducer = (state: State, action: Action): State => {
const reducer = (state: ArtworkListState, action: ArtworkListAction): ArtworkListState => {
switch (action.type) {
case "SET_CREATE_NEW_ARTWORK_LIST_VIEW_VISIBLE":
return {
Expand All @@ -182,22 +192,24 @@ const reducer = (state: State, action: Action): State => {
...state,
recentlyAddedArtworkList: action.payload,
}
case "ADD_OR_REMOVE_ARTWORK_LIST_ID":
case "ADD_OR_REMOVE_ARTWORK_LIST":
// eslint-disable-next-line no-case-declarations
const { artworkListID, mode } = action.payload
const { artworkList, mode } = action.payload
// eslint-disable-next-line no-case-declarations
const ids = state[mode]
const artworkLists = state[mode]
// eslint-disable-next-line no-case-declarations
const ids = artworkLists.map((artworkList) => artworkList.internalID)

if (ids.includes(artworkListID)) {
if (ids.includes(artworkList.internalID)) {
return {
...state,
[mode]: ids.filter((id) => id !== artworkListID),
[mode]: artworkLists.filter((entity) => entity.internalID !== artworkList.internalID),
}
}

return {
...state,
[mode]: [...ids, artworkListID],
[mode]: [...artworkLists, artworkList],
}
case "SET_SELECTED_ARTWORK_LIST_IDS":
return {
Expand All @@ -210,3 +222,7 @@ const reducer = (state: State, action: Action): State => {
return state
}
}

const isArtworkListsIncludes = (artworkListID: string, artworkLists: ArtworkListEntity[]) => {
return artworkLists.find((artworkList) => artworkList.internalID === artworkListID)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ArtworkEntity } from "app/Components/ArtworkLists/ArtworkListsContext"
import { ArtworkInfo } from "app/Components/ArtworkLists/components/ArtworkInfo"
import { ArtworkEntity } from "app/Components/ArtworkLists/types"
import { renderWithHookWrappersTL } from "app/utils/tests/renderWithWrappers"

describe("ArtworkInfo", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/app/Components/ArtworkLists/components/ArtworkInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, Flex, Spacer, Text } from "@artsy/palette-mobile"
import { ArtworkEntity } from "app/Components/ArtworkLists/ArtworkListsContext"
import { EntityPreview } from "app/Components/ArtworkLists/components/EntityPreview"
import { ArtworkEntity } from "app/Components/ArtworkLists/types"
import { FC } from "react"

interface ArtworkInfoProps {
Expand Down
74 changes: 74 additions & 0 deletions src/app/Components/ArtworkLists/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Dispatch } from "react"

export interface RecentlyAddedArtworkList {
internalID: string
name: string
}

export enum ResultAction {
SavedToDefaultArtworkList,
RemovedFromDefaultArtworkList,
ModifiedArtworkLists,
}

export interface ArtworkEntity {
id: string
internalID: string
title: string
year: string | null
artistNames: string | null
imageURL: string | null
}

export type SaveResult =
| {
action: ResultAction.SavedToDefaultArtworkList
artwork: ArtworkEntity
}
| {
action: ResultAction.RemovedFromDefaultArtworkList
}
| {
action: ResultAction.ModifiedArtworkLists
}

export enum ArtworkListMode {
AddingArtworkList = "addingArtworkLists",
RemovingArtworkList = "removingArtworkLists",
}

export interface ArtworkListEntity {
internalID: string
name: string
}

export type ArtworkListState = {
createNewArtworkListViewVisible: boolean
artwork: ArtworkEntity | null
recentlyAddedArtworkList: RecentlyAddedArtworkList | null
selectedArtworkListIDs: string[]
addingArtworkLists: ArtworkListEntity[]
removingArtworkLists: ArtworkListEntity[]
}

export type ArtworkListAction =
| { type: "SET_CREATE_NEW_ARTWORK_LIST_VIEW_VISIBLE"; payload: boolean }
| { type: "SET_ARTWORK"; payload: ArtworkEntity | null }
| { type: "SET_RECENTLY_ADDED_ARTWORK_LIST"; payload: RecentlyAddedArtworkList | null }
| { type: "RESET" }
| {
type: "ADD_OR_REMOVE_ARTWORK_LIST"
payload: { mode: ArtworkListMode; artworkList: ArtworkListEntity }
}
| { type: "SET_SELECTED_ARTWORK_LIST_IDS"; payload: string[] }

export interface ArtworkListsContextState {
state: ArtworkListState
artworkListId?: string
isSavedToArtworkList: boolean
addingArtworkListIDs: string[]
removingArtworkListIDs: string[]
dispatch: Dispatch<ArtworkListAction>
reset: () => void
onSave: (result: SaveResult) => void
}
Loading

0 comments on commit 2e66b3d

Please sign in to comment.