Files
habitica/website/client/components/tasks/column.vue
Keith Holliday b0a980d56e Merge develop into release (#9154)
* Client: fix Apidoc and move email files (#9139)

* fix apidoc

* move emails files

* quest leader can start/end quest; admins can edit challenges/guilds; reverse chat works; remove static/videos link; etc (#9140)

* enable link to markdown info on group and challenge edit screen

* allow admin (moderators and staff) to edit challenges

* allow admin (moderators and staff) to edit guilds

Also add some unrelated TODO comments.

* allow any party member (not just leader) to start quest from party page

* allow quest owner to cancel, begin, abort quest

Previously only the party leader could see those buttons. The leader still can.

This also hides those buttons from all other party members.

* enable reverse chat in guilds and party

* remove outdated videos from press kit

* adjust various wordings

* Be consistent with capitalization of Check-In. (#9118)

* limit for inlined svg images and make home leaner by not bundling it with the rest of static pages

* sep 27 fixes (#9088)

* fix item paddings / drawer width

* expand the width of item-rows by the margin of an item

* fix hatchedPet-dialog

* fix hatching-modal

* remove min-height

* Oct 3 fixes (#9148)

* Only show level after yesterdailies modal

* Fixed zindex

* Added spcial spells to rewards column

* Added single click buy for health and armoire

* Prevented task scoring when casting a spell

* Renamed generic purchase method

* Updated nav for small screen

* Hide checklist while casting

* fix some text describing menu items (#9145)
2017-10-03 21:15:00 -05:00

429 lines
11 KiB
Vue

<template lang="pug">
.tasks-column(:class='type')
b-modal(ref="editTaskModal")
.d-flex
h2.tasks-column-title(v-once) {{ $t(types[type].label) }}
.filters.d-flex.justify-content-end
.filter.small-text(
v-for="filter in types[type].filters",
:class="{active: activeFilters[type].label === filter.label}",
@click="activateFilter(type, filter)",
) {{ $t(filter.label) }}
.tasks-list(ref="taskList", v-sortable='', @onsort='sorted')
task(
v-for="task in taskList",
:key="task.id", :task="task",
v-if="filterTask(task)",
:isUser="isUser",
@editTask="editTask",
:group='group',
)
template(v-if="hasRewardsList")
.reward-items
shopItem(
v-for="reward in inAppRewards",
:item="reward",
:key="reward.key",
:highlightBorder="reward.isSuggested",
@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>
@import '~client/assets/scss/colors.scss';
.tasks-column {
min-height: 556px;
}
.task-wrapper {
position: relative;
z-index: 2;
}
.task-wrapper + .reward-items {
margin-top: 16px;
}
.reward-items {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.tasks-list {
border-radius: 4px;
background: $gray-600;
padding: 8px;
position: relative; // needed for the .bottom-gradient to be position: absolute
height: calc(100% - 56px);
padding-bottom: 30px;
}
.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;
}
.tasks-column-title {
margin-bottom: 8px;
}
.filters {
flex-grow: 1;
}
.filter {
font-weight: bold;
color: $gray-100;
font-style: normal;
padding: 8px;
cursor: pointer;
&:hover {
color: $purple-200;
}
&.active {
color: $purple-200;
border-bottom: 2px solid $purple-200;
padding-bottom: 6px;
}
}
.column-background {
position: absolute;
bottom: 32px;
z-index: 1;
&.initial-description {
top: 30%;
}
.svg-icon {
margin: 0 auto;
margin-bottom: 12px;
}
h3, .small-text {
color: $gray-300;
text-align: center;
}
h3 {
font-weight: normal;
margin-bottom: 4px;
}
.small-text {
font-style: normal;
padding-left: 24px;
padding-right: 24px;
}
}
.icon-habit {
width: 30px;
height: 20px;
color: #A5A1AC;
}
.icon-daily {
width: 30px;
height: 20px;
color: #A5A1AC;
}
.icon-todo {
width: 20px;
height: 20px;
color: #A5A1AC;
}
.icon-reward {
width: 26px;
height: 20px;
color: #A5A1AC;
}
</style>
<script>
import Task from './task';
import sortBy from 'lodash/sortBy';
import throttle from 'lodash/throttle';
import bModal from 'bootstrap-vue/lib/components/modal';
import sortable from 'client/directives/sortable.directive';
import buyMixin from 'client/mixins/buy';
import { mapState, mapActions } from 'client/libs/store';
import shopItem from '../shops/shopItem';
import { shouldDo } from 'common/script/cron';
import inAppRewards from 'common/script/libs/inAppRewards';
import spells from 'common/script/content/spells';
import habitIcon from 'assets/svg/habit.svg';
import dailyIcon from 'assets/svg/daily.svg';
import todoIcon from 'assets/svg/todo.svg';
import rewardIcon from 'assets/svg/reward.svg';
export default {
mixins: [buyMixin],
components: {
Task,
bModal,
shopItem,
},
directives: {
sortable,
},
props: ['type', 'isUser', 'searchText', 'selectedTags', 'taskListOverride', 'group'], // @TODO: maybe we should store the group on state?
data () {
const types = Object.freeze({
habit: {
label: 'habits',
filters: [
{label: 'all', filter: () => true, default: true},
{label: 'yellowred', filter: t => t.value < 1}, // weak
{label: 'greenblue', filter: t => t.value >= 1}, // strong
],
},
daily: {
label: 'dailies',
filters: [
{label: 'all', filter: () => true, default: true},
{label: 'due', filter: t => !t.completed && shouldDo(new Date(), t, this.userPreferences)},
{label: 'notDue', filter: t => t.completed || !shouldDo(new Date(), t, this.userPreferences)},
],
},
todo: {
label: 'todos',
filters: [
{label: 'remaining', filter: t => !t.completed, default: true}, // active
{label: 'scheduled', filter: t => !t.completed && t.date, sort: t => t.date},
{label: 'complete2', filter: t => t.completed},
],
},
reward: {
label: 'rewards',
filters: [
{label: 'all', filter: () => true, default: true},
{label: 'custom', filter: () => true}, // all rewards made by the user
{label: 'wishlist', filter: () => false}, // not user tasks
],
},
});
const icons = Object.freeze({
habit: habitIcon,
daily: dailyIcon,
todo: todoIcon,
reward: rewardIcon,
});
let activeFilters = {};
for (let type in types) {
activeFilters[type] = types[type].filters.find(f => f.default === true);
}
return {
types,
activeFilters,
icons,
openedCompletedTodos: false,
forceRefresh: new Date(),
};
},
computed: {
...mapState({
tasks: 'tasks.data',
user: 'user.data',
userPreferences: 'user.data.preferences',
}),
taskList () {
// @TODO: This should not default to user's tasks. It should require that you pass options in
if (this.taskListOverride) return this.taskListOverride;
return this.tasks[`${this.type}s`];
},
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 = {
snowball: 'salt',
spookySparkles: 'opaquePotion',
shinySeed: 'petalFreePotion',
seafoam: 'sand',
};
for (let key in seasonalSkills) {
if (this.user.stats.buffs[key]) {
let debuff = seasonalSkills[key];
let item = Object.assign({}, spells.special[debuff]);
item.text = item.text();
item.notes = item.notes();
item.class = `shop_${key}`;
rewards.push(item);
}
}
return rewards;
},
hasRewardsList () {
return this.isUser === true && this.type === 'reward' && this.activeFilters[this.type].label !== 'custom';
},
initialColumnDescription () {
// Show the column description in the middle only if there are no elements (tasks or in app items)
if (this.hasRewardsList) {
if (this.inAppRewards && this.inAppRewards.length >= 0) return false;
}
return this.tasks[`${this.type}s`].length === 0;
},
dailyDueDefaultView () {
if (this.user.preferences.dailyDueDefaultView) {
this.activateFilter('daily', this.types.daily.filters[1]);
}
return this.user.preferences.dailyDueDefaultView;
},
},
watch: {
taskList: {
handler: throttle(function setColumnBackgroundVisibility () {
this.setColumnBackgroundVisibility();
}, 250),
deep: true,
},
dailyDueDefaultView () {
if (this.user.preferences.dailyDueDefaultView) {
this.activateFilter('daily', this.types.daily.filters[1]);
}
},
},
mounted () {
this.setColumnBackgroundVisibility();
this.$root.$on('buyModal::boughtItem', () => {
this.forceRefresh = new Date();
});
},
methods: {
...mapActions({loadCompletedTodos: 'tasks:fetchCompletedTodos'}),
sorted (data) {
const sorting = this.taskList;
const taskIdToMove = this.taskList[data.oldIndex]._id;
if (sorting) {
const deleted = sorting.splice(data.oldIndex, 1);
sorting.splice(data.newIndex, 0, deleted[0]);
}
this.$store.dispatch('tasks:move', {
taskId: taskIdToMove,
position: data.newIndex,
});
},
editTask (task) {
this.$emit('editTask', task);
},
activateFilter (type, filter) {
if (type === 'todo' && filter.label === 'complete2') {
this.loadCompletedTodos();
}
this.activeFilters[type] = filter;
if (filter.sort) {
this.tasks[`${type}s`] = sortBy(this.tasks[`${type}s`], filter.sort);
}
},
setColumnBackgroundVisibility () {
this.$nextTick(() => {
const taskListEl = this.$refs.taskList;
const tasklistHeight = taskListEl.offsetHeight;
let combinedTasksHeights = 0;
Array.from(taskListEl.getElementsByClassName('task')).forEach(el => {
combinedTasksHeights += el.offsetHeight;
});
if (!this.$refs.columnBackground) return;
const rewardsList = taskListEl.getElementsByClassName('reward-items')[0];
if (rewardsList) {
combinedTasksHeights += rewardsList.offsetHeight;
}
const columnBackgroundStyle = this.$refs.columnBackground.style;
if (tasklistHeight - combinedTasksHeights < 150) {
columnBackgroundStyle.display = 'none';
} else {
columnBackgroundStyle.display = 'block';
}
});
},
filterTask (task) {
// View
if (!this.activeFilters[task.type].filter(task)) return false;
// Tags
const selectedTags = this.selectedTags;
if (selectedTags && selectedTags.length > 0) {
const hasAllSelectedTag = selectedTags.every(tagId => {
return task.tags.indexOf(tagId) !== -1;
});
if (!hasAllSelectedTag) return false;
}
// Text
const searchText = this.searchText;
if (!searchText) return true;
if (task.text.toLowerCase().indexOf(searchText) !== -1) return true;
if (task.notes.toLowerCase().indexOf(searchText) !== -1) return true;
if (task.checklist && task.checklist.length) {
const checklistItemIndex = task.checklist.findIndex(({text}) => {
return text.toLowerCase().indexOf(searchText) !== -1;
});
return checklistItemIndex !== -1;
}
},
openBuyDialog (rewardItem) {
// Buy armoire and health potions immediately
let itemsToPurchaseImmediately = ['potion', 'armoire'];
if (itemsToPurchaseImmediately.indexOf(rewardItem.key) !== -1) {
this.makeGenericPurchase(rewardItem);
this.$emit('buyPressed', rewardItem);
return;
}
if (rewardItem.purchaseType !== 'gear' || !rewardItem.locked) {
this.$emit('openBuyDialog', rewardItem);
}
},
},
};
</script>