Tasks v2 Part 2 (#9236)

* start updating colors for tasks controls

* finish updating controls colors

* change hoevr behavior

* change transition duration

* update color with transition

* refactor menu wip

* wip

* upgrade vue deps

* fix warnings

* fix menu

* misc fixes

* more fixes

* fix badge

* fix margins in menu

* wip tooltips

* tooltips

* fix checklist colors

* add badges

* fix quick add input

* add transition to task controls too

* add batch add

* fix task filtering

* finish tasks badges

* fix menu

* upgrade deps

* fix shop items using all the same image

* fix animation

* disable client tests until we remove phantomjs

* revert changes to tasks colors

* fix opacity in task modal inputs

* remove client unit tests from travis

* wip task dropdown

* fix z-index for task footer/header

* fixes and add files

* fixes

* bigger clickable area

* more space to open task dropdown

* droddown position

* fix menu position

* make sure other dropdowns get closed correctly

* fixes

* start to fix z-index

* draggable = false for task dropdown

* fix for dropdown position

* implement move to top / bottom

* fix push to bottom

* typo

* fix drag and drop

* use standard code

* wider click area for dropdown

* unify badge look

* fix padding

* misc fixes

* more fixes

* make dropdown scrollable

* misc fixes

* fix padding for notes

* use existing code instead of new props
This commit is contained in:
Matteo Pagliazzi
2017-11-02 21:07:38 +01:00
committed by GitHub
parent 0e958fd306
commit a208ba4aba
38 changed files with 2186 additions and 924 deletions

View File

@@ -34,5 +34,4 @@ env:
- TEST="test:sanity"
- TEST="test:content" COVERAGE=true
- TEST="test:common" COVERAGE=true
- TEST="client:unit" COVERAGE=true
- TEST="apidoc"

1909
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -116,12 +116,12 @@
"validator": "^4.9.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"vue": "^2.1.0",
"vue-loader": "^11.0.0",
"vue": "git://github.com/habitrpg/vue#e8f45fcfc98ed1859669f3a6a8b23ae28bf4d46f",
"vue-loader": "^13.3.0",
"vue-mugen-scroll": "^0.2.1",
"vue-router": "^2.0.0-rc.5",
"vue-router": "^3.0.0",
"vue-style-loader": "^3.0.0",
"vue-template-compiler": "^2.1.10",
"vue-template-compiler": "^2.5.2",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker#45e607a7bccf4e3e089761b3b7b33e3f2c5dc21f",
"webpack": "^2.2.1",
"webpack-merge": "^4.0.0",
@@ -136,7 +136,7 @@
},
"scripts": {
"lint": "eslint --ext .js,.vue .",
"test": "npm run lint && gulp test && npm run client:unit && gulp apidoc",
"test": "npm run lint && gulp test && gulp apidoc",
"test:build": "gulp test:prepare:build",
"test:api-v3": "gulp test:api-v3",
"test:api-v3:unit": "gulp test:api-v3:unit",

View File

@@ -71,8 +71,8 @@
import axios from 'axios';
import { loadProgressBar } from 'axios-progress-bar';
import AppMenu from './components/appMenu';
import AppHeader from './components/appHeader';
import AppMenu from './components/header/menu';
import AppHeader from './components/header/index';
import AppFooter from './components/appFooter';
import notificationsDisplay from './components/notifications';
import snackbars from './components/snackbars/notifications';

View File

@@ -20,3 +20,11 @@
position: absolute;
top: -9px;
}
.badge-purple {
position: absolute;
color: $white;
background: $purple-400;
line-height: 1.2;
font-size: 10px;
}

View File

