Skip to content

Commit

Permalink
feat: Ceate linked collection items support (#3135)
Browse files Browse the repository at this point in the history
* feat: Create new linked wearable collection

* feat: Add features module

* fix: Remove commented lines

* fix: Validate the collection name when creating it

* feat: Create linked collection items support

* fix: dev.json file
  • Loading branch information
LautaroPetaccio authored Jul 4, 2024
1 parent 574f08c commit 66f1e98
Show file tree
Hide file tree
Showing 18 changed files with 201 additions and 150 deletions.
14 changes: 6 additions & 8 deletions src/components/CollectionDetailPage/CollectionDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
isOwner
} from 'modules/collection/utils'
import { CollectionType } from 'modules/collection/types'
import { isSmart } from 'modules/item/utils'
import { isEmote, isSmart, isWearable } from 'modules/item/utils'
import { Item, ItemType, SyncStatus, VIDEO_PATH } from 'modules/item/types'
import CollectionProvider from 'components/CollectionProvider'
import LoggedInDetailPage from 'components/LoggedInDetailPage'
Expand Down Expand Up @@ -239,16 +239,14 @@ export default function CollectionDetailPage({
const canMint = canMintCollectionItems(collection, wallet.address)
const isLocked = isCollectionLocked(collection)
const isOnSale = isCollectionOnSale(collection, wallet)
const hasEmotes = items.some(item => item.type === ItemType.EMOTE)
const hasWearables = items.some(item => item.type === ItemType.WEARABLE)
const isEmoteMissingPrice = hasEmotes ? items.some(item => item.type === ItemType.EMOTE && !item.price) : false
const isWearableMissingPrice = hasWearables ? items.some(item => item.type === ItemType.WEARABLE && !item.price) : false
const hasEmotes = items.some(isEmote)
const hasWearables = items.some(isWearable)
const isEmoteMissingPrice = hasEmotes ? items.some(item => isEmote(item) && !item.price) : false
const isWearableMissingPrice = hasWearables ? items.some(item => isWearable(item) && !item.price) : false
const isSmartWearableMissingVideo = hasWearables && items.some(item => isSmart(item) && !(VIDEO_PATH in item.contents))
const hasOnlyEmotes = hasEmotes && !hasWearables
const hasOnlyWearables = hasWearables && !hasEmotes
const filteredItems = items.filter(item =>
hasOnlyWearables ? item.type === ItemType.WEARABLE : hasOnlyEmotes ? item.type === ItemType.EMOTE : item.type === tab
)
const filteredItems = items.filter(item => (hasOnlyWearables ? isWearable(item) : hasOnlyEmotes ? isEmote(item) : item.type === tab))
const showShowTabs = hasEmotes && hasWearables

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { Link, useHistory } from 'react-router-dom'
import { locations } from 'routing/locations'
import { preventDefault } from 'lib/event'
import { extractThirdPartyTokenId, extractTokenId, isThirdParty } from 'lib/urn'
import { isComplete, isFree, canManageItem, getMaxSupply, isSmart } from 'modules/item/utils'
import { isComplete, isFree, canManageItem, getMaxSupply, isSmart, isEmote } from 'modules/item/utils'
import { isLocked } from 'modules/collection/utils'
import { isEmoteData, ItemType, SyncStatus, VIDEO_PATH, WearableData } from 'modules/item/types'
import { isEmoteData, SyncStatus, VIDEO_PATH, WearableData } from 'modules/item/types'
import { FromParam } from 'modules/location/types'
import ItemStatus from 'components/ItemStatus'
import ItemBadge from 'components/ItemBadge'
Expand Down Expand Up @@ -176,7 +176,7 @@ export default function CollectionItem({ onOpenModal, onSetItems, item, collecti
{item.rarity && data.category ? <RarityBadge size="medium" rarity={item.rarity} withTooltip /> : null}
</Table.Cell>
<Table.Cell className={styles.column}>{data.category ? <div>{t(`${item.type}.category.${data.category}`)}</div> : null}</Table.Cell>
{item.type === ItemType.EMOTE && isEmoteData(data) ? (
{isEmote(item) && isEmoteData(data) ? (
<Table.Cell className={styles.column}>
{data.category ? <div>{t(`emote.play_mode.${data.loop ? 'loop' : 'simple'}.text`)}</div> : null}
</Table.Cell>
Expand Down
4 changes: 2 additions & 2 deletions src/components/ItemDetailPage/ItemDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { Network } from '@dcl/schemas'
import { locations } from 'routing/locations'
import { Collection } from 'modules/collection/types'
import { getMaxSupply, getMissingBodyShapeType, isFree, resizeImage, getThumbnailURL, isSmart } from 'modules/item/utils'
import { getMaxSupply, getMissingBodyShapeType, isFree, resizeImage, getThumbnailURL, isSmart, isWearable } from 'modules/item/utils'
import { getCollectionType, isLocked as isCollectionLocked } from 'modules/collection/utils'
import { dataURLToBlob } from 'modules/media/utils'
import { computeHashes } from 'modules/deployment/contentUtils'
Expand Down Expand Up @@ -347,7 +347,7 @@ export default function ItemDetailPage(props: Props) {
</div>
</div>

{item.type === ItemType.WEARABLE && !isSmart(item) ? (
{isWearable(item) && !isSmart(item) ? (
<div className="card">
<div className="title-card-container">
<div className="title">{t('item_detail_page.representations.title')}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from 'modules/editor/selectors'
import { fetchCollectionItemsRequest, fetchItemsRequest } from 'modules/item/actions'
import { getEmotes, getItem, hasUserOrphanItems } from 'modules/item/selectors'
import { ItemType } from 'modules/item/types'
import { isEmote } from 'modules/item/utils'
import { getSelectedCollectionId, getSelectedItemId } from 'modules/location/selectors'
import { MapStateProps, MapDispatchProps, MapDispatch } from './CenterPanel.types'
import CenterPanel from './CenterPanel'
Expand All @@ -53,7 +53,7 @@ const mapState = (state: RootState): MapStateProps => {
const selectedBaseWearablesByBodyShape = getSelectedBaseWearablesByBodyShape(state)
const visibleItems = getVisibleItems(state)
const emote = getEmote(state)
const isPLayingIdleEmote = !visibleItems.some(item => item.type === ItemType.EMOTE) && emote === PreviewEmote.IDLE
const isPLayingIdleEmote = !visibleItems.some(isEmote) && emote === PreviewEmote.IDLE
/* The library react-dropzone doesn't work as expected when an Iframe is present in the current view.
This way, we're getting when the CreateSingleItemModal is open to disable the drag and drop events in the Iframe
and the library react-dropzone works as expected in the CreateSingleItemModal.
Expand Down
3 changes: 2 additions & 1 deletion src/components/ItemEditorPage/CenterPanel/CenterPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { isDevelopment } from 'lib/environment'
import { extractThirdPartyTokenId, extractTokenId, isThirdParty } from 'lib/urn'
import { isTPCollection } from 'modules/collection/utils'
import { ItemType } from 'modules/item/types'
import { isEmote } from 'modules/item/utils'
import { toBase64, toHex } from 'modules/editor/utils'
import { getSkinColors, getEyeColors, getHairColors } from 'modules/editor/avatar'
import BuilderIcon from 'components/Icon'
Expand Down Expand Up @@ -272,7 +273,7 @@ export default class CenterPanel extends React.PureComponent<Props, State> {
wearableController
} = this.props
const { isShowingAvatarAttributes, showSceneBoundaries, isLoading } = this.state
const isRenderingAnEmote = visibleItems.some(item => item.type === ItemType.EMOTE) && selectedItem?.type === ItemType.EMOTE
const isRenderingAnEmote = visibleItems.some(isEmote) && selectedItem?.type === ItemType.EMOTE
const zoom = emote === PreviewEmote.JUMP ? 1 : undefined

return (
Expand Down
8 changes: 4 additions & 4 deletions src/components/ItemEditorPage/LeftPanel/Items/Items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from 'decentraland-ui'
import { extractThirdPartyTokenId, extractTokenId, isThirdParty } from 'lib/urn'
import { Item, ItemType } from 'modules/item/types'
import { hasBodyShape } from 'modules/item/utils'
import { hasBodyShape, isEmote, isWearable } from 'modules/item/utils'
import { TP_TRESHOLD_TO_REVIEW } from 'modules/collection/constants'
import { LEFT_PANEL_PAGE_SIZE } from '../../constants'
import Collapsable from 'components/Collapsable'
Expand Down Expand Up @@ -77,7 +77,7 @@ export default class Items extends React.PureComponent<Props, State> {

let newVisibleItemIds = visibleItems.filter(_item => _item.id !== item.id)

if (item.type === ItemType.EMOTE) {
if (isEmote(item)) {
if (this.isVisible(item)) {
if (isPlayingEmote) {
wearableController?.emote.pause() as void
Expand Down Expand Up @@ -165,8 +165,8 @@ export default class Items extends React.PureComponent<Props, State> {
}

renderSidebarCategory = (items: Item[]) => {
const wearableItems = items.filter(item => item.type === ItemType.WEARABLE)
const emoteItems = items.filter(item => item.type === ItemType.EMOTE)
const wearableItems = items.filter(isWearable)
const emoteItems = items.filter(isEmote)

if (wearableItems.length === 0 || emoteItems.length === 0) {
return items.map(this.renderSidebarItem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { Icon, Popup } from 'decentraland-ui'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import ItemImage from 'components/ItemImage'
import ItemStatus from 'components/ItemStatus'
import { ItemType } from 'modules/item/types'
import { getMissingBodyShapeType, hasBodyShape } from 'modules/item/utils'
import { getMissingBodyShapeType, hasBodyShape, isEmote } from 'modules/item/utils'
import { locations } from 'routing/locations'
import { Props } from './SidebarItem.types'
import './SidebarItem.css'
Expand All @@ -18,7 +17,7 @@ class SidebarItem extends React.PureComponent<Props> {

renderToggleItem() {
const { item, isVisible, isPlayingEmote } = this.props
if (item.type === ItemType.EMOTE) {
if (isEmote(item)) {
return <Icon className="toggle-emote" name={isVisible && isPlayingEmote ? 'pause' : 'play'} onClick={this.handleClick} />
} else {
return <div className={`toggle ${isVisible ? 'is-visible' : 'is-hidden'}`} onClick={this.handleClick}></div>
Expand Down
7 changes: 4 additions & 3 deletions src/components/ItemEditorPage/RightPanel/RightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
getHideableBodyPartCategories,
getHideableWearableCategories,
isSmart,
hasVideo
hasVideo,
isWearable
} from 'modules/item/utils'
import { isLocked } from 'modules/collection/utils'
import { computeHashes } from 'modules/deployment/contentUtils'
Expand Down Expand Up @@ -83,7 +84,7 @@ export default class RightPanel extends React.PureComponent<Props, State> {
setItem(item: Item) {
const data = item.data

if (item.type === ItemType.WEARABLE && data.replaces?.length) {
if (isWearable(item) && data.replaces?.length) {
// Move all items that are in replaces array to hides array
data.hides = data.hides.concat(data.replaces)
data.replaces = []
Expand Down Expand Up @@ -525,7 +526,7 @@ export default class RightPanel extends React.PureComponent<Props, State> {
const isItemLocked = collection && isLocked(collection)
const canEditItemMetadata = this.canEditItemMetadata(item)

const categories = item ? (item.type === ItemType.WEARABLE ? getWearableCategories(item.contents) : getEmoteCategories()) : []
const categories = item ? (isWearable(item) ? getWearableCategories(item.contents) : getEmoteCategories()) : []

return isLoading ? (
<Loader size="massive" active />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ import { T, t } from 'decentraland-dapps/dist/modules/translation/utils'
import { config } from 'config'
import { EngineType, getModelData } from 'lib/getModelData'
import { getExtension, toMB } from 'lib/file'
import { buildThirdPartyURN, DecodedURN, decodeURN, URNType } from 'lib/urn'
import {
buildThirdPartyURN,
buildThirdPartyV2URN,
decodedCollectionsUrnAreEqual,
DecodedURN,
decodeURN,
isThirdPartyCollectionDecodedUrn,
isThirdPartyV2CollectionDecodedUrn,
URNType
} from 'lib/urn'
import { convertImageIntoWearableThumbnail, dataURLToBlob, getImageType } from 'modules/media/utils'
import { ImageType } from 'modules/media/types'
import { MultipleItemsSaveState } from 'modules/ui/createMultipleItems/reducer'
Expand Down Expand Up @@ -165,33 +174,41 @@ export default class CreateAndEditMultipleItemsModal extends React.PureComponent

// Generate or set the correct URN for the items taking into consideration the selected collection
const decodedCollectionUrn: DecodedURN<any> | null = collection?.urn ? decodeURN(collection.urn) : null
// Check if the collection is a third party collection
if (
decodedCollectionUrn &&
decodedCollectionUrn.type === URNType.COLLECTIONS_THIRDPARTY &&
decodedCollectionUrn.thirdPartyCollectionId
(isThirdPartyCollectionDecodedUrn(decodedCollectionUrn) || isThirdPartyV2CollectionDecodedUrn(decodedCollectionUrn))
) {
const decodedUrn: DecodedURN<any> | null = loadedFile.wearable.id ? decodeURN(loadedFile.wearable.id) : null
if (loadedFile.wearable.id && decodedUrn && decodedUrn.type === URNType.COLLECTIONS_THIRDPARTY) {
const { thirdPartyName, thirdPartyCollectionId } = decodedUrn

if (
(thirdPartyCollectionId && thirdPartyCollectionId !== decodedCollectionUrn.thirdPartyCollectionId) ||
(thirdPartyName && thirdPartyName !== decodedCollectionUrn.thirdPartyName)
) {
throw new Error(t('create_and_edit_multiple_items_modal.invalid_urn'))
}
if (decodedUrn.thirdPartyTokenId) {
itemFactory.withUrn(
buildThirdPartyURN(
decodedCollectionUrn.thirdPartyName,
decodedCollectionUrn.thirdPartyCollectionId,
decodedUrn.thirdPartyTokenId
)
const thirdPartyTokenId =
loadedFile.wearable.id &&
decodedUrn &&
(decodedUrn.type === URNType.COLLECTIONS_THIRDPARTY || decodedUrn.type === URNType.COLLECTIONS_THIRDPARTY_V2)
? decodedUrn.thirdPartyTokenId ?? null
: null

// Check if the decoded collections match a the collection level
if (decodedUrn && !decodedCollectionsUrnAreEqual(decodedCollectionUrn, decodedUrn)) {
throw new Error(t('create_and_edit_multiple_items_modal.invalid_urn'))
}

// Build the third party item URN in accordance ot the collection URN
if (isThirdPartyCollectionDecodedUrn(decodedCollectionUrn)) {
itemFactory.withUrn(
buildThirdPartyURN(
decodedCollectionUrn.thirdPartyName,
decodedCollectionUrn.thirdPartyCollectionId,
thirdPartyTokenId ?? uuid.v4()
)
}
)
} else {
itemFactory.withUrn(
buildThirdPartyURN(decodedCollectionUrn.thirdPartyName, decodedCollectionUrn.thirdPartyCollectionId, uuid.v4())
buildThirdPartyV2URN(
decodedCollectionUrn.thirdPartyLinkedCollectionName,
decodedCollectionUrn.linkedCollectionNetwork,
decodedCollectionUrn.linkedCollectionAddress,
thirdPartyTokenId ?? uuid.v4()
)
)
}
}
Expand Down
Loading

0 comments on commit 66f1e98

Please sign in to comment.