That’s awesome Jeff, here’s the code for both screens and for the merge funcion. Hope you can find the issue!
ObjectInventory:
const PAGINATION_LIMIT = 5
export const ObjectInventoryScreen: React.FC<RootStackScreenProps<'ObjectInventory'>> = ({ navigation }) => {
const { strings } = useLocalization()
const [selectedObjects, setSelectedObjects] = React.useState<ObjectEntity[]>([])
const { onboardingDismissed: salesFlowOnboardingDismissed } = useSalesFlowOnboarding()
const { user } = useAuth()
const [filterSheetVisible, setFilterSheetVisible] = React.useState(false)
const [selectedStatusFilterIds, setSelectedStatusFilterIds] = React.useState<AllowedObjectStatuses[]>([])
const toggleStatusFilter = (filterId: AllowedObjectStatuses) => {
if (selectedStatusFilterIds.includes(filterId)) {
return setSelectedStatusFilterIds(selectedStatusFilterIds.filter((fid) => fid !== filterId))
} else {
return setSelectedStatusFilterIds([...selectedStatusFilterIds, filterId])
}
}
const toggleObject = (object: ObjectEntity) => {
if (selectedObjects.find((selectedObject) => selectedObject.id === object.id)) {
return setSelectedObjects((selected) => selected.filter((selectedObject) => selectedObject.id !== object.id))
} else {
return setSelectedObjects((selected) => [...selected, object])
}
}
const [
getInventoryObjects,
{ data: objectsData, loading: objectsLoading, error: objectsError, fetchMore: fetchMoreObjects },
] = useGetInventoryObjectsLazyQuery({
notifyOnNetworkStatusChange: true,
})
const actualObjects = objectsData?.objects.items
useEffect(() => {
if (user?.realId && !objectsData) {
getInventoryObjects({
variables: {
userId: user.realId,
filters: {
status: selectedStatusFilterIds,
},
options: {
limit: PAGINATION_LIMIT,
},
},
})
}
}, [user?.realId, selectedStatusFilterIds])
const sellableSelectedObjects = selectedObjects.filter((obj) => obj.product.eligibleForSale)
const objectActions = [
// {
// label: 'Devolución',
// onPress: () => console.log('Lorem'),
// icon: TruckSvg,
// },
{
label: 'Solicitar venta',
badgeText: sellableSelectedObjects.length,
disabled: sellableSelectedObjects.length === 0,
onPress: () => {
const nextScreen: NavigatorScreenParams<SaleFlowStackParamList> = {
screen: 'ObjectSummary',
params: { objectsSetExternally: clone(sellableSelectedObjects) },
}
if (!salesFlowOnboardingDismissed) {
navigation.navigate('SaleFlow', { screen: 'SaleFlowOnboarding', params: { followingScreen: nextScreen } })
} else navigation.navigate('SaleFlow', nextScreen)
setSelectedObjects([])
},
icon: ShoppingBag,
},
// {
// label: 'Donar',
// onPress: () => console.log('Lorem'),
// icon: null,
// },
// {
// label: 'Poner en alquiler',
// onPress: () => console.log('Lorem'),
// icon: null,
// },
]
const showActionBar = !!selectedObjects.length
return (
<ViewWithSafeAreaInsets style={[styles.container]}>
<View style={styles.filterBar}>
<Button
icon={
filtersOnlyOnSheet > 0
? () => (
<View style={styles.filterBadge}>
<Text type='body2' weight='medium' paletteColor='contrast'>
{filtersOnlyOnSheet}
</Text>
</View>
)
: 'filter-variant'
}
contentStyle={styles.filterButtonContent}
onPress={() => setFilterSheetVisible(true)}
style={styles.filterButton}>
{strings.generic.filter}
</Button>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
{filtersForMainView.map((filter) => (
<Chip
mode='outlined'
compact
key={filter.value}
selected={!!selectedStatusFilterIds.find((sf) => sf === filter.value)}
onPress={() => toggleStatusFilter(filter.value as AllowedObjectStatuses)}>
{filter.label}
</Chip>
))}
</ScrollView>
</View>
<View style={styles.topBar}>
<Text type='body2' weight='medium'>
{objectsLoading
? `${strings.generic.loading}...`
: actualObjects?.length
? strings.ObjectInventoryScreen.objectTotal(actualObjects?.length ?? 0)
: ''}
</Text>
</View>
<View style={{ flex: 1 }}>
{objectsLoading && !actualObjects?.length ? (
<>
<LoadingLargeObjectCard />
<LoadingLargeObjectCard />
<LoadingLargeObjectCard />
<LoadingLargeObjectCard />
</>
) : null}
{actualObjects?.length ? (
<FlatList
data={actualObjects}
keyExtractor={(o) => o.id.toString()}
ListFooterComponent={() => (objectsLoading ? <LoadingLargeObjectCard /> : null)}
onEndReached={() => {
const endReached = actualObjects.length >= objectsData!.objects.total
if (objectsLoading || endReached) return
fetchMoreObjects({
variables: {
options: {
page: getNewPage(actualObjects.length, PAGINATION_LIMIT),
limit: PAGINATION_LIMIT,
},
},
})
}}
renderItem={({ item }) => (
<LargeObjectCard
object={item}
disableAnimations
onPress={() => navigation.navigate({ name: 'ObjectDetails', params: { objectGuid: item.guid } })}
checked={!!selectedObjects.find((sf) => sf.id === item.id)}
onCheck={() => toggleObject(item)}
onLongPress={() => toggleObject(item)}
/>
)}
/>
) : null}
{!objectsLoading && !actualObjects?.length ? (
<EmptyState title={strings.ObjectInventoryScreen.emptyState} />
) : null}
</View>
<ActionSheet actions={objectActions} visible={showActionBar} onClose={() => {}} />
<FilterSheet
filterCategories={FILTER_CATEGORIES}
selectedFilterIds={selectedStatusFilterIds}
visible={filterSheetVisible}
onClose={() => setFilterSheetVisible(false)}
onFilterClear={() => setSelectedStatusFilterIds([])}
onFilterChange={(item) => toggleStatusFilter(item.value as AllowedObjectStatuses)}
/>
</ViewWithSafeAreaInsets>
)
}
const getNewPage = (currentLength: number, limit: number) => Math.ceil(currentLength / limit) + 1
ObjectDetails
export const ObjectDetailsScreen: React.FC<RootStackScreenProps<'ObjectDetails'>> = ({
route: { params },
navigation,
}) => {
const width = Dimensions.get('window').width
const { strings } = useLocalization()
const [carouselIndex, setCarouselIndex] = React.useState(0)
const [fabOpen, setFabOpen] = React.useState(false)
const { navigate } = useNavigation()
const isFocused = useIsFocused()
const { onboardingDismissed: salesFlowOnboardingDismissed } = useSalesFlowOnboarding()
const utilityStyles = useUtilityStyles()
const { data, loading, error } = useGetObjectDetailsQuery({
variables: { objectId: params.objectGuid },
})
if (error) return <EmptyState title={strings.ObjectDetailsScreen.error} />
return (
<ScrollView style={styles.container}>
<View style={styles.carouselContainer}>
{data?.object.photos && data.object.photos.length > 0 ? (
<>
<Carousel
width={width}
height={width}
data={data?.object?.photos?.map((photo) => photo.url) ?? []}
scrollAnimationDuration={200}
loop={false}
onSnapToItem={(index) => setCarouselIndex(index)}
renderItem={({ item }) => (
<View style={styles.carouselItem}>
<ImageWithLoader source={{ uri: item }} style={styles.carouselImage} />
</View>
)}
/>
<View style={styles.carouselBottomInfo}>
<View style={styles.carouselIndexBadge}>
<Text style={styles.carouselIndexBadgeText}>{`${carouselIndex + 1}/${
data?.object.photos.length
}`}</Text>
</View>
<ObjectStatusBadge statusId={data.object.objectStatus.description as AllowedObjectStatuses} />
</View>
</>
) : (
<View style={[styles.noImage, { width, height: width }]}>
<NoImageSvg />
<Text type='caption'>{strings.ObjectDetailsScreen.photoEmptyState}</Text>
</View>
)}
</View>
<View style={styles.objectBasicInfoContainer}>
<View style={styles.topInfo}>
<View>
{data?.object && !loading ? (
<>
<Text type='body'>ID: {data.object.id}</Text>
<Text type='h1' weight='medium' marginTop={12}>
{data.object.product.name}
</Text>
</>
) : (
<>
<ContentLoader
speed={0.9}
width={250}
height={60}
viewBox='0 0 250 60'
backgroundColor='#f3f3f3'
foregroundColor='#ecebeb'>
<Rect x='0' y='0' rx='0' ry='0' width='80' height='16' key='id' />
<Rect x='0' y='28' rx='0' ry='0' width='100' height='28' key='title' />
</ContentLoader>
</>
)}
</View>
<Pressable
style={{ marginTop: 10 }}
onPress={() => data?.object && navigate({ name: 'ObjectEdit', params: { object: data.object } })}>
<EditSvg />
</Pressable>
</View>
<View style={{ marginTop: 16 }}>
{data && !loading ? (
<>
<Text type='body' marginBottom={16}>
{data.object.name}
</Text>
<Text type='body2'>{data.object.description}</Text>
{data.object.product.eligibleForSale && (
<View style={[utilityStyles.flexRow, { marginTop: 24 }]}>
<ObjectPropBadge icon={<SaleBadgeSvg />} text={strings.ObjectDetailsScreen.propBadges.sellable} />
</View>
)}
</>
) : (
<>
<ContentLoader speed={0.9} width={250} height={57} backgroundColor='#f3f3f3' foregroundColor='#ecebeb'>
<Rect x='0' y='2' rx='0' ry='0' width='140' height='16' key='name' />
<Rect x='0' y='38' rx='0' ry='0' width='120' height='14' key='description' />
</ContentLoader>
</>
)}
</View>
</View>
{data?.object?.product.eligibleForSale && !loading ? (
<FABGroup
open={fabOpen}
visible={Boolean(isFocused)}
handleOpenChange={setFabOpen}
actions={[
{
icon: SaleBagSvg,
label: 'Vender',
onPress: () => {
const nextScreen: NavigatorScreenParams<SaleFlowStackParamList> = {
screen: 'ObjectSummary',
params: { objectsSetExternally: [data!.object] },
}
if (!salesFlowOnboardingDismissed) {
navigate('SaleFlow', {
screen: 'SaleFlowOnboarding',
params: { followingScreen: nextScreen },
})
} else navigate('SaleFlow', nextScreen)
},
},
]}
/>
) : null}
</ScrollView>
)
}
typePolicies
const typePolicies: StrictTypedTypePolicies = {
Query: {
fields: {
objects: {
keyArgs: ['userId', 'filters'],
merge(existing, incoming, { args }) {
const page: number = args?.options?.page ?? 1
const limit: number | undefined = args?.options?.limit
const existingItems: ObjectEntity[] = existing?.items ?? []
const incomingItems: ObjectEntity[] = incoming?.items ?? []
const mergedItems: ObjectEntity[] = existingItems.slice(0)
if (page && limit) {
const offset = (page - 1) * limit
for (let i = 0; i < incomingItems.length; ++i) {
mergedItems[offset + i] = incomingItems[i]
}
} else {
mergedItems.push(...incomingItems)
}
const fullResult = {
...incoming,
items: mergedItems,
}
return fullResult
},
},
},
},
// ObjectEntity: {
// // If I use [] as my keyFields, cache gets corrupted, even though the empty array is supposed to work the same as `false` as per the docs
// // If I instead use or ['id'] or ['guid'] or ['id', 'guid'], it still doesn't work, and I get the same error as before, even though they are unique values
// keyFields: false,
// },
}