mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
fix(lib): ensure user_created is preserved in all item operations
- Add user_created field to all item update operations to maintain proper user association - Update useMyProfile hook to use direct computation instead of useMemo to avoid React hook queue issues - Refactor UserControl to use useMyProfile hook for consistency - Fix user_created handling in LocateControl, ItemFormPopup, useSelectPosition, and itemFunctions - Add user parameter to linkItem, unlinkItem, and related functions with proper TypeScript signatures - Update all function calls and tests to include user parameter - Ensure proper null safety with user ?? undefined pattern 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
db423b26f3
commit
6fcdef0433
@ -1,10 +1,9 @@
|
|||||||
import EllipsisVerticalIcon from '@heroicons/react/16/solid/EllipsisVerticalIcon'
|
import EllipsisVerticalIcon from '@heroicons/react/16/solid/EllipsisVerticalIcon'
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
import { useAuth } from '#components/Auth/useAuth'
|
import { useAuth } from '#components/Auth/useAuth'
|
||||||
import { useItems } from '#components/Map/hooks/useItems'
|
import { useMyProfile } from '#components/Map/hooks/useMyProfile'
|
||||||
|
|
||||||
import { useAppState } from './hooks/useAppState'
|
import { useAppState } from './hooks/useAppState'
|
||||||
|
|
||||||
@ -13,17 +12,14 @@ import type { Item } from '#types/Item'
|
|||||||
export const UserControl = () => {
|
export const UserControl = () => {
|
||||||
const { isAuthenticated, user, logout } = useAuth()
|
const { isAuthenticated, user, logout } = useAuth()
|
||||||
const appState = useAppState()
|
const appState = useAppState()
|
||||||
|
const { myProfile } = useMyProfile()
|
||||||
|
|
||||||
const [userProfile, setUserProfile] = useState<Item>({} as Item)
|
// Use myProfile or create a fallback object for display
|
||||||
const items = useItems()
|
const userProfile: Partial<Item> = myProfile ?? {
|
||||||
|
id: 'new',
|
||||||
useEffect(() => {
|
name: user?.first_name ?? '',
|
||||||
const profile =
|
text: '',
|
||||||
user && items.find((i) => i.user_created?.id === user.id && i.layer?.userProfileLayer)
|
}
|
||||||
profile
|
|
||||||
? setUserProfile(profile)
|
|
||||||
: setUserProfile({ id: 'new', name: user?.first_name ?? '', text: '' })
|
|
||||||
}, [user, items])
|
|
||||||
|
|
||||||
const onLogout = async () => {
|
const onLogout = async () => {
|
||||||
await toast.promise(logout(), {
|
await toast.promise(logout(), {
|
||||||
@ -52,7 +48,7 @@ export const UserControl = () => {
|
|||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<div className='tw:flex tw:mr-2'>
|
<div className='tw:flex tw:mr-2'>
|
||||||
<Link
|
<Link
|
||||||
to={`${userProfile.id && '/item/' + userProfile.id}`}
|
to={userProfile.id ? `/item/${userProfile.id}` : '#'}
|
||||||
className='tw:flex tw:items-center'
|
className='tw:flex tw:items-center'
|
||||||
>
|
>
|
||||||
{avatar && (
|
{avatar && (
|
||||||
@ -62,7 +58,7 @@ export const UserControl = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='tw:ml-2 tw:mr-2'>{userProfile.name || user?.first_name}</div>
|
<div className='tw:ml-2 tw:mr-2'>{userProfile.name ?? user?.first_name}</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div className='tw:dropdown tw:dropdown-end'>
|
<div className='tw:dropdown tw:dropdown-end'>
|
||||||
<label tabIndex={0} className='tw:btn tw:btn-ghost tw:btn-square'>
|
<label tabIndex={0} className='tw:btn tw:btn-ghost tw:btn-square'>
|
||||||
@ -73,7 +69,7 @@ export const UserControl = () => {
|
|||||||
className='tw:menu tw:menu-compact tw:dropdown-content tw:mt-4 tw:p-2 tw:shadow tw:bg-base-100 tw:rounded-box tw:w-52 tw:z-10000!'
|
className='tw:menu tw:menu-compact tw:dropdown-content tw:mt-4 tw:p-2 tw:shadow tw:bg-base-100 tw:rounded-box tw:w-52 tw:z-10000!'
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<Link to={`${userProfile.id && '/edit-item/' + userProfile.id}`}>Profile</Link>
|
<Link to={userProfile.id ? `/edit-item/${userProfile.id}` : '#'}>Profile</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to={'/user-settings'}>Settings</Link>
|
<Link to={'/user-settings'}>Settings</Link>
|
||||||
|
|||||||
@ -154,7 +154,7 @@ export const LocateControl = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
result = await myProfile.myProfile.layer.api.updateItem(updatedProfile as Item)
|
result = await myProfile.myProfile.layer.api.updateItem(updatedProfile as Item)
|
||||||
// Use server response for local state update
|
// Use server response for local state update
|
||||||
updateItem({ ...result, layer: myProfile.myProfile.layer })
|
updateItem({ ...result, layer: myProfile.myProfile.layer, user_created: user })
|
||||||
toast.update(toastId, {
|
toast.update(toastId, {
|
||||||
render: 'Position updated',
|
render: 'Position updated',
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
|||||||
@ -124,13 +124,17 @@ export function ItemFormPopup(props: Props) {
|
|||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
// Ensure the item has the layer object attached
|
// Ensure the item has the layer object attached
|
||||||
const itemWithLayer = { ...result.data, layer: popupForm.layer }
|
const itemWithLayer = {
|
||||||
|
...result.data,
|
||||||
|
layer: popupForm.layer,
|
||||||
|
user_created: user ?? undefined,
|
||||||
|
}
|
||||||
updateItem(itemWithLayer)
|
updateItem(itemWithLayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.success
|
return result.success
|
||||||
},
|
},
|
||||||
[popupForm, handleApiOperation, updateItem],
|
[popupForm, handleApiOperation, updateItem, user],
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create new item or update existing user profile
|
// Create new item or update existing user profile
|
||||||
@ -165,7 +169,11 @@ export function ItemFormPopup(props: Props) {
|
|||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
// Ensure the item has the layer object attached
|
// Ensure the item has the layer object attached
|
||||||
const itemWithLayer = { ...result.data, layer: popupForm.layer }
|
const itemWithLayer = {
|
||||||
|
...result.data,
|
||||||
|
layer: popupForm.layer,
|
||||||
|
user_created: user ?? undefined,
|
||||||
|
}
|
||||||
|
|
||||||
if (isUserProfileUpdate) {
|
if (isUserProfileUpdate) {
|
||||||
updateItem(itemWithLayer)
|
updateItem(itemWithLayer)
|
||||||
|
|||||||
@ -5,16 +5,15 @@ import { useItems, useAllItemsLoaded } from './useItems'
|
|||||||
export const useMyProfile = () => {
|
export const useMyProfile = () => {
|
||||||
const items = useItems()
|
const items = useItems()
|
||||||
const allItemsLoaded = useAllItemsLoaded()
|
const allItemsLoaded = useAllItemsLoaded()
|
||||||
|
const { user } = useAuth()
|
||||||
const user = useAuth().user
|
|
||||||
|
|
||||||
// allItemsLoaded is not reliable, so we check if items.length > 0
|
|
||||||
const isMyProfileLoaded = allItemsLoaded && items.length > 0 && !!user
|
|
||||||
|
|
||||||
// Find the user's profile item
|
// Find the user's profile item
|
||||||
const myProfile = items.find(
|
const myProfile = items.find(
|
||||||
(item) => item.layer?.userProfileLayer && item.user_created?.id === user?.id,
|
(item) => item.layer?.userProfileLayer && item.user_created?.id === user?.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// allItemsLoaded is not reliable, so we check if items.length > 0
|
||||||
|
const isMyProfileLoaded = allItemsLoaded && items.length > 0 && !!user
|
||||||
|
|
||||||
return { myProfile, isMyProfileLoaded }
|
return { myProfile, isMyProfileLoaded }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,8 @@
|
|||||||
import { createContext, useContext, useEffect, useState, useCallback } from 'react'
|
import { createContext, useContext, useEffect, useState, useCallback } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
|
import { useAuth } from '#components/Auth/useAuth'
|
||||||
|
|
||||||
import { useUpdateItem } from './useItems'
|
import { useUpdateItem } from './useItems'
|
||||||
import { useLayers } from './useLayers'
|
import { useLayers } from './useLayers'
|
||||||
import { useHasUserPermission } from './usePermissions'
|
import { useHasUserPermission } from './usePermissions'
|
||||||
@ -47,6 +49,7 @@ function useSelectPositionManager(): {
|
|||||||
const updateItem = useUpdateItem()
|
const updateItem = useUpdateItem()
|
||||||
const hasUserPermission = useHasUserPermission()
|
const hasUserPermission = useHasUserPermission()
|
||||||
const layers = useLayers()
|
const layers = useLayers()
|
||||||
|
const { user } = useAuth()
|
||||||
|
|
||||||
// Handle API operations with consistent error handling and return response data
|
// Handle API operations with consistent error handling and return response data
|
||||||
const handleApiOperation = useCallback(
|
const handleApiOperation = useCallback(
|
||||||
@ -141,7 +144,7 @@ function useSelectPositionManager(): {
|
|||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
// Find the layer object by ID from server response
|
// Find the layer object by ID from server response
|
||||||
const layer = layers.find((l) => l.id === (result.data!.layer as unknown as string))
|
const layer = layers.find((l) => l.id === (result.data!.layer as unknown as string))
|
||||||
const itemWithLayer = { ...result.data, layer }
|
const itemWithLayer = { ...result.data, layer, user_created: user ?? undefined }
|
||||||
updateItem(itemWithLayer)
|
updateItem(itemWithLayer)
|
||||||
await linkItem(updatedItem.id)
|
await linkItem(updatedItem.id)
|
||||||
setSelectPosition(null)
|
setSelectPosition(null)
|
||||||
@ -177,7 +180,7 @@ function useSelectPositionManager(): {
|
|||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
// Find the layer object by ID from server response
|
// Find the layer object by ID from server response
|
||||||
const layer = layers.find((l) => l.id === (result.data!.layer as unknown as string))
|
const layer = layers.find((l) => l.id === (result.data!.layer as unknown as string))
|
||||||
const itemWithLayer = { ...result.data, layer }
|
const itemWithLayer = { ...result.data, layer, user_created: user ?? undefined }
|
||||||
updateItem(itemWithLayer)
|
updateItem(itemWithLayer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,7 +206,7 @@ function useSelectPositionManager(): {
|
|||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
// Find the layer object by ID from server response
|
// Find the layer object by ID from server response
|
||||||
const layer = layers.find((l) => l.id === (result.data!.layer as unknown as string))
|
const layer = layers.find((l) => l.id === (result.data!.layer as unknown as string))
|
||||||
const itemWithLayer = { ...result.data, layer }
|
const itemWithLayer = { ...result.data, layer, user_created: user ?? undefined }
|
||||||
updateItem(itemWithLayer)
|
updateItem(itemWithLayer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,12 @@ vi.mock('react-toastify', () => ({
|
|||||||
describe('linkItem', () => {
|
describe('linkItem', () => {
|
||||||
const id = 'some-id'
|
const id = 'some-id'
|
||||||
let updateApi: (item: Partial<Item>) => Promise<Item> = vi.fn()
|
let updateApi: (item: Partial<Item>) => Promise<Item> = vi.fn()
|
||||||
|
const mockUser = {
|
||||||
|
id: 'user-1',
|
||||||
|
first_name: 'Test',
|
||||||
|
last_name: 'User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
}
|
||||||
const item: Item = {
|
const item: Item = {
|
||||||
layer: {
|
layer: {
|
||||||
id: 'test-layer-id',
|
id: 'test-layer-id',
|
||||||
@ -71,7 +77,7 @@ describe('linkItem', () => {
|
|||||||
describe('api rejects', () => {
|
describe('api rejects', () => {
|
||||||
it('toasts an error', async () => {
|
it('toasts an error', async () => {
|
||||||
updateApi = vi.fn().mockRejectedValue('autsch')
|
updateApi = vi.fn().mockRejectedValue('autsch')
|
||||||
await linkItem(id, item, updateItem)
|
await linkItem(id, item, updateItem, mockUser)
|
||||||
expect(toastUpdateMock).toHaveBeenCalledWith(123, expect.objectContaining({ type: 'error' }))
|
expect(toastUpdateMock).toHaveBeenCalledWith(123, expect.objectContaining({ type: 'error' }))
|
||||||
expect(updateItem).not.toHaveBeenCalled()
|
expect(updateItem).not.toHaveBeenCalled()
|
||||||
expect(toastSuccessMock).not.toHaveBeenCalled()
|
expect(toastSuccessMock).not.toHaveBeenCalled()
|
||||||
@ -87,7 +93,7 @@ describe('linkItem', () => {
|
|||||||
}
|
}
|
||||||
updateApi = vi.fn().mockResolvedValue(serverResponse)
|
updateApi = vi.fn().mockResolvedValue(serverResponse)
|
||||||
|
|
||||||
await linkItem(id, item, updateItem)
|
await linkItem(id, item, updateItem, mockUser)
|
||||||
|
|
||||||
expect(toastUpdateMock).toHaveBeenCalledWith(
|
expect(toastUpdateMock).toHaveBeenCalledWith(
|
||||||
123,
|
123,
|
||||||
|
|||||||
@ -198,8 +198,8 @@ export function ProfileForm() {
|
|||||||
state={state}
|
state={state}
|
||||||
setState={setState}
|
setState={setState}
|
||||||
updatePermission={updatePermission}
|
updatePermission={updatePermission}
|
||||||
linkItem={(id: string) => linkItem(id, item, updateItem)}
|
linkItem={(id: string) => linkItem(id, item, updateItem, user)}
|
||||||
unlinkItem={(id: string) => unlinkItem(id, item, updateItem)}
|
unlinkItem={(id: string) => unlinkItem(id, item, updateItem, user)}
|
||||||
setUrlParams={setUrlParams}
|
setUrlParams={setUrlParams}
|
||||||
></TabsForm>
|
></TabsForm>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { useMap } from 'react-leaflet'
|
import { useMap } from 'react-leaflet'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { useAuth } from '#components/Auth/useAuth'
|
||||||
import { useClusterRef } from '#components/Map/hooks/useClusterRef'
|
import { useClusterRef } from '#components/Map/hooks/useClusterRef'
|
||||||
import { useItems, useRemoveItem, useUpdateItem } from '#components/Map/hooks/useItems'
|
import { useItems, useRemoveItem, useUpdateItem } from '#components/Map/hooks/useItems'
|
||||||
import { useLayers } from '#components/Map/hooks/useLayers'
|
import { useLayers } from '#components/Map/hooks/useLayers'
|
||||||
@ -51,6 +52,7 @@ export function ProfileView({ attestationApi }: { attestationApi?: ItemsApi<any>
|
|||||||
const map = useMap()
|
const map = useMap()
|
||||||
const selectPosition = useSelectPosition()
|
const selectPosition = useSelectPosition()
|
||||||
const removeItem = useRemoveItem()
|
const removeItem = useRemoveItem()
|
||||||
|
const { user } = useAuth()
|
||||||
const tags = useTags()
|
const tags = useTags()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const hasUserPermission = useHasUserPermission()
|
const hasUserPermission = useHasUserPermission()
|
||||||
@ -208,8 +210,8 @@ export function ProfileView({ attestationApi }: { attestationApi?: ItemsApi<any>
|
|||||||
needs={needs}
|
needs={needs}
|
||||||
relations={relations}
|
relations={relations}
|
||||||
updatePermission={updatePermission}
|
updatePermission={updatePermission}
|
||||||
linkItem={(id) => linkItem(id, item, updateItem)}
|
linkItem={(id) => linkItem(id, item, updateItem, user)}
|
||||||
unlinkItem={(id) => unlinkItem(id, item, updateItem)}
|
unlinkItem={(id) => unlinkItem(id, item, updateItem, user)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -62,6 +62,7 @@ export const submitNewItem = async (
|
|||||||
layers,
|
layers,
|
||||||
addItemPopupType,
|
addItemPopupType,
|
||||||
setAddItemPopupType,
|
setAddItemPopupType,
|
||||||
|
user,
|
||||||
) => {
|
) => {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
const formItem: Item = {} as Item
|
const formItem: Item = {} as Item
|
||||||
@ -106,7 +107,7 @@ export const submitNewItem = async (
|
|||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
// Find the layer object by ID from server response
|
// Find the layer object by ID from server response
|
||||||
const layerForItem = layers.find((l) => l.id === result.data!.layer) || layer
|
const layerForItem = layers.find((l) => l.id === result.data!.layer) || layer
|
||||||
const itemWithLayer = { ...result.data, layer: layerForItem }
|
const itemWithLayer = { ...result.data, layer: layerForItem, user_created: user ?? undefined }
|
||||||
addItem(itemWithLayer)
|
addItem(itemWithLayer)
|
||||||
await linkItem(uuid)
|
await linkItem(uuid)
|
||||||
resetFilterTags()
|
resetFilterTags()
|
||||||
@ -115,7 +116,7 @@ export const submitNewItem = async (
|
|||||||
setAddItemPopupType('')
|
setAddItemPopupType('')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const linkItem = async (id: string, item: Item, updateItem) => {
|
export const linkItem = async (id: string, item: Item, updateItem, user) => {
|
||||||
const newRelations = item.relations ?? []
|
const newRelations = item.relations ?? []
|
||||||
newRelations?.push({ items_id: item.id, related_items_id: id })
|
newRelations?.push({ items_id: item.id, related_items_id: id })
|
||||||
const updatedItem = { id: item.id, relations: newRelations }
|
const updatedItem = { id: item.id, relations: newRelations }
|
||||||
@ -134,12 +135,17 @@ export const linkItem = async (id: string, item: Item, updateItem) => {
|
|||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
// Find the layer object by ID from server response or use existing layer
|
// Find the layer object by ID from server response or use existing layer
|
||||||
const layer = item.layer
|
const layer = item.layer
|
||||||
const itemWithLayer = { ...result.data, layer, relations: newRelations }
|
const itemWithLayer = {
|
||||||
|
...result.data,
|
||||||
|
layer,
|
||||||
|
relations: newRelations,
|
||||||
|
user_created: user ?? undefined,
|
||||||
|
}
|
||||||
updateItem(itemWithLayer)
|
updateItem(itemWithLayer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unlinkItem = async (id: string, item: Item, updateItem) => {
|
export const unlinkItem = async (id: string, item: Item, updateItem, user) => {
|
||||||
const newRelations = item.relations?.filter((r) => r.related_items_id !== id)
|
const newRelations = item.relations?.filter((r) => r.related_items_id !== id)
|
||||||
const updatedItem = { id: item.id, relations: newRelations }
|
const updatedItem = { id: item.id, relations: newRelations }
|
||||||
|
|
||||||
@ -157,7 +163,7 @@ export const unlinkItem = async (id: string, item: Item, updateItem) => {
|
|||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
// Find the layer object by ID from server response or use existing layer
|
// Find the layer object by ID from server response or use existing layer
|
||||||
const layer = item.layer
|
const layer = item.layer
|
||||||
const itemWithLayer = { ...result.data, layer }
|
const itemWithLayer = { ...result.data, layer, user_created: user ?? undefined }
|
||||||
updateItem(itemWithLayer)
|
updateItem(itemWithLayer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -302,6 +308,7 @@ export const onUpdateItem = async (
|
|||||||
layer: item.layer,
|
layer: item.layer,
|
||||||
markerIcon: state.marker_icon,
|
markerIcon: state.marker_icon,
|
||||||
gallery: state.gallery,
|
gallery: state.gallery,
|
||||||
|
user_created: user ?? undefined,
|
||||||
}
|
}
|
||||||
updateItem(itemWithLayer)
|
updateItem(itemWithLayer)
|
||||||
navigate(`/item/${item.id}${params && '?' + params}`)
|
navigate(`/item/${item.id}${params && '?' + params}`)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user