42083efb7e
* Add group plan selection modal for upgrades Allow users to select an existing group to upgrade before creating a new one. * crlf -> lf lint * set selection of group plan Also tiny UI fixes * Update group plan selection to include expired plans * Add includeExpiredPlans option to group fetching * force flag when fetching group plans * Update group plan eligibility check * Fix eslint error in push notification import * replace chaining (?.) w/null check * Remove comment * set initial selected group plan, and fix card rounding * format member count * Show warning for pending party invites when upgrading to paid group plan Show warning for pending party invites when upgrading to paid group plan. If user upgrades from party to group, remove any pending invites * suppress error toasts for group modal, and UI tweaks for group modal suppress error toast for 404 on party fetch for users without a party (for group modal), Increase check SVG size in selectableCard, and show "Previously upgraded" label for parties that were canceled group plans * Clear upgradingGroup state after group plan payment * Update emoji system to native Unicode rendering * Fix line endings in habiticaMarkdown test * fix indented code block detection for markdown-it v14 * update habitica-markdown to include v3 emoji dataset (pointed towards test branch) * size emoji in markdown * emoji autocomplete to chat, messages, tasks, and profile add :emoji shortcode autocomplete dropdown (reusing existing autocomplete mixin w/new helper) * try upping github-action fix * trying another github actions fix * update habitica-markdown package version (v3.0.0 -> v4.0.0) * Fix emoji autocomplete overlapping actual text position dropdown below text * update group-plans info card styles * Support Melior emoji autocomplete & more places for emoji autocomplete Include emoji autocomplete in task checklists, tags, challenge name/summary/description * position emoji autocomplete dropdown below text area * fix: replace nested ternary * Emoji autocomplete fixes Fix emoji autocomplete overlapping checklist text, and add short name emoji autocomplete * Have emoji autocomplete dropdown directly below text, add to task tag * Fix emoji autocomplete starting at beginning/end initially * lint/line length * Add group plan selection modal for upgrades Allow users to select an existing group to upgrade before creating a new one. * crlf -> lf lint * set selection of group plan Also tiny UI fixes * Update group plan selection to include expired plans * Add includeExpiredPlans option to group fetching * force flag when fetching group plans * Update group plan eligibility check * Fix eslint error in push notification import * replace chaining (?.) w/null check * Remove comment * set initial selected group plan, and fix card rounding * format member count * Show warning for pending party invites when upgrading to paid group plan Show warning for pending party invites when upgrading to paid group plan. If user upgrades from party to group, remove any pending invites * suppress error toasts for group modal, and UI tweaks for group modal suppress error toast for 404 on party fetch for users without a party (for group modal), Increase check SVG size in selectableCard, and show "Previously upgraded" label for parties that were canceled group plans * Clear upgradingGroup state after group plan payment * Update emoji system to native Unicode rendering * Fix line endings in habiticaMarkdown test * fix indented code block detection for markdown-it v14 * update habitica-markdown to include v3 emoji dataset (pointed towards test branch) * size emoji in markdown * emoji autocomplete to chat, messages, tasks, and profile add :emoji shortcode autocomplete dropdown (reusing existing autocomplete mixin w/new helper) * try upping github-action fix * trying another github actions fix * update habitica-markdown package version (v3.0.0 -> v4.0.0) * Fix emoji autocomplete overlapping actual text position dropdown below text * update group-plans info card styles * Support Melior emoji autocomplete & more places for emoji autocomplete Include emoji autocomplete in task checklists, tags, challenge name/summary/description * position emoji autocomplete dropdown below text area * fix: replace nested ternary * Emoji autocomplete fixes Fix emoji autocomplete overlapping checklist text, and add short name emoji autocomplete * Have emoji autocomplete dropdown directly below text, add to task tag * Fix emoji autocomplete starting at beginning/end initially * lint/line length * Revert "trying another github actions fix" This reverts commit72fc7fc20e. * Revert "try upping github-action fix" This reverts commit70e48a57aa. * fix(git): revert ci changes --------- Co-authored-by: Kalista Payne <kalista@habitica.com>
692 lines
20 KiB
Vue
692 lines
20 KiB
Vue
<template>
|
|
<div
|
|
class="row user-tasks-page"
|
|
@click="openCreateBtn ? openCreateBtn = false : null"
|
|
>
|
|
<broken-task-modal />
|
|
<task-modal
|
|
ref="taskModal"
|
|
:task="editingTask || creatingTask"
|
|
:purpose="creatingTask !== null ? 'create' : 'edit'"
|
|
@cancel="cancelTaskModal()"
|
|
/>
|
|
<task-summary
|
|
ref="taskSummary"
|
|
:task="editingTask"
|
|
@cancel="cancelTaskModal()"
|
|
/>
|
|
<div class="col-12">
|
|
<div class="row tasks-navigation">
|
|
<div class="col-12 col-md-4 offset-md-4">
|
|
<div class="d-flex">
|
|
<input
|
|
v-model="searchText"
|
|
class="form-control input-search"
|
|
type="text"
|
|
:placeholder="$t('search')"
|
|
>
|
|
<button
|
|
class="btn btn-secondary dropdown-toggle ml-2 d-flex align-items-center search-button"
|
|
type="button"
|
|
:class="{active: selectedTags.length > 0}"
|
|
@click="toggleFilterPanel()"
|
|
>
|
|
<div
|
|
class="svg-icon filter-icon mr-2"
|
|
v-html="icons.filter"
|
|
></div>
|
|
<span v-once>{{ $t('tags') }}</span>
|
|
</button>
|
|
</div>
|
|
<div
|
|
v-if="isFilterPanelOpen"
|
|
class="filter-panel"
|
|
@mouseleave="checkMouseOver"
|
|
>
|
|
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
|
<div
|
|
v-for="tagsType in tagsByType"
|
|
v-if="tagsType.tags.length > 0 || tagsType.key === 'tags'"
|
|
:key="tagsType.key"
|
|
class="tags-category d-flex flex-column"
|
|
>
|
|
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
|
<div class="tags-header">
|
|
<strong v-once>{{ $t(tagsType.key) }}</strong>
|
|
<a
|
|
v-if="tagsType.key !== 'groups' && !editingTags"
|
|
class="d-block"
|
|
@click="editTags(tagsType.key)"
|
|
>{{ $t('editTagsText') }}</a>
|
|
</div>
|
|
<div class="tags-list container">
|
|
<div
|
|
class="row"
|
|
:class="{'no-gutters': !editingTags}"
|
|
>
|
|
<template v-if="editingTags && tagsType.key !== 'groups'">
|
|
<draggable
|
|
v-model="tagsSnap[tagsType.key]"
|
|
class="row"
|
|
>
|
|
<div
|
|
v-for="(tag, tagIndex) in tagsSnap[tagsType.key]"
|
|
:key="tag.id"
|
|
class="col-6"
|
|
>
|
|
<div class="inline-edit-input-group tag-edit-item input-group">
|
|
<div
|
|
class="svg-icon inline drag"
|
|
v-html="icons.drag"
|
|
></div>
|
|
<input
|
|
:ref="'tagInput-' + tagIndex"
|
|
v-model="tag.name"
|
|
class="tag-edit-input inline-edit-input form-control"
|
|
type="text"
|
|
@focus="setActiveTag(tagIndex)"
|
|
@keydown="autoCompleteMixinUpdateCarretPosition"
|
|
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
|
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
|
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
|
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
|
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
|
>
|
|
<div
|
|
class="input-group-append"
|
|
@click="removeTag(tagIndex, tagsType.key)"
|
|
>
|
|
<div
|
|
class="svg-icon destroy-icon"
|
|
v-html="icons.destroy"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="tagsType.key === 'tags'"
|
|
class="col-6 dragSpace"
|
|
>
|
|
<input
|
|
ref="newTagInput"
|
|
v-model="newTag"
|
|
class="new-tag-item edit-tag-item inline-edit-input form-control"
|
|
type="text"
|
|
:placeholder="$t('newTag')"
|
|
@focus="setActiveTag(-1)"
|
|
@keydown="autoCompleteMixinUpdateCarretPosition"
|
|
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
|
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
|
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
|
@keypress.enter="newTagEnterHandler($event, tagsType.key)"
|
|
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
|
>
|
|
</div>
|
|
</draggable>
|
|
</template>
|
|
<template v-if="!editingTags || tagsType.key === 'groups'">
|
|
<div
|
|
v-for="(tag) in tagsType.tags"
|
|
:key="tag.id"
|
|
class="col-6"
|
|
>
|
|
<div class="custom-control custom-checkbox">
|
|
<input
|
|
:id="`tag-${tag.id}`"
|
|
class="custom-control-input"
|
|
type="checkbox"
|
|
:checked="isTagSelected(tag)"
|
|
@change="toggleTag(tag)"
|
|
>
|
|
<label
|
|
v-markdown="tag.name"
|
|
class="custom-control-label"
|
|
:for="`tag-${tag.id}`"
|
|
></label>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<emoji-auto-complete
|
|
v-if="editingTags"
|
|
ref="emojiAutocomplete"
|
|
:text="activeTagText"
|
|
:textbox="textbox"
|
|
:coords="mixinData.autoComplete.coords"
|
|
:caret-position="mixinData.autoComplete.caretPosition"
|
|
@select="selectedTagAutocomplete"
|
|
/>
|
|
<div class="filter-panel-footer clearfix">
|
|
<template v-if="editingTags === true">
|
|
<div class="text-center">
|
|
<a
|
|
v-once
|
|
class="mr-3 btn-filters-primary"
|
|
@click="saveTags()"
|
|
>{{ $t('saveEdits') }}</a>
|
|
<a
|
|
v-once
|
|
class="btn-filters-secondary"
|
|
@click="cancelTagsEditing()"
|
|
>{{ $t('cancel') }}</a>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div class="float-left">
|
|
<a
|
|
v-once
|
|
class="btn-filters-danger"
|
|
@click="resetFilters()"
|
|
>{{ $t('resetFilters') }}</a>
|
|
</div>
|
|
<div class="float-right">
|
|
<a
|
|
v-once
|
|
class="btn-filters-secondary"
|
|
@click="closeFilterPanel()"
|
|
>{{ $t('cancel') }}</a>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="create-task-area">
|
|
<div
|
|
id="create-task-btn"
|
|
class="btn btn-primary create-btn d-flex align-items-center"
|
|
:class="{open: openCreateBtn}"
|
|
tabindex="0"
|
|
@click.stop.prevent="openCreateBtn = !openCreateBtn"
|
|
@keypress.enter="openCreateBtn = !openCreateBtn"
|
|
>
|
|
<div
|
|
class="svg-icon icon-10 color"
|
|
v-html="icons.positive"
|
|
></div>
|
|
<div class="ml-75 mr-1">
|
|
{{ $t('addTask') }}
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="openCreateBtn"
|
|
class="dropdown"
|
|
>
|
|
<div
|
|
v-for="type in columns"
|
|
:key="type"
|
|
class="dropdown-item d-flex px-2 py-1"
|
|
@click="createTask(type)"
|
|
>
|
|
<div class="d-flex align-items-center justify-content-center task-icon">
|
|
<div
|
|
class="svg-icon m-auto"
|
|
:class="`icon-${type}`"
|
|
v-html="icons[type]"
|
|
></div>
|
|
</div>
|
|
<div class="task-label ml-2">
|
|
{{ $t(type) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row tasks-columns">
|
|
<task-column
|
|
v-for="column in columns"
|
|
:key="column"
|
|
class="col-lg-3 col-md-6"
|
|
:type="column"
|
|
:is-user="true"
|
|
:search-text="searchTextThrottled"
|
|
:selected-tags="selectedTags"
|
|
@editTask="editTask"
|
|
@taskSummary="taskSummary"
|
|
@openBuyDialog="openBuyDialog($event)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<spells />
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
@import '@/assets/scss/colors.scss';
|
|
@import '@/assets/scss/create-task.scss';
|
|
|
|
.user-tasks-page {
|
|
padding-left: 12px;
|
|
padding-right: 12px;
|
|
padding-top: 16px;
|
|
}
|
|
|
|
.tasks-navigation {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.filter-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.filter-panel {
|
|
position: absolute;
|
|
padding-left: 24px;
|
|
padding-right: 24px;
|
|
max-width: 50vw;
|
|
min-width: 300px;
|
|
width: 100%;
|
|
z-index: 9999;
|
|
background: $white;
|
|
border-radius: 2px;
|
|
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
|
|
top: 44px;
|
|
left: 20vw;
|
|
font-size: 14px;
|
|
line-height: 1.43;
|
|
text-overflow: ellipsis;
|
|
|
|
.tags-category {
|
|
border-bottom: 1px solid $gray-600;
|
|
padding-bottom: 24px;
|
|
padding-top: 24px;
|
|
}
|
|
|
|
.tags-header {
|
|
flex-basis: 60px;
|
|
flex-shrink: 0;
|
|
margin-left: 10px;
|
|
|
|
a {
|
|
font-size: 12px;
|
|
line-height: 1.33;
|
|
margin-top: 4px;
|
|
|
|
&:focus, &:hover, &:active {
|
|
text-decoration: underline;
|
|
}
|
|
}
|
|
}
|
|
|
|
.tag-edit-input {
|
|
border-bottom: 1px solid $gray-500 !important;
|
|
|
|
&:focus, &:focus ~ .input-group-append {
|
|
border-color: $purple-500 !important;
|
|
}
|
|
}
|
|
|
|
.new-tag-item {
|
|
width: 100%;
|
|
background-repeat: no-repeat;
|
|
background-position: center left 10px;
|
|
border-bottom: 1px solid $gray-500 !important;
|
|
background-size: 10px 10px;
|
|
padding-left: 40px;
|
|
background-image: url(@/assets/svg/for-css/positive.svg);
|
|
}
|
|
|
|
.tag-edit-item {
|
|
.input-group-append {
|
|
background: $white;
|
|
border-bottom: 1px solid $gray-500 !important;
|
|
|
|
&:focus {
|
|
border-color: $purple-500;
|
|
}
|
|
}
|
|
|
|
.destroy-icon {
|
|
display: none;
|
|
}
|
|
|
|
&:hover .destroy-icon {
|
|
display: inline;
|
|
}
|
|
}
|
|
|
|
.custom-control-label {
|
|
margin-left: 10px;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.filter-panel-footer {
|
|
padding-top: 16px;
|
|
padding-bottom: 16px;
|
|
|
|
a {
|
|
&:focus, &:hover, &:active {
|
|
text-decoration: underline;
|
|
}
|
|
}
|
|
|
|
.btn-filters-danger {
|
|
color: $red-50;
|
|
}
|
|
|
|
.btn-filters-primary {
|
|
color: $blue-10;
|
|
}
|
|
|
|
.btn-filters-secondary {
|
|
color: $gray-300;
|
|
}
|
|
}
|
|
}
|
|
|
|
.create-task-area {
|
|
top: 1px;
|
|
}
|
|
|
|
.drag {
|
|
cursor: grab;
|
|
margin: auto 0;
|
|
height: 20px;
|
|
width: 20px;
|
|
|
|
color: #C3C0C7;
|
|
|
|
&:hover {
|
|
color: #878190;
|
|
}
|
|
}
|
|
|
|
.dragSpace {
|
|
padding-left: 32px;
|
|
}
|
|
|
|
@media only screen and (max-width: 768px) {
|
|
.filter-panel {
|
|
max-width: none;
|
|
left: 0px;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
import { v4 as uuid } from 'uuid';
|
|
import Vue from 'vue';
|
|
import throttle from 'lodash/throttle';
|
|
import cloneDeep from 'lodash/cloneDeep';
|
|
import draggable from 'vuedraggable';
|
|
import taskDefaults from '@/../../common/script/libs/taskDefaults';
|
|
import TaskColumn from './column';
|
|
import TaskModal from './taskModal';
|
|
import TaskSummary from './taskSummary';
|
|
import spells from './spells';
|
|
import markdown from '@/directives/markdown';
|
|
|
|
import positiveIcon from '@/assets/svg/positive.svg?raw';
|
|
import filterIcon from '@/assets/svg/filter.svg?raw';
|
|
import deleteIcon from '@/assets/svg/delete.svg?raw';
|
|
import habitIcon from '@/assets/svg/habit.svg?raw';
|
|
import dailyIcon from '@/assets/svg/daily.svg?raw';
|
|
import todoIcon from '@/assets/svg/todo.svg?raw';
|
|
import rewardIcon from '@/assets/svg/reward.svg?raw';
|
|
import dragIcon from '@/assets/svg/drag_indicator.svg?raw';
|
|
|
|
import { mapState, mapActions } from '@/libs/store';
|
|
import brokenTaskModal from './brokenTaskModal';
|
|
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
|
|
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
|
|
|
export default {
|
|
components: {
|
|
TaskColumn,
|
|
TaskModal,
|
|
TaskSummary,
|
|
spells,
|
|
brokenTaskModal,
|
|
draggable,
|
|
emojiAutoComplete,
|
|
},
|
|
directives: {
|
|
markdown,
|
|
},
|
|
mixins: [autoCompleteHelperMixin],
|
|
data () {
|
|
return {
|
|
columns: ['habit', 'daily', 'todo', 'reward'],
|
|
searchText: null,
|
|
searchTextThrottled: null,
|
|
isFilterPanelOpen: false,
|
|
openCreateBtn: false,
|
|
icons: Object.freeze({
|
|
positive: positiveIcon,
|
|
filter: filterIcon,
|
|
destroy: deleteIcon,
|
|
habit: habitIcon,
|
|
daily: dailyIcon,
|
|
todo: todoIcon,
|
|
reward: rewardIcon,
|
|
drag: dragIcon,
|
|
}),
|
|
selectedTags: [],
|
|
temporarilySelectedTags: [],
|
|
tagsSnap: {
|
|
tags: [],
|
|
challenges: [],
|
|
}, // tags snapshot when being edited
|
|
editingTags: false,
|
|
newTag: null,
|
|
editingTask: null,
|
|
creatingTask: null,
|
|
textbox: null,
|
|
activeTagIndex: -1,
|
|
};
|
|
},
|
|
computed: {
|
|
...mapState({ user: 'user.data' }),
|
|
activeTagText () {
|
|
if (this.activeTagIndex === -1) {
|
|
return this.newTag || '';
|
|
}
|
|
const tag = this.tagsSnap.tags[this.activeTagIndex];
|
|
return tag ? tag.name || '' : '';
|
|
},
|
|
tagsByType () {
|
|
const userTags = this.user.tags;
|
|
const tagsByType = {
|
|
challenges: {
|
|
key: 'challenges',
|
|
tags: [],
|
|
},
|
|
groups: {
|
|
key: 'groups',
|
|
tags: [],
|
|
},
|
|
user: {
|
|
key: 'tags',
|
|
tags: [],
|
|
},
|
|
};
|
|
|
|
userTags.forEach(t => {
|
|
if (t.group) {
|
|
tagsByType.groups.tags.push(t);
|
|
} else if (t.challenge) {
|
|
tagsByType.challenges.tags.push(t);
|
|
} else {
|
|
tagsByType.user.tags.push(t);
|
|
}
|
|
});
|
|
|
|
return tagsByType;
|
|
},
|
|
},
|
|
watch: {
|
|
searchText: throttle(function throttleSearch () {
|
|
this.searchTextThrottled = this.searchText.toLowerCase();
|
|
}, 250),
|
|
},
|
|
mounted () {
|
|
this.$store.dispatch('common:setTitle', {
|
|
section: this.$t('tasks'),
|
|
});
|
|
if (this.$store.state.postLoadModal) {
|
|
const modalToLoad = this.$store.state.postLoadModal;
|
|
if (modalToLoad.includes('profile')) {
|
|
this.$router.push(modalToLoad);
|
|
} else {
|
|
this.$root.$emit('bv::show::modal', modalToLoad);
|
|
}
|
|
this.$store.state.postLoadModal = '';
|
|
}
|
|
},
|
|
methods: {
|
|
...mapActions({ setUser: 'user:set' }),
|
|
checkMouseOver: throttle(function throttleSearch () {
|
|
if (this.editingTags) return;
|
|
this.closeFilterPanel();
|
|
}, 250),
|
|
editTags () {
|
|
// clone the arrays being edited so that we can revert if needed
|
|
this.tagsSnap.tags = this.tagsByType.user.tags.slice();
|
|
this.tagsSnap.challenges = this.tagsByType.challenges.tags.slice();
|
|
this.editingTags = true;
|
|
},
|
|
addTag (eventObj, key) {
|
|
this.tagsSnap[key].push({ id: uuid(), name: this.newTag });
|
|
this.newTag = null;
|
|
},
|
|
setActiveTag (index) {
|
|
this.activeTagIndex = index;
|
|
if (index === -1) {
|
|
const refArr = this.$refs.newTagInput;
|
|
this.textbox = Array.isArray(refArr) ? refArr[0] : refArr;
|
|
} else {
|
|
const refArr = this.$refs[`tagInput-${index}`];
|
|
if (!refArr) {
|
|
this.textbox = null;
|
|
} else {
|
|
this.textbox = Array.isArray(refArr) ? refArr[0] : refArr;
|
|
}
|
|
}
|
|
},
|
|
newTagEnterHandler (e, key) {
|
|
const ac = this._getActiveAutocomplete();
|
|
if (ac && ac.selected !== null) {
|
|
e.preventDefault();
|
|
ac.makeSelection();
|
|
} else {
|
|
if (ac) ac.cancel();
|
|
this.addTag(e, key);
|
|
}
|
|
},
|
|
selectedTagAutocomplete (newText, newCaret) {
|
|
if (this.activeTagIndex === -1) {
|
|
this.newTag = newText;
|
|
} else {
|
|
this.tagsSnap.tags[this.activeTagIndex].name = newText;
|
|
}
|
|
this.$nextTick(() => {
|
|
if (this.textbox) {
|
|
this.textbox.setSelectionRange(newCaret, newCaret);
|
|
this.textbox.focus();
|
|
}
|
|
});
|
|
},
|
|
removeTag (index, key) {
|
|
const tagId = this.tagsSnap[key][index].id;
|
|
const indexInSelected = this.selectedTags.indexOf(tagId);
|
|
const indexInTempSelected = this.temporarilySelectedTags.indexOf(tagId);
|
|
if (indexInSelected !== -1) {
|
|
this.$delete(this.selectedTags, indexInSelected);
|
|
}
|
|
if (indexInTempSelected !== -1) {
|
|
this.$delete(this.temporarilySelectedTags, indexInTempSelected);
|
|
}
|
|
this.$delete(this.tagsSnap[key], index);
|
|
},
|
|
saveTags () {
|
|
if (this.newTag) this.addTag(null, 'tags');
|
|
|
|
this.tagsByType.user.tags = this.tagsSnap.tags;
|
|
this.tagsByType.challenges.tags = this.tagsSnap.challenges;
|
|
|
|
this.setUser({ tags: this.tagsSnap.tags.concat(this.tagsSnap.challenges) });
|
|
this.cancelTagsEditing();
|
|
},
|
|
cancelTagsEditing () {
|
|
this.editingTags = false;
|
|
this.tagsSnap = {
|
|
tags: [],
|
|
challenges: [],
|
|
};
|
|
this.newTag = null;
|
|
},
|
|
// Need Vue.nextTick() otherwise the first time the modal is not rendered
|
|
editTask (task) {
|
|
this.editingTask = cloneDeep(task);
|
|
Vue.nextTick(() => {
|
|
this.$root.$emit('bv::show::modal', 'task-modal');
|
|
});
|
|
},
|
|
taskSummary (task) {
|
|
this.editingTask = cloneDeep(task);
|
|
Vue.nextTick(() => {
|
|
this.$root.$emit('bv::show::modal', 'task-summary');
|
|
});
|
|
},
|
|
createTask (type) {
|
|
this.openCreateBtn = false;
|
|
this.creatingTask = taskDefaults({ type, text: '' }, this.user);
|
|
this.creatingTask.tags = this.selectedTags.slice();
|
|
|
|
// Necessary otherwise the first time the modal is not rendered
|
|
Vue.nextTick(() => {
|
|
this.$root.$emit('bv::show::modal', 'task-modal');
|
|
});
|
|
},
|
|
cancelTaskModal () {
|
|
this.editingTask = null;
|
|
this.creatingTask = null;
|
|
},
|
|
toggleFilterPanel () {
|
|
if (this.isFilterPanelOpen === true) {
|
|
this.closeFilterPanel();
|
|
} else {
|
|
this.openFilterPanel();
|
|
}
|
|
},
|
|
openFilterPanel () {
|
|
this.isFilterPanelOpen = true;
|
|
this.temporarilySelectedTags = this.selectedTags.slice();
|
|
},
|
|
closeFilterPanel () {
|
|
this.temporarilySelectedTags = [];
|
|
this.isFilterPanelOpen = false;
|
|
},
|
|
resetFilters () {
|
|
this.selectedTags = [];
|
|
this.closeFilterPanel();
|
|
},
|
|
applyFilters () {
|
|
const { temporarilySelectedTags } = this;
|
|
this.selectedTags = temporarilySelectedTags.slice();
|
|
},
|
|
toggleTag (tag) {
|
|
const { temporarilySelectedTags } = this;
|
|
const tagI = temporarilySelectedTags.indexOf(tag.id);
|
|
if (tagI === -1) {
|
|
temporarilySelectedTags.push(tag.id);
|
|
} else {
|
|
temporarilySelectedTags.splice(tagI, 1);
|
|
}
|
|
|
|
this.applyFilters();
|
|
},
|
|
isTagSelected (tag) {
|
|
const tagId = tag.id;
|
|
if (this.temporarilySelectedTags.indexOf(tagId) !== -1) return true;
|
|
return false;
|
|
},
|
|
openBuyDialog (item) {
|
|
this.$root.$emit('buyModal::showItem', item);
|
|
},
|
|
},
|
|
};
|
|
</script>
|