@@ -196,7 +196,7 @@
&-daily-todo-content-disabled {
background: $gray-600;
* {
.task-title, .task-notes {
color: $gray-300 !important;
}
}

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="11" viewBox="0 0 10 11">
<g fill="none" fill-rule="evenodd" stroke="#686274" stroke-width="2">
<path d="M5,0v8 M1,5l4,4l4-4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 213 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 16">
<path fill="#A5A1AC" fill-rule="evenodd" d="M3 14h8V4H3v10zM14 4h-1v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V4H0V2h4V1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1h4v2zm-6 8h1V6H8v6zm-3 0h1V6H5v6z"/>
<path fill-rule="evenodd" d="M3 14h8V4H3v10zM14 4h-1v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V4H0V2h4V1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1h4v2zm-6 8h1V6H8v6zm-3 0h1V6H5v6z"/>
</svg>

Before

Width:  |  Height:  |  Size: 274 B

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2.723 11.859l1.418 1.419-2.219.788.8-2.207zm2.762.686l-2.03-2.03 7.386-7.385 2.03 2.03-7.386 7.385zm8.704-10.731c.56.56.56 1.468 0 2.03l-.285.284-2.03-2.03.286-.284a1.438 1.438 0 0 1 2.027 0h.002zM11.125.782l-.8.8-8.417 8.415a.731.731 0 0 0-.098.122s-.012.024-.02.036a.713.713 0 0 0-.048.1v.012L.044 15.022a.73.73 0 0 0 .934.935l4.755-1.704a.728.728 0 0 0 .102-.05l.034-.018a.731.731 0 0 0 .122-.097l9.227-9.213A2.896 2.896 0 0 0 11.125.782z"/>
</svg>

After

Width:  |  Height:  |  Size: 570 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="16" viewBox="0 0 4 16">
<path fill-rule="evenodd" d="M2 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="11" viewBox="0 0 10 11">
<g fill="none" fill-rule="evenodd" stroke="#686274" stroke-width="2">
<path d="M5 3v8M9 6L5 2 1 6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 212 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="24" viewBox="0 0 22 24">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M15 13h-.4c1.9-1.2 3.3-3.3 3.4-5.8.1-3.8-3.1-7.2-6.9-7.2C7.1 0 4 3.1 4 7c0 2.6 1.3 4.8 3.4 6H7c-3.9 0-7 3.1-7 7v1c0 1.7 1.3 3 3 3h16c1.7 0 3-1.3 3-3v-1c0-3.9-3.1-7-7-7zM6 7c0-2.8 2.2-5 5-5s5 2.2 5 5-2.2 5-5 5-5-2.2-5-5zm13 15H3c-.6 0-1-.4-1-1v-1c0-2.8 2.2-5 5-5h8c2.8 0 5 2.2 5 5v1c0 .6-.4 1-1 1z"/>
</svg>

Before

Width:  |  Height:  |  Size: 424 B

After

Width:  |  Height:  |  Size: 424 B

View File

@@ -102,9 +102,9 @@ div
<script>
import { mapGetters, mapActions } from 'client/libs/store';
import MemberDetails from './memberDetails';
import createPartyModal from './groups/createPartyModal';
import membersModal from './groups/membersModal';
import MemberDetails from '../memberDetails';
import createPartyModal from '../groups/createPartyModal';
import membersModal from '../groups/membersModal';
import ResizeDirective from 'client/directives/resize.directive';
export default {

View File

@@ -53,42 +53,20 @@ div
a.dropdown-item(href="https://trello.com/c/odmhIqyW/440-read-first-table-of-contents", target='_blank') {{ $t('requestAF') }}
a.dropdown-item(href="http://habitica.wikia.com/wiki/Contributing_to_Habitica", target='_blank') {{ $t('contributing') }}
a.dropdown-item(href="http://habitica.wikia.com/wiki/Habitica_Wiki", target='_blank') {{ $t('wiki') }}
.item-with-icon(v-if="userHourglasses > 0")
.svg-icon(v-html="icons.hourglasses")
span {{ userHourglasses }}
.item-with-icon
.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")')
span {{userGems | roundBigNumber}}
.item-with-icon
.svg-icon(v-html="icons.gold")
span {{Math.floor(user.stats.gp * 100) / 100}}
a.item-with-icon(@click="sync")
.svg-icon(v-html="icons.sync")
notification-menu
a.dropdown.item-with-icon.item-user
span.message-count.top-count(v-if='user.inbox.newMessages > 0') {{user.inbox.newMessages}}
.svg-icon.user(v-html="icons.user")
.dropdown-menu.dropdown-menu-right.user-dropdown
a.dropdown-item.edit-avatar.dropdown-separated(@click='showAvatar()')
h3 {{ user.profile.name }}
span.small-text {{ $t('editAvatar') }}
a.nav-link.dropdown-item.dropdown-separated(@click.prevent='showInbox()')
| {{ $t('messages') }}
span.message-count(v-if='user.inbox.newMessages > 0') {{user.inbox.newMessages}}
a.dropdown-item(@click='showAvatar("backgrounds", "2017")') {{ $t('backgrounds') }}
a.dropdown-item(@click='showProfile("stats")') {{ $t('stats') }}
a.dropdown-item(@click='showProfile("achievements")') {{ $t('achievements') }}
a.dropdown-item.dropdown-separated(@click='showProfile("profile")') {{ $t('profile') }}
router-link.dropdown-item(:to="{name: 'site'}") {{ $t('settings') }}
router-link.dropdown-item.dropdown-separated(:to="{name: 'subscription'}") {{ $t('subscription') }}
a.nav-link.dropdown-item.dropdown-separated(to="/", @click.prevent='logout()') {{ $t('logout') }}
li(v-if='!this.user.purchased.plan.customerId', @click='showBuyGemsModal("subscribe")')
.dropdown-item.text-center
h3.purple {{ $t('needMoreGems') }}
span.small-text {{ $t('needMoreGemsInfo') }}
img.float-left.align-self-end(src='~assets/images/gem-rain.png')
button.btn.btn-primary.btn-lg.learn-button Learn More
img.float-right.align-self-end(src='~assets/images/gold-rain.png')
.d-flex.align-items-center
.item-with-icon(v-if="userHourglasses > 0")
.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')")
span {{ userHourglasses }}
.item-with-icon
.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')")
span {{userGems | roundBigNumber}}
.item-with-icon.gold
.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
span {{Math.floor(user.stats.gp * 100) / 100}}
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')")
.svg-icon(v-html="icons.sync")
notification-menu.item-with-icon
user-dropdown.item-with-icon
b-nav-toggle(target='nav_collapse')
</template>
@@ -167,38 +145,11 @@ div
// Make the dropdown menu open on hover
.dropdown:hover .dropdown-menu {
display: block;
margin-top: 0; // remove the gap so it doesn't close
display: block;
margin-top: 0; // remove the gap so it doesn't close
}
.dropdown + .dropdown {
margin-left: 0px;
}
.dropdown-separated {
border-bottom: 1px solid $gray-500;
}
.user-dropdown {
width: 14.75em;
}
.learn-button {
margin: 0.75em 0.75em 0.75em 1em;
}
.purple {
color: $purple-200;
}
.small-text {
color: $gray-200;
font-style: normal;
display: block;
white-space: normal;
}
.dropdown-menu:not(.user-dropdown) {
.dropdown-menu {
background: $purple-200;
border-radius: 0px;
border: none;
@@ -230,81 +181,48 @@ div
}
}
.dropdown + .dropdown {
margin-left: 0px;
}
.item-with-icon {
color: $white;
font-size: 16px;
font-weight: normal;
padding-top: 16px;
padding-left: 16px;
white-space: nowrap;
span {
font-weight: bold;
}
&:hover .svg-icon {
&.gold {
margin-right: 24px;
}
&:hover /deep/ .svg-icon {
color: $white;
}
.svg-icon {
& /deep/ .svg-icon {
color: $header-color;
vertical-align: bottom;
display: inline-block;
width: 20px;
height: 20px;
margin-right: 8px;
margin-left: 8px;
width: 24px;
height: 24px;
margin-right: 12px;
margin-left: 12px;
}
}
.item-notifications, .item-user {
padding-right: 12.5px;
padding-left: 12.5px;
color: $header-color;
transition: none;
.svg-icon {
margin-right: 0px;
}
}
.item-user .edit-avatar {
h3 {
color: $gray-10;
margin-bottom: 0px;
}
padding-top: 16px;
padding-bottom: 16px;
.menu-icon {
margin-left: 24px;
}
.gem:hover {
cursor: pointer;
}
.message-count {
background-color: $blue-50;
border-radius: 50%;
height: 20px;
width: 20px;
float: right;
color: $white;
text-align: center;
font-weight: bold;
font-size: 12px;
}
.message-count.top-count {
background-color: $red-50;
position: absolute;
right: 0;
top: .5em;
padding: .2em;
}
</style>
<script>
import axios from 'axios';
import bNavToggle from 'bootstrap-vue/lib/components/nav-toggle';
import bCollapse from 'bootstrap-vue/lib/components/collapse';
@@ -313,17 +231,18 @@ import * as Analytics from 'client/libs/analytics';
import gemIcon from 'assets/svg/gem.svg';
import goldIcon from 'assets/svg/gold.svg';
import syncIcon from 'assets/svg/sync.svg';
import userIcon from 'assets/svg/user.svg';
import svgHourglasses from 'assets/svg/hourglass.svg';
import logo from 'assets/svg/logo.svg';
import InboxModal from './userMenu/inbox.vue';
import notificationMenu from './notificationMenu';
import creatorIntro from './creatorIntro';
import profile from './userMenu/profile';
import markPMSRead from 'common/script/ops/markPMSRead';
import InboxModal from '../userMenu/inbox.vue';
import notificationMenu from './notificationsDropdown';
import creatorIntro from '../creatorIntro';
import profile from '../userMenu/profile';
import userDropdown from './userDropdown';
import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
export default {
components: {
userDropdown,
InboxModal,
notificationMenu,
creatorIntro,
@@ -331,12 +250,15 @@ export default {
bNavToggle,
bCollapse,
},
directives: {
bTooltip,
},
data () {
return {
isUserDropdownOpen: false,
icons: Object.freeze({
gem: gemIcon,
gold: goldIcon,
user: userIcon,
hourglasses: svgHourglasses,
sync: syncIcon,
logo,
@@ -357,31 +279,15 @@ export default {
this.getUserGroupPlans();
},
methods: {
toggleUserDropdown () {
this.isUserDropdownOpen = !this.isUserDropdownOpen;
},
sync () {
return Promise.all([
this.$store.dispatch('user:fetch', {forceLoad: true}),
this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}),
]);
},
logout () {
this.$store.dispatch('auth:logout');
},
showInbox () {
markPMSRead(this.user);
axios.post('/api/v3/user/mark-pms-read');
this.$root.$emit('show::modal', 'inbox-modal');
},
showAvatar (startingPage, subpage) {
this.$store.state.avatarEditorOptions.editingUser = true;
this.$store.state.avatarEditorOptions.startingPage = startingPage;
this.$store.state.avatarEditorOptions.subpage = subpage;
this.$root.$emit('show::modal', 'avatar-modal');
},
showProfile (startingPage) {
this.$store.state.profileUser = this.user;
this.$store.state.profileOptions.startingPage = startingPage;
this.$root.$emit('show::modal', 'profile');
},
async getUserGroupPlans () {
this.$store.state.groupPlans = await this.$store.dispatch('guilds:getGroupPlans');
},

View File

@@ -0,0 +1,27 @@
<template lang="pug" functional>
span.message-count(:class="{'top-count': props.top === true}") {{props.count}}
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
.message-count {
background-color: $blue-50;
border-radius: 50%;
height: 20px;
width: 20px;
float: right;
color: $white;
text-align: center;
font-weight: bold;
font-size: 12px;
}
.message-count.top-count {
position: absolute;
right: 0.3em;
top: -0.8em;
padding: 0.2em;
background-color: $red-50;
}
</style>

View File

@@ -1,15 +1,17 @@
<template lang="pug">
div.item-with-icon.item-notifications.dropdown
span.message-count.top-count(v-if='notificationsCount > 0') {{ notificationsCount }}
.svg-icon.notifications(v-html="icons.notifications")
.dropdown-menu.dropdown-menu-right.user-dropdown
menu-dropdown.item-notifications(:right="true")
div(slot="dropdown-toggle")
div
message-count(v-if='notificationsCount > 0', :count="notificationsCount", :top="true")
.svg-icon.notifications(v-html="icons.notifications")
div(slot="dropdown-content")
h4.dropdown-item.dropdown-separated(v-if='!hasNoNotifications()') {{ $t('notifications') }}
h4.dropdown-item.toolbar-notifs-no-messages(v-if='hasNoNotifications()') {{ $t('noNotifications') }}
a.dropdown-item(v-if='user.party.quest && user.party.quest.RSVPNeeded')
div {{ $t('invitedTo', {name: quests.quests[user.party.quest.key].text()}) }}
div
button.btn.btn-primary(@click='questAccept(user.party._id)') Accept
button.btn.btn-primary(@click='questReject(user.party._id)') Reject
button.btn.btn-primary(@click.stop='questAccept(user.party._id)') Accept
button.btn.btn-primary(@click.stop='questReject(user.party._id)') Reject
a.dropdown-item(v-if='user.purchased.plan.mysteryItems.length', @click='go("/inventory/items")')
span.glyphicon.glyphicon-gift
span {{ $t('newSubscriberItem') }}
@@ -18,20 +20,19 @@ div.item-with-icon.item-notifications.dropdown
span.glyphicon.glyphicon-user
span {{ $t('invitedTo', {name: party.name}) }}
div
button.btn.btn-primary(@click='accept(party, index, "party")') Accept
button.btn.btn-primary(@click='reject(party, index, "party")') Reject
button.btn.btn-primary(@click.stop='accept(party, index, "party")') Accept
button.btn.btn-primary(@click.stop='reject(party, index, "party")') Reject
a.dropdown-item(v-if='user.flags.cardReceived', @click='go("/inventory/items")')
span.glyphicon.glyphicon-envelope
span {{ $t('cardReceived') }}
a.dropdown-item(@click='clearCards()', :popover="$t('clear')",
popover-placement='right', popover-trigger='mouseenter', popover-append-to-body='true')
a.dropdown-item(@click.stop='clearCards()')
a.dropdown-item(v-for='(guild, index) in user.invitations.guilds')
div
span.glyphicon.glyphicon-user
span {{ $t('invitedTo', {name: guild.name}) }}
div
button.btn.btn-primary(@click='accept(guild, index, "guild")') Accept
button.btn.btn-primary(@click='reject(guild, index, "guild")') Reject
button.btn.btn-primary(@click.stop='accept(guild, index, "guild")') Accept
button.btn.btn-primary(@click.stop='reject(guild, index, "guild")') Reject
a.dropdown-item(v-if='user.flags.classSelected && !user.preferences.disableClasses && user.stats.points',
@click='go("/user/profile")')
span.glyphicon.glyphicon-plus-sign
@@ -40,130 +41,40 @@ div.item-with-icon.item-notifications.dropdown
span(@click='navigateToGroup(message.key)')
span.glyphicon.glyphicon-comment
span {{message.name}}
span.clear-button(@click='clearMessages(message.key)', :popover="$t('clear')",
popover-placement='right', popover-trigger='mouseenter', popover-append-to-body='true') Clear
span.clear-button(@click.stop='clearMessages(message.key)') Clear
a.dropdown-item(v-for='notification in groupNotifications', :key='notification.id')
span(:class="groupApprovalNotificationIcon(notification)")
span {{notification.data.message}}
span.clear-button(@click='viewGroupApprovalNotification(notification)', :popover="$t('clear')",
popover-placement='right', popover-trigger='mouseenter', popover-append-to-body='true') Clear
span.clear-button(@click.stop='viewGroupApprovalNotification(notification)') Clear
</template>
<style lang='scss' scoped>
@import '~client/assets/scss/colors.scss';
.message-count {
background-color: $blue-50;
border-radius: 50%;
height: 20px;
width: 20px;
float: right;
color: $white;
text-align: center;
font-weight: bold;
font-size: 12px;
}
.message-count.top-count {
position: absolute;
right: -.5em;
top: .5em;
padding: .2em;
background-color: $red-50;
}
.clear-button {
margin-left: .5em;
}
.item-notifications {
width: 44px;
}
.item-notifications:hover {
cursor: pointer;
}
.notifications {
color: $header-color;
vertical-align: bottom;
display: inline-block;
width: 20px;
height: 20px;
margin-right: 8px;
margin-left: 8px;
margin-top: .2em;
}
.item-with-icon:hover {
.svg-icon {
color: $white;
}
}
.user-dropdown {
max-height: 350px;
overflow: auto;
}
/* @TODO: Move to shared css */
.dropdown:hover .dropdown-menu {
display: block;
margin-top: 0; // remove the gap so it doesn't close
}
.dropdown + .dropdown {
margin-left: 0px;
}
.dropdown-separated {
border-bottom: 1px solid $gray-500;
}
.dropdown-menu:not(.user-dropdown) {
background: $purple-200;
border-radius: 0px;
border: none;
box-shadow: none;
padding: 0px;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
.dropdown-item {
font-size: 16px;
box-shadow: none;
color: $white;
border: none;
line-height: 1.5;
&.active {
background: $purple-300;
}
&:hover {
background: $purple-300;
&:last-child {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
}
}
}
.clear-button {
margin-left: .5em;
}
</style>
<script>
import axios from 'axios';
import isEmpty from 'lodash/isEmpty';
import map from 'lodash/map';
// import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
import { mapState } from 'client/libs/store';
import * as Analytics from 'client/libs/analytics';
import quests from 'common/script/content/quests';
import notificationsIcon from 'assets/svg/notifications.svg';
import MenuDropdown from '../ui/customMenuDropdown';
import MessageCount from './messageCount';
export default {
components: {
MenuDropdown,
MessageCount,
},
directives: {
// bTooltip,
},
data () {
return {
icons: Object.freeze({

View File

@@ -0,0 +1,126 @@
<template lang="pug">
menu-dropdown.item-user(:right="true")
div(slot="dropdown-toggle")
div(v-b-tooltip.hover.bottom="$t('user')")
message-count(v-if='user.inbox.newMessages > 0', :count="user.inbox.newMessages", :top="true")
.svg-icon.user(v-html="icons.user")
.user-dropdown(slot="dropdown-content")
a.dropdown-item.edit-avatar.dropdown-separated(@click='showAvatar()')
h3 {{ user.profile.name }}
span.small-text {{ $t('editAvatar') }}
a.nav-link.dropdown-item.dropdown-separated(@click.prevent='showInbox()')
| {{ $t('messages') }}
message-count(v-if='user.inbox.newMessages > 0', :count="user.inbox.newMessages")
a.dropdown-item(@click='showAvatar("backgrounds", "2017")') {{ $t('backgrounds') }}
a.dropdown-item(@click='showProfile("stats")') {{ $t('stats') }}
a.dropdown-item(@click='showProfile("achievements")') {{ $t('achievements') }}
a.dropdown-item.dropdown-separated(@click='showProfile("profile")') {{ $t('profile') }}
router-link.dropdown-item(:to="{name: 'site'}") {{ $t('settings') }}
router-link.dropdown-item.dropdown-separated(:to="{name: 'subscription'}") {{ $t('subscription') }}
a.nav-link.dropdown-item.dropdown-separated(@click.prevent='logout()') {{ $t('logout') }}
li(v-if='!this.user.purchased.plan.customerId', @click='showBuyGemsModal("subscribe")')
.dropdown-item.text-center
h3.purple {{ $t('needMoreGems') }}
span.small-text {{ $t('needMoreGemsInfo') }}
img.float-left.align-self-end(src='~assets/images/gem-rain.png')
button.btn.btn-primary.btn-lg.learn-button Learn More
img.float-right.align-self-end(src='~assets/images/gold-rain.png')
</template>
<style lang='scss' scoped>
@import '~client/assets/scss/colors.scss';
.edit-avatar {
h3 {
color: $gray-10;
margin-bottom: 0px;
}
padding-top: 16px;
padding-bottom: 16px;
}
.user-dropdown {
width: 14.75em;
}
.learn-button {
margin: 0.75em 0.75em 0.75em 1em;
}
.purple {
color: $purple-200;
}
.small-text {
color: $gray-200;
font-style: normal;
display: block;
white-space: normal;
font-weight: bold;
}
</style>
<script>
import { mapState } from 'client/libs/store';
import * as Analytics from 'client/libs/analytics';
import userIcon from 'assets/svg/user.svg';
import MenuDropdown from '../ui/customMenuDropdown';
import axios from 'axios';
import markPMSRead from 'common/script/ops/markPMSRead';
import MessageCount from './messageCount';
import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
export default {
components: {
MenuDropdown,
MessageCount,
},
directives: {
bTooltip,
},
data () {
return {
icons: Object.freeze({
user: userIcon,
}),
};
},
computed: {
...mapState({user: 'user.data'}),
},
methods: {
showAvatar (startingPage, subpage) {
this.$store.state.avatarEditorOptions.editingUser = true;
this.$store.state.avatarEditorOptions.startingPage = startingPage;
this.$store.state.avatarEditorOptions.subpage = subpage;
this.$root.$emit('show::modal', 'avatar-modal');
},
showInbox () {
markPMSRead(this.user);
axios.post('/api/v3/user/mark-pms-read');
this.$root.$emit('show::modal', 'inbox-modal');
},
showProfile (startingPage) {
this.$store.state.profileUser = this.user;
this.$store.state.profileOptions.startingPage = startingPage;
this.$root.$emit('show::modal', 'profile');
},
showBuyGemsModal (startingPage) {
this.$store.state.gemModalOptions.startingPage = startingPage;
Analytics.track({
hitType: 'event',
eventCategory: 'button',
eventAction: 'click',
eventLabel: 'Gems > User Dropdown',
});
this.$root.$emit('show::modal', 'buy-gems', {alreadyTracked: true});
},
logout () {
this.$store.dispatch('auth:logout');
},
},
};
</script>

View File

@@ -79,9 +79,9 @@
:showPopover="flatGear[activeItems[group]] && Boolean(flatGear[activeItems[group]].text)",
@click="equipItem(flatGear[activeItems[group]])",
)
template(slot="popoverContent", scope="context")
template(slot="popoverContent", slot-scope="context")
equipmentAttributesPopover(:item="context.item")
template(slot="itemBadge", scope="context")
template(slot="itemBadge", slot-scope="context")
starBadge(
:selected="true",
:show="!costume || user.preferences.costume",
@@ -105,7 +105,7 @@
:type="group.key",
:noItemsLabel="$t('noGearItemsOfType', { type: group.label })"
)
template(slot="item", scope="context")
template(slot="item", slot-scope="context")
item(
:item="context.item",
:itemContentClass="'shop_' + context.item.key",
@@ -113,13 +113,13 @@
:key="context.item.key",
@click="openEquipDialog(context.item)"
)
template(slot="itemBadge", scope="context")
template(slot="itemBadge", slot-scope="context")
starBadge(
:selected="activeItems[context.item.type] === context.item.key",
:show="!costume || user.preferences.costume",
@click="equipItem(context.item)",
)
template(slot="popoverContent", scope="context")
template(slot="popoverContent", slot-scope="context")
equipmentAttributesPopover(:item="context.item")
equipGearModal(

View File

@@ -43,7 +43,7 @@
:type="group.key",
:noItemsLabel="$t('noGearItemsOfType', { type: $t(group.key) })"
)
template(slot="item", scope="context")
template(slot="item", slot-scope="context")
item(
:item="context.item",
:key="context.item.key",
@@ -57,10 +57,10 @@
@click="onEggClicked($event, context.item)",
)
template(slot="popoverContent", scope="context")
template(slot="popoverContent", slot-scope="context")
h4.popover-content-title {{ context.item.text }}
.popover-content-text(v-if="currentDraggingPotion == null") {{ context.item.notes }}
template(slot="itemBadge", scope="context")
template(slot="itemBadge", slot-scope="context")
countBadge(
:show="true",
:count="context.item.quantity"
@@ -74,7 +74,7 @@
:type="group.key",
:noItemsLabel="$t('noGearItemsOfType', { type: $t(group.key) })"
)
template(slot="item", scope="context")
template(slot="item", slot-scope="context")
item(
:item="context.item",
:key="context.item.key",
@@ -88,10 +88,10 @@
@click="onPotionClicked($event, context.item)"
)
template(slot="popoverContent", scope="context")
template(slot="popoverContent", slot-scope="context")
h4.popover-content-title {{ context.item.text }}
.popover-content-text {{ context.item.notes }}
template(slot="itemBadge", scope="context")
template(slot="itemBadge", slot-scope="context")
countBadge(
:show="true",
:count="context.item.quantity"
@@ -105,7 +105,7 @@
:type="group.key",
:noItemsLabel="$t('noGearItemsOfType', { type: $t(group.key) })"
)
template(slot="item", scope="context")
template(slot="item", slot-scope="context")
item(
:item="context.item",
:key="context.item.key",
@@ -113,7 +113,7 @@
:showPopover="currentDraggingPotion == null",
@click="itemClicked(group.key, context.item)",
)
template(slot="popoverContent", scope="context")
template(slot="popoverContent", slot-scope="context")
div.questPopover(v-if="group.key === 'quests'")
h4.popover-content-title {{ context.item.text }}
questInfo(:quest="context.item")
@@ -121,7 +121,7 @@
div(v-else)
h4.popover-content-title {{ context.item.text }}
.popover-content-text(v-html="context.item.notes")
template(slot="itemBadge", scope="context")
template(slot="itemBadge", slot-scope="context")
countBadge(
:show="true",
:count="context.item.quantity"

View File

@@ -112,7 +112,7 @@
div(:class="'Pet_Egg_'+item.eggKey")
div(v-else)
h4.popover-content-title {{ item.name }}
template(slot="itemBadge", scope="context")
template(slot="itemBadge", slot-scope="context")
starBadge(:selected="item.key === currentPet", :show="item.isOwned()", @click="selectPet(item)")
.btn.btn-flat.btn-show-more(@click="setShowMore(petGroup.key)", v-if='petGroup.key !== "specialPets"')
@@ -144,7 +144,7 @@
)
span(slot="popoverContent")
h4.popover-content-title {{ item.name }}
template(slot="itemBadge", scope="context")
template(slot="itemBadge", slot-scope="context")
starBadge(
:selected="item.key === currentMount",
:show="item.isOwned()",
@@ -187,7 +187,7 @@
:itemWidth=94,
:itemMargin=24,
)
template(slot="item", scope="context")
template(slot="item", slot-scope="context")
foodItem(
:item="context.item",
:itemCount="userItems.food[context.item.key]",

View File

@@ -20,17 +20,17 @@
.is-buffed(v-if="isBuffed")
.svg-icon(v-html="icons.buff")
span.small-text.character-level {{ characterLevel }}
.progress-container
.progress-container(b-tooltip.hover.bottom="$t('health')")
.svg-icon(v-html="icons.health")
.progress
.progress-bar.bg-health(:style="{width: `${percent(member.stats.hp, MAX_HEALTH)}%`}")
span.small-text {{member.stats.hp | statFloor}} / {{MAX_HEALTH}}
.progress-container
.progress-container(b-tooltip.hover.bottom="$t('experience')")
.svg-icon(v-html="icons.experience")
.progress
.progress-bar.bg-experience(:style="{width: `${percent(member.stats.exp, toNextLevel)}%`}")
span.small-text {{member.stats.exp | statFloor}} / {{toNextLevel}}
.progress-container(v-if="hasClass")
.progress-container(v-if="hasClass", b-tooltip.hover.bottom="$t('mana')")
.svg-icon(v-html="icons.mana")
.progress
.progress-bar.bg-mana(:style="{width: `${percent(member.stats.mp, maxMP)}%`}")
@@ -186,6 +186,7 @@ import Profile from './userMenu/profile';
import { toNextLevel } from '../../common/script/statHelpers';
import statsComputed from '../../common/script/libs/statsComputed';
import percent from '../../common/script/libs/percent';
// import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
import buffIcon from 'assets/svg/buff.svg';
import healthIcon from 'assets/svg/health.svg';
@@ -198,6 +199,9 @@ export default {
Profile,
ClassBadge,
},
directives: {
// bTooltip,
},
props: {
member: {
type: Object,

View File

@@ -53,7 +53,7 @@
:popoverPosition="'top'",
@click="featuredItemSelected(item)"
)
template(slot="itemBadge", scope="ctx")
template(slot="itemBadge", slot-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)"
@@ -101,7 +101,7 @@
:type="'gear'",
:noItemsLabel="$t('noGearItemsOfClass')"
)
template(slot="item", scope="ctx")
template(slot="item", slot-scope="ctx")
shopItem(
:key="ctx.item.key",
:item="ctx.item",
@@ -110,7 +110,7 @@
@click="gearSelected(ctx.item)"
)
template(slot="itemBadge", scope="ctx")
template(slot="itemBadge", slot-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)"
@@ -151,13 +151,13 @@
strong(v-if='item.key === "gem" && gemsLeft === 0') {{ $t('maxBuyGems') }}
h4.popover-content-title {{ item.text }}
template(slot="itemBadge", scope="ctx")
template(slot="itemBadge", slot-scope="ctx")
countBadge(
v-if="item.showCount != false",
:show="userItems[item.purchaseType][item.key] != 0",
:count="userItems[item.purchaseType][item.key] || 0"
)
.gems-left(v-if='item.key === "gem"')
.badge.badge-pill.badge-purple.gems-left(v-if='item.key === "gem"')
| {{ gemsLeft }}
span.badge.badge-pill.badge-item.badge-svg(
@@ -196,14 +196,14 @@
:itemWidth=94,
:itemMargin=24,
)
template(slot="item", scope="ctx")
template(slot="item", slot-scope="ctx")
item(
:item="ctx.item",
:itemContentClass="getItemClass(selectedDrawerItemType, ctx.item.key)",
popoverPosition="top",
@click="selectedItemToSell = ctx.item"
)
template(slot="itemBadge", scope="ctx")
template(slot="itemBadge", slot-scope="ctx")
countBadge(
:show="true",
:count="userItems[drawerTabs[selectedDrawerTab].contentType][ctx.item.key] || 0"
@@ -218,13 +218,13 @@
:text="selectedItemToSell != null ? getItemName(selectedDrawerItemType, selectedItemToSell) : ''",
@change="resetItemToSell($event)"
)
template(slot="item", scope="ctx")
template(slot="item", slot-scope="ctx")
item.flat(
:item="ctx.item",
:itemContentClass="getItemClass(selectedDrawerItemType, ctx.item.key)",
:showPopover="false"
)
template(slot="itemBadge", scope="ctx")
template(slot="itemBadge", slot-scope="ctx")
countBadge(
:show="true",
:count="userItems[drawerTabs[selectedDrawerTab].contentType][ctx.item.key] || 0"
@@ -356,17 +356,8 @@
}
.market .gems-left {
position: absolute;
right: -.5em;
top: -.5em;
color: $white;
background: $purple-200;
padding: .15em;
text-align: center;
width: 24px;
height: 24px;
border-radius: 50%;
box-shadow: 0 1px 1px 0 rgba($black, 0.12);
}
</style>

View File

@@ -54,12 +54,12 @@
:popoverPosition="'top'",
@click="selectItem(item)"
)
template(slot="popoverContent", scope="ctx")
template(slot="popoverContent", slot-scope="ctx")
div.questPopover
h4.popover-content-title {{ item.text }}
questInfo(:quest="item")
template(slot="itemBadge", scope="ctx")
template(slot="itemBadge", slot-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)"
@@ -94,7 +94,7 @@
:itemMargin=24,
:type="'pet_quests'",
)
template(slot="item", scope="ctx")
template(slot="item", slot-scope="ctx")
shopItem(
:key="ctx.item.key",
:item="ctx.item",
@@ -104,12 +104,12 @@
:emptyItem="false",
@click="selectItem(ctx.item)"
)
span(slot="popoverContent", scope="ctx")
span(slot="popoverContent", slot-scope="ctx")
div.questPopover
h4.popover-content-title {{ ctx.item.text }}
questInfo(:quest="ctx.item")
template(slot="itemBadge", scope="ctx")
template(slot="itemBadge", slot-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)"
@@ -145,7 +145,7 @@
.popover-content-text(v-if='item.lvl > user.stats.lvl') {{ `${$t('mustLvlQuest', {level: item.lvl})}` }}
questInfo(v-if='!item.locked', :quest="item")
template(slot="itemBadge", scope="ctx")
template(slot="itemBadge", slot-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)"
@@ -172,7 +172,7 @@
h4.popover-content-title {{ item.text }}
questInfo(:quest="item")
template(slot="itemBadge", scope="ctx")
template(slot="itemBadge", slot-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)"
@@ -190,7 +190,7 @@
:withPin="true",
@change="resetItemToBuy($event)",
)
template(slot="item", scope="ctx")
template(slot="item", slot-scope="ctx")
item.flat(
:item="ctx.item",
:itemContentClass="ctx.item.class",

View File

@@ -97,7 +97,7 @@
:showEventBadge="false",
@click="itemSelected(item)"
)
template(slot="itemBadge", scope="ctx")
template(slot="itemBadge", slot-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)"

View File

@@ -18,8 +18,7 @@ div
slot(name="itemImage", :item="item")
div.price
span.svg-icon.inline.icon-16(v-html="icons[currencyClass]")
span.svg-icon.inline.icon-16(v-html="icons[currencyClass]", v-once)
span.price-label(:class="currencyClass", v-once) {{ getPrice() }}
b-popover(
:target="itemId",

View File

@@ -63,7 +63,7 @@
:itemMargin=24,
:type="category.identifier",
)
template(slot="item", scope="ctx")
template(slot="item", slot-scope="ctx")
shopItem(
:key="ctx.item.key",
:item="ctx.item",
@@ -72,11 +72,11 @@
:emptyItem="false",
@click="selectItemToBuy(ctx.item)"
)
span(slot="popoverContent", scope="ctx")
span(slot="popoverContent", slot-scope="ctx")
div
h4.popover-content-title {{ ctx.item.text }}
template(slot="itemBadge", scope="ctx")
template(slot="itemBadge", slot-scope="ctx")
span.badge.badge-pill.badge-item.badge-svg(
v-if="ctx.item.pinType !== 'IGNORE'",
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",

View File

@@ -16,9 +16,13 @@ div
</template>
<style scoped>
.task-unclaimed a {
float: right;
}
.claim-bottom-message {
z-index: 9;
}
.task-unclaimed a {
float: right;
}
</style>
<script>

View File

@@ -4,11 +4,15 @@
| {{ message }}
</template>
<style scoped>
.approval {
background: #24cc8f;
color: #fff;
}
<style lang="scss" scoped>
.claim-bottom-message {
z-index: 9;
}
.approval {
background: #24cc8f;
color: #fff;
}
</style>
<script>

View File

@@ -7,7 +7,9 @@
@change="resetItemToBuy($event)"
v-if='type === "reward"')
.d-flex
h2.tasks-column-title(v-once) {{ $t(types[type].label) }}
h2.tasks-column-title
| {{ $t(types[type].label) }}
.badge.badge-pill.badge-purple.column-badge(v-if="badgeCount > 0") {{ badgeCount }}
.filters.d-flex.justify-content-end
.filter.small-text(
v-for="filter in types[type].filters",
@@ -15,18 +17,35 @@
@click="activateFilter(type, filter)",
) {{ $t(filter.label) }}
.tasks-list(ref="tasksWrapper")
input.quick-add(
textarea.quick-add(
:rows="quickAddRows",
v-if="isUser", :placeholder="quickAddPlaceholder",
v-model="quickAddText", @keyup.enter="quickAdd",
v-model="quickAddText", @keypress.enter="quickAdd",
ref="quickAdd",
@focus="quickAddFocused = true", @blur="quickAddFocused = false",
)
transition(name="quick-add-tip-slide")
.quick-add-tip.small-text(v-show="quickAddFocused", v-html="$t('addMultipleTip')")
.column-background(
v-if="isUser === true",
:class="{'initial-description': initialColumnDescription}",
ref="columnBackground",
)
.svg-icon(v-html="icons[type]", :class="`icon-${type}`", v-once)
h3(v-once) {{$t('theseAreYourTasks', {taskType: $t(types[type].label)})}}
.small-text {{$t(`${type}sDesc`)}}
.sortable-tasks(
ref="tasksList",
v-sortable='activeFilters[type].label !== "scheduled"',
@onsort='sorted',
data-sortableId
)
.sortable-tasks(ref="tasksList", v-sortable='activeFilters[type].label !== "scheduled"', @onsort='sorted', data-sortableId)
task(
v-for="task in taskList",
:key="task.id", :task="task",
v-if="filterTask(task)",
:isUser="isUser",
@editTask="editTask",
@moveTo="moveTo",
:group='group',
)
template(v-if="hasRewardsList")
@@ -39,15 +58,6 @@
@click="openBuyDialog(reward)",
:popoverPosition="'left'"
)
.column-background(
v-if="isUser === true",
:class="{'initial-description': initialColumnDescription}",
ref="columnBackground",
)
.svg-icon(v-html="icons[type]", :class="`icon-${type}`", v-once)
h3(v-once) {{$t('theseAreYourTasks', {taskType: $t(types[type].label)})}}
.small-text {{$t(`${type}sDesc`)}}
</template>
<style lang="scss" scoped>
@@ -57,11 +67,6 @@
min-height: 556px;
}
.task-wrapper {
position: relative;
z-index: 2;
}
.sortable-tasks + .reward-items {
margin-top: 16px;
}
@@ -87,9 +92,10 @@
width: 100%;
margin-bottom: 8px;
padding: 12px 16px;
font-weight: bold;
border-color: transparent;
transition: background 0.15s ease-in;
resize: none;
margin-bottom: 0px;
&:hover {
background-color: rgba($black, 0.1);
@@ -101,20 +107,42 @@
border-color: $purple-500;
color: $gray-50;
}
&::placeholder {
font-weight: bold;
}
}
.bottom-gradient {
position: absolute;
bottom: 0px;
left: 0px;
height: 42px;
background-image: linear-gradient(to bottom, rgba($gray-10, 0), rgba($gray-10, 0.24));
width: 100%;
z-index: 99;
.quick-add-tip {
font-style: normal;
padding: 16px;
text-align: center;
overflow-y: hidden;
max-height: 65px; // approximate max height
}
.quick-add-tip-slide-enter-active {
transition: all 0.5s cubic-bezier(0, 1, 0.5, 1);
}
.quick-add-tip-slide-leave-active {
transition: all 0.5s cubic-bezier(0, 1, 0.5, 1);
}
.quick-add-tip-slide-enter, .quick-add-tip-slide-leave-to {
max-height: 0;
padding: 0px 16px;
}
.tasks-column-title {
margin-bottom: 8px;
position: relative;
}
.column-badge {
top: -5px;
right: -24px;
}
.filters {
@@ -142,7 +170,6 @@
.column-background {
position: absolute;
bottom: 32px;
z-index: 1;
&.initial-description {
top: 30%;
@@ -230,6 +257,7 @@ export default {
},
props: ['type', 'isUser', 'searchText', 'selectedTags', 'taskListOverride', 'group'], // @TODO: maybe we should store the group on state?
data () {
// @TODO refactor this so that filter functions aren't in data
const types = Object.freeze({
habit: {
label: 'habits',
@@ -285,6 +313,8 @@ export default {
forceRefresh: new Date(),
quickAddText: '',
quickAddFocused: false,
quickAddRows: 1,
selectedItemToBuy: {},
};
@@ -315,13 +345,14 @@ export default {
taskList = sortBy(taskList, filter.sort);
}
return taskList;
return taskList.filter(t => {
return this.filterTask(t);
});
},
inAppRewards () {
let watchRefresh = this.forceRefresh; // eslint-disable-line
let rewards = inAppRewards(this.user);
// Add season rewards if user is affected
// @TODO: Add buff coniditional
const seasonalSkills = {
@@ -365,6 +396,26 @@ export default {
const type = this.$t(this.type);
return this.$t('addATask', {type});
},
badgeCount () {
// 0 means the badge will not be shown
// It is shown for the all and due views of dailies
// and for the active and scheduled views of todos.
if (this.type === 'todo') {
if (this.activeFilters.todo.label !== 'complete2') return this.taskList.length;
} else if (this.type === 'daily') {
const activeFilter = this.activeFilters.daily.label;
if (activeFilter === 'due') {
return this.taskList.length;
} else if (activeFilter === 'all') {
return this.taskList.reduce((count, t) => {
return !t.completed && shouldDo(new Date(), t, this.userPreferences) ? count + 1 : count;
}, 0);
}
}
return 0;
},
},
watch: {
taskList: {
@@ -392,13 +443,12 @@ export default {
createTask: 'tasks:create',
}),
async sorted (data) {
const filteredList = this.taskList.filter(this.activeFilters[this.type].filter);
const sorting = this.taskList;
const filteredList = this.taskList;
const taskIdToMove = filteredList[data.oldIndex]._id;
// Server
const taskIdToReplace = filteredList[data.newIndex];
const newIndexOnServer = this.taskList.findIndex(taskId => taskId === taskIdToReplace);
const newIndexOnServer = this.tasks[`${this.type}s`].findIndex(taskId => taskId === taskIdToReplace);
let newOrder = await this.$store.dispatch('tasks:move', {
taskId: taskIdToMove,
position: newIndexOnServer,
@@ -406,16 +456,47 @@ export default {
this.user.tasksOrder[`${this.type}s`] = newOrder;
// Client
if (sorting) {
const deleted = sorting.splice(data.oldIndex, 1);
sorting.splice(data.newIndex, 0, deleted[0]);
}
const deleted = this.tasks[`${this.type}s`].splice(data.oldIndex, 1);
this.tasks[`${this.type}s`].splice(data.newIndex, 0, deleted[0]);
},
quickAdd () {
const task = taskDefaults({type: this.type, text: this.quickAddText});
task.tags = this.selectedTags;
async moveTo (task, where) { // where is 'top' or 'bottom'
const taskIdToMove = task._id;
const list = this.tasks[`${this.type}s`];
const oldPosition = list.findIndex(t => t._id === taskIdToMove);
const moved = list.splice(oldPosition, 1);
const newPosition = where === 'top' ? 0 : list.length;
list.splice(newPosition, 0, moved[0]);
let newOrder = await this.$store.dispatch('tasks:move', {
taskId: taskIdToMove,
position: newPosition,
});
this.user.tasksOrder[`${this.type}s`] = newOrder;
},
quickAdd (ev) {
// Add a new line if Shift+Enter Pressed
if (ev.shiftKey) {
this.quickAddRows++;
return true;
}
// Do not add new line is added if only Enter is pressed
ev.preventDefault();
const text = this.quickAddText;
if (!text) return false;
const tasks = text.split('\n').reverse().filter(taskText => {
return taskText ? true : false;
}).map(taskText => {
const task = taskDefaults({type: this.type, text: taskText});
task.tags = this.selectedTags;
return task;
});
this.quickAddText = null;
this.createTask(task);
this.quickAddRows = 1;
this.createTask(tasks);
},
editTask (task) {
this.$emit('editTask', task);

View File

@@ -13,15 +13,45 @@
.svg-icon.check(v-html="icons.check", :class="{'display-check-icon': task.completed}")
// Task title, description and icons
.task-content(:class="contentClass")
.task-clickable-area(@click="edit($event, task)")
h3.task-title(:class="{ 'has-notes': task.notes }", v-markdown="task.text")
.task-notes.small-text(v-markdown="task.notes")
.task-clickable-area(@click="edit($event, task)", :class="{'task-clickable-area-user': isUser}")
.d-flex.justify-content-between
h3.task-title(:class="{ 'has-notes': task.notes }", v-markdown="task.text")
menu-dropdown.task-dropdown(
v-if="isUser && !isRunningYesterdailies",
:right="task.type === 'reward'",
ref="taskDropdown"
)
div(slot="dropdown-toggle", draggable=false)
.svg-icon.dropdown-icon(v-html="icons.menu")
div(slot="dropdown-content", draggable=false)
.dropdown-item.edit-task-item(ref="editTaskItem")
span.dropdown-icon-item
span.svg-icon.inline.edit-icon(v-html="icons.edit")
span.text {{ $t('edit') }}
.dropdown-item(@click="moveToTop")
span.dropdown-icon-item
span.svg-icon.inline.push-to-top(v-html="icons.top")
span.text {{ $t('taskToTop') }}
.dropdown-item(@click="moveToBottom")
span.dropdown-icon-item
span.svg-icon.inline.push-to-bottom(v-html="icons.bottom")
span.text {{ $t('taskToBottom') }}
.dropdown-item(@click="destroy", v-if="canDelete(task)")
span.dropdown-icon-item.delete-task-item
span.svg-icon.inline.delete(v-html="icons.delete")
span.text {{ $t('delete') }}
.task-notes.small-text(
v-markdown="task.notes",
:class="{'has-checklist': task.notes && hasChecklist}",
)
.checklist(v-if="canViewchecklist")
.d-inline-flex
.collapse-checklist.d-flex.align-items-center.expand-toggle(
v-if="isUser",
@click="collapseChecklist(task)",
:class="{open: !task.collapseChecklist}",
v-b-tooltip.hover.bottom="$t(`${task.collapseChecklist ? 'expand': 'collapse'}Checklist`)",
)
.svg-icon(v-html="icons.checklist")
span {{ checklistProgress }}
@@ -70,7 +100,7 @@
approval-footer(:task='task', v-if='this.task.group.id', :group='group')
</template>
<style lang="scss">
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.task {
@@ -78,14 +108,13 @@
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
background: transparent;
border-radius: 2px;
z-index: 9;
position: relative;
&:hover {
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
.left-control, .right-control, .task-content {
border-color: $purple-500;
border-color: $purple-400;
}
}
}
@@ -99,32 +128,112 @@
color: $gray-10;
font-weight: normal;
margin-bottom: 0px;
line-height: 1.43;
font-size: 14px;
&.has-notes {
padding-bottom: 0px;
padding-bottom: 4px;
}
}
.task-clickable-area {
padding: 7px 8px;
padding-bottom: 0px;
&-user {
padding-right: 0px;
}
}
.task-title + .task-dropdown /deep/ .dropdown-menu {
margin-top: 2px !important;
}
.dropdown-icon {
width: 4px;
height: 16px;
color: $gray-100 !important;
}
.task /deep/ .habitica-menu-dropdown .habitica-menu-dropdown-toggle {
opacity: 0;
padding: 0 8px;
transition: opacity 0.15s ease-in;
}
.task:hover /deep/ .habitica-menu-dropdown .habitica-menu-dropdown-toggle {
opacity: 1;
}
.task-clickable-area /deep/ .habitica-menu-dropdown.open .habitica-menu-dropdown-toggle {
opacity: 1;
.svg-icon {
color: $purple-400 !important;
}
}
.task-clickable-area /deep/ .habitica-menu-dropdown .habitica-menu-dropdown-toggle:hover .svg-icon {
color: $purple-400 !important;
}
.task-dropdown {
max-height: 16px;
}
.task-dropdown /deep/ .dropdown-menu {
.dropdown-item {
cursor: pointer !important;
transition: none;
* {
transition: none;
}
&:hover {
color: $purple-300;
.svg-icon.push-to-top, .svg-icon.push-to-bottom {
* {
stroke: $purple-300;
}
}
}
}
}
.task-notes {
color: $gray-100;
font-style: normal;
padding-right: 6px;
&.has-checklist {
padding-bottom: 8px;
}
}
.task-content {
padding: 8px;
padding-top: 0px;
padding-bottom: 7px;
flex-grow: 1;
cursor: pointer;
background: $white;
border: 1px solid transparent;
transition-duration: 0.15;
&.no-right-border {
border-right: none !important;
}
&.reward-content {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
}
}
.checklist {
margin-bottom: 2px;
margin-top: 8px;
margin-top: -3px;
}
.collapse-checklist {
@@ -154,6 +263,7 @@
margin-bottom: 10px;
min-height: 0px;
width: 100%;
margin-left: 8px;
&-done {
color: $gray-300;
@@ -170,6 +280,10 @@
}
}
.icons, .checklist {
padding: 0 8px;
}
.icons {
margin-top: 4px;
color: $gray-300;
@@ -197,6 +311,34 @@
height: 7.1px;
}
.delete-task-item {
color: $red-10;
}
.edit-task-item span.text {
margin-left: -3px;
}
.svg-icon.edit-icon {
width: 16px;
height: 16px;
}
.svg-icon.push-to-top, .svg-icon.push-to-bottom {
width: 10px;
height: 11px;
margin-left: 3px;
svg {
stroke: $purple-300;
}
}
.svg-icon.delete {
width: 14px;
height: 16px;
}
.tags.svg-icon, .calendar.svg-icon {
width: 14px;
height: 14px;
@@ -235,6 +377,12 @@
flex-shrink: 0;
}
.left-control, .right-control, .task-control {
transition-duration: 0.15s;
transition-property: border-color, background, color;
transition-timing-function: ease-in;
}
.left-control {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
@@ -244,6 +392,8 @@
& + .task-content {
border-left: none;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
}
@@ -327,6 +477,7 @@ import axios from 'axios';
import scoreTask from 'common/script/ops/scoreTask';
import Vue from 'vue';
import * as Analytics from 'client/libs/analytics';
import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
import positiveIcon from 'assets/svg/positive.svg';
import negativeIcon from 'assets/svg/negative.svg';
@@ -336,12 +487,18 @@ import calendarIcon from 'assets/svg/calendar.svg';
import challengeIcon from 'assets/svg/challenge.svg';
import tagsIcon from 'assets/svg/tags.svg';
import checkIcon from 'assets/svg/check.svg';
import editIcon from 'assets/svg/edit.svg';
import topIcon from 'assets/svg/top.svg';
import bottomIcon from 'assets/svg/bottom.svg';
import deleteIcon from 'assets/svg/delete.svg';
import checklistIcon from 'assets/svg/checklist.svg';
import menuIcon from 'assets/svg/menu.svg';
import bPopover from 'bootstrap-vue/lib/components/popover';
import markdownDirective from 'client/directives/markdown';
import notifications from 'client/mixins/notifications';
import approvalHeader from './approvalHeader';
import approvalFooter from './approvalFooter';
import MenuDropdown from '../ui/customMenuDropdown';
export default {
mixins: [notifications],
@@ -349,9 +506,11 @@ export default {
bPopover,
approvalFooter,
approvalHeader,
MenuDropdown,
},
directives: {
markdown: markdownDirective,
bTooltip,
},
props: ['task', 'isUser', 'group', 'dueDate'], // @TODO: maybe we should store the group on state?
data () {
@@ -366,6 +525,11 @@ export default {
tags: tagsIcon,
check: checkIcon,
checklist: checklistIcon,
delete: deleteIcon,
edit: editIcon,
top: topIcon,
bottom: bottomIcon,
menu: menuIcon,
}),
};
},
@@ -373,15 +537,19 @@ export default {
...mapState({
user: 'user.data',
castingSpell: 'spellOptions.castingSpell',
isRunningYesterdailies: 'isRunningYesterdailies',
}),
...mapGetters({
getTagsFor: 'tasks:getTagsFor',
getTaskClasses: 'tasks:getTaskClasses',
canDelete: 'tasks:canDelete',
}),
hasChecklist () {
return this.task.checklist && this.task.checklist.length > 0;
},
canViewchecklist () {
let hasChecklist = this.task.checklist && this.task.checklist.length > 0;
let userIsTaskUser = this.task.userId ? this.task.userId === this.user._id : true;
return hasChecklist && userIsTaskUser;
return this.hasChecklist && userIsTaskUser;
},
checklistProgress () {
const totalItems = this.task.checklist.length;
@@ -405,11 +573,19 @@ export default {
return this.getTaskClasses(this.task, 'control', this.dueDate);
},
contentClass () {
const type = this.task.type;
const classes = [];
classes.push(this.getTaskClasses(this.task, 'content', this.dueDate));
if (this.task.type === 'reward' || this.task.type === 'habit') {
if (type === 'reward' || type === 'habit') {
classes.push('no-right-border');
}
if (type === 'reward') {
classes.push('reward-content');
}
return classes;
},
showStreak () {
@@ -432,6 +608,7 @@ export default {
...mapActions({
scoreChecklistItem: 'tasks:scoreChecklistItem',
collapseChecklist: 'tasks:collapseChecklist',
destroyTask: 'tasks:destroy',
}),
toggleChecklistItem (item) {
if (this.castingSpell) return;
@@ -439,15 +616,33 @@ export default {
this.scoreChecklistItem({taskId: this.task._id, itemId: item.id});
},
edit (e, task) {
if (this.isRunningYesterdailies) return;
// Prevent clicking on a link from opening the edit modal
const target = e.target || e.srcElement;
if (target.tagName === 'A') { // Link
return;
} else if (!this.$store.state.spellOptions.castingSpell) {
if (target.tagName === 'A') return; // clicked on a link
const isDropdown = this.$refs.taskDropdown.$el.contains(target);
const isEditAction = this.$refs.editTaskItem.contains(target);
if (isDropdown && !isEditAction) return;
if (!this.$store.state.spellOptions.castingSpell) {
this.$emit('editTask', task);
}
},
moveToTop () {
this.$emit('moveTo', this.task, 'top');
},
moveToBottom () {
this.$emit('moveTo', this.task, 'bottom');
},
destroy () {
if (!confirm(this.$t('sureDelete'))) return;
this.destroyTask(this.task);
this.$emit('taskDestroyed', this.task);
},
castEnd (e, task) {
this.$root.$emit('castEnd', task, 'task', e);
},

View File

@@ -9,7 +9,11 @@
button.btn.btn-secondary(type="submit", v-once) {{ $t('save') }}
.form-group
label(v-once) {{ `${$t('text')}*` }}
input.form-control.title-input(type='text', :class="[`${cssClass}-modal-input`]", required, v-model="task.text", autofocus, spellcheck='true')
input.form-control.title-input(
type="text", :class="[`${cssClass}-modal-input`]",
required, v-model="task.text",
autofocus, spellcheck="true",
)
.form-group
label(v-once) {{ $t('notes') }}
textarea.form-control(:class="[`${cssClass}-modal-input`]", v-model="task.notes", rows="3")
@@ -181,9 +185,12 @@
input, textarea {
border: none;
background-color: rgba(0, 0, 0, 0.16);
opacity: 0.64;
color: $white !important;
&:focus {
color: $white !important;
opacity: 1;
}
}
@@ -221,7 +228,7 @@
input {
background: $white;
border: 1px solid $gray-500;
color: $gray-200;
color: $gray-200 !important;
&:focus {
color: $gray-50 !important;
@@ -580,6 +587,7 @@ export default {
...mapGetters({
getTaskClasses: 'tasks:getTaskClasses',
getTagsFor: 'tasks:getTagsFor',
canDeleteTask: 'tasks:canDelete',
}),
...mapState({
user: 'user.data',
@@ -593,9 +601,7 @@ export default {
return !isUserChallenge && (this.challengeId || this.task.challenge && this.task.challenge.id);
},
canDelete () {
let isUserChallenge = Boolean(this.task.userId);
let activeChallenge = isUserChallenge && this.task.challenge && this.task.challenge.id && !this.task.challenge.broken;
return this.purpose !== 'create' && !activeChallenge;
return this.purpose !== 'create' && this.canDeleteTask(this.task);
},
title () {
const type = this.$t(this.task.type);
@@ -740,7 +746,7 @@ export default {
this.$root.$emit('hide::modal', 'task-modal');
},
destroy () {
if (!confirm('Are you sure you want to delete this task?')) return;
if (!confirm(this.$t('sureDelete'))) return;
this.destroyTask(this.task);
this.$emit('taskDestroyed', this.task);
this.$root.$emit('hide::modal', 'task-modal');

View File

@@ -0,0 +1,75 @@
<!--
A simplified dropdown component that doesn't rely on buttons as toggles like bootstrap-vue
-->
<template lang="pug">
.habitica-menu-dropdown.item-with-icon.dropdown(@click="toggleDropdown()", :class="{open: isDropdownOpen}")
.habitica-menu-dropdown-toggle
slot(name="dropdown-toggle")
.dropdown-menu(:class="{'dropdown-menu-right': right}")
slot(name="dropdown-content")
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
.habitica-menu-dropdown.open {
.habitica-menu-dropdown-toggle .svg-icon {
color: $white !important;
}
}
</style>
<style lang='scss' scoped>
@import '~client/assets/scss/colors.scss';
.dropdown {
&:hover {
cursor: pointer;
}
.dropdown-menu {
cursor: auto;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
max-height: calc(100vh - 100px);
overflow: auto;
/deep/ .dropdown-separated {
border-bottom: 1px solid $gray-500;
}
}
&.open {
.dropdown-menu {
display: block;
margin-top: 16px;
}
}
}
</style>
<script>
export default {
props: ['right'],
data () {
return {
isDropdownOpen: false,
};
},
mounted () {
document.documentElement.addEventListener('click', this._clickOutListener);
},
destroyed () {
document.removeEventListener('click', this._clickOutListener);
},
methods: {
_clickOutListener (e) {
if (!this.$el.contains(e.target) && this.isDropdownOpen) {
this.toggleDropdown();
}
},
toggleDropdown () {
this.isDropdownOpen = !this.isDropdownOpen;
},
},
};
</script>

View File

@@ -12,11 +12,11 @@
p.call-to-action.text-center {{ $t('checkOffYesterDailies') }}
.tasks-list
task(
v-for='task in tasksByType["daily"]',
:key='task.id',
:task='task',
:isUser='true',
:dueDate='dueDate',
v-for="task in tasksByType.daily",
:key="task.id",
:task="task",
:isUser="true",
:dueDate="dueDate",
)
.start-day.text-center
button.btn.btn-primary(@click='close()') {{ $t('yesterDailiesCallToAction') }}

View File

@@ -14,6 +14,7 @@ let sortableReferences = {};
function createSortable (el, vNode) {
let sortableRef = Sortable.create(el, {
filter: '.task-dropdown', // do not make the tasks dropdown draggable or it won't work
onSort: (evt) => {
emit(vNode, 'onsort', {
oldIndex: evt.oldIndex,

View File

@@ -92,18 +92,29 @@ function sanitizeChecklist (task) {
});
}
}
// Supply an array to create multiple tasks
export async function create (store, createdTask) {
const type = `${createdTask.type}s`;
const list = store.state.tasks.data[type];
// Treat all create actions as if we are adding multiple tasks
const payload = Array.isArray(createdTask) ? createdTask : [createdTask];
sanitizeChecklist(createdTask);
payload.forEach(t => {
const type = `${t.type}s`;
const list = store.state.tasks.data[type];
list.unshift(createdTask);
store.state.user.data.tasksOrder[type].unshift(createdTask._id);
sanitizeChecklist(t);
const response = await axios.post('/api/v3/tasks/user', createdTask);
list.unshift(t);
store.state.user.data.tasksOrder[type].unshift(t._id);
});
Object.assign(list[0], response.data.data);
const response = await axios.post('/api/v3/tasks/user', payload);
const data = Array.isArray(response.data.data) ? response.data.data : [response.data.data];
data.forEach(taskRes => {
const taskData = store.state.tasks.data[`${taskRes.type}s`].find(t => t._id === taskRes._id);
Object.assign(taskData, taskRes);
});
}
export async function save (store, editedTask) {

View File

@@ -27,6 +27,14 @@ function getTaskColorByValue (value) {
}
}
export function canDelete () {
return (task) => {
let isUserChallenge = Boolean(task.userId);
let activeChallenge = isUserChallenge && task.challenge && task.challenge.id && !task.challenge.broken;
return !activeChallenge;
};
}
export function getTaskClasses (store) {
const userPreferences = store.state.user.data.preferences;

View File

@@ -182,6 +182,7 @@
"consecutiveMonths": "Consecutive Months:",
"gemCapExtra": "Gem Cap Extra:",
"mysticHourglasses": "Mystic Hourglasses:",
"mysticHourglassesTooltip": "Mystic Hourglasses",
"paypal": "PayPal",
"amazonPayments": "Amazon Payments",
"timezone": "Time Zone",

View File

@@ -2,7 +2,7 @@
"clearCompleted": "Delete Completed",
"lotOfToDos": "Your most recent 30 completed To-Dos are shown here. You can see older completed To-Dos from Data > Data Display Tool or Data > Export Data > User Data.",
"deleteToDosExplanation": "If you click the button below, all of your completed To-Dos and archived To-Dos will be permanently deleted, except for To-Dos from active challenges and Group Plans. Export them first if you want to keep a record of them.",
"addmultiple": "Add Multiple",
"addMultipleTip": "<strong>Tip:</strong> To add multiple Tasks, separate each one using a line break (Shift + Enter) and then press \"Enter.\"",
"addsingle": "Add Single",
"addATask": "Add a <%= type %>",
"editATask": "Edit a <%= type %>",
@@ -25,7 +25,8 @@
"checklist": "Checklist",
"checklistText": "Break a task into smaller pieces! Checklists increase the Experience and Gold gained from a To-Do, and reduce the damage caused by a Daily.",
"newChecklistItem": "New checklist item",
"expandCollapse": "Expand/Collapse",
"expandChecklist": "Expand Checklist",
"collapseChecklist": "Collapse Checklist",
"text": "Title",
"extraNotes": "Extra Notes",
"notes": "Notes",
@@ -114,10 +115,11 @@
"fortifyText": "Fortify will return all your tasks, except challenge tasks, to a neutral (yellow) state, as if you'd just added them, and top your Health off to full. This is great if all your red tasks are making the game too hard, or all your blue tasks are making the game too easy. If starting fresh sounds much more motivating, spend the Gems and catch a reprieve!",
"confirmFortify": "Are you sure?",
"fortifyComplete": "Fortify complete!",
"sureDelete": "Are you sure you want to delete the <%= taskType %> with the text \"<%= taskText %>\"?",
"sureDelete": "Are you sure you want to delete this task?",
"sureDeleteCompletedTodos": "Are you sure you want to delete your completed todos?",
"streakCoins": "Streak Bonus!",
"pushTaskToTop": "Push task to top. Hold ctrl or cmd to push to bottom.",
"taskToTop": "To top",
"taskToBottom": "To bottom",
"emptyTask": "Enter the task's title first.",
"dailiesRestingInInn": "You're Resting in the Inn! Your Dailies will NOT hurt you tonight, but they WILL still refresh every day. If you're in a quest, you won't deal damage/collect items until you check out of the Inn, but you can still be injured by a Boss if your Party mates skip their own Dailies.",
"habitHelp1": "Good Habits are things that you do often. They award Gold and Experience every time you click the <%= plusIcon %>.",