mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 21:27:23 +01:00
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:
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
13
website/client/config/storybook/margin.css
Normal file
13
website/client/config/storybook/margin.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.background {
|
||||
background: teal;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.content {
|
||||
color: white;
|
||||
background: grey;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,3 +37,4 @@
|
||||
@import './tiers';
|
||||
@import './payments';
|
||||
@import './datepicker.scss';
|
||||
@import './spacing';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
.modal {
|
||||
z-index: 1350;
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
|
||||
59
website/client/src/assets/scss/spacing.scss
Normal file
59
website/client/src/assets/scss/spacing.scss
Normal 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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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') }}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@click="click($event)"
|
||||
>
|
||||
<div
|
||||
class="item"
|
||||
class="item transition"
|
||||
:class="{'item-active': active }"
|
||||
>
|
||||
<countBadge
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@click="click()"
|
||||
>
|
||||
<div
|
||||
class="item pet-slot"
|
||||
class="item pet-slot transition"
|
||||
:class="{'item-empty': !isOwned()}"
|
||||
>
|
||||
<slot
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@click="click()"
|
||||
>
|
||||
<div
|
||||
class="item pet-slot"
|
||||
class="item pet-slot transition"
|
||||
:class="{'item-empty': !isOwned(), 'highlight': highlightBorder}"
|
||||
>
|
||||
<slot
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()"
|
||||
>
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
border-color: transparent;
|
||||
transition: background 0.15s ease-in;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($black, 0.1);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
187
website/client/src/components/tasks/modal-controls/multiList.vue
Normal file
187
website/client/src/components/tasks/modal-controls/multiList.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
70
website/client/src/components/ui/checkbox.stories.js
Normal file
70
website/client/src/components/ui/checkbox.stories.js
Normal 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,
|
||||
};
|
||||
},
|
||||
}));
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
71
website/client/src/components/ui/input-group.stories.js
Normal file
71
website/client/src/components/ui/input-group.stories.js
Normal 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),
|
||||
},
|
||||
},
|
||||
}));
|
||||
45
website/client/src/components/ui/margin.stories.js
Normal file
45
website/client/src/components/ui/margin.stories.js
Normal 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,
|
||||
};
|
||||
},
|
||||
}));
|
||||
110
website/client/src/components/ui/selectList.stories.js
Normal file
110
website/client/src/components/ui/selectList.stories.js
Normal 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',
|
||||
};
|
||||
},
|
||||
}));
|
||||
68
website/client/src/components/ui/selectList.vue
Normal file
68
website/client/src/components/ui/selectList.vue
Normal 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>
|
||||
116
website/client/src/components/ui/toggleCheckbox.vue
Normal file
116
website/client/src/components/ui/toggleCheckbox.vue
Normal 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>
|
||||
@@ -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)}`;
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
136
website/client/src/mixins/scoreTask.js
Normal file
136
website/client/src/mixins/scoreTask.js
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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 you’re 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"
|
||||
}
|
||||
|
||||
@@ -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 %>'"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user