Add API Call to bulk score tasks (#11389)

* Add new API call to complete multiple task scorings in one call

* Improve API response

* Improve saving process

* Improve handling for multiple tasks scored at once

* Handle challenge task errors better

* Improve check for alias

* Improve check for task scorings

* Fix merge errors

* make nodemon ignore content_cache

* Fix completing group tasks

* fix test

* fix tests (again)

* typo

* WIP(a11y): task modal updates

* fix(tasks): borders in modal

* fix(tasks): circley locks

* fix(task-modal): placeholders

* WIP(task-modal): disabled states, hide empty options, +/- restyle

* fix(task-modal): box shadows instead of borders, habit control pointer

* fix(task-modal): button states?

* fix(modal): tighten up layout, new spacing utils

* fix(tasks): more stylin

* fix(tasks): habit hovers

* fix(css): checklist labels, a11y colors

* fix(css): one more missed hover issue

* fix(css): lock Challenges, label fixes

* fix(css): scope input/textarea changes

* fix(style): task tweakies

* fix(style): more button fixage

* WIP(component): start select list story

* working example of a templated selectList

* fix(style): more button corrections

* fix(lint): EOL

* fix(buttons): factor btn-secondary to better override Bootstrap

* fix(styles): standardize more buttons

* wip: difficulty select - style fixes

* selectDifficulty works! 🎉 - fix styles

* change the dropdown-item sizes only for the selectList ones

* selectTranslatedArray

* changed many label margins

* more correct dropdown style

* fix(modals): button corrections

* input-group styling + datetime picker without today button

* Style/margins for "repeat every" - extract selectTag.vue

* working tag-selection / update - cleanup

* fix stories

* fix svg color on create modal (purple)

* fix task modal bottom padding

* correct dropdown shadow

* update dropdown-toggle caret size / color

* fixed checklist style

* sync checked state

* selectTag padding

* fix spacing between positive/negative streak inputs

* toggle-checkbox + fix some spacings

* disable repeat-on when its a groupTask

* fix new checklist-item

* fix toggle-checkbox style - fix difficulty style

* fix checklist ui

* add tags label , when there arent any tags selected

* WORKING select-tag component 🎉

* fix taglist story

* show max 5 items in tag dropdown + "X more" label

* fix datetime clear button

* replace m-b-xs to mb-1 (bootstrap) - fix input-group-text style

* fix styles of advanced settings

* fix delete task styles

* always show grippy on hover of the item

* extract modal-text-input mixin + fix the borders/dropshadow

* fix(spacing): revert most to Bootstrap

* feat(checklists): make local copy of master checklist non-editable
also aggressively update checklists because they weren't syncing??

* fix(checklists): handle add/remove options better

* feat(teams): manager notes field

* fix select/dropdown styles

* input border + icon colors

* delete task underline color

* fix checklist "delete icon" vertical position

* selectTag fixes - normal open/close toggle working again - remove icon color

* fixing icons:

Trash can - Delete
Little X - Remove
Big X - Close
Block - Block

* fix taglist margins / icon sizes

* wip margin overview (in storybook)

* fix routerlink

* remove unused method

* new selectTag style + add markdown inside tagList + scrollable tag selection

* fix selectTag / selectList active border

* fix difficulty select (svg default color)

* fix input padding-left + fix reset habit streak fullwidth / padding + "repeat every" gray text (no border)

* feat(teams): improved approval request > approve > reward flow

* fix(tests): address failures

* fix(lint): oops only

* fix(tasks): short-circuit group related logic

* fix(tasks): more short circuiting

* fix(tasks): more lines, less lint

* fix(tasks): how do i keep missing these

* feat(teams): provide assigning user summary

* fix(teams): don't attempt to record assiging user if not supplied

* fix advanced-settings styling / margin

* fix merge + hide advanced streak settings when none enabled

* fix styles

* set Roboto font for advanced settings

* Add Challenge flag to the tag list

* add tag with enter, when no other tag is found

* fix styles + tag cancel button

* refactor footer / margin

* split repeat fields into option mt-3 groups

* button all the things

* fix(tasks): style updates
* no hover state for non-editable tasks on team board
* keep assign/claim footer on task after requesting approval
* disable more fields on user copy of team task, and remove hover states 
for them

* fix(tasks): functional revisions
* "Claim Rewards" instead of "x" in task approved notif
* Remove default transition supplied by Bootstrap, apply individually to 
some elements
* Delete individual tasks and related notifications when master task 
deleted from team board
* Manager notes now save when supplied at task initial creation
* Can no longer dismiss rewards from approved task by hitting Dismiss 
All

* fix(tasks): clean tasksOrder
also adjust related test expectation

* fix(tests): adjust integration expectations

* fix(test): ratzen fratzen only

* fix lint

* fix tests

* fix(teams): checklist, notes

* handleSharedCompletion: handle error, make sure it is run after the user task has been saved

* fix typo

* correctly handle errors in handleSharedCompletion when approving a task

* fix(teams): improve disabled states

* handleSharedCompletion: do not increase completions by 1 manually to adjust for last approval not saved yet

* revert changes to config.json.example

* fix(teams): more style fixage

* add unit tests for findMultipleByIdOrAlias

* exclude api v4 route from apidocs

* BREAKING(teams): return 202 instead of 401 for approval request

* fix(teams): better taskboard sync
also re-re-fix checklist borders

* scoreTasks: validate body

* fix tests, move string to api errors

* fix(tests): update expectations for breaking change

* start updating api docs, process tasks sequentially to avoid conflicts with user._tmp

* do not crash entire bulk operation in case of errors

* save task only if modified

* fix lint

* undo changes to error handling: either all tasks scoring are successfull or none

* remove stale code

* do not return user._tmp when bulk scoring, it would be the last version only

* make sure user._tmp.leveledUp is not lost when bulk scoring

* rewards tests

* mixed tests

* fix tests, allow scoring the same task multiple times

* finish integration tests

* fix api docs for the bulk score route

* refactor(task-modal): lockable label component

* wip loading spinner

* refactor(teams): move task scoring to mixin

* fix(teams): style corrections

* fix(btn): fix padding to have height of 32px

* implement loading spinner

* remove console.log warnings

* fix(tasks): spacing and wording corrections

* fix(teams): don't bork manager notes

* fix(teams): assignment fix and more approval flow revisions

* WIP(teams): use tag dropdown control for assignment

* finish merge - never throw an error when a group task requires approval (wip - needs tests)

* fix taskModal merge

* fix merge

* fix(task modal): add newline

* fix(column.vue): add newline at end of file

* mvp yesterdaily modal

* fix tests

* fix api docs for bulk scoring group tasks

* separate task scoring and _tmp handling

* handle _tmp when bulk scoring

* rya: close modal before calling cron API, prevents issues with modals

* rya: fix conflicts with other modals

* add sounds, support for group plans, analytics

* use asyncResource for group plans

* fix lint

* streak bonus: add comment about missing in rya

* move yesterdailyModal

* fix issues with level up modals and rya

* add comments for future use, fix level up modals not showing up at levels with a quest drop

* handle errors in rya modal

* bundle quest and crit notifications

Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Phillip Thelen <viirus@pherth.net>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
Co-authored-by: negue <eugen.bolz@gmail.com>
This commit is contained in:
Matteo Pagliazzi
2020-08-21 11:46:56 +02:00
committed by GitHub
parent 46b5efcaf6
commit d0bc0dbe49
34 changed files with 1541 additions and 385 deletions
@@ -48,7 +48,7 @@ export default {
computed: {
...mapState({
user: 'user.data',
groupPlans: 'groupPlans',
groupPlans: 'groupPlans.data',
}),
currentGroup () {
const groupFound = this.groupPlans.find(group => group._id === this.groupId);
@@ -212,7 +212,7 @@
'active': $route.path.startsWith('/group-plans')}"
>
<div
v-if="groupPlans.length > 0"
v-if="groupPlans && groupPlans.length > 0"
class="chevron rotate"
@click="dropdownMobile($event)"
>
@@ -761,7 +761,7 @@ export default {
...mapState({
user: 'user.data',
userHourglasses: 'user.data.purchased.plan.consecutive.trinkets',
groupPlans: 'groupPlans',
groupPlans: 'groupPlans.data',
modalStack: 'modalStack',
}),
navbarZIndexClass () {
@@ -789,7 +789,7 @@ export default {
this.isUserDropdownOpen = !this.isUserDropdownOpen;
},
async getUserGroupPlans () {
this.$store.state.groupPlans = await this.$store.dispatch('guilds:getGroupPlans');
await this.$store.dispatch('guilds:getGroupPlans');
},
openPartyModal () {
this.$root.$emit('bv::show::modal', 'create-party-modal');
+24 -18
View File
@@ -2,7 +2,8 @@
<div>
<yesterdaily-modal
:yester-dailies="yesterDailies"
@run-cron="runYesterDailiesAction()"
:cron-action="runCronAction"
@hidden="afterYesterdailies()"
/>
<armoire-empty />
<new-stuff />
@@ -116,7 +117,7 @@ import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
import guide from '@/mixins/guide';
import yesterdailyModal from './yesterdailyModal';
import yesterdailyModal from './tasks/yesterdailyModal';
import newStuff from './achievements/newStuff';
import death from './achievements/death';
import lowHealth from './achievements/lowHealth';
@@ -422,7 +423,6 @@ export default {
unlockLevels,
lastShownNotifications,
alreadyReadNotification,
isRunningYesterdailies: false,
nextCron: null,
handledNotifications,
};
@@ -474,6 +474,10 @@ export default {
const money = after - before;
let bonus;
// NOTE: the streak bonus snackbar
// is not shown when bulk scoring (for example in the RYA modal)
// is used as it bypass the client side scoring
// and doesn't populate the _tmp object
if (this.user._tmp) {
bonus = this.user._tmp.streakBonus || 0;
}
@@ -616,6 +620,7 @@ export default {
}
// Lvl evaluation
// @TODO use LEVELED_UP notification, would remove the need to check for yesterdailies
if (afterLvl !== beforeLvl) {
if (afterLvl <= beforeLvl || this.$store.state.isRunningYesterdailies) return;
this.showLevelUpNotifications(afterLvl);
@@ -656,7 +661,12 @@ export default {
showLevelUpNotifications (newlevel) {
this.lvl();
this.playSound('Level_Up');
if (this.user._tmp && this.user._tmp.drop && this.user._tmp.drop.type === 'Quest') return;
// NOTE this code isn't actually used because no modal is shown when a quest is dropped
// In case it's added again it should keep in mind that it will not work
// when the user progress to the next level using the RYA modal
// as it doesn't score the tasks on the client side and thus this.user._tmp is not filled
// with any value
// if (this.user._tmp && this.user._tmp.drop && this.user._tmp.drop.type === 'Quest') return;
if (this.unlockLevels[`${newlevel}`]) return;
if (!this.user.preferences.suppressModals.levelUp) this.$root.$emit('bv::show::modal', 'level-up');
},
@@ -691,15 +701,13 @@ export default {
// Setup a listener that executes 10 seconds after the next cron time
this.nextCron = Number(nextCron.format('x'));
this.$store.state.isRunningYesterdailies = false;
},
async runYesterDailies () {
if (this.$store.state.isRunningYesterdailies) return;
this.$store.state.isRunningYesterdailies = true;
if (!this.user.needsCron) {
this.scheduleNextCron();
this.handleUserNotifications(this.user.notifications);
this.afterYesterdailies();
return;
}
@@ -717,25 +725,25 @@ export default {
});
if (this.yesterDailies.length === 0) {
this.runYesterDailiesAction();
return;
await this.runCronAction();
this.afterYesterdailies();
} else {
this.levelBeforeYesterdailies = this.user.stats.lvl;
this.$root.$emit('bv::show::modal', 'yesterdaily');
}
this.levelBeforeYesterdailies = this.user.stats.lvl;
this.$root.$emit('bv::show::modal', 'yesterdaily');
},
async runYesterDailiesAction () {
async runCronAction () {
// Run Cron
await axios.post('/api/v4/cron');
// Notifications
// Sync
await Promise.all([
this.$store.dispatch('user:fetch', { forceLoad: true }),
this.$store.dispatch('tasks:fetchUserTasks', { forceLoad: true }),
]);
},
afterYesterdailies () {
this.scheduleNextCron();
this.$store.state.isRunningYesterdailies = false;
if (
@@ -744,8 +752,6 @@ export default {
) {
this.showLevelUpNotifications(this.user.stats.lvl);
}
this.scheduleNextCron();
this.handleUserNotifications(this.user.notifications);
},
async handleUserNotifications (after) {
@@ -371,7 +371,7 @@ export default {
draggable,
},
mixins: [buyMixin, notifications],
// Set default values for props
// @TODO Set default values for props
// allows for better control of props values
// allows for better control of where this component is called
props: {
+20 -3
View File
@@ -846,7 +846,18 @@ export default {
markdown: markdownDirective,
},
mixins: [scoreTask],
props: ['task', 'isUser', 'group', 'challenge', 'dueDate'], // @TODO: maybe we should store the group on state?
// @TODO: maybe we should store the group on state?
props: {
task: {},
isUser: {},
group: {},
challenge: {},
dueDate: {},
isYesterdaily: {
type: Boolean,
default: false,
},
},
data () {
return {
random: uuid(), // used to avoid conflicts between checkboxes ids
@@ -1039,8 +1050,14 @@ export default {
castEnd (e, task) {
setTimeout(() => this.$root.$emit('castEnd', task, 'task', e), 0);
},
score (direction) {
this.taskScore(this.task, direction);
async score (direction) {
if (this.isYesterdaily === true) {
await this.beforeTaskScore(this.task);
this.task.completed = !this.task.completed;
this.playTaskScoreSound(this.task, direction);
} else {
this.taskScore(this.task, direction);
}
},
handleBrokenTask (task) {
if (this.$store.state.isRunningYesterdailies) return;
@@ -0,0 +1,189 @@
<template>
<b-modal
id="yesterdaily"
size="m"
:hide-header="true"
:hide-footer="true"
:no-close-on-backdrop="true"
:no-close-on-esc="true"
@hide="$emit('hide')"
@hidden="$emit('hidden')"
>
<h1 class="header-welcome text-center">
{{ $t('welcomeBack') }}
</h1>
<p class="call-to-action text-center">
{{ $t('checkOffYesterDailies') }}
</p>
<div class="tasks-list">
<task
v-for="task in tasksByType.daily"
:key="task.id"
:task="task"
:is-user="true"
:due-date="dueDate"
:is-yesterdaily="true"
/>
</div>
<div class="start-day text-center">
<button
:disabled="isLoading"
class="btn btn-primary"
@click="processYesterdailies()"
>
<span v-if="!isLoading">{{ $t('yesterDailiesCallToAction') }}</span>
<loading-spinner v-else />
</button>
</div>
</b-modal>
</template>
<style lang="scss">
#yesterdaily {
.modal-dialog {
width: 22.625rem;
}
.task-wrapper:not(:last-of-type) {
margin-bottom: 2px;
}
.modal-content {
border-radius: 8px;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.header-welcome {
color: $purple-200;
margin-top: 1rem;
}
.call-to-action {
font-size: 14px;
font-weight: bold;
}
.tasks-list {
border-radius: 4px;
background: $gray-600;
padding: 8px;
position: relative;
overflow: auto;
}
.start-day {
margin: 1.5rem auto 1rem auto;
}
</style>
<script>
import moment from 'moment';
import * as Analytics from '@/libs/analytics';
import { mapState } from '@/libs/store';
import scoreTask from '@/mixins/scoreTask';
import Task from './task';
import LoadingSpinner from '../ui/loadingSpinner';
export default {
components: {
Task,
LoadingSpinner,
},
mixins: [scoreTask],
props: {
yesterDailies: {
type: Array,
},
cronAction: {
type: Function,
},
},
data () {
return {
isLoading: false,
dueDate: moment().subtract(1, 'days'),
};
},
computed: {
...mapState({ user: 'user.data' }),
tasksByType () {
this.dueDate = moment().subtract(1, 'days'); // eslint-disable-line vue/no-side-effects-in-computed-properties
return {
daily: this.yesterDailies,
};
},
},
methods: {
async processYesterdailies () {
if (this.isLoading) return;
this.isLoading = true;
const bulkScoreParams = this.yesterDailies
.filter(yesterdaily => yesterdaily.completed)
.map(yesterdaily => ({ id: yesterdaily._id, direction: 'up' }));
if (bulkScoreParams.length > 0) {
try {
const bulkScoresponse = await this.$store.dispatch('tasks:bulkScore', bulkScoreParams);
// Bundle critical hits and quests updates into a single notification
const bundledTmp = {};
bulkScoresponse.data.data.tasks.forEach(taskResponse => {
taskResponse._tmp = taskResponse._tmp || {};
const tmp = taskResponse._tmp;
if (tmp.crit) {
if (!bundledTmp.crit) {
bundledTmp.crit = 0;
}
bundledTmp.crit += tmp.crit;
tmp.crit = undefined;
}
if (tmp.quest) {
if (!bundledTmp.quest) {
bundledTmp.quest = { progressDelta: 0, collection: 0 };
}
if (tmp.quest.progressDelta) {
bundledTmp.quest.progressDelta += tmp.quest.progressDelta;
}
if (tmp.quest.collection) bundledTmp.quest.collection += tmp.quest.collection;
tmp.quest = undefined;
}
});
this.handleTaskScoreNotifications(bundledTmp);
bulkScoresponse.data.data.tasks.forEach(taskResponse => {
this.handleTaskScoreNotifications(taskResponse._tmp);
});
} catch (err) {
// Reset the modal so that it can be used again
// Then throw the error again to make sure it's handled correctly
// and the user is notified.
this.yesterDailies.forEach(y => { y.completed = false; });
this.isLoading = false;
throw err;
}
}
await this.cronAction();
this.isLoading = false;
this.$root.$emit('bv::hide::modal', 'yesterdaily');
Analytics.updateUser();
},
},
};
</script>
@@ -0,0 +1,60 @@
<template>
<div
v-once
class="loading-spinner"
role="text"
:aria-label="$t('loading')"
>
<div></div>
<div></div>
<div></div>
</div>
</template>
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
// NOTE: the loader is currently set to work inside standard buttons
// To properly work outside of them some abstraction will be needed
// for the height and width
// Original CSS from https://loading.io/css/ released under the CC0 License
.loading-spinner {
width: 20px;
height: 20px;
margin-top: 1.5px;
margin-bottom: 1.5px;
}
.loading-spinner div {
box-sizing: border-box;
display: block;
position: absolute;
width: 20px;
height: 20px;
border: 2px solid $white;
border-radius: 50%;
animation: loading-spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: $white transparent transparent transparent;
}
.loading-spinner div:nth-child(1) {
animation-delay: -0.45s;
}
.loading-spinner div:nth-child(2) {
animation-delay: -0.3s;
}
.loading-spinner div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes loading-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
@@ -1,111 +0,0 @@
<template>
<b-modal
id="yesterdaily"
size="m"
:hide-header="true"
:hide-footer="true"
:no-close-on-backdrop="true"
:no-close-on-esc="true"
@hide="$emit('hide')"
>
<h1 class="header-welcome text-center">
{{ $t('welcomeBack') }}
</h1>
<p class="call-to-action text-center">
{{ $t('checkOffYesterDailies') }}
</p>
<div class="tasks-list">
<task
v-for="task in tasksByType.daily"
:key="task.id"
:task="task"
:is-user="true"
:due-date="dueDate"
/>
</div>
<div class="start-day text-center">
<button
class="btn btn-primary"
@click="close()"
>
{{ $t('yesterDailiesCallToAction') }}
</button>
</div>
</b-modal>
</template>
<style lang="scss">
#yesterdaily {
.modal-dialog {
width: 22.625rem;
}
.task-wrapper:not(:last-of-type) {
margin-bottom: 2px;
}
.modal-content {
border-radius: 8px;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.header-welcome {
color: $purple-200;
margin-top: 1rem;
}
.call-to-action {
font-size: 14px;
font-weight: bold;
}
.tasks-list {
border-radius: 4px;
background: $gray-600;
padding: 8px;
position: relative;
overflow: auto;
}
.start-day {
margin: 1.5rem auto 1rem auto;
}
</style>
<script>
import moment from 'moment';
import { mapState } from '@/libs/store';
import Task from './tasks/task';
export default {
components: {
Task,
},
props: ['yesterDailies'],
data () {
return {
dueDate: moment().subtract(1, 'days'),
};
},
computed: {
...mapState({ user: 'user.data' }),
tasksByType () {
this.dueDate = moment().subtract(1, 'days'); // eslint-disable-line vue/no-side-effects-in-computed-properties
return {
daily: this.yesterDailies,
};
},
},
methods: {
async close () {
this.$root.$emit('bv::hide::modal', 'yesterdaily');
this.$emit('run-cron');
},
},
};
</script>