fix most customizations not being pinnable (#15578)

* fix most customizations not being pinnable

* set correct pinTypes

* fix(pinning): correct purchase types for base hair and mustaches

* automatically unpin purchased customizations

* ability to pin customization items

* Fix pin not showing on buy modal

* Pin on buy modal tweak

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
Co-authored-by: Hafiz <hafizbhamidi@gmail.com>
This commit is contained in:
Phillip Thelen
2026-01-21 20:57:42 +01:00
committed by GitHub
parent 005d14f6e8
commit f56708cd88
8 changed files with 86 additions and 20 deletions
+9 -1
View File
@@ -20,6 +20,9 @@ describe('shared.ops.unlock', () => {
beforeEach(() => {
user = generateUser();
user.balance = usersStartingGems;
user.pinnedItems.push({ type: 'background', path: 'backgrounds.backgrounds042016.giant_florals' });
user.pinnedItems.push({ type: 'haircolor', path: 'hair.color.rainbow' });
user.pinnedItems.push({ type: 'shirt', path: 'shirt.convict' });
clock = sandbox.useFakeTimers(new Date('2024-04-10'));
});
@@ -272,6 +275,7 @@ describe('shared.ops.unlock', () => {
});
it('unlocks an item (appearance)', async () => {
expect(user.pinnedItems.findIndex(item => item.type === 'shirt')).to.not.equal(-1);
const path = unlockPath.split(',')[0];
const initialShirts = Object.keys(user.purchased.shirt).length;
const [, message] = await unlock(user, { query: { path } });
@@ -282,11 +286,12 @@ describe('shared.ops.unlock', () => {
);
expect(get(user.purchased, path)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 0.5);
expect(user.pinnedItems.findIndex(item => item.type === 'shirt')).to.equal(-1);
});
it('unlocks an item (hair color)', async () => {
user.purchased.hair.color = {};
expect(user.pinnedItems.findIndex(item => item.type === 'haircolor')).to.not.equal(-1);
const path = hairUnlockPath.split(',')[0];
const initialColorHair = Object.keys(user.purchased.hair.color).length;
const [, message] = await unlock(user, { query: { path } });
@@ -297,6 +302,7 @@ describe('shared.ops.unlock', () => {
);
expect(get(user.purchased, path)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 0.5);
expect(user.pinnedItems.findIndex(item => item.type === 'haircolor')).to.equal(-1);
});
it('unlocks an item (facial hair)', async () => {
@@ -334,6 +340,7 @@ describe('shared.ops.unlock', () => {
it('unlocks an item (background)', async () => {
const initialBackgrounds = Object.keys(user.purchased.background).length;
expect(user.pinnedItems.findIndex(item => item.type === 'background')).to.not.equal(-1);
const [, message] = await unlock(user, {
query: { path: backgroundUnlockPath },
});
@@ -344,6 +351,7 @@ describe('shared.ops.unlock', () => {
);
expect(get(user.purchased, backgroundUnlockPath)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 1.75);
expect(user.pinnedItems.findIndex(item => item.type === 'background')).to.equal(-1);
});
it('handles an invalid hair path gracefully', async () => {
@@ -281,6 +281,11 @@
.badge-dialog {
left: -8px;
top: -8px;
.badge-pin {
width: 32px;
height: 32px;
}
}
.avatar {
@@ -903,8 +908,8 @@ export default {
purchaseGems () {
this.$root.$emit('bv::show::modal', 'buy-gems');
},
togglePinned () {
this.isPinned = this.$store.dispatch('user:togglePinnedItem', { type: this.item.pinType, path: this.item.path });
async togglePinned () {
this.isPinned = await this.$store.dispatch('user:togglePinnedItem', { type: this.item.pinType, path: this.item.path });
if (!this.isPinned) {
this.text(this.$t('unpinnedItem', { item: this.item.text }));
@@ -76,7 +76,21 @@
:empty-item="false"
:show-popover="Boolean(ctx.item.text)"
@click="selectItem(ctx.item)"
/>
>
<template
slot="itemBadge"
slot-scope="slotProps"
>
<span
class="badge-top"
@click.prevent.stop="togglePinned(slotProps.item)"
>
<pin-badge
:pinned="slotProps.item.pinned"
/>
</span>
</template>
</shop-item>
</template>
</item-rows>
</div>
@@ -108,6 +122,16 @@
}
</style>
<style lang="scss">
.market .badge-pin:not(.pinned) {
display: none;
}
.market .item:hover .badge-pin {
display: block;
}
</style>
<script>
import find from 'lodash/find';
import shops from '@/../../common/script/libs/shops';
@@ -118,7 +142,9 @@ import Checkbox from '@/components/ui/checkbox';
import FilterGroup from '@/components/ui/filterGroup';
import FilterSidebar from '@/components/ui/filterSidebar';
import ItemRows from '@/components/ui/itemRows';
import PinBadge from '@/components/ui/pinBadge';
import ShopItem from '../shopItem';
import pinUtils from '@/mixins/pinUtils';
export default {
components: {
@@ -126,8 +152,10 @@ export default {
FilterGroup,
FilterSidebar,
ItemRows,
PinBadge,
ShopItem,
},
mixins: [pinUtils],
data () {
return {
searchText: null,
@@ -184,8 +212,12 @@ export default {
methods: {
customizationsItems (options = {}) {
const { category, searchBy } = options;
return category.items.filter(item => !searchBy
|| item.text.toLowerCase().includes(searchBy));
return category.items
.filter(item => !searchBy || item.text.toLowerCase().includes(searchBy))
.map(item => ({
...item,
pinned: this.isPinned(item),
}));
},
emptyClick (identifier, event) {
if (event.target.tagName !== 'A') return;
@@ -180,6 +180,11 @@
.badge-dialog {
left: -8px;
top: -8px;
.badge-pin {
width: 32px;
height: 32px;
}
}
.modal-content {
@@ -16,7 +16,7 @@
.badge-pin {
background-color: $white;
color: $gray-200;
color: $gray-100;
transition: none;
display: flex;
cursor: pointer;
@@ -32,8 +32,8 @@
}
.svg-icon {
width: 100%;
height: 100%;
width: 16px;
height: 16px;
}
}
@@ -2,7 +2,19 @@ import get from 'lodash/get';
import content from '../content/index';
export default function getItemByPathAndType (type, path) {
let item = get(content, path);
let item;
if ([
'haircolor',
'hairbase',
'hairmustache',
'hairbeard',
'shirt',
'skin',
].indexOf(type) !== -1) {
item = get(content, `appearances.${path}`);
} else {
item = get(content, path);
}
if (type === 'timeTravelersStable') {
const [, animalType, key] = path.split('.');
+6 -6
View File
@@ -384,8 +384,8 @@ export default function getItemInfo (user, type, item, officialPinnedItems, lang
locked: false,
notes: '',
path: `hair.color.${item.key}`,
pinType: 'haircolor',
purchaseType: 'customization',
pinType: 'timeTravelersStable',
set: item.set,
text: item.text(language),
type: 'color',
@@ -401,7 +401,7 @@ export default function getItemInfo (user, type, item, officialPinnedItems, lang
locked: false,
notes: '',
path: `hair.base.${item.key}`,
pinType: 'timeTravelersStable',
pinType: 'hairbase',
purchaseType: 'customization',
set: item.set,
text: item.text(language),
@@ -418,7 +418,7 @@ export default function getItemInfo (user, type, item, officialPinnedItems, lang
locked: false,
notes: '',
path: `hair.mustache.${item.key}`,
pinType: 'timeTravelersStable',
pinType: 'hairmustache',
purchaseType: 'customization',
set: item.set,
text: item.text(language),
@@ -435,7 +435,7 @@ export default function getItemInfo (user, type, item, officialPinnedItems, lang
locked: false,
notes: '',
path: `hair.beard.${item.key}`,
pinType: 'timeTravelersStable',
pinType: 'hairbeard',
purchaseType: 'customization',
set: item.set,
text: item.text(language),
@@ -452,7 +452,7 @@ export default function getItemInfo (user, type, item, officialPinnedItems, lang
locked: false,
notes: '',
path: `shirt.${item.key}`,
pinType: 'timeTravelersStable',
pinType: 'shirt',
purchaseType: 'customization',
set: item.set,
text: item.text(language),
@@ -469,7 +469,7 @@ export default function getItemInfo (user, type, item, officialPinnedItems, lang
locked: false,
path: `skin.${item.key}`,
notes: '',
pinType: 'timeTravelersStable',
pinType: 'skin',
purchaseType: 'customization',
set: item.set,
text: item.text(language),
+8 -4
View File
@@ -288,14 +288,16 @@ export default async function unlock (user, req = {}, analytics) {
if (isFullSet) {
paths.forEach(pathPart => purchaseItem(pathPart, setType, user));
if (isBackground) {
paths.forEach(pathPart => {
paths.forEach(pathPart => {
if (isBackground) {
const [key, value] = splitPathItem(pathPart);
const backgroundContent = content.backgroundsFlat[value];
const itemInfo = getItemInfo(user, key, backgroundContent);
removeItemByPath(user, itemInfo.path);
});
}
} else {
removeItemByPath(user, path);
}
});
} else {
const [key, value] = splitPathItem(path);
@@ -309,6 +311,8 @@ export default async function unlock (user, req = {}, analytics) {
const backgroundContent = content.backgroundsFlat[value];
const itemInfo = getItemInfo(user, 'background', backgroundContent);
removeItemByPath(user, itemInfo.path);
} else {
removeItemByPath(user, path);
}
}
}