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
2601
npm-shrinkwrap.json
generated
@@ -40,7 +40,7 @@
|
||||
"coupon-code": "^0.4.5",
|
||||
"css-loader": "^0.28.0",
|
||||
"csv-stringify": "^1.0.2",
|
||||
"cwait": "^1.0.0",
|
||||
"cwait": "~1.0.1",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"estraverse": "^4.1.1",
|
||||
"express": "~4.14.0",
|
||||
@@ -81,7 +81,7 @@
|
||||
"method-override": "^2.3.5",
|
||||
"moment": "^2.13.0",
|
||||
"moment-recur": "habitrpg/moment-recur#v1.0.6",
|
||||
"mongoose": "^4.8.6",
|
||||
"mongoose": "~4.8.6",
|
||||
"mongoose-id-autoinc": "~2013.7.14-4",
|
||||
"morgan": "^1.7.0",
|
||||
"nconf": "~0.8.2",
|
||||
@@ -128,6 +128,7 @@
|
||||
"vue-router": "^2.0.0-rc.5",
|
||||
"vue-style-loader": "^3.0.0",
|
||||
"vue-template-compiler": "^2.1.10",
|
||||
"vuejs-datepicker": "^0.9.4",
|
||||
"webpack": "^2.2.1",
|
||||
"webpack-merge": "^4.0.0",
|
||||
"winston": "^2.1.0",
|
||||
|
||||
@@ -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 {
|
||||
@@ -112,3 +148,26 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
5
website/client/assets/svg/difficulty-hard.svg
Normal 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 |
5
website/client/assets/svg/difficulty-medium.svg
Normal 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 |
5
website/client/assets/svg/difficulty-normal.svg
Normal 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 |
3
website/client/assets/svg/difficulty-trivial.svg
Normal 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 |
@@ -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 |
3
website/client/assets/svg/for-css/positive.svg
Normal 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 |
@@ -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')}}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
.svg-icon.check(v-html="icons.check", v-if="task.completed")
|
||||
// Task title, description and icons
|
||||
.task-content(:class="contentClass")
|
||||
.task-clickable-area(@click="edit($event, task)")
|
||||
h3.task-title(:class="{ 'has-notes': task.notes }", v-markdown="task.text")
|
||||
.task-notes.small-text(v-markdown="task.notes")
|
||||
.checklist(v-if="task.checklist && task.checklist.length > 0")
|
||||
@@ -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>
|
||||
457
website/client/components/tasks/taskModal.vue
Normal 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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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: [],
|
||||
@@ -52,3 +84,48 @@ 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}`);
|
||||
}
|
||||
@@ -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':
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
|
||||
@@ -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!",
|
||||
"where": "Where*",
|
||||
"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",
|
||||
"sortByType": "Type",
|
||||
"sortByPrice": "Price",
|
||||
|
||||