Teams UI Redesign and A11y Updates (#12142)

* 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(teams): checklist, notes

* fix(teams): improve disabled states

* fix(teams): more style fixage

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

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

* fix(tests): update expectations for breaking change

* refactor(task-modal): lockable label component

* refactor(teams): move task scoring to mixin

* fix(teams): style corrections

* 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

* refactor(tasks): better spacing, generic multi select

* fix(tasks): various visual and behavior updates

* fix(tasks): incidental style tweaks

* fix(teams): standardize approval request response

* refactor(teams): correct test, use res.respond message param

* fix(storybook): renamed component

* fix(teams): age approval-required To Do's
Fixes #8730

* fix(teams): sync personal data as well as team on mixin sync

* fix(teams): hide unclaim button, not whole footer; fix switch focus

* fix(achievements): unrevert width fix

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
This commit is contained in:
negue
2020-07-25 14:37:10 +02:00
committed by GitHub
parent 7ee6ff18ce
commit aaf32cc09b
73 changed files with 2769 additions and 1409 deletions

View File

@@ -235,15 +235,16 @@ describe('Group Task Methods', () => {
});
});
it('removes an assigned task and unlinks assignees', async () => {
it('removes assigned tasks when master task is deleted', async () => {
await guild.syncTask(task, leader);
await guild.removeTask(task);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const updatedLeadersTasks = await Tasks.Task.find({ userId: leader._id, type: taskType });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
expect(updatedLeader.tasksOrder[`${taskType}s`]).to.not.include(task._id);
expect(syncedTask).to.not.exist;
});
it('unlinks and deletes group tasks for a user when remove-all is specified', async () => {

View File

@@ -75,12 +75,7 @@ describe('POST /group/:groupId/remove-manager', () => {
await nonLeader.post(`/tasks/${task._id}/assign/${nonManager._id}`);
const memberTasks = await nonManager.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(nonManager.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await nonManager.post(`/tasks/${syncedTask._id}/score/up`);
const updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,

View File

@@ -73,12 +73,7 @@ describe('Groups DELETE /tasks/:id', () => {
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.sync();
await member2.sync();
@@ -96,16 +91,16 @@ describe('Groups DELETE /tasks/:id', () => {
expect(member2.notifications.length).to.equal(1);
});
it('unlinks assigned user', async () => {
it('deletes task from assigned user', async () => {
await user.del(`/tasks/${task._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
expect(syncedTask).to.not.exist;
});
it('unlinks all assigned users', async () => {
it('deletes task from all assigned users', async () => {
await user.del(`/tasks/${task._id}`);
const memberTasks = await member.get('/tasks/user');
@@ -114,8 +109,8 @@ describe('Groups DELETE /tasks/:id', () => {
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
expect(member2SyncedTask.group.broken).to.equal('TASK_DELETED');
expect(syncedTask).to.not.exist;
expect(member2SyncedTask).to.not.exist;
});
it('prevents a user from deleting a task they are assigned to', async () => {
@@ -130,22 +125,6 @@ describe('Groups DELETE /tasks/:id', () => {
});
});
it('allows a user to delete a broken task', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await user.del(`/tasks/${task._id}`);
await member.del(`/tasks/${syncedTask._id}`);
await expect(member.get(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Task not found.',
});
});
it('allows a user to delete a task after leaving a group', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);

View File

@@ -58,22 +58,14 @@ describe('POST /tasks/:id/approve/:userId', () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(3);
expect(member.notifications.length).to.equal(2);
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
expect(member.notifications[2].type).to.equal('SCORED_TASK');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
@@ -93,21 +85,13 @@ describe('POST /tasks/:id/approve/:userId', () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(3);
expect(member.notifications.length).to.equal(2);
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
expect(member.notifications[2].type).to.equal('SCORED_TASK');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
@@ -125,12 +109,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.sync();
await member2.sync();
@@ -157,14 +136,9 @@ describe('POST /tasks/:id/approve/:userId', () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
@@ -197,13 +171,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
@@ -226,13 +194,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
const member2Tasks = await member2.get('/tasks/user');
@@ -258,13 +220,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
const groupTasks = await user.get(`/tasks/group/${guild._id}`);
@@ -287,21 +243,10 @@ describe('POST /tasks/:id/approve/:userId', () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
await expect(member2.post(`/tasks/${member2SyncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${member2SyncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);

View File

@@ -61,13 +61,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
let syncedTask = find(memberTasks, findAssignedTask);
// score task to require approval
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${task._id}/needs-work/${member._id}`);
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
@@ -114,12 +108,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
let syncedTask = find(memberTasks, findAssignedTask);
// score task to require approval
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
const initialNotifications = member.notifications.length;
@@ -172,13 +161,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({

View File

@@ -44,12 +44,11 @@ describe('POST /tasks/:id/score/:direction', () => {
const syncedTask = find(memberTasks, findAssignedTask);
const direction = 'up';
await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
const response = await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
expect(response.data.approvalRequested).to.equal(true);
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
await user.sync();
@@ -76,12 +75,7 @@ describe('POST /tasks/:id/score/:direction', () => {
const syncedTask = find(memberTasks, findAssignedTask);
const direction = 'up';
await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
await user.sync();
await member2.sync();
@@ -111,12 +105,7 @@ describe('POST /tasks/:id/score/:direction', () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.eql({
@@ -130,12 +119,7 @@ describe('POST /tasks/:id/score/:direction', () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);

View File

@@ -71,12 +71,10 @@ describe('PUT /tasks/:id', () => {
const syncedTask = find(memberTasks, memberTask => memberTask.group.taskId === habit._id);
// score up to trigger approval
await expect(member2.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
const response = await member2.post(`/tasks/${syncedTask._id}/score/up`);
expect(response.data.approvalRequested).to.equal(true);
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
});
it('member updates a group task value - not allowed', async () => {

View File

@@ -1,6 +1,8 @@
/* eslint-disable import/no-extraneous-dependencies */
import { configure } from '@storybook/vue';
import './margin.css';
import '../../src/assets/scss/index.scss';
import '../../src/assets/scss/spacing.scss';
import '../../src/assets/css/sprites.css';
import '../../src/assets/css/sprites/spritesmith-main-0.css';

View File

@@ -0,0 +1,13 @@
.background {
background: teal;
display: inline-block;
}
.content {
color: white;
background: grey;
}
.inline-block {
display: inline-block;
}

View File

@@ -52,9 +52,11 @@
border-color: $purple-400;
}
&:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus {
box-shadow: none;
border-color: $purple-400;
&:not(:disabled):not(.disabled) {
&:active:focus, &.active:focus {
box-shadow: none;
border-color: $purple-400;
}
}
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active {

View File

@@ -1,20 +1,27 @@
.dropdown > .btn {
padding: 9px 15.5px;
padding: 0.25rem 0.75rem;
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1.43;
}
.dropdown-toggle:hover {
--caret-color: #{$purple-200};
}
.dropdown.show > .dropdown-toggle:not(.btn-success) {
color: $purple-200;
border-color: $purple-500 !important;
border-color: $purple-400 !important;
box-shadow: none;
&::after {
--caret-color: #{$purple-200};
}
}
.dropdown-toggle::after {
margin-left: 16px;
border-top: 6px solid;
border-top-color: var(--caret-color);
border-right: 5px solid transparent;
border-left: 5px solid transparent;
vertical-align: 0;
@@ -23,14 +30,18 @@
.dropdown-menu {
padding: 0px;
border: none;
border-radius: 4px;
box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($white, 0.1);
border-radius: 2px;
box-shadow: 0 3px 6px 0 rgba(26, 24, 29, 0.16), 0 3px 6px 0 rgba(26, 24, 29, 0.24);
}
// shared dropdown-item styles
.dropdown-item {
// header items & not selectList-items
padding-left: 24px;
padding-top: 8px;
padding-bottom: 8px;
font-size: 14px;
line-height: 1.71;
color: $gray-50;
@@ -42,8 +53,8 @@
}
&:active, &:hover, &.active {
background-color: rgba(#d5c8ff, 0.32);
&:active, &:hover, &:focus, &.active {
background-color: rgba($purple-600, 0.32);
color: $purple-200;
}
@@ -86,16 +97,28 @@
.dropdown-toggle {
width: 100% !important;
height: 32px;
text-align: left;
}
.dropdown-toggle::after {
position: absolute;
right: 16px;
top: 17px;
right: 12px;
top: 14px;
}
.dropdown-menu.show {
width: 100% !important;
}
}
// selectList.vue items sizing
.selectListItem .dropdown-item {
padding: 0.25rem 0.75rem;
height: 32px;
&:active, &:hover, &:focus, &.active {
background-color: rgba($purple-600, 0.25);
color: $purple-300;
}
}

View File

@@ -16,10 +16,10 @@ label small {
}
}
// Inputs and texteares
// Inputs and textareas
input, textarea, input.form-control, textarea.form-control {
padding: 10px 16px;
padding: 10px 12px;
border-radius: 2px;
font-size: 14px;
line-height: 1.43;
@@ -31,14 +31,14 @@ input, textarea, input.form-control, textarea.form-control {
}
&:active:not(:disabled), &:focus:not(:disabled) {
border-color: $purple-500;
border-color: $purple-400;
outline: 0;
box-shadow: none;
}
&:disabled {
opacity: 0.64;
background: $gray-500;
background: $gray-700;
}
&.input-search {
@@ -68,11 +68,48 @@ input, textarea, input.form-control, textarea.form-control {
}
}
.input-group {
.input-group-prepend , .input-group-append {
.input-group-outer {
display: flex;
flex-direction: row;
.input-group {
flex: 1;
}
}
/** Colored Input-Groups, ignoring checklist */
.input-group:not(.checklist-group) {
border-radius: 2px;
border: solid 1px $gray-400;
&:hover {
border-color: $gray-300;
}
&:focus, &:active, &:focus-within {
border: solid 1px $purple-400;
}
.input-group-prepend , .input-group-append {
background: $gray-600;
color: $gray-300;
border-radius: 2px;
}
}
/** Generic Input Group Styles */
.input-group {
height: 2rem;
.input-group-prepend , .input-group-append {
color: $gray-200;
border: 0;
height: 30px;
width: 2rem;
margin: 0;
&.grow {
width: initial;
min-width: 2rem;
}
&.input-group-text {
font-size: 14px;
@@ -83,28 +120,30 @@ input, textarea, input.form-control, textarea.form-control {
}
&.input-group-icon {
border: solid 1px $gray-400;
border-right: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
display: flex;
align-self: center;
align-items: center;
justify-items: center;
justify-content: center;
}
.svg-icon {
margin: 0 !important;
}
&.streak-addon .svg-icon {
width: 11.6px;
height: 7.1px;
margin: 15px 13.4px 15.9px 13px;
}
&.positive-addon .svg-icon {
width: 10px;
height: 10px;
margin: 14px 14px;
}
&.negative-addon .svg-icon {
width: 10px;
height: 2px;
margin: 18px 14px;
}
}
@@ -115,6 +154,19 @@ input, textarea, input.form-control, textarea.form-control {
input:first-child {
border-right: none !important;
}
input {
height: 30px;
border: 0;
background: $white !important;
}
}
.input-group-spaced {
margin-left: 12px;
height: 2rem;
border-radius: 2px;
background-color: $gray-600;
}
.form-check {
@@ -200,9 +252,13 @@ $bg-disabled-control: #34303a;
align-items: center;
}
.destroy-icon {
width: 14px;
height: 16px;
.destroy-icon.svg-icon {
margin-top: 1px !important;
svg {
width: 14px;
height: 16px;
}
}
}

View File

@@ -1,14 +1,14 @@
.svg-icon {
display: block;
transition: none !important;
fill: currentColor;
svg {
display: block;
}
transition: none;
* {
transition: none !important;
path {
transition: none;
}
}
&.color {
@@ -64,4 +64,4 @@
&:hover svg path {
stroke: $gray-100;
}
}
}

View File

@@ -37,3 +37,4 @@
@import './tiers';
@import './payments';
@import './datepicker.scss';
@import './spacing';

View File

@@ -14,4 +14,16 @@
border-right: 4px solid transparent;
border-left: 4px solid transparent;
border-bottom: 0;
}
}
* {
transition: none;
}
.transition {
transition-duration: 0.15s;
transition-property: border-color, color;
transition-property: border-color, box-shadow, color;
transition-property: border-color, box-shadow, color;
transition-timing-function: ease-in;
}

View File

@@ -13,6 +13,7 @@
.modal {
z-index: 1350;
padding-left: 0px !important;
}
.modal-dialog {

View File

@@ -0,0 +1,59 @@
.m-75 {
margin: 0.75rem; // 12px
}
.mx-75 {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.my-75 {
margin-bottom: 0.75rem;
margin-top: 0.75rem;
}
.mb-75 {
margin-bottom: 0.75rem;
}
.ml-75 {
margin-left: 0.75rem;
}
.mr-75 {
margin-right: 0.75rem;
}
.mt-75 {
margin-top: 0.75rem;
}
.p-75 {
padding: 0.75rem;
}
.px-75 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.py-75 {
padding-bottom: 0.75rem;
padding-top: 0.75rem;
}
.pb-75 {
padding-bottom: 0.75rem;
}
.pl-75 {
padding-left: 0.75rem;
}
.pr-75 {
padding-right: 0.75rem;
}
.pt-75 {
padding-top: 0.75rem;
}

View File

@@ -3,7 +3,8 @@
.habit-option-button {
border: 2px solid $disabled-color;
}
&:hover {
// TODO refactor to use more css-vars and less duplicate generated css code
&:hover, &:focus, &:active {
.habit-option-button {
border: 2px solid $active-color;
}
@@ -28,7 +29,7 @@
&-control {
&-bg {
background: $maroon-100 !important;
.habit-control:hover { background: rgba($black, 0.5) !important; }
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $maroon-100 !important; }
@@ -40,6 +41,7 @@
&-modal {
&-bg { background: $maroon-100 !important; }
&-headings { color: $white; }
&-icon { color: $maroon-100 !important; }
&-text { color: $red-1 !important; }
&-content {
@@ -58,7 +60,7 @@
&-control {
&-bg {
background: $red-100 !important;
.habit-control:hover { background: rgba($black, 0.5) !important; }
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $red-100 !important; }
@@ -70,8 +72,8 @@
&-modal {
&-bg { background: $red-100 !important; }
&-headings, &-text { color: $red-1 !important; }
&-icon { color: $red-100 !important; }
&-text { color: $red-1 !important; }
&-content {
--svg-color: #{$red-100};
}
@@ -89,7 +91,7 @@
&-control {
&-bg {
background: $orange-100 !important;
.habit-control:hover { background: rgba($orange-1, 0.5) !important; }
.habit-control:not(.task-not-scoreable):hover { background: rgba($orange-1, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $orange-100 !important; }
@@ -101,8 +103,8 @@
&-modal {
&-bg { background: $orange-100 !important; }
&-headings, &-text { color: $orange-1 !important; }
&-icon { color: $orange-100 !important; }
&-text { color: $orange-1 !important; }
&-content {
--svg-color: #{$orange-100};
}
@@ -120,7 +122,7 @@
&-control {
&-bg {
background: $yellow-100 !important;
.habit-control:hover { background: rgba($yellow-1, 0.5) !important; }
.habit-control:not(.task-not-scoreable):hover { background: rgba($yellow-1, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $yellow-100 !important; }
@@ -132,8 +134,8 @@
&-modal {
&-bg { background: $yellow-100 !important; }
&-headings, &-text { color: $yellow-1 !important; }
&-icon { color: $yellow-100 !important; }
&-text { color: $yellow-1 !important; }
@include modal-text-input($yellow-1);
&-option-disabled:hover {
.svg-icon { color: $yellow-100 !important; }
@@ -151,7 +153,7 @@
&-control {
&-bg {
background: $green-100 !important;
.habit-control:hover { background: rgba($black, 0.5) !important; }
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $green-100 !important; }
@@ -163,8 +165,8 @@
&-modal {
&-bg { background: $green-100 !important; }
&-headings, &-text { color: $green-1 !important; }
&-icon { color: $green-10 !important; }
&-text { color: $green-1 !important; }
&-content {
--svg-color: #{$green-100};
}
@@ -183,7 +185,7 @@
&-control {
&-bg {
background: $teal-100 !important;
.habit-control:hover { background: rgba($black, 0.5) !important; }
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $teal-100 !important; }
@@ -195,8 +197,8 @@
&-modal {
&-bg { background: $teal-100 !important; }
&-headings, &-text { color: $teal-1 !important; }
&-icon { color: $teal-100 !important; }
&-text { color: $teal-1 !important; }
&-content {
--svg-color: #{$teal-100};
}
@@ -214,7 +216,7 @@
&-control {
&-bg {
background: $blue-100 !important;
.habit-control:hover { background: rgba($black, 0.5) !important; }
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $blue-100 !important; }
@@ -226,8 +228,8 @@
&-modal {
&-bg { background: $blue-100 !important; }
&-headings, &-text { color: $blue-1 !important; }
&-icon { color: $blue-100 !important; }
&-text { color: $blue-1 !important; }
&-content {
--svg-color: #{$blue-100};
}
@@ -246,7 +248,7 @@
&-control {
&-bg {
background: $purple-task !important;
.habit-control:hover { background: rgba($black, 0.5) !important; }
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
}
&-inner-habit { background: rgba($black, 0.25) !important; }
@@ -256,6 +258,7 @@
&-modal {
&-bg { background: $purple-300 !important; }
&-headings { color: $white; }
&-icon { color: $purple-300 !important; }
&-text { color: $black !important; }
&-content {

View File

@@ -53,7 +53,7 @@ h1 {
h2 {
font-size: 20px;
line-height: 1.2;
line-height: 1.4;
margin-bottom: 16px;
}
@@ -73,3 +73,7 @@ h4 {
font-family: 'Roboto Condensed', sans-serif;
font-weight: bold;
}
.opacity-75 {
opacity: 0.75;
}

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#A5A1AC" fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM7 7h2v5H7V7zm0-3h2v2H7V4z"/>
<path fill="#878190" fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM7 7h2v5H7V7zm0-3h2v2H7V4z"/>
</svg>

Before

Width:  |  Height:  |  Size: 238 B

After

Width:  |  Height:  |  Size: 238 B

View File

@@ -13,20 +13,20 @@
v-html="html"
></div>
</div>
<div class="modal-footer">
<div class="modal-footer d-flex align-items-center pb-0">
<a
class="btn btn-info"
href="http://habitica.fandom.com/wiki/Whats_New"
target="_blank"
class="mr-auto"
>{{ this.$t('newsArchive') }}</a>
<button
class="btn btn-secondary"
class="btn btn-secondary ml-auto"
@click="tellMeLater()"
>
{{ this.$t('tellMeLater') }}
</button>
<button
class="btn btn-warning"
class="btn btn-primary"
@click="dismissAlert();"
>
{{ this.$t('dismissAlert') }}

View File

@@ -179,7 +179,7 @@
<div class="col-12 col-md-4">
<button
v-if="user"
class="btn btn-contribute btn-flat"
class="btn btn-contribute btn-front btn-flat"
@click="donate()"
>
<div
@@ -192,7 +192,7 @@
</button>
<div
v-else
class="btn btn-contribute btn-flat"
class="btn btn-contribute btn-front btn-flat"
>
<a
href="http://habitica.fandom.com/wiki/Contributing_to_Habitica"
@@ -417,6 +417,7 @@
background: #c3c0c7;
box-shadow: none;
border-radius: 4px;
font-family: Roboto Condensed,sans-serif;
&:hover {
background: #a5a1ac;

View File

@@ -214,16 +214,13 @@ export default {
if (this.$route.query.showGroupOverview) {
this.$root.$emit('bv::show::modal', 'group-plan-overview');
}
this.$root.$on('habitica:team-sync', () => {
this.loadTasks();
});
},
methods: {
async load () {
this.tasksByType = {
habit: [],
daily: [],
todo: [],
reward: [],
};
this.group = await this.$store.dispatch('guilds:getGroup', {
groupId: this.searchId,
});
@@ -231,6 +228,16 @@ export default {
const members = await this.$store.dispatch('members:getGroupMembers', { groupId: this.searchId });
this.group.members = members;
this.loadTasks();
},
async loadTasks () {
this.tasksByType = {
habit: [],
daily: [],
todo: [],
reward: [],
};
const tasks = await this.$store.dispatch('tasks:getGroupTasks', {
groupId: this.searchId,
});

View File

@@ -11,7 +11,7 @@
<div class="col-md-auto col-md-12 col-xl-4">
<button
v-once
class="btn btn-info btn-follow-guidelines"
class="btn btn-secondary btn-follow-guidelines"
@click="acceptCommunityGuidelines()"
>
{{ $t('acceptCommunityGuidelines') }}

View File

@@ -1,6 +1,6 @@
<template>
<base-notification
:can-remove="canRemove"
:can-remove="false"
:has-icon="false"
:notification="notification"
@click="action"
@@ -9,13 +9,13 @@
<div v-html="notification.data.message"></div>
<div class="notifications-buttons">
<div
class="btn btn-small btn-success"
class="btn btn-small btn-success mr-2"
@click.stop="approve()"
>
{{ $t('approve') }}
</div>
<div
class="btn btn-small btn-warning"
class="btn btn-small btn-secondary"
@click.stop="needsWork()"
>
{{ $t('needsWork') }}
@@ -45,8 +45,7 @@ export default {
},
methods: {
action () {
const groupId = this.notification.data.group.id;
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId } });
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId: this.notification.data.groupId } });
},
async approve () {
// Redirect users to the group tasks page if the notification doesn't have data

View File

@@ -1,31 +1,44 @@
<template>
<base-notification
:can-remove="canRemove"
:can-remove="false"
:has-icon="false"
:notification="notification"
:read-after-click="true"
@click="action"
>
<div
slot="content"
class="notification-green"
v-html="notification.data.message"
></div>
<div slot="content">
<div
class="notification-green"
v-html="notification.data.message"
></div>
<div class="notifications-buttons">
<div
class="btn btn-small btn-primary"
@click.stop="action()"
>
{{ $t('claimRewards') }}
</div>
</div>
</div>
</base-notification>
</template>
<script>
import BaseNotification from './base';
import scoreTask from '@/mixins/scoreTask';
import sync from '@/mixins/sync';
export default {
components: {
BaseNotification,
},
props: ['notification', 'canRemove'],
mixins: [scoreTask, sync],
methods: {
action () {
const { groupId } = this.notification.data;
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId } });
async action () {
const { task, direction } = this.notification.data;
await this.taskScore(task, direction);
this.sync();
},
},
};

View File

@@ -191,7 +191,7 @@ export default {
openStatus: undefined,
actionableNotifications: [
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION', 'GROUP_TASK_NEEDS_WORK',
'QUEST_INVITATION', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
],
// A list of notifications handled by this component,
// listed in the order they should appear in the notifications panel.

View File

@@ -24,14 +24,14 @@
class="user-dropdown"
>
<a
class="dropdown-item edit-avatar dropdown-separated"
class="topbar-dropdown-item dropdown-item edit-avatar dropdown-separated"
@click="showAvatar('body', 'size')"
>
<h3>{{ user.profile.name }}</h3>
<span class="small-text">{{ $t('editAvatar') }}</span>
</a>
<a
class="nav-link dropdown-item
class="topbar-dropdown-item nav-link dropdown-item
dropdown-separated d-flex justify-content-between align-items-center"
@click.prevent="showPrivateMessages()"
>
@@ -42,42 +42,42 @@
/>
</a>
<a
class="dropdown-item"
class="topbar-dropdown-item dropdown-item"
@click="showAvatar('backgrounds', '2020')"
>{{ $t('backgrounds') }}</a>
<a
class="dropdown-item"
class="topbar-dropdown-item dropdown-item"
@click="showProfile('stats')"
>{{ $t('stats') }}</a>
<a
class="dropdown-item"
class="topbar-dropdown-item dropdown-item"
@click="showProfile('achievements')"
>{{ $t('achievements') }}</a>
<a
class="dropdown-item dropdown-separated"
class="topbar-dropdown-item dropdown-item dropdown-separated"
@click="showProfile('profile')"
>{{ $t('profile') }}</a>
<router-link
class="dropdown-item"
class="topbar-dropdown-item dropdown-item"
:to="{name: 'site'}"
>
{{ $t('settings') }}
</router-link>
<router-link
class="dropdown-item dropdown-separated"
class="topbar-dropdown-item dropdown-item dropdown-separated"
:to="{name: 'subscription'}"
>
{{ $t('subscription') }}
</router-link>
<a
class="nav-link dropdown-item dropdown-separated"
class="topbar-dropdown-item nav-link dropdown-item dropdown-separated"
@click.prevent="logout()"
>{{ $t('logout') }}</a>
<li
v-if="!user.purchased.plan.customerId"
@click="showBuyGemsModal()"
>
<div class="dropdown-item text-center">
<div class="topbar-dropdown-item dropdown-item text-center">
<h3 class="purple">
{{ $t('needMoreGems') }}
</h3>

View File

@@ -1,7 +1,7 @@
<template>
<div v-if="emptyItem">
<div class="item-wrapper">
<div class="item item-empty">
<div class="item transition item-empty">
<div class="item-content"></div>
</div><span
v-if="label"
@@ -15,7 +15,7 @@
@click="click"
>
<div
class="item"
class="item transition"
:class="{'item-active': active, 'highlight-border':highlightBorder }"
>
<slot

View File

@@ -6,7 +6,7 @@
@click="click($event)"
>
<div
class="item"
class="item transition"
:class="{'item-active': active }"
>
<countBadge

View File

@@ -6,7 +6,7 @@
@click="click()"
>
<div
class="item pet-slot"
class="item pet-slot transition"
:class="{'item-empty': !isOwned()}"
>
<slot

View File

@@ -6,7 +6,7 @@
@click="click()"
>
<div
class="item pet-slot"
class="item pet-slot transition"
:class="{'item-empty': !isOwned(), 'highlight': highlightBorder}"
>
<slot

View File

@@ -380,7 +380,7 @@ export default {
'GUILD_PROMPT', 'REBIRTH_ENABLED', 'WON_CHALLENGE', 'STREAK_ACHIEVEMENT',
'ULTIMATE_GEAR_ACHIEVEMENT', 'REBIRTH_ACHIEVEMENT', 'GUILD_JOINED_ACHIEVEMENT',
'CHALLENGE_JOINED_ACHIEVEMENT', 'INVITED_FRIEND_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL',
'CRON', 'SCORED_TASK', 'LOGIN_INCENTIVE', 'ACHIEVEMENT_ALL_YOUR_BASE', 'ACHIEVEMENT_BACK_TO_BASICS',
'CRON', 'LOGIN_INCENTIVE', 'ACHIEVEMENT_ALL_YOUR_BASE', 'ACHIEVEMENT_BACK_TO_BASICS',
'GENERIC_ACHIEVEMENT', 'ACHIEVEMENT_PARTY_UP', 'ACHIEVEMENT_PARTY_ON', 'ACHIEVEMENT_BEAST_MASTER',
'ACHIEVEMENT_MOUNT_MASTER', 'ACHIEVEMENT_TRIAD_BINGO', 'ACHIEVEMENT_DUST_DEVIL', 'ACHIEVEMENT_ARID_AUTHORITY',
'ACHIEVEMENT_MONSTER_MAGUS', 'ACHIEVEMENT_UNDEAD_UNDERTAKER', 'ACHIEVEMENT_PRIMED_FOR_PAINTING',
@@ -729,7 +729,6 @@ export default {
if (!after || after.length === 0 || !Array.isArray(after)) return;
const notificationsToRead = [];
const scoreTaskNotification = [];
after.forEach(notification => {
// This notification type isn't implemented here
@@ -820,23 +819,6 @@ export default {
// Not needed because it's shown already by the userHp and userMp watchers
// Keeping an empty block so that it gets read
break;
case 'SCORED_TASK':
// Search if it is a read notification
for (let i = 0; i < this.alreadyReadNotification.length; i += 1) {
if (this.alreadyReadNotification[i] === notification.id) {
markAsRead = false; // Do not let it be read again
break;
}
}
// Only process the notification if it is an unread notification
if (markAsRead) {
scoreTaskNotification.push(notification);
// Add to array of read notifications
this.alreadyReadNotification.push(notification.id);
}
break;
case 'LOGIN_INCENTIVE':
if (this.user.flags.tour.intro === this.TOUR_END && this.user.flags.welcomed) {
this.notificationData = notification.data;
@@ -862,43 +844,12 @@ export default {
if (markAsRead) notificationsToRead.push(notification.id);
});
const userReadNotifsPromise = false;
if (notificationsToRead.length > 0) {
await axios.post('/api/v4/notifications/read', {
notificationIds: notificationsToRead,
});
}
// @TODO this code is never run because userReadNotifsPromise is never true
if (userReadNotifsPromise) {
userReadNotifsPromise.then(() => {
// Only run this code for scoring approved tasks
if (scoreTaskNotification.length > 0) {
const approvedTasks = [];
for (let i = 0; i < scoreTaskNotification.length; i += 1) {
// Array with all approved tasks
const scoreData = scoreTaskNotification[i].data;
let direction = 'up';
if (scoreData.direction) direction = scoreData.direction;
approvedTasks.push({
params: {
task: scoreData.scoreTask,
direction,
},
});
// Show notification of task approved
this.markdown(scoreTaskNotification[i].data.message);
}
// Score approved tasks
// TODO: User.bulkScore(approvedTasks);
}
});
}
this.debounceCheckUserAchievements();
},
},

View File

@@ -24,36 +24,38 @@
{{ $t('gemsPopoverTitle') }}
</h3>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<input
v-model="gift.gems.amount"
class="form-control"
type="number"
placeholder="Number of Gems"
min="0"
:max="gift.gems.fromBalance ? userLoggedIn.balance * 4 : 9999"
>
</div>
<div class="d-flex mb-3">
<div class="form-group mb-0">
<input
v-model="gift.gems.amount"
class="form-control"
type="number"
placeholder="Number of Gems"
min="0"
:max="gift.gems.fromBalance ? userLoggedIn.balance * 4 : 9999"
>
</div>
<div class="col-md-6">
<div class="btn-group">
<button
class="btn btn-secondary"
:class="{active: gift.gems.fromBalance}"
@click="gift.gems.fromBalance = true"
>
{{ $t('sendGiftFromBalance') }}
</button>
<button
class="btn btn-secondary"
:class="{active: !gift.gems.fromBalance}"
@click="gift.gems.fromBalance = false"
>
{{ $t('sendGiftPurchase') }}
</button>
</div>
<div class="btn-group ml-auto">
<button
class="btn"
:class="{
'btn-primary': gift.gems.fromBalance,
'btn-secondary': !gift.gems.fromBalance,
}"
@click="gift.gems.fromBalance = true"
>
{{ $t('sendGiftFromBalance') }}
</button>
<button
class="btn"
:class="{
'btn-primary': !gift.gems.fromBalance,
'btn-secondary': gift.gems.fromBalance,
}"
@click="gift.gems.fromBalance = false"
>
{{ $t('sendGiftPurchase') }}
</button>
</div>
</div>
<div class="row">
@@ -77,7 +79,7 @@
<div class="panel-body">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<div class="form-group mb-0">
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
<div
v-for="block in subscriptionBlocks"

View File

@@ -79,19 +79,19 @@
>
</td>
<td>
<a
class="btn btn-warning checklist-icons"
<div
class="btn btn-danger checklist-icons mr-2"
@click="deleteWebhook(webhook, index)"
>
<span
class="glyphicon glyphicon-trash"
:tooltip="$t('delete')"
>Delete</span>
</a>
<a
class="btn btn-success checklist-icons"
> {{ $t('delete') }} </span>
</div>
<div
class="btn btn-primary checklist-icons"
@click="saveWebhook(webhook, index)"
>Update</a>
> {{ $t('subUpdateTitle') }} </div>
</td>
</tr>
<tr>

View File

@@ -2,7 +2,7 @@
<transition name="fade">
<div
v-if="show"
class="notification callout animated"
class="notification callout animated pt-0"
:class="classes"
@click="handleOnClick()"
>

View File

@@ -128,7 +128,7 @@
</div>
<router-link
v-if="$route.name === 'home'"
class="btn btn-primary login-button pull-right"
class="btn btn-primary btn-front login-button pull-right"
to="/login"
>
{{ $t('login') }}

View File

@@ -304,7 +304,7 @@
<div class="row">
<div class="col-12 text-center">
<button
class="btn btn-primary join-button"
class="btn btn-primary btn-front join-button"
@click="playButtonClick()"
>
{{ $t('joinToday') }}
@@ -680,10 +680,13 @@
padding-bottom: 5em;
}
.join-button:hover {
.join-button {
cursor: pointer;
background-color: #b288ff;
box-shadow: 0 4px 4px 0 rgba(26, 24, 29, 0.16), 0 1px 8px 0 rgba(26, 24, 29, 0.12);
&:hover {
background-color: #5d3b9c;
box-shadow: 0 4px 4px 0 rgba(26, 24, 29, 0.16), 0 1px 8px 0 rgba(26, 24, 29, 0.12);
}
}
.featured .row {

View File

@@ -67,7 +67,7 @@
}
.login-button:hover {
background-color: #b288ff;
background-color: #5d3b9c;
box-shadow: 0 4px 4px 0 rgba(26, 24, 29, 0.16), 0 1px 8px 0 rgba(26, 24, 29, 0.12) !important;
}
}

View File

@@ -2,7 +2,6 @@
<div>
<approval-modal :task="task" />
<div
v-if="!approvalRequested && !multipleApprovalsRequested"
class="claim-bottom-message d-flex align-items-center"
>
<div
@@ -10,7 +9,7 @@
v-html="message"
></div>
<div
v-if="!userIsAssigned"
v-if="!userIsAssigned && !task.completed"
class="ml-auto mr-2"
>
<a
@@ -19,7 +18,7 @@
>{{ $t('claim') }}</a>
</div>
<div
v-if="userIsAssigned"
v-if="userIsAssigned && !approvalRequested && !task.completed"
class="ml-auto mr-2"
>
<a
@@ -44,6 +43,16 @@
>
<a @click="showRequests()">{{ $t('viewRequests') }}</a>
</div>
<div
v-if="userIsAssigned && task.group.approval.approved
&& !task.completed && task.type !== 'habit'"
class="claim-bottom-message d-flex align-items-center justify-content-around"
>
<a
class="approve-color"
@click="$emit('claimRewards')"
>{{ $t('claimRewards') }}</a>
</div>
</div>
</template>
@@ -176,6 +185,8 @@ export default {
});
this.task.group.assignedUsers.splice(0, 1);
this.task.approvals.splice(0, 1);
this.sync();
},
needsWork () {
if (!window.confirm(this.$t('confirmNeedsWork'))) return;
@@ -185,6 +196,8 @@ export default {
userId: userIdNeedsMoreWork,
});
this.task.approvals.splice(0, 1);
this.sync();
},
showRequests () {
this.$root.$emit('bv::show::modal', 'approval-modal');

View File

@@ -2,7 +2,8 @@
<div
v-if="message"
class="claim-top-message d-flex align-content-center"
:class="{'approval-action': userIsAdmin, 'approval-pending': !userIsAdmin}"
:class="{ 'approval-action': userIsAdmin || task.group.approval.approved,
'approval-pending': !userIsAdmin && !task.group.approval.approved }"
>
<div
class="m-auto"
@@ -47,14 +48,19 @@ export default {
if (approvalsLength === 1 && !userIsRequesting) {
return this.$t('userRequestsApproval', { userName: approvals[0].userId.profile.name });
} if (approvalsLength > 1 && !userIsRequesting) {
}
if (approvalsLength > 1 && !userIsRequesting) {
return this.$t('userCountRequestsApproval', { userCount: approvalsLength });
} if (
(approvalsLength === 1 && userIsRequesting)
}
if ((approvalsLength === 1 && userIsRequesting)
|| (this.task.group.approval
&& this.task.group.approval.requested && !this.task.group.approval.approved)) {
&& this.task.group.approval.requested
&& !this.task.group.approval.approved)) {
return this.$t('youAreRequestingApproval');
}
if (this.task.group.approval.approved && !this.task.completed) {
return this.$t('thisTaskApproved');
}
return null;
},
userIsAdmin () {

View File

@@ -201,6 +201,7 @@
border-color: transparent;
transition: background 0.15s ease-in;
resize: none;
overflow: hidden;
&:hover {
background-color: rgba($black, 0.1);

View File

@@ -1,10 +1,9 @@
<template>
<div class="checklist-component">
<label
v-once
class="mb-1"
>{{ $t('checklist') }}</label>
<br>
<lockable-label
:locked="disabled || disableItems"
:text="$t('checklist')"
/>
<draggable
v-model="checklist"
:options="{
@@ -20,7 +19,7 @@
class="inline-edit-input-group checklist-group input-group"
>
<span
v-if="!disabled"
v-if="!disabled && !disableDrag"
class="grippy"
v-html="icons.grip"
>
@@ -29,19 +28,19 @@
<checkbox
:id="`checklist-${item.id}`"
:checked.sync="item.completed"
:disabled="disabled"
:disabled="disabled || disableItems"
class="input-group-prepend"
:class="{'cursor-auto': disabled}"
:class="{'cursor-auto': disabled || disableItems}"
/>
<input
v-model="item.text"
class="inline-edit-input checklist-item form-control"
type="text"
:disabled="disabled"
:disabled="disabled || disableItems"
>
<span
v-if="!disabled"
v-if="!disabled && !disableItems"
class="input-group-append"
@click="removeChecklistItem($index)"
>
@@ -55,8 +54,9 @@
</div>
</draggable>
<div
v-if="!disabled"
v-if="!disabled && !disableItems"
class="inline-edit-input-group checklist-group input-group new-checklist"
:class="{'top-border': items.length === 0}"
>
<span
v-once
@@ -88,17 +88,25 @@ import deleteIcon from '@/assets/svg/delete.svg';
import chevronIcon from '@/assets/svg/chevron.svg';
import gripIcon from '@/assets/svg/grip.svg';
import checkbox from '@/components/ui/checkbox';
import lockableLabel from './lockableLabel';
export default {
name: 'Checklist',
components: {
draggable,
checkbox,
draggable,
lockableLabel,
},
props: {
disabled: {
type: Boolean,
},
disableDrag: {
type: Boolean,
},
disableItems: {
type: Boolean,
},
items: {
type: Array,
},
@@ -158,12 +166,20 @@ export default {
.checklist-component {
.top-border {
border-top: 1px solid $gray-500;
}
.lock-icon {
color: $gray-200;
}
.checklist-group {
height: 2rem;
border-top: 1px solid $gray-500;
border-bottom: 1px solid $gray-500;
&.new-checklist {
border-bottom: 1px solid $gray-500;
&:first-of-type {
border-top: 1px solid $gray-500;
}
.inline-edit-input {
@@ -172,7 +188,7 @@ export default {
.input-group-prepend {
margin-left: 0.375rem;
margin-top: 0.475rem;
margin-top: 0.375rem;
margin-right: 0;
padding: 0;
&:not(.new-icon) {
@@ -183,6 +199,8 @@ export default {
margin-left: 0.688rem;
margin-top: 0.625rem;
margin-bottom: 0.625rem;
height: 10px;
width: 13px;
}
}
@@ -267,14 +285,5 @@ export default {
}
}
}
label {
height: 1.5rem;
font-size: 14px;
font-weight: bold;
line-height: 1.71;
letter-spacing: normal;
color: $gray-50;
}
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div
class="d-flex"
:class="[{ 'opacity-75': locked }, classOverride]"
>
<span
v-once
class="svg-icon lock-icon icon-10 mr-1"
:class="classOverride ? classOverride : 'gray-200'"
v-html="icons.lock"
v-if="locked"
>
</span>
<label
v-once
class="mb-1"
v-html="text"
></label>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
label {
height: 1.5rem;
font-size: 14px;
font-weight: bold;
line-height: 1.71;
letter-spacing: normal;
}
.gray-200 {
color: $gray-200;
}
.lock-icon {
margin-top: 5px;
}
</style>
<script>
import lockIcon from '@/assets/svg/lock.svg';
export default {
props: {
classOverride: {
type: String,
},
locked: {
type: Boolean,
},
text: {
type: String,
},
},
data () {
return {
icons: Object.freeze({
lock: lockIcon,
}),
};
},
};
</script>

View File

@@ -0,0 +1,187 @@
<template>
<div
class="multi-list"
:class="{ 'break': maxItems === 0 }"
>
<template v-if="items.length === 0">
<div class="items-none">{{ emptyMessage }}</div>
</template>
<template v-else>
<div
:key="item.id"
:title="item.name"
class="multi-item mr-1 d-inline-flex align-items-center"
:class="{'margin-adjust': maxItems !== 0, 'pill-invert': pillInvert}"
v-for="item in truncatedSelectedItems"
@click.stop="removeItem($event, item)"
>
<div
class="multi-label my-auto ml-75 mr-2"
v-markdown="item.name"
></div>
<div
class="remove ml-auto mr-75"
v-html="icons.remove"
></div>
</div>
<div
class="items-more ml-75"
v-if="remainingSelectedItems.length > 0"
>
+{{remainingSelectedItems.length}}
</div>
</template>
</div>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
.multi-list {
p {
display: inline;
margin: 0 !important;
padding: 0 !important;
}
.multi-item {
&:hover {
.remove svg path {
stroke: $maroon-50;
}
}
.remove {
svg {
width: 0.5rem;
height: 0.5rem;
path {
stroke: $gray-200;
}
}
}
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.margin-adjust {
margin-top: -1px;
}
.multi-list {
width: 100%;
&.break {
display: flex;
flex-wrap: wrap;
.multi-item {
margin-bottom: 0.375rem;
}
}
}
.multi-item {
display: inline-block;
height: 1.5rem;
border-radius: 100px;
background-color: $gray-600;
cursor: pointer;
&.pill-invert {
background-color: $white;
border: solid 1px $gray-400;
}
.multi-label {
height: 1rem;
font-size: 12px;
line-height: 16px;
letter-spacing: normal;
color: $gray-100;
}
.remove {
display: inline-block;
object-fit: contain;
margin-top: -0.125rem;
}
}
.items-more {
color: $gray-100;
font-size: 12px;
display: inline-block;
height: 1rem;
font-weight: bold;
line-height: 1.33;
letter-spacing: normal;
}
</style>
<script>
import removeIcon from '@/assets/svg/remove.svg';
import markdownDirective from '@/directives/markdown';
export default {
directives: {
markdown: markdownDirective,
},
components: {},
data () {
return {
icons: Object.freeze({
remove: removeIcon,
}),
};
},
computed: {
truncatedSelectedItems () {
if (this.maxItems <= 0) {
return this.items;
}
return this.items.slice(0, this.maxItems);
},
remainingSelectedItems () {
if (this.maxItems <= 0) {
return [];
}
return this.items.slice(this.maxItems);
},
},
props: {
addNew: {
type: Boolean,
default: false,
},
emptyMessage: {
type: String,
},
maxItems: {
type: Number,
default: 3,
},
pillInvert: {
type: Boolean,
default: false,
},
items: {
type: Array,
},
},
methods: {
removeItem ($event, item) {
this.$emit('remove-item', item.id);
},
},
};
</script>

View File

@@ -0,0 +1,182 @@
<template>
<div>
<select-list :items="items"
:key-prop="'icon'"
class="difficulty-select"
:class="{disabled: disabled}"
:disabled="disabled"
:value="selected"
@select="$emit('select', $event.value)">
<template v-slot:item="{ item, button }">
<div v-if="item" class="difficulty-item" :class="{ 'isButton': button }">
<span class="label">{{ item.label }}</span>
<div class="svg-icon" >
<span v-for="n in item.stars"
v-html="icons.difficultyTrivial"
:key="n">
</span>
</div>
</div>
</template>
</select-list>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.difficulty-item {
display: flex;
align-items: center;
div.svg-icon {
::v-deep svg {
fill: $gray-200;
}
}
&:hover, &.isButton {
div.svg-icon {
::v-deep svg {
fill: var(--svg-color);
}
}
}
}
.label {
flex: 1;
}
div.svg-icon {
flex-grow: 0;
width: 80px;
display: flex;
justify-content: flex-end;
margin-left: 0.375rem;
span {
display: inline;
}
::v-deep svg {
margin-left: 0.125rem;
width: 0.625rem;
height: 0.625rem;
object-fit: contain;
fill: $gray-200;
}
}
</style>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
.difficulty-select {
&.disabled {
.btn-secondary:disabled, .btn-secondary.disabled, .dropdown >
.btn-secondary.dropdown-toggle:not(.btn-success):disabled, .dropdown >
.btn-secondary.dropdown-toggle:not(.btn-success).disabled, .show >
.btn-secondary.dropdown-toggle:not(.btn-success):disabled, .show >
.btn-secondary.dropdown-toggle:not(.btn-success).disabled {
background: $gray-700;
}
}
// restyle the selected item
.dropdown-toggle {
&.disabled {
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
&::after {
color: $gray-300;
border-top-color: $gray-300;
}
.label {
color: $gray-200;
}
}
.label {
flex: 0;
}
div.svg-icon {
justify-content: flex-start;
}
}
// highlight the svg icons,
// when focusing with keyboard (it is outside of the template item
.dropdown-item:focus .difficulty-item {
div.svg-icon {
svg {
fill: var(--svg-color);
}
}
}
}
</style>
<script>
import difficultyNormalIcon from '@/assets/svg/difficulty-normal.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 selectList from '@/components/ui/selectList';
export default {
components: {
selectList,
},
data () {
const items = [
{
value: 0.1,
label: this.$t('trivial'),
stars: 1,
},
{
value: 1,
label: this.$t('easy'),
stars: 2,
},
{
value: 1.5,
label: this.$t('medium'),
stars: 3,
},
{
value: 2,
label: this.$t('hard'),
stars: 4,
},
];
return {
items,
icons: Object.freeze({
difficultyNormal: difficultyNormalIcon,
difficultyTrivial: difficultyTrivialIcon,
difficultyMedium: difficultyMediumIcon,
difficultyHard: difficultyHardIcon,
}),
selected: items.find(i => i.value === this.value),
};
},
props: {
disabled: {
type: Boolean,
},
value: {
type: Number,
},
},
};
</script>

View File

@@ -0,0 +1,133 @@
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from '@storybook/vue';
import { withKnobs, number } from '@storybook/addon-knobs';
import MultiList from './multiList';
import SelectMulti from './selectMulti';
import getStore from '@/store';
const stories = storiesOf('Multiple Select List', module);
stories.addDecorator(withKnobs);
// Needed for SelectTag
const store = getStore();
store.state.user.data = {
tags: [],
};
const exampleTagList = [
1, 2, 3,
];
const allTags = [
{
id: 1,
name: 'Small Tag',
},
{
id: 2,
name: 'This is a long tag',
},
{
id: 3,
name: 'This is a long tag',
},
{
id: 12,
name: 'This is a different tag',
},
{
id: 9001,
name: 'OVER 9000',
},
{
id: 4,
name: 'Four',
},
{
id: 5,
name: 'Five :tada:',
challenge: true,
},
{
id: 6,
name: 'Six',
},
{
id: 7,
name: 'Seven **Markdown**',
},
];
stories
.add('tag-list', () => ({
components: { MultiList },
template: `
<div style="position: absolute; margin: 20px">
<MultiList :max-items="maxTags" :items="tagList"></MultiList>
</div>
`,
props: {
tagList: {
default: allTags,
},
maxTags: {
default: number('Max-Tags', 3),
},
},
}))
.add('select-tag', () => ({
components: { SelectMulti },
template: `
<div style="position: absolute; margin: 20px">
<SelectMulti :selectedItems="tagList"
:add-new="true"
:all-items="allTags"
style="width: 400px"
@changed="tagList = $event"
@addNew="added = $event">
</SelectMulti>
<br/>
<br/>
Added event: {{ added }}
</div>
`,
store,
data () {
return {
tagList: exampleTagList,
added: '',
};
},
props: {
allTags: {
default: allTags,
},
},
}))
.add('longer select-tag', () => ({
components: { SelectMulti },
template: `
<div style="position: absolute; margin: 20px">
<SelectMulti :selectedItems="tagList"
:all-items="allTags"
style="width: 400px"
@changed="tagList = $event"></SelectMulti>
</div>
`,
store,
data () {
return {
tagList: [],
};
},
props: {
allTags: {
default: allTags,
},
},
}));

View File

@@ -0,0 +1,294 @@
multi<template>
<div>
<b-dropdown
class="inline-dropdown select-multi"
@show="wasOpened()"
@hide="hideCallback($event)"
@toggle="openOrClose($event)"
:toggle-class="isOpened ? 'active' : null"
ref="dropdown"
>
<b-dropdown-header>
<div class="mb-2">
<b-form-input type="text"
:placeholder="searchPlaceholder"
v-model="search"
@keyup.enter="handleSubmit"
/>
</div>
<multi-list v-if="selectedItems.length > 0"
:add-new="addNew"
:pill-invert="pillInvert"
:items="selectedItemsAsObjects"
@remove-item="removeItem($event)"
:max-items="0" />
</b-dropdown-header>
<template v-slot:button-content>
<multi-list :items="selectedItemsAsObjects"
:add-new="addNew"
:pill-invert="pillInvert"
:empty-message="emptyMessage"
@remove-item="removeItem($event)"/>
</template>
<div
v-if="addNew || availableToSelect.length > 0"
:class="{
'item-group': true,
'add-new': availableToSelect.length === 0 && search !== '',
'scroll': availableToSelect.length > 5
}"
>
<b-dropdown-item-button
v-for="item in availableToSelect"
:key="item.id"
@click.prevent.stop="selectItem(item)"
class="ignore-hide multi-item"
:class="{ 'none': item.id === 'none', selectListItem: true }"
>
<div class="label" v-markdown="item.name"></div>
<div class="challenge" v-if="item.challenge">{{$t('challenge')}}</div>
</b-dropdown-item-button>
<div v-if="addNew" class="hint">
{{$t('pressEnterToAddTag', { tagName: search })}}
</div>
</div>
</b-dropdown>
</div>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
$itemHeight: 2rem;
.select-multi {
.dropdown-toggle {
padding-left: 0.75rem;
}
.dropdown-header {
background-color: $gray-700;
padding-bottom: 0;
min-height: 3rem;
}
.dropdown-item, .dropdown-header {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.none {
cursor: default;
pointer-events: none;
}
.multi-item button {
height: $itemHeight;
display: flex;
.label {
height: 1.5rem;
font-size: 14px;
line-height: 1.71;
flex: 1;
}
.challenge {
height: 1rem;
font-size: 12px;
font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: 1.33;
letter-spacing: normal;
text-align: right;
color: $gray-100;
align-self: center;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
margin-right: 0.25rem;
}
&:hover {
.challenge {
color: $purple-300;
}
}
}
.item-group {
max-height: #{5*$itemHeight};
&.add-new {
height: 30px;
.hint {
display: block;
}
}
&.scroll {
overflow-y: scroll;
}
}
.hint {
display: none;
height: 2rem;
font-size: 12px;
font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: 1.33;
letter-spacing: normal;
color: $gray-100;
margin-left: 0.75rem;
margin-top: 0.5rem;
}
}
</style>
<script>
import Vue from 'vue';
import MultiList from '@/components/tasks/modal-controls/multiList';
import markdownDirective from '@/directives/markdown';
export default {
directives: {
markdown: markdownDirective,
},
components: {
MultiList,
},
data () {
return {
preventHide: true,
isOpened: false,
selected: this.selectedItems,
search: '',
};
},
created () {
document.addEventListener('keyup', this.handleEsc);
},
beforeDestroy () {
document.removeEventListener('keyup', this.handleEsc);
},
methods: {
closeSelectPopup () {
this.preventHide = false;
this.isOpened = false;
Vue.nextTick(() => {
this.$refs.dropdown.hide();
});
},
openOrClose ($event) {
if (this.isOpened) {
this.closeSelectPopup();
$event.preventDefault();
}
},
closeIfOpen () {
this.closeSelectPopup();
},
selectItem (item) {
this.selectedItems.push(item.id);
this.$emit('toggle', item.id);
},
removeItem ($event) {
const foundIndex = this.selectedItems.findIndex(t => t === $event);
this.selectedItems.splice(foundIndex, 1);
this.$emit('toggle', $event);
},
hideCallback ($event) {
if (this.preventHide) {
$event.preventDefault();
return;
}
this.isOpened = false;
},
wasOpened () {
this.isOpened = true;
this.preventHide = true;
},
handleEsc (e) {
if (e.keyCode === 27) {
this.closeSelectPopup();
}
},
handleSubmit () {
if (!this.addNew) return;
const { search } = this;
this.$emit('addNew', search);
this.search = '';
},
},
computed: {
selectedItemsIdList () {
return this.selectedItems
? this.selectedItems.map(t => t)
: [];
},
allItemsMap () {
const obj = {};
this.allItems.forEach(t => {
obj[t.id] = t;
});
return obj;
},
selectedItemsAsObjects () {
return this.selectedItems.map(t => this.allItemsMap[t]);
},
availableToSelect () {
const availableItems = this.allItems.filter(t => !this.selectedItemsIdList.includes(t.id));
const searchString = this.search.toLowerCase();
const filteredItems = availableItems.filter(i => i.name.toLowerCase().includes(searchString));
return filteredItems;
},
},
props: {
addNew: {
type: Boolean,
default: false,
},
allItems: {
type: Array,
},
emptyMessage: {
type: String,
},
pillInvert: {
type: Boolean,
default: false,
},
searchPlaceholder: {
type: String,
},
selectedItems: {
type: Array,
},
},
watch: {
selected () {
this.$emit('changed', this.selected);
},
},
mounted () {
this.$refs.dropdown.clickOutHandler = () => {
this.closeSelectPopup();
};
},
};
</script>

View File

@@ -0,0 +1,74 @@
<template>
<div>
<select-list :items="items"
:value="selected"
class="array-select"
:class="{disabled: disabled}"
:disabled="disabled"
@select="selectItem($event)">
<template v-slot:item="{ item }">
<span class="label">{{ $t(item) }}</span>
</template>
</select-list>
</div>
</template>
<style lang="scss" scoped>
</style>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
.array-select.disabled {
.btn-secondary:disabled, .btn-secondary.disabled, .dropdown >
.btn-secondary.dropdown-toggle:not(.btn-success):disabled, .dropdown >
.btn-secondary.dropdown-toggle:not(.btn-success).disabled, .show >
.btn-secondary.dropdown-toggle:not(.btn-success):disabled, .show >
.btn-secondary.dropdown-toggle:not(.btn-success).disabled {
background: $gray-700;
}
.dropdown-toggle::after {
color: $gray-300;
border-top-color: $gray-300;
}
.label {
color: $gray-200;
}
}
.array-select .disabled, .array-select .disabled:hover {
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
}
</style>
<script>
import selectList from '@/components/ui/selectList';
export default {
components: {
selectList,
},
data () {
return {
selected: this.items.find(i => i === this.value),
};
},
methods: {
selectItem (item) {
this.selected = item;
this.$emit('select', item);
},
},
props: {
items: {
type: Array,
},
disabled: {
type: Boolean,
},
value: [String, Number, Object],
},
};
</script>

View File

@@ -1,187 +0,0 @@
<template>
<div class="tags-popup">
<!-- 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"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<div class="tags-header">
<strong v-once>{{ $t(tagsType.key) }}</strong>
</div>
<div class="tags-list container">
<div class="row">
<div
v-for="(tag) in tagsType.tags"
:key="tag.id"
class="col-4"
>
<div class="custom-control custom-checkbox">
<input
:id="`tag-${tag.id}`"
v-model="selectedTags"
class="custom-control-input"
type="checkbox"
:value="tag.id"
>
<label
v-markdown="tag.name"
class="custom-control-label"
:title="tag.name"
:for="`tag-${tag.id}`"
></label>
</div>
</div>
</div>
</div>
</div>
<div class="tags-footer">
<span
class="clear-tags"
@click="clearTags()"
>{{ $t("clearTags") }}</span>
<span
class="close-tags"
@click="close()"
>{{ $t("close") }}</span>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.tags-popup {
padding-left: 24px;
padding-right: 24px;
width: 593px;
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);
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: 96px;
flex-shrink: 0;
a {
font-size: 12px;
line-height: 1.33;
color: $blue-10;
margin-top: 4px;
&:focus, &:hover, &:active {
text-decoration: underline;
}
}
}
.tags-list {
.custom-control-label {
color: $gray-50 !important;
font-weight: normal;
}
}
.tags-footer {
border-top: 1px solid $gray-600;
display: flex;
justify-content: center;
.close-tags {
color: $blue-10;
margin: 1.1em 0;
margin-left: 2em;
font-size: 14px;
&:hover {
text-decoration: underline;
cursor: pointer;
}
}
.clear-tags {
cursor: pointer;
margin: 1.1em 0;
color: $red-50;
font-size: 14px;
&:hover {
text-decoration: underline;
}
}
}
}
</style>
<script>
import markdownDirective from '@/directives/markdown';
export default {
directives: {
markdown: markdownDirective,
},
props: ['tags', 'value'],
data () {
return {
selectedTags: [],
};
},
computed: {
tagsByType () {
const tagsByType = {
challenges: {
key: 'challenges',
tags: [],
},
groups: {
key: 'groups',
tags: [],
},
user: {
key: 'tags',
tags: [],
},
};
this.$props.tags.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: {
selectedTags () {
this.$emit('input', this.selectedTags);
},
},
mounted () {
this.selectedTags = this.value;
},
methods: {
clearTags () {
this.selectedTags = [];
},
close () {
this.$emit('close');
},
},
};
</script>

View File

@@ -1,8 +1,12 @@
<template>
<div class="task-wrapper">
<div
class="task"
:class="[{'groupTask': task.group.id}, `type_${task.type}`]"
class="task transition"
:class="[{
'groupTask': task.group.id,
'task-not-editable': !teamManagerAccess},
`type_${task.type}`
]"
@click="castEnd($event, task)"
>
<approval-header
@@ -12,12 +16,13 @@
/>
<div
class="d-flex"
:class="{'task-not-scoreable': isUser !== true}"
:class="{'task-not-scoreable': isUser !== true || task.group.approval.requested
&& !(task.group.approval.approved && task.type === 'habit')}"
>
<!-- Habits left side control-->
<div
v-if="task.type === 'habit'"
class="left-control d-flex align-items-center justify-content-center"
class="left-control d-flex justify-content-center pt-3"
:class="[{
'control-bottom-box': task.group.id,
'control-top-box': approvalsClass
@@ -28,8 +33,11 @@
:class="[{
'habit-control-positive-enabled': task.up && isUser,
'habit-control-positive-disabled': !task.up && isUser,
'task-not-scoreable': isUser !== true
|| (task.group.approval.requested && !task.group.approval.approved),
}, controlClass.up.inner]"
@click="(isUser && task.up) ? score('up') : null"
@click="(isUser && task.up && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('up') : null"
>
<div
v-if="!isUser"
@@ -55,7 +63,8 @@
<div
class="task-control daily-todo-control"
:class="controlClass.inner"
@click="isUser ? score(task.completed ? 'down' : 'up') : null"
@click="isUser && !task.group.approval.requested
? score(task.completed ? 'down' : 'up' ) : null"
>
<div
v-if="!isUser"
@@ -66,7 +75,10 @@
<div
v-else
class="svg-icon check"
:class="{'display-check-icon': task.completed, [controlClass.checkbox]: true}"
:class="{
'display-check-icon': task.completed || task.group.approval.requested,
[controlClass.checkbox]: true,
}"
v-html="icons.check"
></div>
</div>
@@ -74,7 +86,7 @@
<!-- Task title, description and icons-->
<div
class="task-content"
:class="contentClass"
:class="[{'cursor-auto': !teamManagerAccess}, contentClass]"
>
<div
class="task-clickable-area"
@@ -85,7 +97,7 @@
<h3
v-markdown="task.text"
class="task-title"
:class="{ 'has-notes': task.notes }"
:class="{ 'has-notes': task.notes || (!isUser && task.group.managerNotes)}"
></h3>
<menu-dropdown
v-if="!isRunningYesterdailies && showOptions"
@@ -157,7 +169,7 @@
</menu-dropdown>
</div>
<div
v-markdown="task.notes"
v-markdown="displayNotes"
class="task-notes small-text"
:class="{'has-checklist': task.notes && hasChecklist}"
></div>
@@ -171,13 +183,13 @@
<div
v-b-tooltip.hover.right="$t(`${task.collapseChecklist
? 'expand': 'collapse'}Checklist`)"
class="collapse-checklist d-flex align-items-center expand-toggle"
class="collapse-checklist mb-2 d-flex align-items-center expand-toggle"
:class="{open: !task.collapseChecklist}"
@click="collapseChecklist(task)"
>
<div
class="svg-icon"
v-html="icons.checklist"
<div v-once
class="svg-icon"
v-html="icons.checklist"
></div>
<span>{{ checklistProgress }}</span>
</div>
@@ -302,7 +314,7 @@
<!-- Habits right side control-->
<div
v-if="task.type === 'habit'"
class="right-control d-flex align-items-center justify-content-center"
class="right-control d-flex justify-content-center pt-3"
:class="[{
'control-bottom-box': task.group.id,
'control-top-box': approvalsClass}, controlClass.down.bg]"
@@ -312,8 +324,11 @@
:class="[{
'habit-control-negative-enabled': task.down && isUser,
'habit-control-negative-disabled': !task.down && isUser,
'task-not-scoreable': isUser !== true
|| (task.group.approval.requested && !task.group.approval.approved),
}, controlClass.down.inner]"
@click="(isUser && task.down) ? score('down') : null"
@click="(isUser && task.down && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('down') : null"
>
<div
v-if="!isUser"
@@ -348,6 +363,7 @@
v-if="task.group.id"
:task="task"
:group="group"
@claimRewards="score('up')"
/>
</div>
</div>
@@ -368,7 +384,7 @@
}
.cursor-auto {
cursor: auto;
cursor: auto !important;
}
.task {
@@ -378,7 +394,7 @@
border-radius: 2px;
position: relative;
&:hover {
&:hover:not(.task-not-editable) {
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
z-index: 11;
}
@@ -393,8 +409,7 @@
}
.task.groupTask {
&:hover {
&:hover:not(.task-not-editable) {
border: $purple-400 solid 1px;
border-radius: 3px;
margin: -1px; // to counter the border width
@@ -551,7 +566,6 @@
line-height: 1.2;
text-align: center;
color: $gray-200;
margin-bottom: 9px;
&.open {
}
@@ -795,13 +809,9 @@
<script>
import moment from 'moment';
import axios from 'axios';
import Vue from 'vue';
import { v4 as uuid } from 'uuid';
import isEmpty from 'lodash/isEmpty';
import { mapState, mapGetters, mapActions } from '@/libs/store';
import scoreTask from '@/../../common/script/ops/scoreTask';
import * as Analytics from '@/libs/analytics';
import positiveIcon from '@/assets/svg/positive.svg';
import negativeIcon from '@/assets/svg/negative.svg';
@@ -820,7 +830,7 @@ import checklistIcon from '@/assets/svg/checklist.svg';
import lockIcon from '@/assets/svg/lock.svg';
import menuIcon from '@/assets/svg/menu.svg';
import markdownDirective from '@/directives/markdown';
import notifications from '@/mixins/notifications';
import scoreTask from '@/mixins/scoreTask';
import approvalHeader from './approvalHeader';
import approvalFooter from './approvalFooter';
import MenuDropdown from '../ui/customMenuDropdown';
@@ -834,7 +844,7 @@ export default {
directives: {
markdown: markdownDirective,
},
mixins: [notifications],
mixins: [scoreTask],
props: ['task', 'isUser', 'group', 'challenge', 'dueDate'], // @TODO: maybe we should store the group on state?
data () {
return {
@@ -918,6 +928,7 @@ export default {
return classes;
},
showStreak () {
if (!this.task.userId) return false;
if (this.task.streak !== undefined) return true;
if (this.task.type === 'habit' && (this.task.up || this.task.down)) return true;
return false;
@@ -948,7 +959,7 @@ export default {
return this.task.challenge.shortName ? this.task.challenge.shortName.toString() : '';
},
isChallangeTask () {
isChallengeTask () {
return !isEmpty(this.task.challenge);
},
isGroupTask () {
@@ -957,7 +968,7 @@ export default {
taskCategory () {
let taskCategory = 'default';
if (this.isGroupTask) taskCategory = 'group';
else if (this.isChallangeTask) taskCategory = 'challenge';
else if (this.isChallengeTask) taskCategory = 'challenge';
return taskCategory;
},
showDelete () {
@@ -969,6 +980,14 @@ export default {
showOptions () {
return this.showEdit || this.showDelete || this.isUser;
},
teamManagerAccess () {
if (!this.isGroupTask || !this.group) return true;
return (this.group.leader._id === this.user._id || this.group.managers[this.user._id]);
},
displayNotes () {
if (this.isGroupTask && !this.isUser) return this.task.group.managerNotes;
return this.task.notes;
},
},
methods: {
...mapActions({
@@ -1021,125 +1040,8 @@ export default {
castEnd (e, task) {
setTimeout(() => this.$root.$emit('castEnd', task, 'task', e), 0);
},
async score (direction) {
if (this.castingSpell) return;
// TODO move to an action
const Content = this.$store.state.content;
const { user } = this;
const { task } = this;
if (task.group.approval.required) {
task.group.approval.requested = true;
const groupResponse = await axios.get(`/api/v4/groups/${task.group.id}`);
const managers = Object.keys(groupResponse.data.data.managers);
managers.push(groupResponse.data.data.leader._id);
if (managers.indexOf(user._id) !== -1) {
task.group.approval.approved = true;
}
}
try {
scoreTask({ task, user, direction });
} catch (err) {
this.text(err.message);
return;
}
switch (this.task.type) { // eslint-disable-line default-case
case 'habit':
this.$root.$emit('playSound', direction === 'up' ? 'Plus_Habit' : 'Minus_Habit');
break;
case 'todo':
this.$root.$emit('playSound', 'Todo');
break;
case 'daily':
this.$root.$emit('playSound', 'Daily');
break;
case 'reward':
this.$root.$emit('playSound', 'Reward');
break;
}
Analytics.updateUser();
const response = await axios.post(`/api/v4/tasks/${task._id}/score/${direction}`);
// used to notify drops, critical hits and other bonuses
const tmp = response.data.data._tmp || {};
const { crit } = tmp;
const { drop } = tmp;
const { firstDrops } = tmp;
const { quest } = tmp;
if (crit) {
const critBonus = crit * 100 - 100;
this.crit(critBonus);
}
if (quest && user.party.quest && user.party.quest.key) {
const userQuest = Content.quests[user.party.quest.key];
if (quest.progressDelta && userQuest.boss) {
this.damage(quest.progressDelta.toFixed(1));
} else if (quest.collection && userQuest.collect) {
user.party.quest.progress.collectedItems += 1;
this.quest('questCollection', quest.collection);
}
}
if (firstDrops) {
if (!user.items.eggs[firstDrops.egg]) Vue.set(user.items.eggs, firstDrops.egg, 0);
if (!user.items.hatchingPotions[firstDrops.hatchingPotion]) {
Vue.set(user.items.hatchingPotions, firstDrops.hatchingPotion, 0);
}
user.items.eggs[firstDrops.egg] += 1;
user.items.hatchingPotions[firstDrops.hatchingPotion] += 1;
}
if (drop) {
let dropText;
let dropNotes;
let type;
this.$root.$emit('playSound', 'Item_Drop');
// Note: For Mystery Item gear, drop.type will be 'head', 'armor', etc
// so we use drop.notificationType below.
if (drop.type !== 'gear' && drop.type !== 'Quest' && drop.notificationType !== 'Mystery') {
if (drop.type === 'Food') {
type = 'food';
} else if (drop.type === 'HatchingPotion') {
type = 'hatchingPotions';
} else {
type = `${drop.type.toLowerCase()}s`;
}
if (!user.items[type][drop.key]) {
Vue.set(user.items[type], drop.key, 0);
}
user.items[type][drop.key] += 1;
}
if (drop.type === 'HatchingPotion') {
dropText = Content.hatchingPotions[drop.key].text();
dropNotes = Content.hatchingPotions[drop.key].notes();
this.drop(this.$t('messageDropPotion', { dropText, dropNotes }), drop);
} else if (drop.type === 'Egg') {
dropText = Content.eggs[drop.key].text();
dropNotes = Content.eggs[drop.key].notes();
this.drop(this.$t('messageDropEgg', { dropText, dropNotes }), drop);
} else if (drop.type === 'Food') {
dropText = Content.food[drop.key].textA();
dropNotes = Content.food[drop.key].notes();
this.drop(this.$t('messageDropFood', { dropText, dropNotes }), drop);
} else if (drop.type === 'Quest') {
// TODO $rootScope.selectedQuest = Content.quests[drop.key];
// $rootScope.openModal('questDrop', {controller:'PartyCtrl', size:'sm'});
} else {
// Keep support for another type of drops that might be added
this.drop(drop.dialog);
}
}
score (direction) {
this.taskScore(this.task, direction);
},
handleBrokenTask (task) {
if (this.$store.state.isRunningYesterdailies) return;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,70 @@
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from '@storybook/vue';
import { withKnobs } from '@storybook/addon-knobs';
import Checkbox from './checkbox';
import ToggleCheckbox from './toggleCheckbox';
const stories = storiesOf('Checkbox', module);
stories.addDecorator(withKnobs);
stories
.add('checkbox', () => ({
components: {
Checkbox,
},
template: `
<div style="position: absolute; margin: 20px">
<Checkbox text="My Checkbox" id="someId"></Checkbox> <br/>
<Checkbox text="My Checked Checkbox" id="someOtherId" :checked.sync="checked"></Checkbox>
</div>
`,
data () {
return {
checked: true,
};
},
}))
.add('Toggle Checkbox Group', () => ({
components: {
ToggleCheckbox,
},
template: `
<div style="position: absolute; margin: 20px">
{{ checked }}
<div class="toggle-group" style="width: 300px">
<ToggleCheckbox text="Su"></ToggleCheckbox>
<ToggleCheckbox text="Mo"
:checked.sync="checked"></ToggleCheckbox>
<ToggleCheckbox text="Tu"></ToggleCheckbox>
<ToggleCheckbox text="We"
:checked.sync="checked"></ToggleCheckbox>
<ToggleCheckbox text="Th"></ToggleCheckbox>
<ToggleCheckbox text="Fr"
:checked.sync="checked"></ToggleCheckbox>
<ToggleCheckbox text="Sa"
:checked.sync="checked"
:disabled="true"></ToggleCheckbox>
</div>
<br/>
<br/>
Disabled:
<div class="toggle-group" style="width: 300px">
<ToggleCheckbox text="Su" :disabled="true"></ToggleCheckbox>
<ToggleCheckbox text="Mo" :disabled="true"></ToggleCheckbox>
<ToggleCheckbox text="Tu" :disabled="true"></ToggleCheckbox>
<ToggleCheckbox text="We" :disabled="true"></ToggleCheckbox>
<ToggleCheckbox text="Th" :disabled="true"></ToggleCheckbox>
<ToggleCheckbox text="Fr" :disabled="true"></ToggleCheckbox>
<ToggleCheckbox text="Sa" :disabled="true"></ToggleCheckbox>
</div>
</div>
`,
data () {
return {
checked: true,
};
},
}));

View File

@@ -6,18 +6,24 @@
v-model="isChecked"
class="custom-control-input"
type="checkbox"
:disabled="disabled"
>
<label
v-once
class="custom-control-label"
:class="{disabled: disabled}"
:for="id"
:disabled="disabled"
>{{ text }}</label>
</div>
</div>
</template>
<style lang="scss" scoped>
label {
.custom-control.custom-checkbox {
margin-bottom: 0 !important;
}
label:not(:disabled):not(.disabled) {
cursor: pointer;
}
</style>
@@ -26,6 +32,7 @@
export default {
props: {
checked: Boolean,
disabled: Boolean,
id: String,
text: String,
},
@@ -35,6 +42,9 @@ export default {
};
},
watch: {
checked (after) {
this.isChecked = after;
},
isChecked (after) {
this.$emit('update:checked', after);
},

View File

@@ -0,0 +1,71 @@
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from '@storybook/vue';
import { number, text, withKnobs } from '@storybook/addon-knobs';
import positiveIcon from '@/assets/svg/positive.svg';
const stories = storiesOf('Input-Group', module);
stories.addDecorator(withKnobs);
stories
.add('states', () => ({
components: { },
template: `
<div style="position: absolute; margin: 20px">
<div class="input-group">
<div class="input-group-prepend positive-addon input-group-icon">
<div
class="svg-icon"
v-html="icon"
>
</div>
</div>
<input
v-model="number"
class="form-control"
type="number"
min="0"
required="required"
ref="input"
>
</div>
<br />
<button class="btn btn-dark" @click="$refs.input.focus()">Focus ^</button>
<br />
<br />
<div class="input-group">
<input
v-model="number"
class="form-control"
type="number"
min="0"
required="required"
>
<div class="input-group-append positive-addon input-group-icon">
<div
class="svg-icon"
v-html="icon"
>
</div>
</div>
</div>
</div>
`,
data () {
return {
icon: positiveIcon,
};
},
props: {
text: {
default: text('Input Text', 'example text'),
},
number: {
default: number('Input Number', 0),
},
},
}));

View File

@@ -0,0 +1,45 @@
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from '@storybook/vue';
const stories = storiesOf('Margins', module);
const margins = [
'mr-1 ml-1 my-1',
'mx-2 ml-3 my-2',
'mx-2 ml-1 my-1',
'ml-1 mr-4',
'ml-2 mr-2 my-1',
'ml-75 my-3 mr-2',
];
stories
.add('overview', () => ({
components: { },
template: `
<div style="position: absolute; margin: 20px">
<span class="background inline-block">
<span class="content mx-1 my-1 inline-block">
<span class="text mx-1 my-1 inline-block">
The margin between gray and teal is the margin content.
</span>
</span>
</span>
<br />
<br />
<span v-for="m in margins"
class="background mx-1 my-1 inline-block">
<span class="content inline-block" :class="m">
<span class="mx-1 my-1 inline-block">{{m}}</span>
</span>
</span>
</div>
`,
data () {
return {
margins,
};
},
}));

View File

@@ -0,0 +1,110 @@
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from '@storybook/vue';
import { withKnobs } from '@storybook/addon-knobs';
import SelectList from './selectList.vue';
import SelectDifficulty from '../tasks/modal-controls/selectDifficulty';
import SelectTranslatedArray from '../tasks/modal-controls/selectTranslatedArray';
const stories = storiesOf('Select List', module);
stories.addDecorator(withKnobs);
stories
.add('states', () => ({
components: { SelectList },
template: `
<div class="m-xl">
Hover / Click on:
<select-list class="mb-4"
:items="items"
:key-prop="'key'"
:value="selected"
@select="selected = $event">
<template v-slot:item="{ item }">
<div v-if="item">
Template: {{ item?.key }} - {{ item?.value.text }}
</div>
<div v-else>
Nothing selected
</div>
</template>
</select-list>
Disabled:
<select-list :disabled="true"
:value="selected"
:items="items"
:key-prop="'key'"
class="mb-4">
<template v-slot:item="{ item }">
Template: {{ item?.key }} - {{ item?.value.text }}
</template>
</select-list>
<br/>
Selected: {{ selected }} <br/>
</div>
`,
data () {
return {
selected: null,
items: [
{
key: 1,
value: {
text: 'First',
},
},
{
key: 2,
value: {
text: 'Second',
},
},
],
};
},
}))
.add('difficulty', () => ({
components: { SelectDifficulty },
template: `
<div class="m-xl">
<select-difficulty
:value="selected"
@select="selected = $event"
>
</select-difficulty>
Selected: {{ selected }}
</div>
`,
data () {
return {
selected: 2,
};
},
}))
.add('translated array', () => ({
components: { SelectTranslatedArray },
template: `
<div class="m-xl">
<select-translated-array
:items="['daily', 'weekly', 'monthly']"
:value="selected"
@select="selected = $event"
>
</select-translated-array>
Selected: {{ selected }}
</div>
`,
data () {
return {
selected: 'weekly',
};
},
}));

View File

@@ -0,0 +1,68 @@
<template>
<div>
<b-dropdown
class="inline-dropdown"
@show="isOpened = true"
@hide="isOpened = false"
:toggle-class="isOpened ? 'active' : null"
:disabled="disabled"
>
<template v-slot:button-content>
<slot name="item" v-bind:item="selected" v-bind:button="true">
<!-- Fallback content -->
{{ value }}
</slot>
</template>
<b-dropdown-item
v-for="item in items"
:key="keyProp ? item[keyProp] : item"
:disabled="typeof item[disabledProp] === 'undefined' ? false : item[disabledProp]"
:class="{active: item === selected, selectListItem: true}"
@click="selectItem(item)"
>
<slot name="item" v-bind:item="item" v-bind:button="false">
<!-- Fallback content -->
{{ item }}
</slot>
</b-dropdown-item>
</b-dropdown>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
</style>
<script>
export default {
data () {
return {
isOpened: false,
selected: this.value,
};
},
props: {
items: {
type: Array,
},
disabled: {
type: Boolean,
},
value: [String, Number, Object],
keyProp: {
type: String,
},
disabledProp: {
type: String,
},
},
methods: {
selectItem (item) {
this.selected = item;
this.$emit('select', item);
},
},
};
</script>

View File

@@ -0,0 +1,116 @@
<template>
<button class="toggle-checkbox"
:class="{checked: isChecked}"
@click="isChecked = !isChecked"
type="button"
:disabled="disabled">
{{ text }}
</button>
</template>
<script>
export default {
props: {
checked: Boolean,
disabled: Boolean,
text: String,
},
data () {
return {
isChecked: this.checked,
};
},
watch: {
checked (after) {
this.isChecked = after;
},
isChecked (after) {
this.$emit('update:checked', after);
},
},
};
</script>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.toggle-checkbox {
display: flex;
justify-content: center;
align-items: center;
height: 2rem;
border: solid 1px $gray-400;
background-color: $white;
cursor: pointer;
> * {
line-height: 1.71;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
&:disabled, &.disabled {
cursor: default;
opacity: 0.75;
}
&.checked {
border-color: $purple-100;
background-color: $purple-300;
color: $white;
&:active {
outline: 0;
}
}
&:not(.disabled):not(:disabled) {
&:not(.checked) {
&:hover {
border-color: $gray-300;
background-color: $white;
color: $purple-300;
}
&:focus, &:active {
border-color: $purple-400;
background-color: $white;
color: $gray-50;
outline: 0;
}
}
&:focus, &:active {
outline: 1px solid $purple-400;
}
}
&:not(:first-of-type) {
border-left: none !important;
}
&:first-of-type {
border-bottom-left-radius: 2px;
border-top-left-radius: 2px;
}
&:last-of-type {
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
}
}
</style>
<style lang="scss">
.toggle-group {
display: flex;
flex-direction: row;
width: 100%;
.toggle-checkbox {
flex: 1;
}
}
</style>

View File

@@ -25,7 +25,13 @@
:for="toggleId"
>
<span class="toggle-switch-inner"></span>
<span class="toggle-switch-switch"></span>
<span
class="toggle-switch-switch"
tabindex="0"
@focus="handleFocus"
@blur="handleBlur"
@keyup.space="handleSpace"
></span>
</label>
</div>
</div>
@@ -89,18 +95,18 @@
.toggle-switch-inner:before {
content: "";
padding-left: 10px;
background-color: $purple-400;
background-color: $green-50;
}
.toggle-switch-inner:after {
content: "";
padding-right: 10px;
background-color: $gray-200;
background-color: $gray-300;
text-align: right;
}
.toggle-switch-switch {
box-shadow: 0 1px 2px 0 rgba($black, 0.32);
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
display: block;
width: 20px;
margin: -2px;
@@ -113,6 +119,11 @@
right: 22px;
border-radius: 100px;
transition: all 0.3s ease-in 0s;
&:focus {
border: 1px solid $purple-400;
outline: none;
}
}
.toggle-switch-checkbox:checked + .toggle-switch-label .toggle-switch-inner {
@@ -151,6 +162,7 @@ export default {
toggleId: this.generateId(),
// The container requires a unique id to link it to the pop-over
containerId: this.generateId(),
focused: false,
};
},
computed: {
@@ -159,9 +171,20 @@ export default {
},
},
methods: {
handleBlur () {
this.focused = false;
},
handleChange ({ target: { checked } }) {
this.$emit('change', checked);
},
handleFocus () {
this.focused = true;
},
handleSpace () {
if (this.focused) {
document.getElementById(this.toggleId).click();
}
},
generateId () {
return `id-${Math.random().toString(36).substr(2, 16)}`;
},

View File

@@ -9,12 +9,7 @@
class="profile"
>
<div class="header">
<span
class="close-icon svg-icon inline icon-10"
@click="close()"
v-html="icons.close"
></span>
<div class="profile-actions">
<div class="profile-actions d-flex">
<router-link
:to="{ path: '/private-messages', query: { uuid: user._id } }"
replace
@@ -23,9 +18,9 @@
v-b-tooltip.hover.left="$t('sendMessage')"
class="btn btn-secondary message-icon"
>
<div
class="svg-icon message-icon"
v-html="icons.message"
<div class="svg-icon message-icon"
v-html="icons.message"
v-once
></div>
</button>
</router-link>
@@ -42,7 +37,7 @@
<button
v-if="user._id !== userLoggedIn._id && userLoggedIn.inbox.blocks.indexOf(user._id) === -1"
v-b-tooltip.hover.right="$t('blockWarning')"
class="btn btn-secondary block-icon"
class="btn btn-secondary block-icon d-flex justify-content-center align-items-center"
@click="blockUser()"
>
<div
@@ -65,7 +60,7 @@
<button
v-if="userLoggedIn.contributor.admin"
v-b-tooltip.hover.right="'Admin - Toggle Tools'"
class="btn btn-secondary positive-icon"
class="btn btn-secondary positive-icon d-flex justify-content-center align-items-center"
@click="toggleAdminTools()"
>
<div
@@ -292,13 +287,13 @@
</div>
<div class="col-12 text-center">
<button
class="btn btn-primary"
class="btn btn-primary mr-2"
@click="save()"
>
{{ $t("save") }}
</button>
<button
class="btn btn-warning"
class="btn btn-secondary"
@click="editing = false"
>
{{ $t("cancel") }}
@@ -329,7 +324,7 @@
>
<div
:id="achievKey + '-achievement'"
class="box achievement-container d-flex align-items-center justify-content-center"
class="box achievement-container"
:class="{'achievement-unearned': !achievement.earned}"
>
<b-popover
@@ -437,6 +432,10 @@
}
}
.standard-page {
padding-bottom: 0rem;
}
.modal-content {
background: #f9f9f9;
}
@@ -570,8 +569,8 @@
.achievement-wrapper {
width: 94px;
min-width: 94px !important;
max-width: 94px;
min-width: 94px;
margin-right: 12px;
margin-left: 12px;
padding: 0px;
@@ -580,6 +579,7 @@
.box {
margin: 0 auto;
margin-bottom: 1em;
padding-top: 1.2em;
background: $white;
}
@@ -730,7 +730,6 @@ import lock from '@/assets/svg/lock.svg';
import challenge from '@/assets/svg/challenge.svg';
import member from '@/assets/svg/member-icon.svg';
import staff from '@/assets/svg/tier-staff.svg';
import svgClose from '@/assets/svg/close.svg';
import error404 from '../404';
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
@@ -758,7 +757,6 @@ export default {
lock,
member,
staff,
close: svgClose,
}),
adminToolsLoaded: false,
userIdToMessage: '',
@@ -1022,9 +1020,6 @@ export default {
const status = this.achievementsCategories[categoryKey].open;
this.achievementsCategories[categoryKey].open = !status;
},
close () {
this.$root.$emit('bv::hide::modal', 'profile');
},
},
};
</script>

View File

@@ -0,0 +1,136 @@
import axios from 'axios';
import Vue from 'vue';
import * as Analytics from '@/libs/analytics';
import notifications from './notifications';
import scoreTask from '@/../../common/script/ops/scoreTask';
import { mapState } from '@/libs/store';
export default {
mixins: [notifications],
computed: {
...mapState({
castingSpell: 'spellOptions.castingSpell',
user: 'user.data',
}),
},
methods: {
async taskScore (task, direction) {
if (this.castingSpell) return;
const { user } = this;
const Content = this.$store.state.content;
if (task.group.approval.required && !task.group.approval.approved) {
task.group.approval.requested = true;
const groupResponse = await axios.get(`/api/v4/groups/${task.group.id}`);
const managers = Object.keys(groupResponse.data.data.managers);
managers.push(groupResponse.data.data.leader._id);
if (managers.indexOf(user._id) !== -1) {
task.group.approval.approved = true;
}
}
try {
scoreTask({ task, user, direction });
} catch (err) {
this.text(err.message);
return;
}
switch (task.type) { // eslint-disable-line default-case
case 'habit':
this.$root.$emit('playSound', direction === 'up' ? 'Plus_Habit' : 'Minus_Habit');
break;
case 'todo':
this.$root.$emit('playSound', 'Todo');
break;
case 'daily':
this.$root.$emit('playSound', 'Daily');
break;
case 'reward':
this.$root.$emit('playSound', 'Reward');
break;
}
Analytics.updateUser();
const response = await axios.post(`/api/v4/tasks/${task._id}/score/${direction}`);
// used to notify drops, critical hits and other bonuses
const tmp = response.data.data._tmp || {};
const { crit } = tmp;
const { drop } = tmp;
const { firstDrops } = tmp;
const { quest } = tmp;
if (crit) {
const critBonus = crit * 100 - 100;
this.crit(critBonus);
}
if (quest && user.party.quest && user.party.quest.key) {
const userQuest = Content.quests[user.party.quest.key];
if (quest.progressDelta && userQuest.boss) {
this.damage(quest.progressDelta.toFixed(1));
} else if (quest.collection && userQuest.collect) {
user.party.quest.progress.collectedItems += 1;
this.quest('questCollection', quest.collection);
}
}
if (firstDrops) {
if (!user.items.eggs[firstDrops.egg]) Vue.set(user.items.eggs, firstDrops.egg, 0);
if (!user.items.hatchingPotions[firstDrops.hatchingPotion]) {
Vue.set(user.items.hatchingPotions, firstDrops.hatchingPotion, 0);
}
user.items.eggs[firstDrops.egg] += 1;
user.items.hatchingPotions[firstDrops.hatchingPotion] += 1;
}
if (drop) {
let dropText;
let dropNotes;
let type;
this.$root.$emit('playSound', 'Item_Drop');
// Note: For Mystery Item gear, drop.type will be 'head', 'armor', etc
// so we use drop.notificationType below.
if (drop.type !== 'gear' && drop.type !== 'Quest' && drop.notificationType !== 'Mystery') {
if (drop.type === 'Food') {
type = 'food';
} else if (drop.type === 'HatchingPotion') {
type = 'hatchingPotions';
} else {
type = `${drop.type.toLowerCase()}s`;
}
if (!user.items[type][drop.key]) {
Vue.set(user.items[type], drop.key, 0);
}
user.items[type][drop.key] += 1;
}
if (drop.type === 'HatchingPotion') {
dropText = Content.hatchingPotions[drop.key].text();
dropNotes = Content.hatchingPotions[drop.key].notes();
this.drop(this.$t('messageDropPotion', { dropText, dropNotes }), drop);
} else if (drop.type === 'Egg') {
dropText = Content.eggs[drop.key].text();
dropNotes = Content.eggs[drop.key].notes();
this.drop(this.$t('messageDropEgg', { dropText, dropNotes }), drop);
} else if (drop.type === 'Food') {
dropText = Content.food[drop.key].textA();
dropNotes = Content.food[drop.key].notes();
this.drop(this.$t('messageDropFood', { dropText, dropNotes }), drop);
} else if (drop.type === 'Quest') {
// TODO $rootScope.selectedQuest = Content.quests[drop.key];
// $rootScope.openModal('questDrop', {controller:'PartyCtrl', size:'sm'});
} else {
// Keep support for another type of drops that might be added
this.drop(drop.dialog);
}
}
},
},
};

View File

@@ -4,6 +4,9 @@ export default {
methods: {
async sync () {
this.$root.$emit(EVENTS.RESYNC_REQUESTED);
if (this.$route.fullPath.indexOf('task-information') !== -1) {
this.$root.$emit('habitica:team-sync');
}
await Promise.all([
this.$store.dispatch('user:fetch', { forceLoad: true }),
this.$store.dispatch('tasks:fetchUserTasks', { forceLoad: true }),

View File

@@ -9,9 +9,13 @@ export async function getTags () {
export async function createTag (store, payload) {
const url = 'api/v4/tags';
const response = await axios.post(url, {
tagDetails: payload.tagDetails,
name: payload.name,
});
return response.data.data;
const tag = response.data.data;
store.state.user.data.tags.push(tag);
return tag;
}
export async function getTag (store, payload) {

View File

@@ -13,6 +13,15 @@ export function getTagsFor (store) {
.map(tag => tag.name);
}
export function getTagsByIdList (store) {
return function tagsByIdListFunc (taskIdArray) {
return (taskIdArray || []).length > 0
? store.state.user.data.tags
.filter(tag => taskIdArray.indexOf(tag.id) !== -1)
: [];
};
}
function getTaskColor (task) {
if (task.type === 'reward' || task.byHabitica) return 'purple';
@@ -110,7 +119,9 @@ export function canEdit (store) {
function _nonInteractive (task) {
return (task.group && task.group.id && !task.userId)
|| (task.challenge && task.challenge.id && !task.userId);
|| (task.challenge && task.challenge.id && !task.userId)
|| (task.group && task.group.approval && task.group.approval.requested
&& task.type !== 'habit');
}
export function getTaskClasses (store) {
@@ -132,6 +143,8 @@ export function getTaskClasses (store) {
return `task-${color}-modal-content`;
case 'create-modal-content':
return 'task-purple-modal-content';
case 'edit-modal-headings':
return `task-${color}-modal-headings`;
case 'edit-modal-text':
return `task-${color}-modal-text`;
case 'edit-modal-icon':
@@ -144,6 +157,8 @@ export function getTaskClasses (store) {
return `task-${color}-modal-habit-control-disabled`;
case 'create-modal-bg':
return 'task-purple-modal-bg';
case 'create-modal-headings':
return 'task-purple-modal-headings';
case 'create-modal-text':
return 'task-purple-modal-text';
case 'create-modal-input':

View File

@@ -261,12 +261,14 @@
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
"assignedTo": "Assigned To",
"assignedTo": "Assign To",
"assignedToUser": "Assigned to <strong><%= userName %></strong>",
"assignedToMembers": "Assigned to <strong><%= userCount %> members</strong>",
"assignedToYouAndMembers": "Assigned to you and <strong><%= userCount %> members</strong>",
"youAreAssigned": "You are assigned to this task",
"youAreAssigned": "Assigned to you",
"taskIsUnassigned": "This task is unassigned",
"unassigned": "Unassigned",
"chooseTeamMember": "Choose a team member",
"confirmClaim": "Are you sure you want to claim this task?",
"confirmUnClaim": "Are you sure you want to unclaim this task?",
"confirmApproval": "Are you sure you want to approve this task?",
@@ -293,6 +295,7 @@
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
"youHaveBeenAssignedTask": "<%= managerName %> has assigned you the task <span class=\"notification-bold\"><%= taskText %></span>.",
"yourTaskHasBeenApproved": "Your task <span class=\"notification-green notification-bold\"><%= taskText %></span> has been approved.",
"thisTaskApproved": "This task was approved",
"taskClaimed": "<%= userName %> has claimed the task <span class=\"notification-bold\"><%= taskText %></span>.",
"taskNeedsWork": "<span class=\"notification-bold\"><%= managerName %></span> marked <span class=\"notification-bold\"><%= taskText %></span> as needing additional work.",
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%= user %></span> requests approval for <span class=\"notification-bold\"><%= taskName %></span>",
@@ -498,5 +501,9 @@
"singleCompletion": "Single - Completes when any assigned user finishes",
"allAssignedCompletion": "All - Completes when all assigned users finish",
"suggestedGroup": "Suggested because youre new to Habitica.",
"groupActivityNotificationTitle": "<%= user %> posted in <%= group %>"
"groupActivityNotificationTitle": "<%= user %> posted in <%= group %>",
"managerNotes": "Manager's Notes",
"assignedDateOnly": "Assigned on <strong><%= date %></strong>",
"assignedDateAndUser": "Assigned by <strong>@<%= username %></strong> on <strong><%= date %></strong>",
"claimRewards": "Claim Rewards"
}

View File

@@ -211,5 +211,8 @@
"repeatDayError": "Please ensure that you have at least one day of the week selected.",
"searchTasks": "Search titles and descriptions...",
"sessionOutdated": "Your session is outdated. Please refresh or sync.",
"errorTemporaryItem": "This item is temporary and cannot be pinned."
"errorTemporaryItem": "This item is temporary and cannot be pinned.",
"addTags": "Add tags...",
"enterTag": "Enter a tag",
"pressEnterToAddTag": "Press Enter to add tag: '<%= tagName %>'"
}

View File

@@ -235,7 +235,7 @@ export default function scoreTask (options = {}, req = {}, analytics) {
if (
task.group && task.group.approval && task.group.approval.required
&& !task.group.approval.approved
&& !task.group.approval.approved && !(task.type === 'todo' && cron)
) return 0;
// This is for setting one-time temporary flags,

View File

@@ -658,7 +658,7 @@ api.updateTask = {
if (!challenge && task.userId && task.challenge && task.challenge.id) {
sanitizedObj = Tasks.Task.sanitizeUserChallengeTask(updatedTaskObj);
} else if (!group && task.userId && task.group && task.group.id) {
sanitizedObj = Tasks.Task.sanitizeUserChallengeTask(updatedTaskObj);
sanitizedObj = Tasks.Task.sanitizeUserGroupTask(updatedTaskObj);
} else {
sanitizedObj = Tasks.Task.sanitize(updatedTaskObj);
}
@@ -677,6 +677,9 @@ api.updateTask = {
if (sanitizedObj.sharedCompletion) {
task.group.sharedCompletion = sanitizedObj.sharedCompletion;
}
if (sanitizedObj.managerNotes) {
task.group.managerNotes = sanitizedObj.managerNotes;
}
setNextDue(task, user);
const savedTask = await task.save();
@@ -719,6 +722,8 @@ api.updateTask = {
*
* @apiSuccess {Object} data The user stats
* @apiSuccess {Object} data._tmp If an item was dropped it'll be returned in te _tmp object
* @apiSuccess (202) {Boolean} data.approvalRequested Approval was requested for team task
* @apiSuccess (202) {String} message Acknowledgment of team task approval request
* @apiSuccess {Number} data.delta The delta
*
* @apiSuccessExample {json} Example result:
@@ -811,7 +816,22 @@ api.scoreTask = {
managerPromises.push(task.save());
await Promise.all(managerPromises);
throw new NotAuthorized(res.t('taskApprovalHasBeenRequested'));
res.respond(
202,
{ approvalRequested: true },
res.t('taskApprovalHasBeenRequested'),
);
return;
}
}
if (task.group.approval.required && task.group.approval.approved) {
const notificationIndex = user.notifications.findIndex(notification => notification
&& notification.data && notification.data.task
&& notification.data.task._id === task._id && notification.type === 'GROUP_TASK_APPROVED');
if (notificationIndex !== -1) {
user.notifications.splice(notificationIndex, 1);
}
}

View File

@@ -237,7 +237,7 @@ api.assignTask = {
});
}
promises.push(group.syncTask(task, assignedUser));
promises.push(group.syncTask(task, assignedUser, user));
promises.push(group.save());
await Promise.all(promises);
@@ -362,7 +362,7 @@ api.approveTask = {
const firstNotificationIndex = firstManagerNotifications.findIndex(notification => notification && notification.data && notification.data.taskId === task._id && notification.type === 'GROUP_TASK_APPROVAL');
let direction = 'up';
if (firstManagerNotifications[firstNotificationIndex]) {
direction = firstManagerNotifications[firstNotificationIndex].direction;
direction = firstManagerNotifications[firstNotificationIndex].direction || direction;
}
// Remove old notifications
@@ -380,11 +380,7 @@ api.approveTask = {
assignedUser.addNotification('GROUP_TASK_APPROVED', {
message: res.t('yourTaskHasBeenApproved', { taskText: task.text }),
groupId: group._id,
});
assignedUser.addNotification('SCORED_TASK', {
message: res.t('yourTaskHasBeenApproved', { taskText: task.text }),
scoreTask: task,
task,
direction,
});

View File

@@ -100,6 +100,7 @@ export async function createTasks (req, res, options = {}) {
newTask.group.approval.required = true;
}
newTask.group.sharedCompletion = taskData.sharedCompletion || SHARED_COMPLETION.default;
newTask.group.managerNotes = taskData.managerNotes || '';
} else {
newTask.userId = user._id;

View File

@@ -1472,6 +1472,7 @@ schema.methods.updateTask = async function updateTask (taskToSync, options = {})
updateCmd.$set['group.approval.required'] = taskToSync.group.approval.required;
updateCmd.$set['group.assignedUsers'] = taskToSync.group.assignedUsers;
updateCmd.$set['group.sharedCompletion'] = taskToSync.group.sharedCompletion;
updateCmd.$set['group.managerNotes'] = taskToSync.group.managerNotes;
const taskSchema = Tasks[taskToSync.type];
@@ -1492,26 +1493,8 @@ schema.methods.updateTask = async function updateTask (taskToSync, options = {})
updateCmd.$pull = { checklist: { linkId: { $in: [options.removedCheckListItemId] } } };
}
if (options.updateCheckListItems && options.updateCheckListItems.length > 0) {
const checkListIdsToRemove = [];
const checkListItemsToAdd = [];
options.updateCheckListItems.forEach(updateCheckListItem => {
checkListIdsToRemove.push(updateCheckListItem.id);
const newCheckList = { completed: false };
newCheckList.linkId = updateCheckListItem.id;
newCheckList.text = updateCheckListItem.text;
checkListItemsToAdd.push(newCheckList);
});
updateCmd.$pull = { checklist: { linkId: { $in: checkListIdsToRemove } } };
await taskSchema.update(updateQuery, updateCmd, { multi: true }).exec();
delete updateCmd.$pull;
updateCmd.$push = { checklist: { $each: checkListItemsToAdd } };
await taskSchema.update(updateQuery, updateCmd, { multi: true }).exec();
return;
if (options.updateCheckListItems) {
updateCmd.$set.checklist = taskToSync.checklist;
}
// Updating instead of loading and saving for performances,
@@ -1519,7 +1502,7 @@ schema.methods.updateTask = async function updateTask (taskToSync, options = {})
await taskSchema.update(updateQuery, updateCmd, { multi: true }).exec();
};
schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
schema.methods.syncTask = async function groupSyncTask (taskToSync, user, assigningUser) {
const group = this;
const toSave = [];
@@ -1570,6 +1553,10 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
matchingTask.group.approval.required = taskToSync.group.approval.required;
matchingTask.group.assignedUsers = taskToSync.group.assignedUsers;
matchingTask.group.sharedCompletion = taskToSync.group.sharedCompletion;
matchingTask.group.managerNotes = taskToSync.group.managerNotes;
if (assigningUser && user._id !== assigningUser._id) {
matchingTask.group.assigningUsername = assigningUser.auth.local.username;
}
// sync checklist
if (taskToSync.checklist) {
@@ -1632,15 +1619,48 @@ schema.methods.unlinkTask = async function groupUnlinkTask (
schema.methods.removeTask = async function groupRemoveTask (task) {
const group = this;
const removalPromises = [];
// Set the task as broken
await Tasks.Task.update({
// Delete individual task copies and related notifications
const userTasks = await Tasks.Task.find({
userId: { $exists: true },
'group.id': group.id,
'group.taskId': task._id,
}, {
$set: { 'group.broken': 'TASK_DELETED' },
}, { multi: true }).exec();
}, { userId: 1, _id: 1 }).exec();
userTasks.forEach(async userTask => {
const assignedUser = await User.findOne({ _id: userTask.userId }, 'notifications tasksOrder').exec();
let notificationIndex = assignedUser.notifications.findIndex(notification => notification
&& notification.type === 'GROUP_TASK_ASSIGNED'
&& notification.data && notification.data.taskId === task._id);
if (notificationIndex !== -1) {
assignedUser.notifications.splice(notificationIndex, 1);
}
notificationIndex = assignedUser.notifications.findIndex(notification => notification
&& notification.type === 'GROUP_TASK_NEEDS_WORK'
&& notification.data && notification.data.task
&& notification.data.task.id === userTask._id);
if (notificationIndex !== -1) {
assignedUser.notifications.splice(notificationIndex, 1);
}
notificationIndex = assignedUser.notifications.findIndex(notification => notification
&& notification.type === 'GROUP_TASK_APPROVED'
&& notification.data && notification.data.task
&& notification.data.task._id === userTask._id);
if (notificationIndex !== -1) {
assignedUser.notifications.splice(notificationIndex, 1);
}
await Tasks.Task.remove({ _id: userTask._id });
removeFromArray(assignedUser.tasksOrder[`${task.type}s`], userTask._id);
removalPromises.push(assignedUser.save());
});
// Get Managers
const managerIds = Object.keys(group.managers);
@@ -1648,14 +1668,15 @@ schema.methods.removeTask = async function groupRemoveTask (task) {
const managers = await User.find({ _id: managerIds }, 'notifications').exec(); // Use this method so we can get access to notifications
// Remove old notifications
const removalPromises = [];
managers.forEach(manager => {
const notificationIndex = manager.notifications.findIndex(notification => notification && notification.data && notification.data.groupTaskId === task._id && notification.type === 'GROUP_TASK_APPROVAL');
const notificationIndex = manager.notifications.findIndex(notification => notification
&& notification.data && notification.data.groupTaskId === task._id
&& notification.type === 'GROUP_TASK_APPROVAL');
if (notificationIndex !== -1) {
manager.notifications.splice(notificationIndex, 1);
removalPromises.push(manager.save());
}
removalPromises.push(manager.save());
});
removeFromArray(group.tasksOrder[`${task.type}s`], task._id);

View File

@@ -128,16 +128,17 @@ export const TaskSchema = new Schema({
},
group: {
id: { $type: String, ref: 'Group', validate: [v => validator.isUUID(v), 'Invalid uuid for task group.'] },
id: { $type: String, ref: 'Group', validate: [v => validator.isUUID(v), 'Invalid uuid for group task.'] },
broken: { $type: String, enum: ['GROUP_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED'] },
assignedUsers: [{ $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for task group user.'] }],
assignedUsers: [{ $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for group assigned user.'] }],
assignedDate: { $type: Date },
taskId: { $type: String, ref: 'Task', validate: [v => validator.isUUID(v), 'Invalid uuid for task group task.'] },
assigningUsername: { $type: String },
taskId: { $type: String, ref: 'Task', validate: [v => validator.isUUID(v), 'Invalid uuid for group task.'] },
approval: {
required: { $type: Boolean, default: false },
approved: { $type: Boolean, default: false },
dateApproved: { $type: Date },
approvingUser: { $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid task group approvingUser.'] },
approvingUser: { $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for group approving user.'] },
requested: { $type: Boolean, default: false },
requestedDate: { $type: Date },
},
@@ -146,6 +147,7 @@ export const TaskSchema = new Schema({
enum: _.values(SHARED_COMPLETION),
default: SHARED_COMPLETION.single,
},
managerNotes: { $type: String },
},
reminders: [reminderSchema],
@@ -220,6 +222,16 @@ TaskSchema.statics.sanitizeUserChallengeTask = function sanitizeUserChallengeTas
]);
};
TaskSchema.statics.sanitizeUserGroupTask = function sanitizeUserGroupTask (taskObj) {
const initialSanitization = this.sanitize(taskObj);
return _.pick(initialSanitization, [
'streak', 'attribute', 'reminders',
'tags', 'notes', 'collapseChecklist',
'alias', 'yesterDaily', 'counterDown', 'counterUp',
]);
};
// Sanitize checklist objects (disallowing id)
TaskSchema.statics.sanitizeChecklist = function sanitizeChecklist (checklistObj) {
delete checklistObj.id;