ecd393ec1a
This will allow a new Items section to be added.
502 lines
18 KiB
Vue
502 lines
18 KiB
Vue
<template>
|
|
<div v-if="user.contributor.admin" class="row standard-page">
|
|
<div class="well">
|
|
<h1>Admin Panel</h1>
|
|
<div class="row">
|
|
<div class="form-inline col-12">
|
|
<div class="form-group">
|
|
<input
|
|
v-model="heroID"
|
|
class="form-control"
|
|
type="text"
|
|
:placeholder="'User ID or Username; blank for your account'"
|
|
:style="{ 'min-width': '45ch' }"
|
|
@keyup.enter="loadHero(heroID)"
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<button
|
|
class="btn btn-secondary"
|
|
@click="loadHero(heroID)"
|
|
>
|
|
Load User (click or hit enter)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="hero && hero.profile" class="row">
|
|
<div class="form col-12">
|
|
<h2>@{{ hero.auth.local.username }} / {{ hero.profile.name }}</h2>
|
|
<div class="form-group">
|
|
{{ hero._id }}
|
|
<router-link :to="{'name': 'userProfile', 'params': {'userId': hero._id}}">
|
|
profile link
|
|
</router-link>
|
|
</div>
|
|
|
|
<div class="accordion-group">
|
|
<h3
|
|
class="expand-toggle"
|
|
:class="{'open': expandPriv}"
|
|
@click="expandPriv = !expandPriv"
|
|
>
|
|
Privileges, Gem Balance
|
|
</h3>
|
|
<div v-if="expandPriv">
|
|
<div class="form-group">
|
|
<div class="checkbox">
|
|
<label>
|
|
<input
|
|
v-if="hero.flags"
|
|
v-model="hero.flags.chatShadowMuted"
|
|
type="checkbox"
|
|
> Shadow Mute
|
|
</label>
|
|
</div>
|
|
<div class="checkbox">
|
|
<label>
|
|
<input
|
|
v-if="hero.flags"
|
|
v-model="hero.flags.chatRevoked"
|
|
type="checkbox"
|
|
> Mute (Revoke Chat Privileges)
|
|
</label>
|
|
</div>
|
|
<div class="checkbox">
|
|
<label>
|
|
<input
|
|
v-model="hero.auth.blocked"
|
|
type="checkbox"
|
|
> Ban / Block
|
|
</label>
|
|
</div>
|
|
<div class="form-inline">
|
|
<label>Balance</label>
|
|
<input
|
|
v-model="hero.balance"
|
|
class="form-control"
|
|
type="number"
|
|
step="0.25"
|
|
:style="{ 'width': '15ch' }"
|
|
>
|
|
<span>
|
|
<small>
|
|
Balance is in USD, not in Gems.
|
|
E.g., if this number is 1, it means 4 Gems.
|
|
Arrows change Balance by 0.25 (i.e., 1 Gem per click).
|
|
Do not use when awarding tiers; tier gems are automatic.
|
|
</small>
|
|
</span>
|
|
</div>
|
|
<button class="form-control btn btn-primary" @click="saveHero()">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="accordion-group">
|
|
<h3
|
|
class="expand-toggle"
|
|
:class="{'open': expandAuth}"
|
|
@click="expandAuth = !expandAuth"
|
|
>
|
|
Timestamps, Time Zone, Authentication, Email Address
|
|
</h3>
|
|
<div v-if="expandAuth">
|
|
<div>Account created:
|
|
<strong>{{ formatDate(hero.auth.timestamps.created) }}</strong>
|
|
</div>
|
|
<div>Most recent cron:
|
|
<strong>{{ formatDate(hero.auth.timestamps.loggedin) }}</strong>
|
|
</div>
|
|
<div>"lastCron":
|
|
<strong>{{ formatDate(hero.lastCron) }}</strong>
|
|
(if different than above, cron crashed before finishing)
|
|
</div>
|
|
<div class="subsection-start">Time zone:
|
|
<strong>{{ formatTimeZone(hero.preferences.timezoneOffset) }}</strong>
|
|
</div>
|
|
<div>Time zone at previous cron:
|
|
<strong>{{ formatTimeZone(hero.preferences.timezoneOffsetAtLastCron) }}</strong>
|
|
<br>(if different than above, the user changed zones / daylight savings
|
|
OR has devices on different zones OR uses a VPN with varying zones
|
|
OR something similarly unpleasant is happening)
|
|
</div>
|
|
<div class="subsection-start">Local authentication:
|
|
<span v-if="hero.auth.local.email">Yes,
|
|
<strong>{{ hero.auth.local.email }}</strong></span>
|
|
<span v-else><strong>None</strong></span>
|
|
</div>
|
|
<div>Google authentication:
|
|
<pre v-if="authMethodExists('google')">{{ hero.auth.google }}</pre>
|
|
<span v-else><strong>None</strong></span>
|
|
</div>
|
|
<div>Facebook authentication:
|
|
<pre v-if="authMethodExists('facebook')">{{ hero.auth.facebook }}</pre>
|
|
<span v-else><strong>None</strong></span>
|
|
</div>
|
|
<div>Apple ID authentication (not live yet as of March 2020):
|
|
<pre v-if="authMethodExists('apple')">{{ hero.auth.apple }}</pre>
|
|
<span v-else><strong>None</strong></span>
|
|
</div>
|
|
<div class="subsection-start">Full "auth" object for checking above is correct:
|
|
<pre>{{ hero.auth }}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="accordion-group">
|
|
<h3
|
|
class="expand-toggle"
|
|
:class="{'open': expandParty}"
|
|
@click="expandParty = !expandParty"
|
|
>
|
|
Party, Quest
|
|
</h3>
|
|
<div v-if="expandParty">
|
|
<div>Party:
|
|
<span v-if="hero.party._id">Yes
|
|
(party ID {{ hero.party._id }})
|
|
although this has not been verified by searching for the Party.
|
|
</span>
|
|
<span v-else>No</span>
|
|
</div>
|
|
<div class="subsection-start">Quest:
|
|
{{ questStatus }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="accordion-group">
|
|
<h3
|
|
class="expand-toggle"
|
|
:class="{'open': expandAvatar}"
|
|
@click="expandAvatar = !expandAvatar"
|
|
>
|
|
Current Avatar Appearance, Drop Count Today
|
|
</h3>
|
|
<div v-if="expandAvatar">
|
|
<div>Drops Today: {{ hero.items.lastDrop.count }}</div>
|
|
<div>Most Recent Drop: {{ formatDate(hero.items.lastDrop.date) }}</div>
|
|
<div>Use Costume: {{ hero.preferences.costume ? 'on' : 'off' }}</div>
|
|
<div class="subsection-start">
|
|
Equipped Gear:
|
|
<ul v-html="formatEquipment(hero.items.gear.equipped)"></ul>
|
|
</div>
|
|
<div>
|
|
Costume:
|
|
<ul v-html="formatEquipment(hero.items.gear.costume)"></ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="accordion">
|
|
<div
|
|
class="accordion-group"
|
|
heading="Update Items"
|
|
>
|
|
<h3
|
|
class="expand-toggle"
|
|
:class="{'open': expandUpdateItems}"
|
|
@click="expandUpdateItems = !expandUpdateItems"
|
|
>
|
|
Update Items
|
|
</h3>
|
|
<div
|
|
v-if="expandUpdateItems"
|
|
class="form-group well"
|
|
>
|
|
<input
|
|
v-model="hero.itemPath"
|
|
class="form-control"
|
|
type="text"
|
|
placeholder="Path (eg, items.pets.BearCub-Base)"
|
|
>
|
|
<small class="muted">
|
|
Enter the
|
|
<strong>item path</strong>. E.g.,
|
|
<code>items.pets.BearCub-Zombie</code> or
|
|
<code>items.gear.owned.head_special_0</code> or
|
|
<code>items.gear.equipped.head</code>. You can find all the item paths below.
|
|
</small>
|
|
<br>
|
|
<input
|
|
v-model="hero.itemVal"
|
|
class="form-control"
|
|
type="text"
|
|
placeholder="Value (eg, 5)"
|
|
>
|
|
<small class="muted">
|
|
Enter the
|
|
<strong>item value</strong>. E.g.,
|
|
<code>5</code> or
|
|
<code>false</code> or
|
|
<code>head_warrior_3</code>. All values are listed in the All Item Paths section below. <!-- eslint-disable-line max-len -->
|
|
</small>
|
|
<div class="accordion">
|
|
<div
|
|
class="accordion-group"
|
|
heading="All Item Paths"
|
|
>
|
|
<pre>{{ allItemPaths }}</pre>
|
|
</div>
|
|
<div
|
|
class="accordion-group"
|
|
heading="Current Items"
|
|
>
|
|
<pre>{{ hero.items }}</pre>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<button class="form-control btn btn-primary" @click="saveHero()">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="accordion-group">
|
|
<h3
|
|
class="expand-toggle"
|
|
:class="{'open': expandContrib}"
|
|
@click="expandContrib = !expandContrib"
|
|
>
|
|
Contributor Details
|
|
</h3>
|
|
<div v-if="expandContrib">
|
|
<div class="form-group form-inline">
|
|
<label>Title</label>
|
|
<input
|
|
v-model="hero.contributor.text"
|
|
class="form-control"
|
|
type="text"
|
|
:style="{ 'min-width': '50ch' }"
|
|
>
|
|
<small>
|
|
Common titles:
|
|
<strong>Ambassador, Artisan, Bard, Blacksmith, Challenger, Comrade, Fletcher,
|
|
Linguist, Linguistic Scribe, Scribe, Socialite, Storyteller</strong>.
|
|
</small>
|
|
<small>
|
|
Rare titles:
|
|
Advisor, Chamberlain, Designer, Mathematician, Shirtster, Spokesperson,
|
|
Statistician, Tinker, Transcriber, Troubadour.
|
|
</small>
|
|
</div>
|
|
<div class="form-group form-inline">
|
|
<label>Tier</label>
|
|
<input
|
|
v-model="hero.contributor.level"
|
|
class="form-control"
|
|
type="number"
|
|
:style="{ 'width': '10ch' }"
|
|
>
|
|
<small>
|
|
1-7 for normal contributors, 8 for moderators, 9 for staff.
|
|
This determines which items, pets, mounts are available, and name-tag coloring.
|
|
Tiers 8 and 9 are automatically given admin status.
|
|
<a
|
|
target="_blank"
|
|
href="https://trello.com/c/wkFzONhE/277-contributor-gear"
|
|
>More details (1-7)</a>,
|
|
<a
|
|
target="_blank"
|
|
href="https://github.com/HabitRPG/habitica/issues/3801"
|
|
>more details (8-9)</a>
|
|
</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Contributions</label>
|
|
<textarea
|
|
v-model="hero.contributor.contributions"
|
|
class="form-control"
|
|
cols="5"
|
|
rows="5"
|
|
></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<button class="form-control btn btn-primary" @click="saveHero()">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
h3 {
|
|
margin-top: 2em;
|
|
}
|
|
h3.expand-toggle::after {
|
|
margin-left: 5px;
|
|
}
|
|
.subsection-start {
|
|
margin-top: 1em;
|
|
}
|
|
.form-inline {
|
|
margin-bottom: 1em;
|
|
input {
|
|
margin-left: 10px;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
import each from 'lodash/each';
|
|
import moment from 'moment';
|
|
|
|
import markdownDirective from '@/directives/markdown';
|
|
import styleHelper from '@/mixins/styleHelper';
|
|
import { mapState } from '@/libs/store';
|
|
import * as quests from '@/../../common/script/content/quests';
|
|
import { mountInfo, petInfo } from '@/../../common/script/content/stable';
|
|
import content from '@/../../common/script/content';
|
|
import gear from '@/../../common/script/content/gear';
|
|
import notifications from '@/mixins/notifications';
|
|
|
|
export default {
|
|
components: {
|
|
},
|
|
directives: {
|
|
markdown: markdownDirective,
|
|
},
|
|
mixins: [notifications, styleHelper],
|
|
data () {
|
|
return {
|
|
hero: {},
|
|
heroID: '',
|
|
allItemPaths: this.getAllItemPaths(),
|
|
quests,
|
|
mountInfo,
|
|
petInfo,
|
|
food: content.food,
|
|
hatchingPotions: content.hatchingPotions,
|
|
special: content.special,
|
|
gear,
|
|
expandPriv: false,
|
|
expandAuth: false,
|
|
expandParty: false,
|
|
expandAvatar: false,
|
|
expandItems: false,
|
|
expandUpdateItems: false,
|
|
expandContrib: false,
|
|
};
|
|
},
|
|
computed: {
|
|
...mapState({ user: 'user.data' }),
|
|
questStatus () {
|
|
if (!this.hero.party || !this.hero.party.quest) return 'No';
|
|
const questKey = this.hero.party.quest.key || '';
|
|
if (this.hero.party.quest.RSVPNeeded) {
|
|
if (questKey) return `${questKey} : Invitation waiting.`;
|
|
return 'BUG! '
|
|
+ 'Invitation is waiting but Quest "key" is not assigned. '
|
|
+ 'Party might or might not have a Quest. '
|
|
+ 'An admin needs to delete the invitation or assign the Quest key.';
|
|
}
|
|
if (questKey) {
|
|
return `${questKey} : Invitation has been accepted. `
|
|
+ 'Quest is either running or still in invitation stage.';
|
|
}
|
|
return 'No (party might have a quest but user is not in it).';
|
|
},
|
|
},
|
|
// async mounted () {
|
|
// },
|
|
methods: {
|
|
authMethodExists (authMethod) {
|
|
if (this.hero.auth[authMethod] && this.hero.auth[authMethod].length !== 0) return true;
|
|
return false;
|
|
},
|
|
formatDate (inputDate) {
|
|
const date = moment(inputDate).utcOffset(0).format('YYYY-MM-DD HH:mm');
|
|
return `${date} UTC`;
|
|
},
|
|
formatTimeZone (inputTimeZoneOffset) {
|
|
const timeZone = (inputTimeZoneOffset / 60) * -1;
|
|
const sign = (timeZone < 0) ? '-' : '+';
|
|
return `${sign}${timeZone} UTC`;
|
|
},
|
|
formatEquipment (gearWorn) {
|
|
const gearTypes = ['head', 'armor', 'weapon', 'shield', 'headAccessory',
|
|
'eyewear', 'body', 'back'];
|
|
let description = '';
|
|
gearTypes.forEach(gearType => {
|
|
const item = gearWorn[gearType] || 'none';
|
|
description += `<li>${gearType} : ${item}</li>\n`;
|
|
});
|
|
return description;
|
|
},
|
|
getAllItemPaths () {
|
|
// let questsFormat = this.getFormattedItemReference
|
|
// ('items.quests', keys(this.quests), 'Numeric Quantity');
|
|
// let mountsFormat = this.getFormattedItemReference
|
|
// ('items.mounts', keys(this.mountInfo), 'Boolean');
|
|
// let foodFormat = this.getFormattedItemReference
|
|
// ('items.food', keys(this.food), 'Numeric Quantity');
|
|
// let eggsFormat = this.getFormattedItemReference
|
|
// ('items.eggs', keys(this.eggs), 'Numeric Quantity');
|
|
// let hatchingPotionsFormat = this.getFormattedItemReference
|
|
// ('items.hatchingPotions', keys(this.hatchingPotions), 'Numeric Quantity');
|
|
// let petsFormat = this.getFormattedItemReference
|
|
// ('items.pets', keys(this.petInfo), '-1:
|
|
// Owns Mount, 0: Not Owned, 1-49: Progress to mount');
|
|
// let specialFormat = this.getFormattedItemReference
|
|
// ('items.special', keys(this.special), 'Numeric Quantity');
|
|
// let gearFormat = this.getFormattedItemReference
|
|
// ('items.gear.owned', keys(this.gear.flat), 'Boolean');
|
|
//
|
|
// let equippedGearFormat = ''; // @TODO:
|
|
// '\nEquipped Gear:\n\titems.gear.{equipped/costume}
|
|
// .{head/headAccessory/eyewear/armor/body/back/shield/weapon}.{gearKey}\n';
|
|
// let equippedPetFormat = ''; // @TODO:'\nEquipped Pet:\n\titems.currentPet.{petKey}\n';
|
|
// let equippedMountFormat = ''; // @TODO:
|
|
// '\nEquipped Mount:\n\titems.currentMount.{mountKey}\n';
|
|
//
|
|
// let data = questsFormat.concat(mountsFormat,
|
|
// foodFormat, eggsFormat, hatchingPotionsFormat,
|
|
// petsFormat, specialFormat, gearFormat, equippedGearFormat,
|
|
// equippedPetFormat, equippedMountFormat);
|
|
//
|
|
// return data;
|
|
},
|
|
getFormattedItemReference (pathPrefix, itemKeys, values) { // XXX check if this is used
|
|
let finishedString = '\n'.concat('path: ', pathPrefix, ', ', 'value: {', values, '}\n');
|
|
|
|
each(itemKeys, key => {
|
|
finishedString = finishedString.concat('\t', pathPrefix, '.', key, '\n');
|
|
});
|
|
|
|
return finishedString;
|
|
},
|
|
async loadHero (id) {
|
|
const uuid = id || this.user._id;
|
|
const hero = await this.$store.dispatch('hall:getHero', { uuid });
|
|
this.hero = { ...hero };
|
|
if (!this.hero.flags) {
|
|
this.hero.flags = {
|
|
chatRevoked: false,
|
|
chatShadowMuted: false,
|
|
};
|
|
}
|
|
this.expandPriv = false; // XXX true
|
|
this.expandAuth = false;
|
|
this.expandParty = false;
|
|
this.expandAvatar = false;
|
|
this.expandItems = true; // XXX false
|
|
this.expandUpdateItems = false;
|
|
this.expandContrib = false; // XXX true
|
|
},
|
|
async saveHero () {
|
|
this.hero.contributor.admin = this.hero.contributor.level > 7;
|
|
await this.$store.dispatch('hall:updateHero', { heroDetails: this.hero });
|
|
this.text('User updated');
|
|
this.hero = {};
|
|
// this.heroID = ''; // uncomment if we want to clear the search box after saving
|
|
},
|
|
},
|
|
};
|
|
</script>
|