Client Tasks (#8894)

* fix filter button style

* display completed todos

* fix reward control position

* begin to add edit modal

* start adding settings to edit modal

* add task saving, creating and deleting

* fixes

* add tags and repeat frequency for habits

* clicking on links should not open the edit modal

* checklist editing

* repeatables and checklists

* delete checklist items

* add rewards price

* update shrinkwrap

* pin cwait
This commit is contained in:
Matteo Pagliazzi
2017-08-01 14:30:17 +02:00
committed by GitHub
parent ade6d9689f
commit bca52cb6fa
18 changed files with 2495 additions and 907 deletions
+59
View File
@@ -2,6 +2,14 @@
// for editing rewards or when a task is created
&-purple {
background: $purple-300;
&-control-habit {
background: $purple-300;
}
&-modal-input {
color: $header-color !important;
}
}
&-worst {
@@ -13,6 +21,10 @@
&-control-daily-todo {
background: $maroon-500;
}
&-modal-input {
color: $maroon-500 !important;
}
}
&-worse {
@@ -24,6 +36,10 @@
&-control-daily-todo {
background: $red-500;
}
&-modal-input {
color: $red-500 !important;
}
}
&-bad {
@@ -35,6 +51,10 @@
&-control-daily-todo {
background: $orange-500;
}
&-modal-input {
color: $orange-500 !important;
}
}
&-neutral {
@@ -46,6 +66,10 @@
&-control-daily-todo {
background: $yellow-500;
}
&-modal-input {
color: $yellow-500 !important;
}
}
&-good {
@@ -57,6 +81,10 @@
&-control-daily-todo {
background: $green-500;
}
&-modal-input {
color: $green-500 !important;
}
}
&-better {
@@ -68,6 +96,10 @@
&-control-daily-todo {
background: $blue-500;
}
&-modal-input {
color: $blue-500 !important;
}
}
&-best {
@@ -79,6 +111,10 @@
&-control-daily-todo {
background: $teal-500;
}
&-modal-input {
color: $teal-500 !important;
}
}
&-reward {
@@ -111,4 +147,27 @@
border: 1px solid rgba(0, 0, 0, 0.12);
}
}
}
.task-control {
width: 28px;
height: 28px;
}
.habit-control {
border-radius: 100px;
color: $white;
.svg-icon {
width: 10px;
margin: 0 auto;
}
.positive {
margin-top: 9px;
}
.negative {
margin-top: 13px;
}
}
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36">
<g fill-rule="evenodd">
<path d="M10.667 10.667L16 8l-5.333-2.667L8 0 5.333 5.333 0 8l5.333 2.667L8 16zM10.667 30.667L16 28l-5.333-2.667L8 20l-2.667 5.333L0 28l5.333 2.667L8 36zM30.667 10.667L36 8l-5.333-2.667L28 0l-2.667 5.333L20 8l5.333 2.667L28 16zM30.667 30.667L36 28l-5.333-2.667L28 20l-2.667 5.333L20 28l5.333 2.667L28 36z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 444 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="32" viewBox="0 0 36 32">
<g fill-rule="evenodd">
<path d="M10.667 26.667L16 24l-5.333-2.667L8 16l-2.667 5.333L0 24l5.333 2.667L8 32zM30.667 26.667L36 24l-5.333-2.667L28 16l-2.667 5.333L20 24l5.333 2.667L28 32zM20.667 10.667L26 8l-5.333-2.667L18 0l-2.667 5.333L10 8l5.333 2.667L18 16z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 374 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="16" viewBox="0 0 36 16">
<g fill-rule="evenodd">
<path d="M10.667 10.667L16 8l-5.333-2.667L8 0 5.333 5.333 0 8l5.333 2.667L8 16zM30.667 10.667L36 8l-5.333-2.667L28 0l-2.667 5.333L20 8l5.333 2.667L28 16z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 293 B

