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

2609
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,7 @@
"coupon-code": "^0.4.5", "coupon-code": "^0.4.5",
"css-loader": "^0.28.0", "css-loader": "^0.28.0",
"csv-stringify": "^1.0.2", "csv-stringify": "^1.0.2",
"cwait": "^1.0.0", "cwait": "~1.0.1",
"domain-middleware": "~0.1.0", "domain-middleware": "~0.1.0",
"estraverse": "^4.1.1", "estraverse": "^4.1.1",
"express": "~4.14.0", "express": "~4.14.0",
@@ -81,7 +81,7 @@
"method-override": "^2.3.5", "method-override": "^2.3.5",
"moment": "^2.13.0", "moment": "^2.13.0",
"moment-recur": "habitrpg/moment-recur#v1.0.6", "moment-recur": "habitrpg/moment-recur#v1.0.6",
"mongoose": "^4.8.6", "mongoose": "~4.8.6",
"mongoose-id-autoinc": "~2013.7.14-4", "mongoose-id-autoinc": "~2013.7.14-4",
"morgan": "^1.7.0", "morgan": "^1.7.0",
"nconf": "~0.8.2", "nconf": "~0.8.2",
@@ -128,6 +128,7 @@
"vue-router": "^2.0.0-rc.5", "vue-router": "^2.0.0-rc.5",
"vue-style-loader": "^3.0.0", "vue-style-loader": "^3.0.0",
"vue-template-compiler": "^2.1.10", "vue-template-compiler": "^2.1.10",
"vuejs-datepicker": "^0.9.4",
"webpack": "^2.2.1", "webpack": "^2.2.1",
"webpack-merge": "^4.0.0", "webpack-merge": "^4.0.0",
"winston": "^2.1.0", "winston": "^2.1.0",

View File

@@ -2,6 +2,14 @@
// for editing rewards or when a task is created // for editing rewards or when a task is created
&-purple { &-purple {
background: $purple-300; background: $purple-300;
&-control-habit {
background: $purple-300;
}
&-modal-input {
color: $header-color !important;
}
} }
&-worst { &-worst {
@@ -13,6 +21,10 @@
&-control-daily-todo { &-control-daily-todo {
background: $maroon-500; background: $maroon-500;
} }
&-modal-input {
color: $maroon-500 !important;
}
} }
&-worse { &-worse {
@@ -24,6 +36,10 @@
&-control-daily-todo { &-control-daily-todo {
background: $red-500; background: $red-500;
} }
&-modal-input {
color: $red-500 !important;
}
} }
&-bad { &-bad {
@@ -35,6 +51,10 @@
&-control-daily-todo { &-control-daily-todo {
background: $orange-500; background: $orange-500;
} }
&-modal-input {
color: $orange-500 !important;
}
} }
&-neutral { &-neutral {
@@ -46,6 +66,10 @@
&-control-daily-todo { &-control-daily-todo {
background: $yellow-500; background: $yellow-500;
} }
&-modal-input {
color: $yellow-500 !important;
}
} }
&-good { &-good {
@@ -57,6 +81,10 @@
&-control-daily-todo { &-control-daily-todo {
background: $green-500; background: $green-500;
} }
&-modal-input {
color: $green-500 !important;
}
} }
&-better { &-better {
@@ -68,6 +96,10 @@
&-control-daily-todo { &-control-daily-todo {
background: $blue-500; background: $blue-500;
} }
&-modal-input {
color: $blue-500 !important;
}
} }
&-best { &-best {
@@ -79,6 +111,10 @@
&-control-daily-todo { &-control-daily-todo {
background: $teal-500; background: $teal-500;
} }
&-modal-input {
color: $teal-500 !important;
}
} }
&-reward { &-reward {
@@ -111,4 +147,27 @@
border: 1px solid rgba(0, 0, 0, 0.12); 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;
}
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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

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="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

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="10" viewBox="0 0 12 10"> <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> </svg>

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 168 B

View File

@@ -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

View File

@@ -70,7 +70,7 @@
:key="group.key", :key="group.key",
) )
label.custom-control.custom-checkbox 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-indicator
span.custom-control-description(v-once) {{ $t(group.label) }} span.custom-control-description(v-once) {{ $t(group.label) }}
button.btn.btn-primary(@click.prevent="toggleCategorySelect") {{$t('close')}} button.btn.btn-primary(@click.prevent="toggleCategorySelect") {{$t('close')}}

View File

