mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
Task page : task filters (#9898)
* add tasklist getter * add unit test + refactor for getters * add task order sorting + update unit tests * remove direct access to store.state.tasks * add tag and search filter back to column.vue + unit tests * add unit test for task order setting function * add task filters to helper file + modify taskColumn state access to getter * update column to get values at runtime. TODO set active filter at runtime * add TaskColumn init state + daily-due-default * add check for task type daily before set/reset dailyDueView * remove unused sortBy import in column.vue * remove unused sortBy * pr review requested updates * pr review clean up updates
This commit is contained in:
committed by
Matteo Pagliazzi
parent
f592103754
commit
9919faeed8
127
test/client/unit/specs/components/tasks/column.js
Normal file
127
test/client/unit/specs/components/tasks/column.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// VueJS produces following warnings if the corresponding imports are not used
|
||||||
|
// [Vue warn]: You are using the runtime-only build of Vue where the template option is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
|
||||||
|
import Vue from 'vue/dist/vue';
|
||||||
|
|
||||||
|
import TaskColumn from 'client/components/tasks/column.vue';
|
||||||
|
|
||||||
|
describe('Task Column Component', () => {
|
||||||
|
let vm, Ctor, tasks;
|
||||||
|
|
||||||
|
describe('Task Filtering', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Ctor = Vue.extend(TaskColumn);
|
||||||
|
vm = new Ctor({
|
||||||
|
propsData: {
|
||||||
|
type: 'habit',
|
||||||
|
},
|
||||||
|
}).$mount();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('by Tags', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tasks = [
|
||||||
|
{ tags: [3, 4] },
|
||||||
|
{ tags: [2, 3] },
|
||||||
|
{ tags: [] },
|
||||||
|
{ tags: [1, 3] },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns all tasks when given no tags', () => {
|
||||||
|
let returnedTasks = vm.filterByTagList(tasks);
|
||||||
|
expect(returnedTasks).to.have.lengthOf(tasks.length);
|
||||||
|
tasks.forEach((task, i) => {
|
||||||
|
expect(returnedTasks[i]).to.eq(task);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns all tasks with given single tag', () => {
|
||||||
|
let returnedTasks = vm.filterByTagList(tasks, [3]);
|
||||||
|
|
||||||
|
expect(returnedTasks).to.have.lengthOf(3);
|
||||||
|
expect(returnedTasks[0]).to.eq(tasks[0]);
|
||||||
|
expect(returnedTasks[1]).to.eq(tasks[1]);
|
||||||
|
expect(returnedTasks[2]).to.eq(tasks[3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns all tasks with given multiple tags', () => {
|
||||||
|
let returnedTasks = vm.filterByTagList(tasks, [2, 3]);
|
||||||
|
|
||||||
|
expect(returnedTasks).to.have.lengthOf(1);
|
||||||
|
expect(returnedTasks[0]).to.eq(tasks[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('by Search Text', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tasks = [
|
||||||
|
{
|
||||||
|
text: 'Hello world 1',
|
||||||
|
note: '',
|
||||||
|
checklist: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Hello world 2',
|
||||||
|
note: '',
|
||||||
|
checklist: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Generic Task Title',
|
||||||
|
note: '',
|
||||||
|
checklist: [
|
||||||
|
{ text: 'Check 1' },
|
||||||
|
{ text: 'Check 2' },
|
||||||
|
{ text: 'Check 3' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Hello world 3',
|
||||||
|
note: 'Generic Task Note',
|
||||||
|
checklist: [
|
||||||
|
{ text: 'Checkitem 1' },
|
||||||
|
{ text: 'Checkitem 2' },
|
||||||
|
{ text: 'Checkitem 3' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all tasks with empty search term', () => {
|
||||||
|
let returnedTasks = vm.filterBySearchText(tasks);
|
||||||
|
expect(returnedTasks).to.have.lengthOf(tasks.length);
|
||||||
|
tasks.forEach((task, i) => {
|
||||||
|
expect(returnedTasks[i]).to.eq(task);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return tasks with search term in title /i', () => {
|
||||||
|
['Title', 'TITLE', 'title', 'tItLe'].forEach((term) => {
|
||||||
|
expect(vm.filterBySearchText(tasks, term)[0]).to.eq(tasks[2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return tasks with search term in note /i', () => {
|
||||||
|
['Note', 'NOTE', 'note', 'nOtE'].forEach((term) => {
|
||||||
|
expect(vm.filterBySearchText(tasks, term)[0]).to.eq(tasks[3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return tasks with search term in checklist title /i', () => {
|
||||||
|
['Check', 'CHECK', 'check', 'cHeCK'].forEach((term) => {
|
||||||
|
let returnedTasks = vm.filterBySearchText(tasks, term);
|
||||||
|
|
||||||
|
expect(returnedTasks[0]).to.eq(tasks[2]);
|
||||||
|
expect(returnedTasks[1]).to.eq(tasks[3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
['Checkitem', 'CHECKITEM', 'checkitem', 'cHeCKiTEm'].forEach((term) => {
|
||||||
|
expect(vm.filterBySearchText(tasks, term)[0]).to.eq(tasks[3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vm.$destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
62
test/client/unit/specs/libs/store/helpers/filterTasks.js
Normal file
62
test/client/unit/specs/libs/store/helpers/filterTasks.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
getTypeLabel,
|
||||||
|
getFilterLabels,
|
||||||
|
getActiveFilter,
|
||||||
|
} from 'client/libs/store/helpers/filterTasks.js';
|
||||||
|
|
||||||
|
describe('Filter Category for Tasks', () => {
|
||||||
|
describe('getTypeLabel', () => {
|
||||||
|
it('should return correct task type labels', () => {
|
||||||
|
expect(getTypeLabel('habit')).to.eq('habits');
|
||||||
|
expect(getTypeLabel('daily')).to.eq('dailies');
|
||||||
|
expect(getTypeLabel('todo')).to.eq('todos');
|
||||||
|
expect(getTypeLabel('reward')).to.eq('rewards');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFilterLabels', () => {
|
||||||
|
let habit, daily, todo, reward;
|
||||||
|
beforeEach(() => {
|
||||||
|
habit = ['all', 'yellowred', 'greenblue'];
|
||||||
|
daily = ['all', 'due', 'notDue'];
|
||||||
|
todo = ['remaining', 'scheduled', 'complete2'];
|
||||||
|
reward = ['all', 'custom', 'wishlist'];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all task type filter labels by type', () => {
|
||||||
|
// habits
|
||||||
|
getFilterLabels('habit').forEach((item, i) => {
|
||||||
|
expect(item).to.eq(habit[i]);
|
||||||
|
});
|
||||||
|
// dailys
|
||||||
|
getFilterLabels('daily').forEach((item, i) => {
|
||||||
|
expect(item).to.eq(daily[i]);
|
||||||
|
});
|
||||||
|
// todos
|
||||||
|
getFilterLabels('todo').forEach((item, i) => {
|
||||||
|
expect(item).to.eq(todo[i]);
|
||||||
|
});
|
||||||
|
// rewards
|
||||||
|
getFilterLabels('reward').forEach((item, i) => {
|
||||||
|
expect(item).to.eq(reward[i]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getActiveFilter', () => {
|
||||||
|
it('should return single function by default', () => {
|
||||||
|
let activeFilter = getActiveFilter('habit');
|
||||||
|
expect(activeFilter).to.be.an('object');
|
||||||
|
expect(activeFilter).to.have.all.keys('label', 'filterFn', 'default');
|
||||||
|
expect(activeFilter.default).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return single function for given filter type', () => {
|
||||||
|
let activeFilterLabel = 'yellowred';
|
||||||
|
let activeFilter = getActiveFilter('habit', activeFilterLabel);
|
||||||
|
expect(activeFilter).to.be.an('object');
|
||||||
|
expect(activeFilter).to.have.all.keys('label', 'filterFn');
|
||||||
|
expect(activeFilter.label).to.eq(activeFilterLabel);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
43
test/client/unit/specs/libs/store/helpers/orderTasks.js
Normal file
43
test/client/unit/specs/libs/store/helpers/orderTasks.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
orderSingleTypeTasks,
|
||||||
|
// orderMultipleTypeTasks,
|
||||||
|
} from 'client/libs/store/helpers/orderTasks.js';
|
||||||
|
|
||||||
|
import shuffle from 'lodash/shuffle';
|
||||||
|
|
||||||
|
describe('Task Order Helper Function', () => {
|
||||||
|
let tasks, shuffledTasks, taskOrderList;
|
||||||
|
beforeEach(() => {
|
||||||
|
taskOrderList = [1, 2, 3, 4];
|
||||||
|
tasks = [];
|
||||||
|
taskOrderList.forEach(i => tasks.push({ _id: i, id: i }));
|
||||||
|
shuffledTasks = shuffle(tasks);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return tasks as is for no task order', () => {
|
||||||
|
expect(orderSingleTypeTasks(shuffledTasks)).to.eq(shuffledTasks);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return tasks in expected order', () => {
|
||||||
|
let newOrderedTasks = orderSingleTypeTasks(shuffledTasks, taskOrderList);
|
||||||
|
newOrderedTasks.forEach((item, index) => {
|
||||||
|
expect(item).to.eq(tasks[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return new tasks at end of expected order', () => {
|
||||||
|
let newTaskIds = [10, 15, 20];
|
||||||
|
newTaskIds.forEach(i => tasks.push({ _id: i, id: i }));
|
||||||
|
shuffledTasks = shuffle(tasks);
|
||||||
|
|
||||||
|
let newOrderedTasks = orderSingleTypeTasks(shuffledTasks, taskOrderList);
|
||||||
|
// checking tasks with order
|
||||||
|
newOrderedTasks.slice(0, taskOrderList.length).forEach((item, index) => {
|
||||||
|
expect(item).to.eq(tasks[index]);
|
||||||
|
});
|
||||||
|
// check for new task ids
|
||||||
|
newOrderedTasks.slice(-3).forEach(item => {
|
||||||
|
expect(item.id).to.be.oneOf(newTaskIds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
118
test/client/unit/specs/store/getters/tasks/getTaskList.js
Normal file
118
test/client/unit/specs/store/getters/tasks/getTaskList.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import generateStore from 'client/store';
|
||||||
|
|
||||||
|
describe('Store Getters for Tasks', () => {
|
||||||
|
let store, habits, dailys, todos, rewards;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = generateStore();
|
||||||
|
// Get user preference data and user tasks order data
|
||||||
|
store.state.user.data = {
|
||||||
|
preferences: {},
|
||||||
|
tasksOrder: {
|
||||||
|
habits: [],
|
||||||
|
dailys: [],
|
||||||
|
todos: [],
|
||||||
|
rewards: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Task List', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
habits = [
|
||||||
|
{ id: 1 },
|
||||||
|
{ id: 2 },
|
||||||
|
];
|
||||||
|
dailys = [
|
||||||
|
{ id: 3 },
|
||||||
|
{ id: 4 },
|
||||||
|
];
|
||||||
|
todos = [
|
||||||
|
{ id: 5 },
|
||||||
|
{ id: 6 },
|
||||||
|
];
|
||||||
|
rewards = [
|
||||||
|
{ id: 7 },
|
||||||
|
{ id: 8 },
|
||||||
|
];
|
||||||
|
store.state.tasks.data = {
|
||||||
|
habits,
|
||||||
|
dailys,
|
||||||
|
todos,
|
||||||
|
rewards,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should returns all tasks by task type', () => {
|
||||||
|
let returnedTasks = store.getters['tasks:getUnfilteredTaskList']('habit');
|
||||||
|
expect(returnedTasks).to.eq(habits);
|
||||||
|
|
||||||
|
returnedTasks = store.getters['tasks:getUnfilteredTaskList']('daily');
|
||||||
|
expect(returnedTasks).to.eq(dailys);
|
||||||
|
|
||||||
|
returnedTasks = store.getters['tasks:getUnfilteredTaskList']('todo');
|
||||||
|
expect(returnedTasks).to.eq(todos);
|
||||||
|
|
||||||
|
returnedTasks = store.getters['tasks:getUnfilteredTaskList']('reward');
|
||||||
|
expect(returnedTasks).to.eq(rewards);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// @TODO add task filter check for rewards and dailys
|
||||||
|
describe('Task Filters', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
habits = [
|
||||||
|
// weak habit
|
||||||
|
{ value: 0 },
|
||||||
|
// strong habit
|
||||||
|
{ value: 2 },
|
||||||
|
];
|
||||||
|
todos = [
|
||||||
|
// scheduled todos
|
||||||
|
{ completed: false, date: 'Mon, 15 Jan 2018 12:18:29 GMT' },
|
||||||
|
// completed todos
|
||||||
|
{ completed: true },
|
||||||
|
];
|
||||||
|
store.state.tasks.data = {
|
||||||
|
habits,
|
||||||
|
todos,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return weak habits', () => {
|
||||||
|
let returnedTasks = store.getters['tasks:getFilteredTaskList']({
|
||||||
|
type: 'habit',
|
||||||
|
filterType: 'yellowred',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(returnedTasks[0]).to.eq(habits[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return strong habits', () => {
|
||||||
|
let returnedTasks = store.getters['tasks:getFilteredTaskList']({
|
||||||
|
type: 'habit',
|
||||||
|
filterType: 'greenblue',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(returnedTasks[0]).to.eq(habits[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return scheduled todos', () => {
|
||||||
|
let returnedTasks = store.getters['tasks:getFilteredTaskList']({
|
||||||
|
type: 'todo',
|
||||||
|
filterType: 'scheduled',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(returnedTasks[0]).to.eq(todos[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return completed todos', () => {
|
||||||
|
let returnedTasks = store.getters['tasks:getFilteredTaskList']({
|
||||||
|
type: 'todo',
|
||||||
|
filterType: 'complete2',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(returnedTasks[0]).to.eq(todos[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,14 +8,14 @@
|
|||||||
v-if='type === "reward"')
|
v-if='type === "reward"')
|
||||||
.d-flex
|
.d-flex
|
||||||
h2.tasks-column-title
|
h2.tasks-column-title
|
||||||
| {{ $t(types[type].label) }}
|
| {{ $t(typeLabel) }}
|
||||||
.badge.badge-pill.badge-purple.column-badge(v-if="badgeCount > 0") {{ badgeCount }}
|
.badge.badge-pill.badge-purple.column-badge(v-if="badgeCount > 0") {{ badgeCount }}
|
||||||
.filters.d-flex.justify-content-end
|
.filters.d-flex.justify-content-end
|
||||||
.filter.small-text(
|
.filter.small-text(
|
||||||
v-for="filter in types[type].filters",
|
v-for="filter in typeFilters",
|
||||||
:class="{active: activeFilters[type].label === filter.label}",
|
:class="{active: activeFilter.label === filter}",
|
||||||
@click="activateFilter(type, filter)",
|
@click="activateFilter(type, filter)",
|
||||||
) {{ $t(filter.label) }}
|
) {{ $t(filter) }}
|
||||||
.tasks-list(ref="tasksWrapper")
|
.tasks-list(ref="tasksWrapper")
|
||||||
textarea.quick-add(
|
textarea.quick-add(
|
||||||
:rows="quickAddRows",
|
:rows="quickAddRows",
|
||||||
@@ -26,19 +26,19 @@
|
|||||||
)
|
)
|
||||||
transition(name="quick-add-tip-slide")
|
transition(name="quick-add-tip-slide")
|
||||||
.quick-add-tip.small-text(v-show="quickAddFocused", v-html="$t('addMultipleTip')")
|
.quick-add-tip.small-text(v-show="quickAddFocused", v-html="$t('addMultipleTip')")
|
||||||
clear-completed-todos(v-if="activeFilters[type].label === 'complete2'")
|
clear-completed-todos(v-if="activeFilter.label === 'complete2'")
|
||||||
.column-background(
|
.column-background(
|
||||||
v-if="isUser === true",
|
v-if="isUser === true",
|
||||||
:class="{'initial-description': initialColumnDescription}",
|
:class="{'initial-description': initialColumnDescription}",
|
||||||
ref="columnBackground",
|
ref="columnBackground",
|
||||||
)
|
)
|
||||||
.svg-icon(v-html="icons[type]", :class="`icon-${type}`", v-once)
|
.svg-icon(v-html="icons[type]", :class="`icon-${type}`", v-once)
|
||||||
h3(v-once) {{$t('theseAreYourTasks', {taskType: $t(types[type].label)})}}
|
h3(v-once) {{$t('theseAreYourTasks', {taskType: $t(typeLabel)})}}
|
||||||
.small-text {{$t(`${type}sDesc`)}}
|
.small-text {{$t(`${type}sDesc`)}}
|
||||||
draggable.sortable-tasks(
|
draggable.sortable-tasks(
|
||||||
ref="tasksList",
|
ref="tasksList",
|
||||||
@update='sorted',
|
@update='sorted',
|
||||||
:options='{disabled: activeFilters[type].label === "scheduled"}',
|
:options='{disabled: activeFilter.label === "scheduled"}',
|
||||||
)
|
)
|
||||||
task(
|
task(
|
||||||
v-for="task in taskList",
|
v-for="task in taskList",
|
||||||
@@ -245,10 +245,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import Task from './task';
|
import Task from './task';
|
||||||
import ClearCompletedTodos from './clearCompletedTodos';
|
import ClearCompletedTodos from './clearCompletedTodos';
|
||||||
import sortBy from 'lodash/sortBy';
|
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import buyMixin from 'client/mixins/buy';
|
import buyMixin from 'client/mixins/buy';
|
||||||
import { mapState, mapActions } from 'client/libs/store';
|
import { mapState, mapActions, mapGetters } from 'client/libs/store';
|
||||||
import shopItem from '../shops/shopItem';
|
import shopItem from '../shops/shopItem';
|
||||||
import BuyQuestModal from 'client/components/shops/quests/buyQuestModal.vue';
|
import BuyQuestModal from 'client/components/shops/quests/buyQuestModal.vue';
|
||||||
|
|
||||||
@@ -258,6 +258,12 @@ import inAppRewards from 'common/script/libs/inAppRewards';
|
|||||||
import spells from 'common/script/content/spells';
|
import spells from 'common/script/content/spells';
|
||||||
import taskDefaults from 'common/script/libs/taskDefaults';
|
import taskDefaults from 'common/script/libs/taskDefaults';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTypeLabel,
|
||||||
|
getFilterLabels,
|
||||||
|
getActiveFilter,
|
||||||
|
} from 'client/libs/store/helpers/filterTasks.js';
|
||||||
|
|
||||||
import svgPin from 'assets/svg/pin.svg';
|
import svgPin from 'assets/svg/pin.svg';
|
||||||
import habitIcon from 'assets/svg/habit.svg';
|
import habitIcon from 'assets/svg/habit.svg';
|
||||||
import dailyIcon from 'assets/svg/daily.svg';
|
import dailyIcon from 'assets/svg/daily.svg';
|
||||||
@@ -276,42 +282,6 @@ export default {
|
|||||||
},
|
},
|
||||||
props: ['type', 'isUser', 'searchText', 'selectedTags', 'taskListOverride', 'group'], // @TODO: maybe we should store the group on state?
|
props: ['type', 'isUser', 'searchText', 'selectedTags', 'taskListOverride', 'group'], // @TODO: maybe we should store the group on state?
|
||||||
data () {
|
data () {
|
||||||
// @TODO refactor this so that filter functions aren't in 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({
|
const icons = Object.freeze({
|
||||||
habit: habitIcon,
|
habit: habitIcon,
|
||||||
daily: dailyIcon,
|
daily: dailyIcon,
|
||||||
@@ -320,14 +290,15 @@ export default {
|
|||||||
pin: svgPin,
|
pin: svgPin,
|
||||||
});
|
});
|
||||||
|
|
||||||
let activeFilters = {};
|
let typeLabel = '';
|
||||||
for (let type in types) {
|
let typeFilters = [];
|
||||||
activeFilters[type] = types[type].filters.find(f => f.default === true);
|
let activeFilter = {};
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
types,
|
typeLabel,
|
||||||
activeFilters,
|
typeFilters,
|
||||||
|
activeFilter,
|
||||||
|
|
||||||
icons,
|
icons,
|
||||||
openedCompletedTodos: false,
|
openedCompletedTodos: false,
|
||||||
|
|
||||||
@@ -339,45 +310,38 @@ export default {
|
|||||||
selectedItemToBuy: {},
|
selectedItemToBuy: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
// Set Task Column Label
|
||||||
|
this.typeLabel = getTypeLabel(this.type);
|
||||||
|
// Get Category Filter Labels
|
||||||
|
this.typeFilters = getFilterLabels(this.type);
|
||||||
|
// Set default filter for task column
|
||||||
|
this.activateFilter(this.type);
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
tasks: 'tasks.data',
|
|
||||||
user: 'user.data',
|
user: 'user.data',
|
||||||
userPreferences: 'user.data.preferences',
|
|
||||||
}),
|
}),
|
||||||
onUserPage () {
|
...mapGetters({
|
||||||
let onUserPage = Boolean(this.taskList.length) && (!this.taskListOverride || this.taskListOverride.length === 0);
|
getFilteredTaskList: 'tasks:getFilteredTaskList',
|
||||||
|
getUnfilteredTaskList: 'tasks:getUnfilteredTaskList',
|
||||||
if (!onUserPage) {
|
getUserPreferences: 'user:preferences',
|
||||||
this.activateFilter('daily', this.types.daily.filters[0]);
|
getUserBuffs: 'user:buffs',
|
||||||
this.types.reward.filters = [];
|
}),
|
||||||
}
|
|
||||||
|
|
||||||
return onUserPage;
|
|
||||||
},
|
|
||||||
taskList () {
|
taskList () {
|
||||||
// @TODO: This should not default to user's tasks. It should require that you pass options in
|
// @TODO: This should not default to user's tasks. It should require that you pass options in
|
||||||
const filter = this.activeFilters[this.type];
|
|
||||||
|
|
||||||
let taskList = this.tasks[`${this.type}s`];
|
let filteredTaskList = isEmpty(this.taskListOverride) ?
|
||||||
if (this.taskListOverride) taskList = this.taskListOverride;
|
this.getFilteredTaskList({
|
||||||
|
type: this.type,
|
||||||
|
filterType: this.activeFilter.label,
|
||||||
|
}) :
|
||||||
|
this.taskListOverride;
|
||||||
|
|
||||||
if (taskList.length > 0 && ['scheduled', 'due'].indexOf(filter.label) === -1) {
|
let taggedList = this.filterByTagList(filteredTaskList, this.selectedTags);
|
||||||
let taskListSorted = this.$store.dispatch('tasks:order', [
|
let searchedList = this.filterBySearchText(taggedList, this.searchText);
|
||||||
taskList,
|
|
||||||
this.user.tasksOrder,
|
|
||||||
]);
|
|
||||||
|
|
||||||
taskList = taskListSorted[`${this.type}s`];
|
return searchedList;
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.sort) {
|
|
||||||
taskList = sortBy(taskList, filter.sort);
|
|
||||||
}
|
|
||||||
|
|
||||||
return taskList.filter(t => {
|
|
||||||
return this.filterTask(t);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
inAppRewards () {
|
inAppRewards () {
|
||||||
let watchRefresh = this.forceRefresh; // eslint-disable-line
|
let watchRefresh = this.forceRefresh; // eslint-disable-line
|
||||||
@@ -393,7 +357,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (let key in seasonalSkills) {
|
for (let key in seasonalSkills) {
|
||||||
if (this.user.stats.buffs[key]) {
|
if (this.getUserBuffs(key)) {
|
||||||
let debuff = seasonalSkills[key];
|
let debuff = seasonalSkills[key];
|
||||||
let item = Object.assign({}, spells.special[debuff]);
|
let item = Object.assign({}, spells.special[debuff]);
|
||||||
item.text = item.text();
|
item.text = item.text();
|
||||||
@@ -406,7 +370,7 @@ export default {
|
|||||||
return rewards;
|
return rewards;
|
||||||
},
|
},
|
||||||
hasRewardsList () {
|
hasRewardsList () {
|
||||||
return this.isUser === true && this.type === 'reward' && this.activeFilters[this.type].label !== 'custom';
|
return this.isUser === true && this.type === 'reward' && this.activeFilter.label !== 'custom';
|
||||||
},
|
},
|
||||||
initialColumnDescription () {
|
initialColumnDescription () {
|
||||||
// Show the column description in the middle only if there are no elements (tasks or in app items)
|
// Show the column description in the middle only if there are no elements (tasks or in app items)
|
||||||
@@ -414,13 +378,12 @@ export default {
|
|||||||
if (this.inAppRewards && this.inAppRewards.length >= 0) return false;
|
if (this.inAppRewards && this.inAppRewards.length >= 0) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.tasks[`${this.type}s`].length === 0;
|
return this.taskList.length === 0;
|
||||||
},
|
},
|
||||||
dailyDueDefaultView () {
|
dailyDueDefaultView () {
|
||||||
if (this.user.preferences.dailyDueDefaultView) {
|
if (this.type === 'daily' && this.user.preferences.dailyDueDefaultView) {
|
||||||
this.activateFilter('daily', this.types.daily.filters[1]);
|
this.activateFilter('daily', this.typeFilters[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.user.preferences.dailyDueDefaultView;
|
return this.user.preferences.dailyDueDefaultView;
|
||||||
},
|
},
|
||||||
quickAddPlaceholder () {
|
quickAddPlaceholder () {
|
||||||
@@ -431,16 +394,14 @@ export default {
|
|||||||
// 0 means the badge will not be shown
|
// 0 means the badge will not be shown
|
||||||
// It is shown for the all and due views of dailies
|
// It is shown for the all and due views of dailies
|
||||||
// and for the active and scheduled views of todos.
|
// and for the active and scheduled views of todos.
|
||||||
if (this.type === 'todo') {
|
if (this.type === 'todo' && this.activeFilter.label !== 'complete2') {
|
||||||
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;
|
return this.taskList.length;
|
||||||
} else if (activeFilter === 'all') {
|
} else if (this.type === 'daily') {
|
||||||
|
if (this.activeFilter.label === 'due') {
|
||||||
|
return this.taskList.length;
|
||||||
|
} else if (this.activeFilter.label === 'all') {
|
||||||
return this.taskList.reduce((count, t) => {
|
return this.taskList.reduce((count, t) => {
|
||||||
return !t.completed && shouldDo(new Date(), t, this.userPreferences) ? count + 1 : count;
|
return !t.completed && shouldDo(new Date(), t, this.getUserPreferences) ? count + 1 : count;
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -457,7 +418,9 @@ export default {
|
|||||||
},
|
},
|
||||||
dailyDueDefaultView () {
|
dailyDueDefaultView () {
|
||||||
if (!this.dailyDueDefaultView) return;
|
if (!this.dailyDueDefaultView) return;
|
||||||
this.activateFilter('daily', this.types.daily.filters[1]);
|
if (this.type === 'daily' && this.dailyDueDefaultView) {
|
||||||
|
this.activateFilter('daily', this.typeFilters[1]);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
quickAddFocused (newValue) {
|
quickAddFocused (newValue) {
|
||||||
if (newValue) this.quickAddRows = this.quickAddText.split('\n').length;
|
if (newValue) this.quickAddRows = this.quickAddText.split('\n').length;
|
||||||
@@ -491,7 +454,7 @@ export default {
|
|||||||
const filteredList = this.taskList;
|
const filteredList = this.taskList;
|
||||||
const taskToMove = filteredList[data.oldIndex];
|
const taskToMove = filteredList[data.oldIndex];
|
||||||
const taskIdToMove = taskToMove._id;
|
const taskIdToMove = taskToMove._id;
|
||||||
let originTasks = this.tasks[`${this.type}s`];
|
let originTasks = this.getUnfilteredTaskList(this.type);
|
||||||
if (this.taskListOverride) originTasks = this.taskListOverride;
|
if (this.taskListOverride) originTasks = this.taskListOverride;
|
||||||
|
|
||||||
// Server
|
// Server
|
||||||
@@ -518,7 +481,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async moveTo (task, where) { // where is 'top' or 'bottom'
|
async moveTo (task, where) { // where is 'top' or 'bottom'
|
||||||
const taskIdToMove = task._id;
|
const taskIdToMove = task._id;
|
||||||
const list = this.tasks[`${this.type}s`];
|
const list = this.getUnfilteredTaskList(this.type);
|
||||||
|
|
||||||
const oldPosition = list.findIndex(t => t._id === taskIdToMove);
|
const oldPosition = list.findIndex(t => t._id === taskIdToMove);
|
||||||
const moved = list.splice(oldPosition, 1);
|
const moved = list.splice(oldPosition, 1);
|
||||||
@@ -558,12 +521,14 @@ export default {
|
|||||||
editTask (task) {
|
editTask (task) {
|
||||||
this.$emit('editTask', task);
|
this.$emit('editTask', task);
|
||||||
},
|
},
|
||||||
activateFilter (type, filter) {
|
activateFilter (type, filter = '') {
|
||||||
if (type === 'todo' && filter.label === 'complete2') {
|
// Needs a separate API call as this data may not reside in store
|
||||||
|
if (type === 'todo' && filter === 'complete2') {
|
||||||
this.loadCompletedTodos();
|
this.loadCompletedTodos();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activeFilters[type] = filter;
|
// this.activeFilters[type] = filter;
|
||||||
|
this.activeFilter = getActiveFilter(type, filter);
|
||||||
},
|
},
|
||||||
setColumnBackgroundVisibility () {
|
setColumnBackgroundVisibility () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -591,35 +556,36 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
filterTask (task) {
|
filterByTagList (taskList, tagList = []) {
|
||||||
// View
|
let filteredTaskList = taskList;
|
||||||
if (!this.activeFilters[task.type].filter(task)) return false;
|
// fitler requested tasks by tags
|
||||||
|
if (!isEmpty(tagList)) {
|
||||||
// Tags
|
filteredTaskList = taskList.filter(
|
||||||
const selectedTags = this.selectedTags;
|
task => tagList.every(tag => task.tags.indexOf(tag) !== -1)
|
||||||
|
);
|
||||||
if (selectedTags && selectedTags.length > 0) {
|
|
||||||
const hasAllSelectedTag = selectedTags.every(tagId => {
|
|
||||||
return task.tags.indexOf(tagId) !== -1;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasAllSelectedTag) return false;
|
|
||||||
}
|
}
|
||||||
|
return filteredTaskList;
|
||||||
// Text
|
},
|
||||||
const searchText = this.searchText;
|
filterBySearchText (taskList, searchText = '') {
|
||||||
|
let filteredTaskList = taskList;
|
||||||
if (!searchText) return true;
|
// filter requested tasks by search text
|
||||||
if (task.text.toLowerCase().indexOf(searchText) !== -1) return true;
|
if (searchText) {
|
||||||
if (task.notes.toLowerCase().indexOf(searchText) !== -1) return true;
|
// to ensure broadest case insensitive search matching
|
||||||
|
let searchTextLowerCase = searchText.toLowerCase();
|
||||||
if (task.checklist && task.checklist.length) {
|
filteredTaskList = taskList.filter(
|
||||||
const checklistItemIndex = task.checklist.findIndex(({text}) => {
|
task => {
|
||||||
return text.toLowerCase().indexOf(searchText) !== -1;
|
// eslint rule disabled for block to allow nested binary expression
|
||||||
|
/* eslint-disable no-extra-parens */
|
||||||
|
return (
|
||||||
|
task.text.toLowerCase().indexOf(searchTextLowerCase) > -1 ||
|
||||||
|
(task.note && task.note.toLowerCase().indexOf(searchTextLowerCase) > -1) ||
|
||||||
|
(task.checklist && task.checklist.length > 0 &&
|
||||||
|
task.checklist.some(checkItem => checkItem.text.toLowerCase().indexOf(searchTextLowerCase) > -1))
|
||||||
|
);
|
||||||
|
/* eslint-enable no-extra-parens */
|
||||||
});
|
});
|
||||||
|
|
||||||
return checklistItemIndex !== -1;
|
|
||||||
}
|
}
|
||||||
|
return filteredTaskList;
|
||||||
},
|
},
|
||||||
openBuyDialog (rewardItem) {
|
openBuyDialog (rewardItem) {
|
||||||
if (rewardItem.locked) return;
|
if (rewardItem.locked) return;
|
||||||
|
|||||||
68
website/client/libs/store/helpers/filterTasks.js
Normal file
68
website/client/libs/store/helpers/filterTasks.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { shouldDo } from 'common/script/cron';
|
||||||
|
|
||||||
|
// Task filter data
|
||||||
|
// @TODO find a way to include user preferences w.r.t sort and defaults
|
||||||
|
const taskFilters = {
|
||||||
|
habit: {
|
||||||
|
label: 'habits',
|
||||||
|
filters: [
|
||||||
|
{ label: 'all', filterFn: () => true, default: true },
|
||||||
|
{ label: 'yellowred', filterFn: t => t.value < 1 }, // weak
|
||||||
|
{ label: 'greenblue', filterFn: t => t.value >= 1 }, // strong
|
||||||
|
],
|
||||||
|
},
|
||||||
|
daily: {
|
||||||
|
label: 'dailies',
|
||||||
|
filters: [
|
||||||
|
{ label: 'all', filterFn: () => true, default: true },
|
||||||
|
{ label: 'due', filterFn: userPrefs => t => !t.completed && shouldDo(new Date(), t, userPrefs) },
|
||||||
|
{ label: 'notDue', filterFn: userPrefs => t => t.completed || !shouldDo(new Date(), t, userPrefs) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
todo: {
|
||||||
|
label: 'todos',
|
||||||
|
filters: [
|
||||||
|
{ label: 'remaining', filterFn: t => !t.completed, default: true }, // active
|
||||||
|
{ label: 'scheduled', filterFn: t => !t.completed && t.date, sort: t => t.date },
|
||||||
|
{ label: 'complete2', filterFn: t => t.completed },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
reward: {
|
||||||
|
label: 'rewards',
|
||||||
|
filters: [
|
||||||
|
{ label: 'all', filterFn: () => true, default: true },
|
||||||
|
{ label: 'custom', filterFn: () => true }, // all rewards made by the user
|
||||||
|
{ label: 'wishlist', filterFn: () => false }, // not user tasks
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function typeLabel (filterList) {
|
||||||
|
return (type) => filterList[type].label;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTypeLabel = typeLabel(taskFilters);
|
||||||
|
|
||||||
|
function filterLabel (filterList) {
|
||||||
|
return (type) => {
|
||||||
|
let filterListByType = filterList[type].filters;
|
||||||
|
let filterListOfLabels = new Array(filterListByType.length);
|
||||||
|
filterListByType.forEach(({ label }, i) => filterListOfLabels[i] = label);
|
||||||
|
|
||||||
|
return filterListOfLabels;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFilterLabels = filterLabel(taskFilters);
|
||||||
|
|
||||||
|
function activeFilter (filterList) {
|
||||||
|
return (type, filterType = '') => {
|
||||||
|
let filterListByType = filterList[type].filters;
|
||||||
|
if (filterType) {
|
||||||
|
return filterListByType.find(f => f.label === filterType);
|
||||||
|
}
|
||||||
|
return filterListByType.find(f => f.default === true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getActiveFilter = activeFilter(taskFilters);
|
||||||
31
website/client/libs/store/helpers/orderTasks.js
Normal file
31
website/client/libs/store/helpers/orderTasks.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import compact from 'lodash/compact';
|
||||||
|
|
||||||
|
// sets task order for single task type only.
|
||||||
|
// Accepts task list and corresponding taskorder for its task type.
|
||||||
|
export function orderSingleTypeTasks (rawTasks, taskOrder) {
|
||||||
|
// if there is no predefined task order return task list as is.
|
||||||
|
if (!taskOrder) return rawTasks;
|
||||||
|
const orderedTasks = new Array(rawTasks.length);
|
||||||
|
const unorderedTasks = []; // What we want to add later
|
||||||
|
|
||||||
|
rawTasks.forEach((task, index) => {
|
||||||
|
const taskId = task._id;
|
||||||
|
const i = taskOrder[index] === taskId ? index : taskOrder.indexOf(taskId);
|
||||||
|
if (i === -1) {
|
||||||
|
unorderedTasks.push(task);
|
||||||
|
} else {
|
||||||
|
orderedTasks[i] = task;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return compact(orderedTasks).concat(unorderedTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function orderMultipleTypeTasks (rawTasks, tasksOrder) {
|
||||||
|
return {
|
||||||
|
habits: orderSingleTypeTasks(rawTasks.habits, tasksOrder.habits),
|
||||||
|
dailys: orderSingleTypeTasks(rawTasks.dailys, tasksOrder.dailys),
|
||||||
|
todos: orderSingleTypeTasks(rawTasks.todos, tasksOrder.todos),
|
||||||
|
rewards: orderSingleTypeTasks(rawTasks.rewards, tasksOrder.rewards),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import { shouldDo } from 'common/script/cron';
|
import { shouldDo } from 'common/script/cron';
|
||||||
|
|
||||||
|
// Library / Utility function
|
||||||
|
import { orderSingleTypeTasks } from 'client/libs/store/helpers/orderTasks.js';
|
||||||
|
import { getActiveFilter } from 'client/libs/store/helpers/filterTasks.js';
|
||||||
|
|
||||||
|
import sortBy from 'lodash/sortBy';
|
||||||
|
|
||||||
// Return all the tags belonging to an user task
|
// Return all the tags belonging to an user task
|
||||||
export function getTagsFor (store) {
|
export function getTagsFor (store) {
|
||||||
return (task) => {
|
return (task) => {
|
||||||
@@ -109,3 +115,48 @@ export function getTaskClasses (store) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns all list for given task type
|
||||||
|
export function getUnfilteredTaskList ({state}) {
|
||||||
|
return (type) => state.tasks.data[`${type}s`];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns filtered, sorted, ordered, tag filtered, and search filtered task list
|
||||||
|
// @TODO: sort task list based on used preferences
|
||||||
|
export function getFilteredTaskList ({state, getters}) {
|
||||||
|
return ({
|
||||||
|
type,
|
||||||
|
filterType = '',
|
||||||
|
}) => {
|
||||||
|
// get requested tasks
|
||||||
|
// check if task list has been passed as override props
|
||||||
|
// assumption: type will always be passed as param
|
||||||
|
let requestedTasks = getters['tasks:getUnfilteredTaskList'](type);
|
||||||
|
|
||||||
|
let userPreferences = state.user.data.preferences;
|
||||||
|
let taskOrderForType = state.user.data.tasksOrder[type];
|
||||||
|
|
||||||
|
// order tasks based on user set task order
|
||||||
|
// Still needs unit test for this..
|
||||||
|
if (requestedTasks.length > 0 && ['scheduled', 'due'].indexOf(filterType.label) === -1) {
|
||||||
|
requestedTasks = orderSingleTypeTasks(requestedTasks, taskOrderForType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter requested tasks by filter type
|
||||||
|
let selectedFilter = getActiveFilter(type, filterType);
|
||||||
|
// @TODO find a way (probably thru currying) to implicitly pass user preference data to task filters
|
||||||
|
if (type === 'daily' && (filterType === 'due' || filterType === 'notDue')) {
|
||||||
|
selectedFilter = {
|
||||||
|
...selectedFilter,
|
||||||
|
filterFn: selectedFilter.filterFn(userPreferences),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedTasks = requestedTasks.filter(selectedFilter.filterFn);
|
||||||
|
if (selectedFilter.sort) {
|
||||||
|
requestedTasks = sortBy(requestedTasks, selectedFilter.sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestedTasks;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,3 +5,15 @@ export function data (store) {
|
|||||||
export function gems (store) {
|
export function gems (store) {
|
||||||
return store.state.user.data.balance * 4;
|
return store.state.user.data.balance * 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buffs (store) {
|
||||||
|
return (key) => store.state.user.data.stats.buffs[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preferences (store) {
|
||||||
|
return store.state.user.data.preferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tasksOrder (store) {
|
||||||
|
return (type) => store.state.user.tasksOrder[`${type}s`];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user