@@ -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="M10.667 10.667L16 8l-5.333-2.667L8 0 5.333 5.333 0 8l5.333 2.667L8 16z"/>
</svg>

After

Width:  |  Height:  |  Size: 198 B

+1 -1
View File
@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="10" viewBox="0 0 12 10">
<path fill="#686274" fill-rule="evenodd" d="M0 0h12v2H0V0zm2 4h8v2H2V4zm2 4h4v2H4V8z"/>
<path fill-rule="evenodd" d="M0 0h12v2H0V0zm2 4h8v2H2V4zm2 4h4v2H4V8z"/>
</svg>

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 168 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<path fill="#24CC8F" fill-rule="evenodd" d="M6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/>
</svg>

After

Width:  |  Height:  |  Size: 172 B

@@ -70,7 +70,7 @@
:key="group.key",
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", :value='group.key' v-model="workingGuild.categories")
input.custom-control-input(type="checkbox", :value="group.key", v-model="workingGuild.categories")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t(group.label) }}
button.btn.btn-primary(@click.prevent="toggleCategorySelect") {{$t('close')}}
+18 -2
View File
@@ -1,18 +1,21 @@
<template lang="pug">
.tasks-column
b-modal(ref="editTaskModal")
span Hello From My Modal!
.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: activeFilter.label === filter.label}",
@click="activeFilter = filter",
@click="activateFilter(type, filter)",
) {{ $t(filter.label) }}
.tasks-list
task(
v-for="task in tasks[`${type}s`]",
:key="task.id", :task="task",
v-if="filterTask(task)",
@editTask="editTask",
)
.bottom-gradient
.column-background(v-if="isUser === true", :class="{'initial-description': tasks[`${type}s`].length === 0}")
@@ -129,16 +132,18 @@
<script>
import Task from './task';
import { mapState } from 'client/libs/store';
import { mapState, mapActions } from 'client/libs/store';
import { shouldDo } from 'common/script/cron';
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';
import bModal from 'bootstrap-vue/lib/components/modal';
export default {
components: {
Task,
bModal,
},
props: ['type', 'isUser', 'searchText', 'selectedTags'],
data () {
@@ -188,6 +193,7 @@ export default {
types,
activeFilter: types[this.type].filters.find(f => f.default === true),
icons,
openedCompletedTodos: false,
};
},
computed: {
@@ -197,6 +203,16 @@ export default {
}),
},
methods: {
...mapActions({loadCompletedTodos: 'tasks:fetchCompletedTodos'}),
editTask (task) {
this.$emit('editTask', task);
},
activateFilter (type, filter) {
if (type === 'todo' && filter.label === 'complete2') {
this.loadCompletedTodos();
}
this.activeFilter = filter;
},
filterTask (task) {
// View
if (!this.activeFilter.filter(task)) return false;
+29 -36
View File
@@ -10,15 +10,16 @@
.svg-icon.check(v-html="icons.check", v-if="task.completed")
// Task title, description and icons
.task-content(:class="contentClass")
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)")
h3.task-title(:class="{ 'has-notes': task.notes }", v-markdown="task.text")
.task-notes.small-text(v-markdown="task.notes")
.checklist(v-if="task.checklist && task.checklist.length > 0")
label.custom-control.custom-checkbox.checklist-item(
v-for="item in task.checklist", :class="{'checklist-item-done': item.completed}",
)
input.custom-control-input(type="checkbox", :checked="item.completed")
span.custom-control-indicator
span.custom-control-description {{ item.text }}
label.custom-control.custom-checkbox.checklist-item(
v-for="item in task.checklist", :class="{'checklist-item-done': item.completed}",
)
input.custom-control-input(type="checkbox", :checked="item.completed")
span.custom-control-indicator
span.custom-control-description {{ item.text }}
.icons.small-text.d-flex.align-items-center
.d-flex.align-items-center(v-if="task.type === 'todo' && task.date", :class="{'due-overdue': isDueOverdue}")
.svg-icon.calendar(v-html="icons.calendar")
@@ -67,12 +68,13 @@
}
.task-title {
margin-bottom: 8px;
padding-bottom: 8px;
color: $gray-10;
font-weight: normal;
margin-bottom: 0px;
&.has-notes {
margin-bottom: 0px;
padding-bottom: 0px;
}
}
@@ -98,6 +100,7 @@
line-height: 1.43;
margin-bottom: 10px;
min-height: 0px;
width: 100%;
&-done {
color: $gray-300;
@@ -178,34 +181,13 @@
.left-control {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
min-height: 60px;
}
.right-control {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
.task-control {
width: 28px;
height: 28px;
}
.habit-control {
border-radius: 100px;
color: $white;
.svg-icon {
width: 10px;
margin: 0 auto;
}
.positive {
margin-top: 9px;
}
.negative {
margin-top: 13px;
}
min-height: 56px;
}
.daily-todo-control {
@@ -215,8 +197,8 @@
.reward-control {
flex-direction: column;
padding-top: 16px;
padding-bottom: 12px;
padding-top: 8px;
padding-bottom: 4px;
.svg-icon {
width: 24px;
@@ -271,7 +253,6 @@ import checkIcon from 'assets/svg/check.svg';
import bPopover from 'bootstrap-vue/lib/components/popover';
import markdownDirective from 'client/directives/markdown';
export default {
components: {
bPopover,
@@ -330,5 +311,17 @@ export default {
return this.$t('dueIn', {dueIn});
},
},
methods: {
edit (e, task) {
// Prevent clicking on a link from opening the edit modal
const target = e.target || e.srcElement;
if (target.tagName === 'A') { // Link
return;
} else {
this.$emit('editTask', task);
}
},
},
};
</script>
@@ -0,0 +1,457 @@
<template lang="pug">
form(
v-if="task",
@submit.stop.prevent="submit()",
)
b-modal#task-modal(
size="sm",
@hidden="cancel()",
)
.task-modal-header(
slot="modal-header",
:class="[cssClass]",
)
h1 {{ title }}
.form-group
label(v-once) {{ `${$t('title')}*` }}
input.form-control(type='text', :class="[`${cssClass}-modal-input`]", required, v-model="task.text")
.form-group
label(v-once) {{ $t('notes') }}
textarea.form-control(:class="[`${cssClass}-modal-input`]", v-model="task.notes", rows="3")
.task-modal-content
.option(v-if="task.type === 'reward'")
label(v-once) {{ $t('cost') }}
input(type="number", v-model="task.value", required, min="0")
.option(v-if="['daily', 'todo'].indexOf(task.type) > -1")
label(v-once) {{ $t('checklist') }}
br
.checklist-group.input-group(v-for="(item, $index) in task.checklist")
input.checklist-item.form-control(type="text", :value="item.text")
span.input-group-btn(@click="removeChecklistItem($index)")
.svg-icon.destroy-icon(v-html="icons.destroy")
input.checklist-item.form-control(type="text", :placeholder="$t('newChecklistItem')", @keydown.enter="addChecklistItem($event)", v-model="newChecklistItem")
.d-flex.justify-content-center(v-if="task.type === 'habit'")
.option-item(:class="{'option-item-selected': task.up === true}", @click="task.up = !task.up")
.option-item-box
.task-control.habit-control(:class="controlClass.up + '-control-habit'")
.svg-icon.positive(v-html="icons.positive")
.option-item-label(v-once) {{ $t('positive') }}
.option-item(:class="{'option-item-selected': task.down === true}", @click="task.down = !task.down")
.option-item-box
.task-control.habit-control(:class="controlClass.down + '-control-habit'")
.svg-icon.negative(v-html="icons.negative")
.option-item-label(v-once) {{ $t('negative') }}
template(v-if="task.type !== 'reward'")
label(v-once)
span.float-left {{ $t('difficulty') }}
.svg-icon.info-icon(v-html="icons.information")
.d-flex.justify-content-center
.option-item(:class="{'option-item-selected': task.priority === 0.1}", @click="task.priority = 0.1")
.option-item-box
.svg-icon.difficulty-trivial-icon(v-html="icons.difficultyTrivial")
.option-item-label(v-once) {{ $t('trivial') }}
.option-item(:class="{'option-item-selected': task.priority === 1}", @click="task.priority = 1")
.option-item-box
.svg-icon.difficulty-normal-icon(v-html="icons.difficultyNormal")
.option-item-label(v-once) {{ $t('easy') }}
.option-item(:class="{'option-item-selected': task.priority === 1.5}", @click="task.priority = 1.5")
.option-item-box
.svg-icon.difficulty-medium-icon(v-html="icons.difficultyMedium")
.option-item-label(v-once) {{ $t('medium') }}
.option-item(:class="{'option-item-selected': task.priority === 2}", @click="task.priority = 2")
.option-item-box
.svg-icon.difficulty-hard-icon(v-html="icons.difficultyHard")
.option-item-label(v-once) {{ $t('hard') }}
.option(v-if="task.type === 'todo'")
label(v-once) {{ $t('dueDate') }}
datepicker(v-model="task.date")
.option(v-if="task.type === 'daily'")
label(v-once) {{ $t('startDate') }}
datepicker(v-model="task.startDate")
.option(v-if="task.type === 'daily'")
label(v-once) {{ $t('repeats') }}
b-dropdown(:text="$t(task.frequency)")
b-dropdown-item(v-for="frequency in ['daily', 'weekly', 'monthly', 'yearly']", :key="frequency", @click="task.frequency = frequency", :class="{active: task.frequency === frequency}")
| {{ $t(frequency) }}
label(v-once) {{ $t('repeatEvery') }}
input.form-control(type="number", v-model="task.everyX", min="0", required)
| {{ repeatSuffix }}
template(v-if="task.frequency === 'weekly'")
.form-check(
v-for="(day, dayNumber) in dayMapping",
:key="dayNumber",
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="task.repeat[day]")
span.custom-control-indicator
span.custom-control-description(v-once) {{ weekdaysMin(dayNumber) }}
template(v-if="task.frequency === 'monthly'")
label.custom-control.custom-radio
input.custom-control-input(type='radio', v-model="repeatsOn", value="dayOfMonth")
span.custom-control-indicator
span.custom-control-description {{ $t('dayOfMonth') }}
label.custom-control.custom-radio
input.custom-control-input(type='radio', v-model="repeatsOn", value="dayOfWeek")
span.custom-control-indicator
span.custom-control-description {{ $t('dayOfWeek') }}
.option
label(v-once) {{ $t('tags') }}
.category-wrap(@click="showTagsSelect = !showTagsSelect")
span.category-select(v-if='task.tags.length === 0') {{$t('none')}}
span.category-select(v-else) {{getTagsFor(task)[0]}}
.category-box(v-if="showTagsSelect")
.form-check(
v-for="tag in user.tags",
:key="tag.id",
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", :value="tag.id", v-model="task.tags")
span.custom-control-indicator
span.custom-control-description(v-once) {{ tag.name }}
button.btn.btn-primary(@click="showTagsSelect = !showTagsSelect") {{$t('close')}}
.option(v-if="task.type === 'habit'")
label(v-once) {{ $t('resetStreak') }}
b-dropdown(:text="$t(task.frequency)")
b-dropdown-item(v-for="frequency in ['daily', 'weekly', 'monthly']", :key="frequency", @click="task.frequency = frequency", :class="{active: task.frequency === frequency}")
| {{ $t(frequency) }}
.task-modal-footer(slot="modal-footer")
button.btn.btn-primary(type="submit", v-once) {{ $t('save') }}
span.cancel-task-btn(v-once, v-if="purpose === 'create'", @click="cancel()") {{ $t('cancel') }}
span.delete-task-btn(v-once, v-else, @click="destroy()") {{ $t('delete') }}
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
#task-modal {
.modal-dialog.modal-sm {
max-width: 448px;
}
label {
font-weight: bold;
}
input, textarea {
border: none;
background-color: rgba(0, 0, 0, 0.16);
&:focus {
color: $white !important;
}
}
.modal-content {
border-radius: 8px;
border: none;
}
.modal-header, .modal-body, .modal-footer {
padding: 0px;
border: none;
}
.task-modal-content, .task-modal-header {
padding-left: 23px;
padding-right: 23px;
}
.task-modal-header {
color: $white;
width: 100%;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
padding-top: 16px;
padding-bottom: 24px;
h1 {
color: $white;
}
}
.task-modal-content {
padding-top: 24px;
input {
background: $white;
border: 1px solid $gray-500;
color: $gray-200;
&:focus {
color: $gray-50 !important;
}
}
}
.info-icon {
float: left;
height: 16px;
width: 16px;
margin-left: 8px;
margin-top: 2px;
}
.difficulty-trivial-icon {
width: 16px;
height: 16px;
}
.difficulty-normal-icon {
width: 36px;
height: 16px;
}
.difficulty-medium-icon {
width: 36px;
height: 32px;
}
.difficulty-hard-icon {
width: 36px;
height: 36px;
}
.option {
margin-bottom: 12px;
margin-top: 12px;
position: relative;
}
.option-item {
margin-right: 48px;
cursor: pointer;
&:last-child {
margin-right: 0px;
}
&-box {
width: 64px;
height: 64px;
border-radius: 2px;
background: $gray-600;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: center;
.habit-control.task-habit-disabled-control-habit {
color: $white !important;
border: none;
background: $gray-300;
}
}
&-label {
color: $gray-50;
text-align: center;
}
}
.category-wrap {
cursor: pointer;
margin-top: 0px;
}
.category-box {
bottom: 0px;
left: 40px;
top: auto;
}
.checklist-group {
border-top: 1px solid $gray-500;
.input-group-btn {
cursor: pointer;
padding-left: 10px;
padding-right: 10px;
}
.destroy-icon {
width: 14px;
height: 16px;
}
}
.checklist-item {
margin-bottom: 0px;
border-radius: 0px;
border: none !important;
padding-left: 36px;
&:last-child {
background-size: 10px 10px;
background-image: url(~client/assets/svg/for-css/positive.svg);
background-repeat: no-repeat;
background-position: center left 10px;
border-top: 1px solid $gray-500 !important;
border-bottom: 1px solid $gray-500 !important;
}
}
.task-modal-footer {
margin: 0 auto;
padding-bottom: 24px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
margin-top: 50px;
.delete-task-btn, .cancel-task-btn {
margin-left: 16px;
cursor: pointer;
&:hover, &:focus, &:active {
text-decoration: underline;
}
}
.delete-task-btn {
color: $red-50;
}
.cancel-task-btn {
color: $blue-10;
}
}
}
</style>
<script>
import bModal from 'bootstrap-vue/lib/components/modal';
import { mapGetters, mapActions, mapState } from 'client/libs/store';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import Datepicker from 'vuejs-datepicker';
import moment from 'moment';
import informationIcon from 'assets/svg/information.svg';
import difficultyTrivialIcon from 'assets/svg/difficulty-trivial.svg';
import difficultyMediumIcon from 'assets/svg/difficulty-medium.svg';
import difficultyHardIcon from 'assets/svg/difficulty-hard.svg';
import difficultyNormalIcon from 'assets/svg/difficulty-normal.svg';
import positiveIcon from 'assets/svg/positive.svg';
import negativeIcon from 'assets/svg/negative.svg';
import deleteIcon from 'assets/svg/delete.svg';
export default {
components: {
bModal,
bDropdown,
bDropdownItem,
Datepicker,
},
props: ['task', 'purpose'], // purpose is either create or edit, task is the task created or edited
data () {
return {
showTagsSelect: false,
newChecklistItem: null,
icons: Object.freeze({
information: informationIcon,
difficultyNormal: difficultyNormalIcon,
difficultyTrivial: difficultyTrivialIcon,
difficultyMedium: difficultyMediumIcon,
difficultyHard: difficultyHardIcon,
negative: negativeIcon,
positive: positiveIcon,
destroy: deleteIcon,
}),
};
},
computed: {
...mapGetters({
getTaskClasses: 'tasks:getTaskClasses',
getTagsFor: 'tasks:getTagsFor',
}),
...mapState({
user: 'user.data',
dayMapping: 'constants.DAY_MAPPING',
}),
title () {
const type = this.$t(this.task.type);
return this.$t(this.purpose === 'edit' ? 'editATask' : 'createTask', {type});
},
cssClass () {
return this.getTaskClasses(this.task, this.purpose === 'edit' ? 'editModal' : 'createModal');
},
controlClass () {
return this.getTaskClasses(this.task, this.purpose === 'edit' ? 'control' : 'controlCreate');
},
repeatSuffix () {
const task = this.task;
if (task.frequency === 'daily') {
return task.everyX === 1 ? this.$t('day') : this.$t('days');
} else if (task.frequency === 'weekly') {
return task.everyX === 1 ? this.$t('week') : this.$t('weeks');
} else if (task.frequency === 'monthly') {
return task.everyX === 1 ? this.$t('month') : this.$t('months');
} else if (task.frequency === 'yearly') {
return task.everyX === 1 ? this.$t('year') : this.$t('years');
}
},
repeatsOn: {
get () {
let repeatsOn = 'dayOfMonth';
if (this.task.type === 'daily' && this.task.weeksOfMonth && this.task.weeksOfMonth.length > 0) {
repeatsOn = 'dayOfWeek';
}
return repeatsOn;
},
set (newValue) {
const task = this.task;
if (task.frequency === 'monthly' && newValue === 'dayOfMonth') {
const date = moment(task.startDate).date();
task.weeksOfMonth = [];
task.daysOfMonth = [date];
} else if (task.frequency === 'monthly' && newValue === 'dayOfWeek') {
const week = Math.ceil(moment(task.startDate).date() / 7) - 1;
const dayOfWeek = moment(task.startDate).day();
const shortDay = this.dayMapping[dayOfWeek];
task.daysOfMonth = [];
task.weeksOfMonth = [week];
for (let key in task.repeat) {
task.repeat[key] = false;
}
task.repeat[shortDay] = true;
}
},
},
},
methods: {
...mapActions({saveTask: 'tasks:save', destroyTask: 'tasks:destroy', createTask: 'tasks:create'}),
addChecklistItem (e) {
this.task.checklist.push({text: this.newChecklistItem, completed: false});
this.newChecklistItem = null;
e.preventDefault();
},
removeChecklistItem (i) {
this.task.checklist.splice(i, 1);
},
weekdaysMin (dayNumber) {
return moment.weekdaysMin(dayNumber);
},
submit () {
if (this.purpose === 'create') {
this.createTask(this.task);
} else {
this.saveTask(this.task);
}
this.$root.$emit('hide::modal', 'task-modal');
},
destroy () {
this.destroyTask(this.task);
this.$root.$emit('hide::modal', 'task-modal');
},
cancel () {
this.showTagsSelect = false;
this.$emit('cancel');
},
},
};
</script>
+57 -6
View File
@@ -1,5 +1,11 @@
<template lang="pug">
.row.user-tasks-page
task-modal(
:task="editingTask || creatingTask",
:purpose="creatingTask !== null ? 'create' : 'edit'",
@cancel="cancelTaskModal()",
ref="taskModal",
)
.col-12
.row.tasks-navigation
.col-4.offset-4
@@ -32,21 +38,26 @@
button.btn.btn-secondary.filter-button(
type="button",
@click="toggleFilterPanel()",
:class="{open: isFilterPanelOpen}",
:class="{'filter-button-open': selectedTags.length > 0}",
)
.d-flex.align-items-center
span(v-once) {{ $t('filter') }}
.svg-icon.filter-icon(v-html="icons.filter")
.col-1.offset-3
button.btn.btn-success(v-once)
//button.btn.btn-success(v-once)
.svg-icon.positive(v-html="icons.positive")
| {{ $t('create') }}
b-dropdown(:text="$t('create')")
b-dropdown-item(v-for="type in columns", :key="type", @click="createTask(type)")
| {{$t(type)}}
.row.tasks-columns
task-column.col-3(
v-for="column in columns",
:type="column", :key="column",
:isUser="true", :searchText="searchTextThrottled",
:selectedTags="selectedTags",
@editTask="editTask",
)
</template>
@@ -69,7 +80,7 @@
padding-top: 6px;
}
.filter-button {
button.btn.btn-secondary.filter-button {
box-shadow: none;
border-radius: 2px;
border: 1px solid $gray-400 !important;
@@ -77,12 +88,21 @@
&:hover, &:active, &:focus, &.open {
box-shadow: none;
border-color: $purple-500 !important;
color: $gray-50 !important;
}
&.filter-button-open {
color: $purple-200 !important;
.filter-icon {
color: $purple-200 !important;
}
}
.filter-icon {
height: 10px;
width: 12px;
color: $green-500;
color: $gray-50;
margin-left: 15px;
}
}
@@ -154,15 +174,26 @@
</style>
<script>
import Column from './column';
import TaskColumn from './column';
import TaskModal from './taskModal';
import positiveIcon from 'assets/svg/positive.svg';
import filterIcon from 'assets/svg/filter.svg';
import Vue from 'vue';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import throttle from 'lodash/throttle';
import cloneDeep from 'lodash/cloneDeep';
import { mapState } from 'client/libs/store';
import taskDefaults from 'common/script/libs/taskDefaults';
export default {
components: {
TaskColumn: Column,
TaskColumn,
TaskModal,
bDropdown,
bDropdownItem,
},
data () {
return {
@@ -176,6 +207,8 @@ export default {
}),
selectedTags: [],
temporarilySelectedTags: [],
editingTask: null,
creatingTask: null,
};
},
computed: {
@@ -216,6 +249,24 @@ export default {
}, 250),
},
methods: {
editTask (task) {
this.editingTask = cloneDeep(task);
// Necessary otherwise the first time the modal is not rendered
Vue.nextTick(() => {
this.$root.$emit('show::modal', 'task-modal');
});
},
createTask (type) {
this.creatingTask = taskDefaults({type, text: ''});
// Necessary otherwise the first time the modal is not rendered
Vue.nextTick(() => {
this.$root.$emit('show::modal', 'task-modal');
});
},
cancelTaskModal () {
this.editingTask = null;
this.creatingTask = null;
},
toggleFilterPanel () {
if (this.isFilterPanelOpen === true) {
this.closeFilterPanel();
+78 -1
View File
@@ -1,6 +1,7 @@
import { loadAsyncResource } from 'client/libs/asyncResource';
import axios from 'axios';
import compact from 'lodash/compact';
import omit from 'lodash/omit';
export function fetchUserTasks (store, forceLoad = false) {
return loadAsyncResource({
@@ -18,6 +19,37 @@ export function fetchUserTasks (store, forceLoad = false) {
});
}
export async function fetchCompletedTodos (store, forceLoad = false) {
// Wait for the user to be loaded before deserializing
// because user.tasksOrder is necessary
await store.dispatch('tasks:fetchUserTasks');
const loadStatus = store.state.completedTodosStatus;
if (loadStatus === 'NOT_LOADED' || forceLoad) {
store.state.completedTodosStatus = 'LOADING';
const response = await axios.get('/api/v3/tasks/user?type=completedTodos');
const completedTodos = response.data.data;
const tasks = store.state.tasks.data;
// Remove existing completed todos
tasks.todos = tasks.todos.filter(t => !t.completed);
tasks.todos.push(...completedTodos);
store.state.completedTodosStatus = 'LOADED';
} else if (status === 'LOADED') {
return;
} else if (loadStatus === 'LOADING') {
const watcher = store.watch(state => state.completedTodosStatus, (newLoadingStatus) => {
watcher(); // remove the watcher
if (newLoadingStatus === 'LOADED') {
return;
} else {
throw new Error(); // TODO add reason?
}
});
}
}
export function order (store, [rawTasks, tasksOrder]) {
const tasks = {
habits: [],
@@ -51,4 +83,49 @@ export function order (store, [rawTasks, tasksOrder]) {
});
return tasks;
}
function sanitizeChecklist (task) {
if (task.checklist) {
task.checklist = task.checklist.filter((i) => {
return Boolean(i.text);
});
}
}
export async function create (store, createdTask) {
const type = `${createdTask.type}s`;
const list = store.state.tasks.data[type];
sanitizeChecklist(createdTask);
list.unshift(createdTask);
store.state.user.data.tasksOrder[type].unshift(createdTask._id);
const response = await axios.post('/api/v3/tasks/user', createdTask);
Object.assign(list[0], response.data.data);
}
export async function save (store, editedTask) {
const taskId = editedTask._id;
const type = editedTask.type;
const originalTask = store.state.tasks.data[`${type}s`].find(t => t._id === taskId);
sanitizeChecklist(editedTask);
Object.assign(originalTask, editedTask);
const taskDataToSend = omit(originalTask, ['history']);
const response = await axios.put(`/api/v3/tasks/${originalTask._id}`, taskDataToSend);
Object.assign(originalTask, response.data.data);
}
export async function destroy (store, task) {
const list = store.state.tasks.data[`${task.type}s`];
const taskIndex = list.findIndex(t => t._id === task._id);
if (taskIndex > -1) {
list.splice(taskIndex, 1);
}
await axios.delete(`/api/v3/tasks/${task._id}`);
}
+5
View File
@@ -37,6 +37,11 @@ export function getTaskClasses (store) {
return 'task-purple';
case 'editModal':
return type === 'reward' ? 'task-purple' : getTaskColorByValue(task.value);
case 'controlCreate':
return {
up: task.up ? 'task-purple' : 'task-habit-disabled',
down: task.down ? 'task-purple' : 'task-habit-disabled',
};
case 'control':
switch (type) {
case 'daily':
+4 -2
View File
@@ -1,7 +1,8 @@
import Store from 'client/libs/store';
import deepFreeze from 'client/libs/deepFreeze';
import content from 'common/script/content/index';
import * as constants from 'common/script/constants';
import * as commonConstants from 'common/script/constants';
import { DAY_MAPPING } from 'common/script/cron';
import { asyncResourceFactory } from 'client/libs/asyncResource';
import axios from 'axios';
@@ -39,6 +40,7 @@ export default function () {
isUserLoggedIn,
user: asyncResourceFactory(),
tasks: asyncResourceFactory(), // user tasks
completedTodosStatus: 'NOT_LOADED',
party: {
quest: {},
members: asyncResourceFactory(),
@@ -59,7 +61,7 @@ export default function () {
// TODO apply freezing to the entire codebase (the server) and not only to the client side?
// NOTE this takes about 10-15ms on a fast computer
content: deepFreeze(content),
constants: deepFreeze(constants),
constants: deepFreeze({...commonConstants, DAY_MAPPING}),
hideHeader: false,
viewingMembers: [],
},