@@ -1,18 +1,21 @@
<template lang="pug"> <template lang="pug">
.tasks-column .tasks-column
b-modal(ref="editTaskModal")
span Hello From My Modal!
.d-flex .d-flex
h2.tasks-column-title(v-once) {{ $t(types[type].label) }} h2.tasks-column-title(v-once) {{ $t(types[type].label) }}
.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 types[type].filters",
:class="{active: activeFilter.label === filter.label}", :class="{active: activeFilter.label === filter.label}",
@click="activeFilter = filter", @click="activateFilter(type, filter)",
) {{ $t(filter.label) }} ) {{ $t(filter.label) }}
.tasks-list .tasks-list
task( task(
v-for="task in tasks[`${type}s`]", v-for="task in tasks[`${type}s`]",
:key="task.id", :task="task", :key="task.id", :task="task",
v-if="filterTask(task)", v-if="filterTask(task)",
@editTask="editTask",
) )
.bottom-gradient .bottom-gradient
.column-background(v-if="isUser === true", :class="{'initial-description': tasks[`${type}s`].length === 0}") .column-background(v-if="isUser === true", :class="{'initial-description': tasks[`${type}s`].length === 0}")
@@ -129,16 +132,18 @@
<script> <script>
import Task from './task'; import Task from './task';
import { mapState } from 'client/libs/store'; import { mapState, mapActions } from 'client/libs/store';
import { shouldDo } from 'common/script/cron'; import { shouldDo } from 'common/script/cron';
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';
import todoIcon from 'assets/svg/todo.svg'; import todoIcon from 'assets/svg/todo.svg';
import rewardIcon from 'assets/svg/reward.svg'; import rewardIcon from 'assets/svg/reward.svg';
import bModal from 'bootstrap-vue/lib/components/modal';
export default { export default {
components: { components: {
Task, Task,
bModal,
}, },
props: ['type', 'isUser', 'searchText', 'selectedTags'], props: ['type', 'isUser', 'searchText', 'selectedTags'],
data () { data () {
@@ -188,6 +193,7 @@ export default {
types, types,
activeFilter: types[this.type].filters.find(f => f.default === true), activeFilter: types[this.type].filters.find(f => f.default === true),
icons, icons,
openedCompletedTodos: false,
}; };
}, },
computed: { computed: {
@@ -197,6 +203,16 @@ export default {
}), }),
}, },
methods: { 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) { filterTask (task) {
// View // View
if (!this.activeFilter.filter(task)) return false; if (!this.activeFilter.filter(task)) return false;

View File

@@ -10,15 +10,16 @@
.svg-icon.check(v-html="icons.check", v-if="task.completed") .svg-icon.check(v-html="icons.check", v-if="task.completed")
// Task title, description and icons // Task title, description and icons
.task-content(:class="contentClass") .task-content(:class="contentClass")
h3.task-title(:class="{ 'has-notes': task.notes }", v-markdown="task.text") .task-clickable-area(@click="edit($event, task)")
.task-notes.small-text(v-markdown="task.notes") 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") .checklist(v-if="task.checklist && task.checklist.length > 0")
label.custom-control.custom-checkbox.checklist-item( label.custom-control.custom-checkbox.checklist-item(
v-for="item in task.checklist", :class="{'checklist-item-done': item.completed}", v-for="item in task.checklist", :class="{'checklist-item-done': item.completed}",
) )
input.custom-control-input(type="checkbox", :checked="item.completed") input.custom-control-input(type="checkbox", :checked="item.completed")
span.custom-control-indicator span.custom-control-indicator
span.custom-control-description {{ item.text }} span.custom-control-description {{ item.text }}
.icons.small-text.d-flex.align-items-center .icons.small-text.d-flex.align-items-center
.d-flex.align-items-center(v-if="task.type === 'todo' && task.date", :class="{'due-overdue': isDueOverdue}") .d-flex.align-items-center(v-if="task.type === 'todo' && task.date", :class="{'due-overdue': isDueOverdue}")
.svg-icon.calendar(v-html="icons.calendar") .svg-icon.calendar(v-html="icons.calendar")
@@ -67,12 +68,13 @@
} }
.task-title { .task-title {
margin-bottom: 8px; padding-bottom: 8px;
color: $gray-10; color: $gray-10;
font-weight: normal; font-weight: normal;
margin-bottom: 0px;
&.has-notes { &.has-notes {
margin-bottom: 0px; padding-bottom: 0px;
} }
} }
@@ -98,6 +100,7 @@
line-height: 1.43; line-height: 1.43;
margin-bottom: 10px; margin-bottom: 10px;
min-height: 0px; min-height: 0px;
width: 100%;
&-done { &-done {
color: $gray-300; color: $gray-300;
@@ -178,34 +181,13 @@
.left-control { .left-control {
border-top-left-radius: 2px; border-top-left-radius: 2px;
border-bottom-left-radius: 2px; border-bottom-left-radius: 2px;
min-height: 60px;
} }
.right-control { .right-control {
border-top-right-radius: 2px; border-top-right-radius: 2px;
border-bottom-right-radius: 2px; border-bottom-right-radius: 2px;
} min-height: 56px;
.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;
}
} }
.daily-todo-control { .daily-todo-control {
@@ -215,8 +197,8 @@
.reward-control { .reward-control {
flex-direction: column; flex-direction: column;
padding-top: 16px; padding-top: 8px;
padding-bottom: 12px; padding-bottom: 4px;
.svg-icon { .svg-icon {
width: 24px; width: 24px;
@@ -271,7 +253,6 @@ import checkIcon from 'assets/svg/check.svg';
import bPopover from 'bootstrap-vue/lib/components/popover'; import bPopover from 'bootstrap-vue/lib/components/popover';
import markdownDirective from 'client/directives/markdown'; import markdownDirective from 'client/directives/markdown';
export default { export default {
components: { components: {
bPopover, bPopover,
@@ -330,5 +311,17 @@ export default {
return this.$t('dueIn', {dueIn}); 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> </script>

View File

@@ -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>

View File

@@ -1,5 +1,11 @@
<template lang="pug"> <template lang="pug">
.row.user-tasks-page .row.user-tasks-page
task-modal(
:task="editingTask || creatingTask",
:purpose="creatingTask !== null ? 'create' : 'edit'",
@cancel="cancelTaskModal()",
ref="taskModal",
)
.col-12 .col-12
.row.tasks-navigation .row.tasks-navigation
.col-4.offset-4 .col-4.offset-4
@@ -32,21 +38,26 @@
button.btn.btn-secondary.filter-button( button.btn.btn-secondary.filter-button(
type="button", type="button",
@click="toggleFilterPanel()", @click="toggleFilterPanel()",
:class="{open: isFilterPanelOpen}", :class="{'filter-button-open': selectedTags.length > 0}",
) )
.d-flex.align-items-center .d-flex.align-items-center
span(v-once) {{ $t('filter') }} span(v-once) {{ $t('filter') }}
.svg-icon.filter-icon(v-html="icons.filter") .svg-icon.filter-icon(v-html="icons.filter")
.col-1.offset-3 .col-1.offset-3
button.btn.btn-success(v-once) //button.btn.btn-success(v-once)
.svg-icon.positive(v-html="icons.positive") .svg-icon.positive(v-html="icons.positive")
| {{ $t('create') }} | {{ $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 .row.tasks-columns
task-column.col-3( task-column.col-3(
v-for="column in columns", v-for="column in columns",
:type="column", :key="column", :type="column", :key="column",
:isUser="true", :searchText="searchTextThrottled", :isUser="true", :searchText="searchTextThrottled",
:selectedTags="selectedTags", :selectedTags="selectedTags",
@editTask="editTask",
) )
</template> </template>
@@ -69,7 +80,7 @@
padding-top: 6px; padding-top: 6px;
} }
.filter-button { button.btn.btn-secondary.filter-button {
box-shadow: none; box-shadow: none;
border-radius: 2px; border-radius: 2px;
border: 1px solid $gray-400 !important; border: 1px solid $gray-400 !important;
@@ -77,12 +88,21 @@
&:hover, &:active, &:focus, &.open { &:hover, &:active, &:focus, &.open {
box-shadow: none; box-shadow: none;
border-color: $purple-500 !important; border-color: $purple-500 !important;
color: $gray-50 !important;
}
&.filter-button-open {
color: $purple-200 !important;
.filter-icon {
color: $purple-200 !important;
}
} }
.filter-icon { .filter-icon {
height: 10px; height: 10px;
width: 12px; width: 12px;
color: $green-500; color: $gray-50;
margin-left: 15px; margin-left: 15px;
} }
} }
@@ -154,15 +174,26 @@
</style> </style>
<script> <script>
import Column from './column'; import TaskColumn from './column';
import TaskModal from './taskModal';
import positiveIcon from 'assets/svg/positive.svg'; import positiveIcon from 'assets/svg/positive.svg';
import filterIcon from 'assets/svg/filter.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 throttle from 'lodash/throttle';
import cloneDeep from 'lodash/cloneDeep';
import { mapState } from 'client/libs/store'; import { mapState } from 'client/libs/store';
import taskDefaults from 'common/script/libs/taskDefaults';
export default { export default {
components: { components: {
TaskColumn: Column, TaskColumn,
TaskModal,
bDropdown,
bDropdownItem,
}, },
data () { data () {
return { return {
@@ -176,6 +207,8 @@ export default {
}), }),
selectedTags: [], selectedTags: [],
temporarilySelectedTags: [], temporarilySelectedTags: [],
editingTask: null,
creatingTask: null,
}; };
}, },
computed: { computed: {
@@ -216,6 +249,24 @@ export default {
}, 250), }, 250),
}, },
methods: { 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 () { toggleFilterPanel () {
if (this.isFilterPanelOpen === true) { if (this.isFilterPanelOpen === true) {
this.closeFilterPanel(); this.closeFilterPanel();

View File

@@ -1,6 +1,7 @@
import { loadAsyncResource } from 'client/libs/asyncResource'; import { loadAsyncResource } from 'client/libs/asyncResource';
import axios from 'axios';
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import omit from 'lodash/omit';
export function fetchUserTasks (store, forceLoad = false) { export function fetchUserTasks (store, forceLoad = false) {
return loadAsyncResource({ 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]) { export function order (store, [rawTasks, tasksOrder]) {
const tasks = { const tasks = {
habits: [], habits: [],
@@ -51,4 +83,49 @@ export function order (store, [rawTasks, tasksOrder]) {
}); });
return tasks; 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}`);
} }

View File

@@ -37,6 +37,11 @@ export function getTaskClasses (store) {
return 'task-purple'; return 'task-purple';
case 'editModal': case 'editModal':
return type === 'reward' ? 'task-purple' : getTaskColorByValue(task.value); 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': case 'control':
switch (type) { switch (type) {
case 'daily': case 'daily':

View File

@@ -1,7 +1,8 @@
import Store from 'client/libs/store'; import Store from 'client/libs/store';
import deepFreeze from 'client/libs/deepFreeze'; import deepFreeze from 'client/libs/deepFreeze';
import content from 'common/script/content/index'; 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 { asyncResourceFactory } from 'client/libs/asyncResource';
import axios from 'axios'; import axios from 'axios';
@@ -39,6 +40,7 @@ export default function () {
isUserLoggedIn, isUserLoggedIn,
user: asyncResourceFactory(), user: asyncResourceFactory(),
tasks: asyncResourceFactory(), // user tasks tasks: asyncResourceFactory(), // user tasks
completedTodosStatus: 'NOT_LOADED',
party: { party: {
quest: {}, quest: {},
members: asyncResourceFactory(), 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? // 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 // NOTE this takes about 10-15ms on a fast computer
content: deepFreeze(content), content: deepFreeze(content),
constants: deepFreeze(constants), constants: deepFreeze({...commonConstants, DAY_MAPPING}),
hideHeader: false, hideHeader: false,
viewingMembers: [], viewingMembers: [],
}, },

View File

@@ -213,6 +213,15 @@
"challengeInformationPlaceHolder": "Write a short description advertising your Challenge to other Habiticans. What is the main purpose of your Challenge and why should people join it? Try to include useful keywords in the description so that Habiticans can easily find it when they search!", "challengeInformationPlaceHolder": "Write a short description advertising your Challenge to other Habiticans. What is the main purpose of your Challenge and why should people join it? Try to include useful keywords in the description so that Habiticans can easily find it when they search!",
"where": "Where*", "where": "Where*",
"challengeMinimum": "Minimum 1 Gem for public Challenges (helps prevent spam, it really does).", "challengeMinimum": "Minimum 1 Gem for public Challenges (helps prevent spam, it really does).",
"editATask": "Edit a <%= type %>",
"createTask": "Create <%= type %>",
"notes": "Notes",
"positive": "Positive",
"negative": "Negative",
"resetStreak": "Reset Streak",
"newChecklistItem": "New checklist item",
"repeats": "Repeats",
"cost": "Cost",
"group": "Group", "group": "Group",
"sortByType": "Type", "sortByType": "Type",
"sortByPrice": "Price", "sortByPrice": "Price",