diff --git a/migrations/20170811_pinned_items.js b/migrations/20170811_pinned_items.js
new file mode 100644
index 0000000000..3c12a01531
--- /dev/null
+++ b/migrations/20170811_pinned_items.js
@@ -0,0 +1,108 @@
+var updateStore = require('../website/common/script/libs/updateStore');
+var getItemInfo = require('../website/common/script/libs/getItemInfo');
+
+var migrationName = '20170811_pinned_items.js';
+var authorName = 'paglias'; // in case script author needs to know when their ...
+var authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; //... own data is done
+
+/*
+ * Migrate existing in app rewards lists to pinned items
+ */
+
+var monk = require('monk');
+var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
+var dbUsers = monk(connectionString).get('users', { castIds: false });
+
+function processUsers(lastId) {
+ // specify a query to limit the affected users (empty for all users):
+ var query = {
+ 'migration':{$ne:migrationName},
+ };
+
+ if (lastId) {
+ query._id = {
+ $gt: lastId
+ }
+ }
+
+ return dbUsers.find(query, {
+ sort: {_id: 1},
+ limit: 250,
+ })
+ .then(updateUsers)
+ .catch(function (err) {
+ console.log(err);
+ return exiting(1, 'ERROR! ' + err);
+ });
+}
+
+var progressCount = 1000;
+var count = 0;
+
+function updateUsers (users) {
+ if (!users || users.length === 0) {
+ console.warn('All appropriate users found and modified.');
+ displayData();
+ return;
+ }
+
+ var userPromises = users.map(updateUser);
+ var lastUser = users[users.length - 1];
+
+ return Promise.all(userPromises)
+ .then(function () {
+ processUsers(lastUser._id);
+ });
+}
+
+function updateUser (user) {
+ count++;
+
+ var set = {'migration': migrationName};
+
+ var oldRewardsList = updateStore(user);
+ var newPinnedItems = [
+ {
+ type: 'armoire',
+ path: 'armoire',
+ },
+ {
+ type: 'potion',
+ path: 'potion',
+ },
+ ];
+
+ oldRewardsList.forEach(item => {
+ var type = 'marketGear';
+
+ var itemInfo = getItemInfo(user, 'marketGear', item);
+ newPinnedItems.push({
+ type,
+ path: itemInfo.path,
+ })
+ });
+
+ set.pinnedItems = newPinnedItems;
+
+ if (count % progressCount == 0) console.warn(count + ' ' + user._id);
+ if (user._id == authorUuid) console.warn(authorName + ' processed');
+
+ return dbUsers.update({_id: user._id}, {$set:set});
+}
+
+function displayData() {
+ console.warn('\n' + count + ' users processed\n');
+ return exiting(0);
+}
+
+function exiting(code, msg) {
+ code = code || 0; // 0 = success
+ if (code && !msg) { msg = 'ERROR!'; }
+ if (msg) {
+ if (code) { console.error(msg); }
+ else { console.log( msg); }
+ }
+ process.exit(code);
+}
+
+module.exports = processUsers;
diff --git a/test/common/ops/buyArmoire.js b/test/common/ops/buyArmoire.js
index 22dcc8a442..facf87132c 100644
--- a/test/common/ops/buyArmoire.js
+++ b/test/common/ops/buyArmoire.js
@@ -68,23 +68,6 @@ describe('shared.ops.buyArmoire', () => {
done();
}
});
-
- it('does not open without Ultimate Gear achievement', (done) => {
- user.achievements.ultimateGearSets = {healer: false, wizard: false, rogue: false, warrior: false};
-
- try {
- buyArmoire(user);
- } catch (err) {
- expect(err).to.be.an.instanceof(NotAuthorized);
- expect(err.message).to.equal(i18n.t('cannotBuyItem'));
- expect(user.items.gear.owned).to.eql({
- weapon_warrior_0: true,
- });
- expect(user.items.food).to.be.empty;
- expect(user.stats.exp).to.eql(0);
- done();
- }
- });
});
context('non-gear awards', () => {
diff --git a/website/client/components/inventory/equipment/attributesPopover.vue b/website/client/components/inventory/equipment/attributesPopover.vue
index 1beca9046c..a65347bd65 100644
--- a/website/client/components/inventory/equipment/attributesPopover.vue
+++ b/website/client/components/inventory/equipment/attributesPopover.vue
@@ -1,7 +1,7 @@
div
- h4.popover-content-title {{ item.text() }}
- .popover-content-text {{ item.notes() }}
+ h4.popover-content-title {{ itemText }}
+ .popover-content-text {{ itemNotes }}
.popover-content-attr(v-for="attr in ATTRIBUTES", :key="attr", v-once)
span.popover-content-attr-key {{ `${$t(attr)}: ` }}
span.popover-content-attr-val {{ `+${item[attr]}` }}
@@ -20,6 +20,20 @@ div
...mapState({
ATTRIBUTES: 'constants.ATTRIBUTES',
}),
+ itemText () {
+ if (this.item.text instanceof Function) {
+ return this.item.text();
+ } else {
+ return this.item.text;
+ }
+ },
+ itemNotes () {
+ if (this.item.notes instanceof Function) {
+ return this.item.notes();
+ } else {
+ return this.item.notes;
+ }
+ },
},
};
diff --git a/website/client/components/shops/_isPinned.js b/website/client/components/shops/_isPinned.js
new file mode 100644
index 0000000000..99635e40a0
--- /dev/null
+++ b/website/client/components/shops/_isPinned.js
@@ -0,0 +1,6 @@
+export default function _isPinned (user, item) {
+ const isUnpinned = user.unpinnedItems.findIndex(unpinned => unpinned.path === item.path) > -1;
+ const isPinned = user.pinnedItems.findIndex(pinned => pinned.path === item.path) > -1;
+
+ return isPinned && !isUnpinned;
+}
diff --git a/website/client/components/shops/buyModal.vue b/website/client/components/shops/buyModal.vue
index af788ef7c2..40c96594fc 100644
--- a/website/client/components/shops/buyModal.vue
+++ b/website/client/components/shops/buyModal.vue
@@ -6,8 +6,9 @@
@change="onChange($event)"
)
span.badge.badge-pill.badge-dialog(
- :class="{'item-selected-badge': true}",
- v-if="withPin"
+ :class="{'item-selected-badge': item.pinned}",
+ v-if="withPin",
+ @click.prevent.stop="togglePinned()"
)
span.svg-icon.inline.color.icon-10(v-html="icons.pin")
@@ -127,7 +128,15 @@
padding: 8px 10px;
top: -12px;
background: white;
+ cursor: pointer;
+
+ &.item-selected-badge {
+ background: $purple-300;
+ color: $white;
+ }
}
+
+
}
@@ -182,6 +191,9 @@
this.$emit('buyPressed', this.item);
this.hideDialog();
},
+ togglePinned () {
+ this.$emit('togglePinned', this.item);
+ },
hideDialog () {
this.$root.$emit('hide::modal', 'buy-modal');
},
diff --git a/website/client/components/shops/index.vue b/website/client/components/shops/index.vue
index 350c3035b6..71d7b2c608 100644
--- a/website/client/components/shops/index.vue
+++ b/website/client/components/shops/index.vue
@@ -11,10 +11,17 @@
diff --git a/website/client/components/shops/market/index.vue b/website/client/components/shops/market/index.vue
index b9d8354837..0685ea8fc4 100644
--- a/website/client/components/shops/market/index.vue
+++ b/website/client/components/shops/market/index.vue
@@ -48,14 +48,11 @@
:key="item.key",
:item="item",
:price="item.value",
- :priceType="item.currency",
:itemContentClass="'shop_'+item.key",
:emptyItem="false",
:popoverPosition="'top'",
@click="selectedGearToBuy = item"
)
- template(slot="popoverContent", scope="ctx")
- equipmentAttributesPopover(:item="ctx.item")
h1.mb-0.page-header(v-once) {{ $t('market') }}
@@ -102,15 +99,10 @@
shopItem(
:key="ctx.item.key",
:item="ctx.item",
- :price="ctx.item.value",
- :priceType="ctx.item.currency",
- :itemContentClass="'shop_'+ctx.item.key",
:emptyItem="userItems.gear[ctx.item.key] === undefined",
:popoverPosition="'top'",
@click="selectedGearToBuy = ctx.item"
)
- template(slot="popoverContent", scope="ctx")
- equipmentAttributesPopover(:item="ctx.item")
template(slot="itemBadge", scope="ctx")
span.badge.badge-pill.badge-item.badge-svg(
@@ -142,12 +134,9 @@
div.items
shopItem(
- v-for="item in sortedMarketItems(category, selectedSortItemsBy, searchTextThrottled)",
+ v-for="item in sortedMarketItems(category, selectedSortItemsBy, searchTextThrottled, hidePinned)",
:key="item.key",
:item="item",
- :price="item.value",
- :priceType="item.currency",
- :itemContentClass="item.class",
:emptyItem="false",
:popoverPosition="'top'",
@click="selectedItemToBuy = item"
@@ -161,6 +150,12 @@
:count="userItems[item.purchaseType][item.key] || 0"
)
+ span.badge.badge-pill.badge-item.badge-svg(
+ :class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
+ @click.prevent.stop="togglePinned(ctx.item)"
+ )
+ span.svg-icon.inline.icon-12.color(v-html="icons.pin")
+
drawer(
:title="$t('quickInventory')"
@@ -226,7 +221,8 @@
priceType="gold",
:withPin="true",
@change="resetGearToBuy($event)",
- @buyPressed="buyGear($event)"
+ @buyPressed="buyGear($event)",
+ @togglePinned="togglePinned($event)"
)
template(slot="item", scope="ctx")
div
@@ -244,7 +240,8 @@
:item="selectedItemToBuy",
:priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''",
@change="resetItemToBuy($event)",
- @buyPressed="buyItem($event)"
+ @buyPressed="buyItem($event)",
+ @togglePinned="togglePinned($event)"
)
template(slot="item", scope="ctx")
item.flat(
@@ -380,8 +377,6 @@
import toggleSwitch from 'client/components/ui/toggleSwitch';
import Avatar from 'client/components/avatar';
- import EquipmentAttributesPopover from 'client/components/inventory/equipment/attributesPopover';
-
import SellModal from './sellModal.vue';
import BuyModal from '../buyModal.vue';
import EquipmentAttributesGrid from './equipmentAttributesGrid.vue';
@@ -398,12 +393,15 @@
import svgHealer from 'assets/svg/healer.svg';
import featuredItems from 'common/script/content/shop-featuredItems';
+ import getItemInfo from 'common/script/libs/getItemInfo';
import _filter from 'lodash/filter';
import _sortBy from 'lodash/sortBy';
import _map from 'lodash/map';
import _throttle from 'lodash/throttle';
+ import _isPinned from '../_isPinned';
+
const sortGearTypes = ['sortByType', 'sortByPrice', 'sortByCon', 'sortByPer', 'sortByStr', 'sortByInt'];
const sortGearTypeMap = {
@@ -429,7 +427,6 @@ export default {
bDropdown,
bDropdownItem,
- EquipmentAttributesPopover,
SellModal,
BuyModal,
EquipmentAttributesGrid,
@@ -523,7 +520,7 @@ export default {
featuredItems () {
return featuredItems.market.map(i => {
- return this.content.gear.flat[i];
+ return getItemInfo(this.user, 'marketGear', this.content.gear.flat[i]);
});
},
},
@@ -577,11 +574,12 @@ export default {
filteredGear (groupByClass, searchBy, sortBy, hideLocked, hidePinned) {
let result = _filter(this.content.gear.flat, ['klass', groupByClass]);
result = _map(result, (e) => {
- return {
- ...e,
- pinned: false, // TODO read pinned state
- locked: this.isGearLocked(e),
- };
+ let newItem = getItemInfo(this.user, 'marketGear', e);
+
+ newItem.pinned = _isPinned(this.user, newItem);
+ newItem.locked = this.isGearLocked(newItem);
+
+ return newItem;
});
result = _filter(result, (gear) => {
@@ -607,9 +605,27 @@ export default {
return result;
},
- sortedMarketItems (category, sortBy, searchBy) {
- let result = _filter(category.items, (i) => {
- return !searchBy || i.text.toLowerCase().indexOf(searchBy) !== -1;
+ sortedMarketItems (category, sortBy, searchBy, hidePinned) {
+ let result = _map(category.items, (e) => {
+ return {
+ ...e,
+ pinned: _isPinned(this.user, e),
+ };
+ });
+
+ result = _filter(result, (item) => {
+ if (hidePinned && item.pinned) {
+ return false;
+ }
+
+ if (searchBy) {
+ let foundPosition = item.text().toLowerCase().indexOf(searchBy);
+ if (foundPosition === -1) {
+ return false;
+ }
+ }
+
+ return true;
});
switch (sortBy) {
@@ -656,9 +672,9 @@ export default {
};
},
togglePinned (item) {
- let isPinned = Boolean(item.pinned);
- item.pinned = !isPinned;
- this.$store.dispatch(isPinned ? 'shops:unpinGear' : 'shops:pinGear', {key: item.key});
+ if (!this.$store.dispatch('user:togglePinnedItem', {type: item.pinType, path: item.path})) {
+ this.$parent.showUnpinNotification(item);
+ }
},
buyGear (item) {
this.$store.dispatch('shops:buyItem', {key: item.key});
@@ -669,7 +685,6 @@ export default {
},
created () {
this.$store.dispatch('shops:fetchMarket');
-
this.selectedGroupGearByClass = this.userStats.class;
},
};
diff --git a/website/client/components/shops/quests/buyQuestModal.vue b/website/client/components/shops/quests/buyQuestModal.vue
index 2f91b924be..2b8d5671e8 100644
--- a/website/client/components/shops/quests/buyQuestModal.vue
+++ b/website/client/components/shops/quests/buyQuestModal.vue
@@ -6,8 +6,9 @@
@change="onChange($event)"
)
span.badge.badge-pill.badge-dialog(
- :class="{'item-selected-badge': true}",
- v-if="withPin"
+ :class="{'item-selected-badge': item.pinned}",
+ v-if="withPin",
+ @click.prevent.stop="togglePinned()"
)
span.svg-icon.inline.color.icon-10(v-html="icons.pin")
@@ -178,6 +179,12 @@
padding: 8px 10px;
top: -12px;
background: white;
+ cursor: pointer;
+
+ &.item-selected-badge {
+ background: $purple-300;
+ color: $white;
+ }
}
}
@@ -239,6 +246,9 @@
this.$emit('buyPressed', this.item);
this.hideDialog();
},
+ togglePinned () {
+ this.$emit('togglePinned', this.item);
+ },
hideDialog () {
this.$root.$emit('hide::modal', 'buy-quest-modal');
},
diff --git a/website/client/components/shops/quests/index.vue b/website/client/components/shops/quests/index.vue
index a72f0fe065..6c872339e3 100644
--- a/website/client/components/shops/quests/index.vue
+++ b/website/client/components/shops/quests/index.vue
@@ -56,8 +56,8 @@
)
template(slot="popoverContent", scope="ctx")
div
- h4.popover-content-title {{ item.text() }}
- .popover-content-text(v-html="item.notes()")
+ h4.popover-content-title {{ item.text }}
+ .popover-content-text(v-html="item.notes")
h1.mb-0.page-header(v-once) {{ $t('quests') }}
@@ -124,8 +124,6 @@
:key="item.key",
:item="item",
:price="item.value",
- :priceType="item.currency",
- :itemContentClass="item.class",
:emptyItem="false",
:popoverPosition="'top'",
@click="selectedItemToBuy = item"
@@ -153,8 +151,6 @@
:key="item.key",
:item="item",
:price="item.value",
- :priceType="item.currency",
- :itemContentClass="item.class",
:emptyItem="false",
:popoverPosition="'top'",
@click="selectedItemToBuy = item"
@@ -181,7 +177,8 @@
:priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''",
:withPin="true",
@change="resetItemToBuy($event)",
- @buyPressed="buyItem($event)"
+ @buyPressed="buyItem($event)",
+ @togglePinned="togglePinned($event)"
)
template(slot="item", scope="ctx")
item.flat(
@@ -335,11 +332,15 @@
import svgPin from 'assets/svg/pin.svg';
import featuredItems from 'common/script/content/shop-featuredItems';
+ import getItemInfo from 'common/script/libs/getItemInfo';
+
+ import _isPinned from '../_isPinned';
import _filter from 'lodash/filter';
import _sortBy from 'lodash/sortBy';
import _throttle from 'lodash/throttle';
import _groupBy from 'lodash/groupBy';
+ import _map from 'lodash/map';
export default {
components: {
@@ -405,13 +406,20 @@ export default {
featuredItems () {
return featuredItems.quests.map(i => {
- return this.content.quests[i];
+ return getItemInfo(this.user, 'quest', this.content.quests[i]);
});
},
},
methods: {
questItems (category, sortBy, searchBy, hideLocked, hidePinned) {
- let result = _filter(category.items, (i) => {
+ let result = _map(category.items, (e) => {
+ return {
+ ...e,
+ pinned: _isPinned(this.user, e),
+ };
+ });
+
+ result = _filter(result, (i) => {
if (hideLocked && i.locked) {
return false;
}
@@ -453,9 +461,9 @@ export default {
return false;
},
togglePinned (item) {
- let isPinned = Boolean(item.pinned);
- item.pinned = !isPinned;
- this.$store.dispatch(isPinned ? 'shops:unpinGear' : 'shops:pinGear', {key: item.key});
+ if (!this.$store.dispatch('user:togglePinnedItem', {type: item.pinType, path: item.path})) {
+ this.$parent.showUnpinNotification(item);
+ }
},
buyItem (item) {
this.$store.dispatch('shops:purchase', {type: item.purchaseType, key: item.key});
diff --git a/website/client/components/shops/seasonal/index.vue b/website/client/components/shops/seasonal/index.vue
index 81f1557dd0..cdbf32748e 100644
--- a/website/client/components/shops/seasonal/index.vue
+++ b/website/client/components/shops/seasonal/index.vue
@@ -30,7 +30,7 @@
span.rectangle
span.text Leslie
span.rectangle
- div.content
+ div.content(v-if="featuredSet")
div.featured-label.with-border
span.rectangle
span.text(v-once) {{ $t('featuredset', { name: featuredSet.text }) }}
@@ -42,8 +42,6 @@
:key="item.key",
:item="item",
:price="item.value",
- :priceType="item.currency",
- :itemContentClass="item.class",
:emptyItem="false",
:popoverPosition="'top'",
@click="selectedItemToBuy = item"
@@ -88,8 +86,6 @@
:key="item.key",
:item="item",
:price="item.value",
- :priceType="item.currency",
- :itemContentClass="item.class",
:emptyItem="false",
:popoverPosition="'top'",
@click="selectedItemToBuy = item"
@@ -106,37 +102,13 @@
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
-
- div.items(v-if="false")
- shopItem(
- v-for="item in seasonalItems(category, selectedSortItemsBy, searchTextThrottled, hidePinned)",
- :key="item.key",
- :item="item",
- :price="item.value",
- :priceType="item.currency",
- :itemContentClass="item.class",
- :emptyItem="false",
- :popoverPosition="'top'",
- @click="selectedItemToBuy = item"
- )
- span(slot="popoverContent")
- div
- h4.popover-content-title {{ item.text }}
- .popover-content-text {{ item.notes }}
-
- template(slot="itemBadge", scope="ctx")
- span.badge.badge-pill.badge-item.badge-svg(
- :class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
- @click.prevent.stop="togglePinned(ctx.item)"
- )
- span.svg-icon.inline.icon-12.color(v-html="icons.pin")
-
buyModal(
:item="selectedItemToBuy",
:priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''",
:withPin="true",
@change="resetItemToBuy($event)",
- @buyPressed="buyItem($event)"
+ @buyPressed="buyItem($event)",
+ @togglePinned="togglePinned($event)"
)
template(slot="item", scope="ctx")
item.flat(
@@ -328,7 +300,9 @@
import _throttle from 'lodash/throttle';
import _groupBy from 'lodash/groupBy';
-export default {
+ import _isPinned from '../_isPinned';
+
+ export default {
components: {
ShopItem,
Item,
@@ -377,7 +351,6 @@ export default {
seasonal: 'shops.seasonal.data',
user: 'user.data',
userStats: 'user.data.stats',
- userItems: 'user.data.items',
}),
categories () {
if (this.seasonal) {
@@ -428,7 +401,14 @@ export default {
}
},
seasonalItems (category, sortBy, searchBy, viewOptions, hidePinned) {
- let result = _filter(category.items, (i) => {
+ let result = _map(category.items, (e) => {
+ return {
+ ...e,
+ pinned: _isPinned(this.user, e),
+ };
+ });
+
+ result = _filter(result, (i) => {
if (hidePinned && i.pinned) {
return false;
}
@@ -482,9 +462,9 @@ export default {
return false;
},
togglePinned (item) {
- let isPinned = Boolean(item.pinned);
- item.pinned = !isPinned;
- this.$store.dispatch(isPinned ? 'shops:unpinGear' : 'shops:pinGear', {key: item.key});
+ if (!this.$store.dispatch('user:togglePinnedItem', {type: item.pinType, path: item.path})) {
+ this.$parent.showUnpinNotification(item);
+ }
},
buyItem (item) {
this.$store.dispatch('shops:purchase', {type: item.purchaseType, key: item.key});
diff --git a/website/client/components/shops/shopItem.vue b/website/client/components/shops/shopItem.vue
index 8071f70686..c33eee90f1 100644
--- a/website/client/components/shops/shopItem.vue
+++ b/website/client/components/shops/shopItem.vue
@@ -5,31 +5,41 @@ b-popover(
)
span(slot="content")
slot(name="popoverContent", :item="item")
+ equipmentAttributesPopover(
+ v-if="item.purchaseType==='gear'",
+ :item="item"
+ )
+ div(v-else)
+ h4.popover-content-title {{ item.text }}
+ .popover-content-text(v-if="showNotes") {{ item.notes }}
.item-wrapper(@click="click()")
.item(
- :class="{'item-empty': emptyItem, 'highlight': highlightBorder}",
+ :class="{'item-empty': emptyItem, 'highlight-border': highlightBorder}",
)
slot(name="itemBadge", :item="item", :emptyItem="emptyItem")
div.shop-content
span.svg-icon.inline.lock(v-if="item.locked" v-html="icons.lock")
-
div.image
- div(:class="itemContentClass")
+ div(:class="item.class")
div.price
span.svg-icon.inline.icon-16(v-html="icons[getSvgClass()]")
- span.price-label(:class="getSvgClass()") {{ price }}
+ span.price-label(:class="getSvgClass()") {{ getPrice() }}