• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

decentraland / marketplace / 13294446524

12 Feb 2025 08:39PM UTC coverage: 66.073% (-0.1%) from 66.189%
13294446524

push

github

web-flow
feat: update decentraland-ui2 v0.11.2 decentraland-dapps v24.1 (#2376)

* feat: update decentraland-ui2 v0.11.1 and decentraland-dapps v24

* feat: update getAnalytics behavior

* chore: update decentraland-ui2 v0.11.2 decentraland-dapps v24.1

2704 of 5329 branches covered (50.74%)

Branch coverage included in aggregate %.

15 of 27 new or added lines in 14 files covered. (55.56%)

12 existing lines in 5 files now uncovered.

7900 of 10720 relevant lines covered (73.69%)

77.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.15
/webapp/src/components/Modals/SaveToListModal/SaveToListModal.tsx
1
import React, { useCallback, useEffect, useMemo, useState } from 'react'
1✔
2
import AutoSizer from 'react-virtualized-auto-sizer'
1✔
3
import { FixedSizeList } from 'react-window'
1✔
4
import InfiniteLoader from 'react-window-infinite-loader'
1✔
5
import Modal from 'decentraland-dapps/dist/containers/Modal'
1✔
6
import { getAnalytics } from 'decentraland-dapps/dist/modules/analytics/utils'
1✔
7
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
1✔
8
import { Button, Checkbox, Icon, Loader, Message, ModalNavigation } from 'decentraland-ui'
1✔
9
import { isErrorWithMessage } from '../../../lib/error'
1✔
10
import { CreateListParameters } from '../../../modules/favorites/types'
11
import { MARKETPLACE_SERVER_URL } from '../../../modules/vendor/decentraland'
1✔
12
import { FavoritesAPI, ListOfLists } from '../../../modules/vendor/decentraland/favorites'
1✔
13
import { retryParams } from '../../../modules/vendor/decentraland/utils'
1✔
14
import * as events from '../../../utils/events'
1✔
15
import { PrivateTag } from '../../PrivateTag'
1✔
16
import {
17
  CREATE_LIST_BUTTON_DATA_TEST_ID,
18
  DEFAULT_LIST_HEIGHT,
19
  DEFAULT_LIST_WIDTH,
20
  ITEM_HEIGHT,
21
  LISTS_LOADER_DATA_TEST_ID,
22
  LIST_CHECKBOX,
23
  LIST_ITEMS_COUNT,
24
  LIST_NAME,
25
  LIST_PRIVATE,
26
  SAVE_BUTTON_DATA_TEST_ID
27
} from './constants'
1✔
28
import { PickType, Props } from './SaveToListModal.types'
1✔
29
import styles from './SaveToListModal.module.css'
1✔
30

31
const SaveToListModal = (props: Props) => {
1✔
32
  const {
33
    onClose,
34
    onSavePicks,
35
    onCreateList,
36
    isSavingPicks,
37
    identity,
38
    onFinishListCreation,
39
    metadata: { item }
40
  } = props
37✔
41

42
  const [lists, setLists] = useState<{ total: number; data: ListOfLists[] }>({
36✔
43
    total: 0,
44
    data: []
45
  })
46
  const [isLoadingLists, setIsLoadingLists] = useState(false)
36✔
47
  const [error, setError] = useState<string>()
36✔
48
  const [picks, setPicks] = useState<{
36✔
49
    pickFor: ListOfLists[]
50
    unpickFrom: ListOfLists[]
51
  }>({
52
    pickFor: [],
53
    unpickFrom: []
54
  })
55

56
  const hasChanges = picks.pickFor.length === 0 && picks.unpickFrom.length === 0
36✔
57
  const saveButtonMessage = useMemo(() => {
36✔
58
    if (picks.pickFor.length === 1 && picks.unpickFrom.length === 0) {
9!
59
      return t('save_to_list_modal.save_in_a_list', {
×
60
        name: picks.pickFor[0].name
61
      })
62
    } else if (picks.pickFor.length === 0 && picks.unpickFrom.length === 1) {
9!
63
      return t('save_to_list_modal.remove_from_a_list', {
×
64
        name: picks.unpickFrom[0].name
65
      })
66
    } else if (picks.pickFor.length > 0 && picks.unpickFrom.length === 0) {
9!
67
      return t('save_to_list_modal.save_in_lists', {
×
68
        name: picks.pickFor[0].name,
69
        count: picks.pickFor.length
70
      })
71
    } else if (picks.pickFor.length === 0 && picks.unpickFrom.length > 0) {
9!
72
      return t('save_to_list_modal.remove_from_lists', {
×
73
        name: picks.unpickFrom[0].name,
74
        count: picks.unpickFrom.length
75
      })
76
    } else {
77
      return t('save_to_list_modal.save_changes')
9✔
78
    }
79
  }, [picks.pickFor, picks.unpickFrom])
80

81
  const handleSavePicks = useCallback(() => {
36✔
82
    onSavePicks(picks.pickFor, picks.unpickFrom)
×
83
  }, [onSavePicks, picks.pickFor, picks.unpickFrom])
84

85
  const handleClose = useCallback(() => (!isLoadingLists ? onClose() : undefined), [isLoadingLists, onClose])
36!
86

87
  const addOrRemovePick = useCallback(
36✔
88
    (list: ListOfLists, type: PickType) => {
89
      if (picks[type].includes(list)) {
×
90
        setPicks({
×
91
          ...picks,
92
          [type]: picks[type].filter(l => l.id !== list.id)
×
93
        })
94
      } else {
95
        setPicks({
×
96
          ...picks,
97
          [type]: picks[type].concat(list)
98
        })
99
      }
100
    },
101
    [setPicks, picks]
102
  )
103

104
  const handlePickItem = useCallback(
36✔
105
    (index: number) => {
106
      if (lists.data[index].isItemInList) {
×
107
        addOrRemovePick(lists.data[index], PickType.UNPICK_FROM)
×
108
      } else {
109
        addOrRemovePick(lists.data[index], PickType.PICK_FOR)
×
110
      }
111
    },
112
    [addOrRemovePick, lists.data]
113
  )
114

115
  const favoritesAPI = useMemo(() => {
36✔
116
    return new FavoritesAPI(MARKETPLACE_SERVER_URL, {
9✔
117
      retries: retryParams.attempts,
118
      retryDelay: retryParams.delay,
119
      identity
120
    })
121
  }, [identity])
122

123
  const fetchNextPage = useCallback(
36✔
124
    async (startIndex: number, stopIndex: number) => {
125
      setIsLoadingLists(true)
9✔
126
      try {
9✔
127
        const result = await favoritesAPI.getLists({
9✔
128
          first: stopIndex - startIndex,
129
          skip: startIndex,
130
          itemId: item.id
131
        })
132

133
        setLists({
8✔
134
          data: lists.data.concat(result.results),
135
          total: result.total
136
        })
137
      } catch (error) {
138
        setError(isErrorWithMessage(error) ? error.message : t('global.unknown_error'))
1!
139
      } finally {
140
        setIsLoadingLists(false)
9✔
141
      }
142
    },
143
    [favoritesAPI, item.id, lists.data]
144
  )
145

146
  const isItemLoaded = useCallback(
36✔
147
    index => {
148
      const hasNextPage = lists.data.length < lists.total
22✔
149
      return !hasNextPage || index < lists.data.length
22!
150
    },
151
    [lists]
152
  )
153

154
  const createListFunction = useCallback(
36✔
155
    (params: CreateListParameters) => {
156
      onCreateList({ isLoading: true, onCreateList: createListFunction })
×
157
      favoritesAPI
×
158
        .createList(params)
159
        .then(response => {
160
          const stateLists = [...lists.data]
×
161
          stateLists.splice(1, 0, {
×
162
            ...response,
163
            itemsCount: 0,
164
            previewOfItemIds: []
165
          })
166
          setLists({
×
167
            total: lists.total++,
168
            data: stateLists
169
          })
NEW
170
          getAnalytics()?.track(events.CREATE_LIST, {
×
171
            list: response
172
          })
173
          onFinishListCreation()
×
174
        })
175
        .catch(error => {
176
          const errorMessage = isErrorWithMessage(error) ? error.message : t('global.unknown_error')
×
177
          onCreateList({
×
178
            isLoading: false,
179
            onCreateList: createListFunction,
180
            error: errorMessage
181
          })
NEW
182
          getAnalytics()?.track(events.CREATE_LIST, {
×
183
            error: errorMessage
184
          })
185
        })
186
    },
187
    [favoritesAPI, lists.data, lists.total, onCreateList, onFinishListCreation]
188
  )
189

190
  const handleOnCreateListClick = useCallback(() => {
36✔
191
    onCreateList({ onCreateList: createListFunction })
1✔
192
  }, [createListFunction, onCreateList])
193

194
  const Row = useCallback(
36✔
195
    ({ index, style }: { index: number; style: object }) => {
196
      const isPicked =
197
        (lists.data[index]?.isItemInList && !picks.unpickFrom.includes(lists.data[index])) ||
11✔
198
        (!lists.data[index]?.isItemInList && picks.pickFor.includes(lists.data[index]))
199
      return (
200
        <div style={style} tabIndex={0}>
201
          {isItemLoaded(index) ? (
11!
202
            <div className={styles.listRow}>
203
              <div className={styles.left}>
204
                <Checkbox
205
                  checked={isPicked}
206
                  disabled={isSavingPicks}
207
                  data-testid={LIST_CHECKBOX + lists.data[index].id}
208
                  className={styles.checkbox}
209
                  onChange={() => handlePickItem(index)}
×
210
                />
211
                <div className={styles.listInfo}>
212
                  <div className={styles.name} data-testid={LIST_NAME + lists.data[index].id}>
213
                    {lists.data[index].name}
214
                  </div>
215
                  <div data-testid={LIST_ITEMS_COUNT + lists.data[index].id}>
216
                    {t('save_to_list_modal.items_count', {
217
                      count: lists.data[index].itemsCount
218
                    })}
219
                  </div>
220
                </div>
221
              </div>
222
              <div className={styles.right}>
223
                {lists.data[index].isPrivate ? <PrivateTag data-testid={LIST_PRIVATE + lists.data[index].id} /> : undefined}
11✔
224
              </div>
225
            </div>
226
          ) : (
227
            t('global.loading')
228
          )}
229
        </div>
230
      )
231
    },
232
    [lists.data, picks.unpickFrom, picks.pickFor, isItemLoaded, isSavingPicks, handlePickItem]
233
  )
234

235
  useEffect(() => {
36✔
236
    void fetchNextPage(0, 24)
9✔
237
    // eslint-disable-next-line react-hooks/exhaustive-deps
238
  }, [])
239

240
  // Makes the modal dynamic in size.
241
  const desktopHeight = lists.data.length * ITEM_HEIGHT > 500 ? 500 : lists.data.length * ITEM_HEIGHT
36!
242

243
  return (
244
    <Modal size="tiny" onClose={handleClose}>
245
      <ModalNavigation title={t('save_to_list_modal.title')} onClose={handleClose} />
246
      <Modal.Content>
247
        {isLoadingLists && lists.data.length === 0 ? (
90✔
248
          <div data-testid={LISTS_LOADER_DATA_TEST_ID} className={styles.loading}>
249
            <Loader inline size="medium" active />
250
            <span>{t('global.loading')}...</span>
251
          </div>
252
        ) : null}
253
        <>
254
          {lists.data.length !== 0 ? (
36✔
255
            <>
256
              <div className={styles.separator}></div>
257
              <div className={styles.favoritesList} style={{ height: desktopHeight }}>
258
                <AutoSizer>
259
                  {({ height, width }) => (
260
                    <InfiniteLoader isItemLoaded={isItemLoaded} itemCount={lists.total} loadMoreItems={fetchNextPage}>
261
                      {({ onItemsRendered, ref }) => (
262
                        <FixedSizeList
263
                          itemCount={lists.total}
264
                          onItemsRendered={onItemsRendered}
265
                          itemSize={ITEM_HEIGHT}
266
                          height={height ?? DEFAULT_LIST_HEIGHT}
14!
267
                          width={width ?? DEFAULT_LIST_WIDTH}
14!
268
                          ref={ref}
269
                        >
270
                          {Row}
271
                        </FixedSizeList>
272
                      )}
273
                    </InfiniteLoader>
274
                  )}
275
                </AutoSizer>
276
              </div>
277
            </>
278
          ) : null}
279
          {error ? <Message error size="tiny" visible content={error} header={t('global.error')} /> : null}
36✔
280
        </>
281
      </Modal.Content>
282
      <Modal.Actions className={styles.actions}>
283
        <Button
284
          fluid
285
          secondary
286
          disabled={isLoadingLists || isSavingPicks}
54✔
287
          data-testid={CREATE_LIST_BUTTON_DATA_TEST_ID}
288
          onClick={handleOnCreateListClick}
289
        >
290
          <Icon name="plus" className={styles.icon} />
291
          {t('save_to_list_modal.create_list')}
292
        </Button>
293
        <Button
294
          fluid
295
          primary
296
          disabled={isLoadingLists || isSavingPicks || hasChanges}
66✔
297
          data-testid={SAVE_BUTTON_DATA_TEST_ID}
298
          loading={isSavingPicks}
299
          onClick={handleSavePicks}
300
        >
301
          {saveButtonMessage}
302
        </Button>
303
      </Modal.Actions>
304
    </Modal>
305
  )
306
}
307

308
export default React.memo(SaveToListModal)
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc