diff --git a/test/api/v3/integration/challenges/POST-challenges_challengeId_leave.test.js b/test/api/v3/integration/challenges/POST-challenges_challengeId_leave.test.js index 0502be654c..0a0a89258a 100644 --- a/test/api/v3/integration/challenges/POST-challenges_challengeId_leave.test.js +++ b/test/api/v3/integration/challenges/POST-challenges_challengeId_leave.test.js @@ -117,7 +117,9 @@ describe('POST /challenges/:challengeId/leave', () => { }); expect(testTask).to.not.be.undefined; - expect(testTask.challenge).to.eql({}); + expect(testTask.challenge).to.eql({ + shortName: 'None', + }); }); }); }); diff --git a/test/api/v3/integration/groups/GET-groups_groupId_members.test.js b/test/api/v3/integration/groups/GET-groups_groupId_members.test.js index 75511cca4c..72b12453d9 100644 --- a/test/api/v3/integration/groups/GET-groups_groupId_members.test.js +++ b/test/api/v3/integration/groups/GET-groups_groupId_members.test.js @@ -84,7 +84,7 @@ describe('GET /groups/:groupId/members', () => { expect(Object.keys(memberRes.auth)).to.eql(['timestamps']); expect(Object.keys(memberRes.preferences).sort()).to.eql([ 'size', 'hair', 'skin', 'shirt', - 'chair', 'costume', 'sleep', 'background', + 'chair', 'costume', 'sleep', 'background', 'tasks', ].sort()); expect(memberRes.stats.maxMP).to.exist; diff --git a/test/api/v3/integration/members/GET-members_id.test.js b/test/api/v3/integration/members/GET-members_id.test.js index 06151be350..fa34fac096 100644 --- a/test/api/v3/integration/members/GET-members_id.test.js +++ b/test/api/v3/integration/members/GET-members_id.test.js @@ -37,7 +37,7 @@ describe('GET /members/:memberId', () => { expect(Object.keys(memberRes.auth)).to.eql(['timestamps']); expect(Object.keys(memberRes.preferences).sort()).to.eql([ 'size', 'hair', 'skin', 'shirt', - 'chair', 'costume', 'sleep', 'background', + 'chair', 'costume', 'sleep', 'background', 'tasks', ].sort()); expect(memberRes.stats.maxMP).to.exist; diff --git a/website/client-old/js/app.js b/website/client-old/js/app.js index e2cc0b90ee..b42effb7c3 100644 --- a/website/client-old/js/app.js +++ b/website/client-old/js/app.js @@ -167,8 +167,8 @@ window.habitrpg = angular.module('habitrpg', url: '/:gid', templateUrl: 'partials/options.social.guilds.detail.html', title: env.t('titleGuilds'), - controller: ['$scope', 'Groups', 'Chat', '$stateParams', 'Members', 'Challenges', 'Tasks', 'User', '$location', - function($scope, Groups, Chat, $stateParams, Members, Challenges, Tasks, User, $location) { + controller: ['$scope', 'Groups', 'Chat', '$stateParams', 'Members', 'Challenges', 'Tasks', 'User', '$location', '$rootScope', + function($scope, Groups, Chat, $stateParams, Members, Challenges, Tasks, User, $location, $rootScope) { $scope.groupPanel = 'chat'; $scope.upgrade = false; @@ -222,6 +222,7 @@ window.habitrpg = angular.module('habitrpg', }).value(); $scope.group.approvals = []; + $rootScope.$broadcast('obj-updated', $scope.group); if (User.user._id === $scope.group.leader._id) { return Tasks.getGroupApprovals($scope.group._id); } @@ -257,7 +258,7 @@ window.habitrpg = angular.module('habitrpg', tasks.forEach(function (element, index, array) { if (!$scope.challenge[element.type + 's']) $scope.challenge[element.type + 's'] = []; $scope.challenge[element.type + 's'].push(element); - }) + }); return Members.getChallengeMembers($scope.challenge._id); }) diff --git a/website/client-old/js/components/groupTasks/groupTasksController.js b/website/client-old/js/components/groupTasks/groupTasksController.js index a5a14c2a3f..f65799be3b 100644 --- a/website/client-old/js/components/groupTasks/groupTasksController.js +++ b/website/client-old/js/components/groupTasks/groupTasksController.js @@ -1,4 +1,4 @@ -habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', function ($scope, Shared, Tasks, User) { +habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', '$rootScope', function ($scope, Shared, Tasks, User, $rootScope) { function handleGetGroupTasks (response) { var group = $scope.obj; @@ -15,8 +15,9 @@ habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', func tasks.forEach(function (element, index, array) { if (!$scope.group[element.type + 's']) $scope.group[element.type + 's'] = []; $scope.group[element.type + 's'].unshift(element); - }) + }); + $rootScope.$broadcast('obj-updated', $scope.group); $scope.loading = false; }; diff --git a/website/client-old/js/controllers/tasksCtrl.js b/website/client-old/js/controllers/tasksCtrl.js index 2d5ddc1bed..baa43d9605 100644 --- a/website/client-old/js/controllers/tasksCtrl.js +++ b/website/client-old/js/controllers/tasksCtrl.js @@ -186,11 +186,6 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N Checklists ------------------------ */ - /* - ------------------------ - Checklists - ------------------------ - */ $scope.addChecklist = Tasks.addChecklist; $scope.addChecklistItem = Tasks.addChecklistItemToUI; @@ -347,4 +342,8 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N var content = task.notes; return content; }; + + $scope.getClasses = function (task, user, list, main) { + return Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main); + } }]); diff --git a/website/client-old/js/directives/directives.js b/website/client-old/js/directives/directives.js index 789783cf22..c1fcd48ba4 100644 --- a/website/client-old/js/directives/directives.js +++ b/website/client-old/js/directives/directives.js @@ -21,7 +21,7 @@ }; }(); - habitrpg.directive('markdown', ['$timeout', function($timeout) { + habitrpg.directive('markdown', ['$timeout', 'User', function($timeout, User) { return { restrict: 'E', link: function(scope, element, attrs) { @@ -34,7 +34,7 @@ var markdown = value; var linktarget = attrs.target || '_self'; - var userName = scope.User.user.profile.name; + var userName = User.user.profile.name; var userHighlight = "@"+userName; var html = md.toHtml(markdown); diff --git a/website/client-old/js/directives/task-list.directive.js b/website/client-old/js/directives/task-list.directive.js new file mode 100644 index 0000000000..91d0a77105 --- /dev/null +++ b/website/client-old/js/directives/task-list.directive.js @@ -0,0 +1,78 @@ +'use strict'; + +(function(){ + angular + .module('habitrpg') + .directive('taskList', taskList); + + taskList.$inject = [ + '$state', + 'User', + '$rootScope', + ]; + + function taskList($state, User, $rootScope) { + return { + restrict: 'EA', + templateUrl: 'templates/task-list.html', + transclude: true, + scope: true, + // scope: { + // taskList: '=list', + // list: '=listDetails', + // obj: '=object', + // user: "=", + // }, + link: function($scope, element, attrs) { + // @TODO: The use of scope with tasks is incorrect. We need to fix all task ctrls to use directives/services + // $scope.obj = {}; + function setObj (obj, force) { + if (!force && ($scope.obj || scope.obj !== {} || !obj)) return; + $scope.obj = obj; + setUpGroupedList(); + setUpTaskWatch(); + } + + $rootScope.$on('obj-updated', function (event, obj) { + setObj(obj, true); + }); + + function setUpGroupedList () { + if (!$scope.obj) return; + $scope.groupedList = {}; + ['habit', 'daily', 'todo', 'reward'].forEach(function (listType) { + groupTasksByChallenge($scope.obj[listType + 's'], listType); + }); + } + setUpGroupedList(); + + function groupTasksByChallenge (taskList, type) { + $scope.groupedList[type] = _.groupBy(taskList, 'challenge.shortName'); + }; + + function setUpTaskWatch () { + if (!$scope.obj) return; + $scope.$watch(function () { return $scope.obj.tasksOrder; }, function () { + setUpGroupedList(); + }, true); + } + setUpTaskWatch(); + + $scope.getTaskList = function (list, taskList, obj) { + setObj(obj); + if (!$scope.obj) return []; + if (taskList) return taskList; + return $scope.obj[list.type+'s']; + }; + + $scope.showNormalList = function () { + return !$state.includes("options.social.challenges") && !User.user.preferences.tasks.groupByChallenge; + }; + + $scope.showChallengeList = function () { + return $state.includes("options.social.challenges"); + }; + } + } + } +}()); diff --git a/website/client-old/js/directives/task.directive.js b/website/client-old/js/directives/task.directive.js new file mode 100644 index 0000000000..3c692e7f54 --- /dev/null +++ b/website/client-old/js/directives/task.directive.js @@ -0,0 +1,24 @@ +'use strict'; + +(function(){ + angular + .module('habitrpg') + .directive('task', task); + + task.$inject = [ + 'Shared', + ]; + + function task(Shared) { + return { + restrict: 'E', + templateUrl: 'templates/task.html', + scope: true, + link: function($scope, element, attrs) { + $scope.getClasses = function (task, user, list, main) { + return Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main); + } + } + } + } +}()); diff --git a/website/client-old/manifest.json b/website/client-old/manifest.json index 5e7f31c037..841940aac7 100644 --- a/website/client-old/manifest.json +++ b/website/client-old/manifest.json @@ -85,6 +85,8 @@ "js/directives/popover-html.directive.js", "js/directives/submit-form-on-enter.directive.js", "js/directives/when-scrolled.directive.js", + "js/directives/task-list.directive.js", + "js/directives/task.directive.js", "js/controllers/authCtrl.js", "js/controllers/autoCompleteCtrl.js", diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index 55b0e098ee..38beee1023 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -146,5 +146,6 @@ "taskRequiresApproval": "This task must be approved before you can complete it. Approval has already been requested", "taskApprovalHasBeenRequested": "Approval has been requested", "approvals": "Approvals", - "approvalRequired": "Approval Required" + "approvalRequired": "Approval Required", + "groupTasksByChallenge": "Group tasks by challenge title" } diff --git a/website/common/script/libs/taskDefaults.js b/website/common/script/libs/taskDefaults.js index f5d8c4ad85..d774039c25 100644 --- a/website/common/script/libs/taskDefaults.js +++ b/website/common/script/libs/taskDefaults.js @@ -22,7 +22,9 @@ module.exports = function taskDefaults (task = {}) { tags: [], value: task.type === 'reward' ? 10 : 0, priority: 1, - challenge: {}, + challenge: { + shortName: 'None', + }, reminders: [], attribute: 'str', createdAt: new Date(), // TODO these are going to be overwritten by the server... diff --git a/website/server/models/challenge.js b/website/server/models/challenge.js index 4a317f45f3..2aabbd66ec 100644 --- a/website/server/models/challenge.js +++ b/website/server/models/challenge.js @@ -121,7 +121,7 @@ schema.methods.syncToUser = async function syncChallengeToUser (user) { if (!matchingTask) { // If the task is new, create it matchingTask = new Tasks[chalTask.type](Tasks.Task.sanitize(syncableAttrs(chalTask))); - matchingTask.challenge = {taskId: chalTask._id, id: challenge._id}; + matchingTask.challenge = {taskId: chalTask._id, id: challenge._id, shortName: challenge.shortName}; matchingTask.userId = user._id; user.tasksOrder[`${chalTask.type}s`].push(matchingTask._id); } else { diff --git a/website/server/models/task.js b/website/server/models/task.js index b1af8efc6a..350ac3fd01 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -58,6 +58,7 @@ export let TaskSchema = new Schema({ userId: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']}, // When not set it belongs to a challenge challenge: { + shortName: {type: String, default: 'None'}, id: {type: String, ref: 'Challenge', validate: [validator.isUUID, 'Invalid uuid.']}, // When set (and userId not set) it's the original task taskId: {type: String, ref: 'Task', validate: [validator.isUUID, 'Invalid uuid.']}, // When not set but challenge.id defined it's the original task broken: {type: String, enum: ['CHALLENGE_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED', 'CHALLENGE_CLOSED', 'CHALLENGE_TASK_NOT_FOUND']}, // CHALLENGE_TASK_NOT_FOUND comes from v3 migration diff --git a/website/server/models/user/index.js b/website/server/models/user/index.js index b996405489..0b3af02fdf 100644 --- a/website/server/models/user/index.js +++ b/website/server/models/user/index.js @@ -7,7 +7,7 @@ require('./methods'); // A list of publicly accessible fields (not everything from preferences because there are also a lot of settings tha should remain private) export let publicFields = `preferences.size preferences.hair preferences.skin preferences.shirt - preferences.chair preferences.costume preferences.sleep preferences.background profile stats + preferences.chair preferences.costume preferences.sleep preferences.background preferences.tasks profile stats achievements party backer contributor auth.timestamps items inbox.optOut`; // The minimum amount of data needed when populating multiple users diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index d5dd67e44e..6e5721881d 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -467,6 +467,9 @@ let schema = new Schema({ raisePet: {type: Boolean, default: false}, streak: {type: Boolean, default: false}, }, + tasks: { + groupByChallenge: {type: Boolean, default: false}, + }, improvementCategories: { type: Array, validate: (categories) => { diff --git a/website/views/options/settings/settings.jade b/website/views/options/settings/settings.jade index b2202aa84d..6b32fcacd4 100644 --- a/website/views/options/settings/settings.jade +++ b/website/views/options/settings/settings.jade @@ -1,4 +1,4 @@ -script(type='text/ng-template', id='partials/options.settings.settings.html') +script(type='text/ng-template', id='partials/options.settings.settings.html') .container-fluid .row .personal-options.col-md-6 @@ -65,6 +65,10 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') label=env.t('suppressStreakModal') input(type='checkbox', ng-model='user.preferences.suppressModals.streak', ng-change='set({"preferences.suppressModals.streak": user.preferences.suppressModals.streak?true: false})') + .checkbox + label=env.t('groupTasksByChallenge') + input(type='checkbox', ng-model='user.preferences.tasks.groupByChallenge', ng-change='set({"preferences.tasks.groupByChallenge": user.preferences.tasks.groupByChallenge ? true: false})') + hr button.btn.btn-default(ng-click='showBailey()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('showBaileyPop'))= env.t('showBailey') diff --git a/website/views/shared/tasks/index.jade b/website/views/shared/tasks/index.jade index 495e5e26a9..100a946c22 100644 --- a/website/views/shared/tasks/index.jade +++ b/website/views/shared/tasks/index.jade @@ -3,12 +3,16 @@ // started to get unwieldy include ./task_view/mixins +include ./task-list +include ./task + script(id='templates/habitrpg-tasks.html', type="text/ng-template") .tasks-lists.container-fluid .row - .col-sm-6.col-md-3(ng-repeat='list in lists', ng-class='::{ "rewards-module": list.type==="reward", "new-row-sm": list.type==="todo" }') + .col-sm-6.col-md-3( + ng-repeat='list in lists', + ng-class='::{ "rewards-module": list.type==="reward", "new-row-sm": list.type==="todo" }') .task-column(class='{{::list.type}}s') - include ./task_view/graph h2.task-column_title(class='{{::list.type}}-title') {{::list.header}} @@ -27,13 +31,9 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") | {{env.t('innCheckOut')}} +taskColumnTabs('top') - + // Actual List - ul(class='{{::list.type}}s main-list', ng-show='obj[list.type + "s"].length > 0', hrpg-sort-tasks, ng-if='!$state.includes("options.social.challenges")') - include ./task - //Loads the non-sortable lists for challenges - ul(class='{{::list.type}}s main-list', ng-show='obj[list.type + "s"].length > 0', ng-if='$state.includes("options.social.challenges")') - include ./task + task-list include ./task_view/static_rewards diff --git a/website/views/shared/tasks/task-list.jade b/website/views/shared/tasks/task-list.jade new file mode 100644 index 0000000000..0e7df096c5 --- /dev/null +++ b/website/views/shared/tasks/task-list.jade @@ -0,0 +1,21 @@ +script(id='templates/task-list.html', type="text/ng-template") + ul(ng-init='setObj(obj)', class='{{::list.type}}s main-list', + ng-show='obj[list.type+"s"].length > 0', + hrpg-sort-tasks, + ng-if='showNormalList()') + task + + div(ng-init='setObj(obj); console.log(obj)') + div( + ng-repeat="(key, taskList) in groupedList[list.type]", + ng-if='user.preferences.tasks.groupByChallenge && !$state.includes("options.social.challenges")') + h3 {{key}} + ul(class='{{::list.type}}s main-list', + ng-show='taskList.length > 0', hrpg-sort-tasks) + task + + //Loads the non-sortable lists for challenges + ul(ng-init='setObj(obj)', class='{{::list.type}}s main-list', + ng-show='obj[list.type + "s"].length > 0', + ng-if='showChallengeList()') + task diff --git a/website/views/shared/tasks/task.jade b/website/views/shared/tasks/task.jade index 43201ecb5e..7cd70f521f 100644 --- a/website/views/shared/tasks/task.jade +++ b/website/views/shared/tasks/task.jade @@ -1,15 +1,16 @@ -li(id='task-{{::task._id}}', - ng-repeat='task in obj[list.type+"s"] | filterByTaskInfo: obj.filterQuery | conditionalOrderBy: list.view=="dated":"date"', - class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', - ng-class='{"cast-target":spell && (list.type != "reward"), "locked-task":obj._locked === true}', - ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', - ng-show='shouldShow(task, list, user.preferences)', - popover-trigger='mouseenter', popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}', - data-popover-html="{{taskPopover(task) | markdown}}") +script(id='templates/task.html', type="text/ng-template") + li(id='task-{{::task._id}}', + ng-repeat='task in getTaskList(list, taskList, obj) | filterByTaskInfo: obj.filterQuery | conditionalOrderBy: list.view=="dated":"date"', + class='task {{getClasses(task, user, list, main)}}', + ng-class='{"cast-target":spell && (list.type != "reward"), "locked-task":obj._locked === true}', + ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', + ng-show='shouldShow(task, list, user.preferences)', + popover-trigger='mouseenter', popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}', + data-popover-html="{{taskPopover(task) | markdown}}") - ng-form(name='taskForm') - include ./meta_controls + ng-form(name='taskForm') + include ./meta_controls - include ./task_view/index + include ./task_view/index - div(class='{{obj._id}}{{task._id}}-chart', ng-show='charts[obj._id+task._id]') + div(class='{{obj._id}}{{task._id}}-chart', ng-show='charts[obj._id+task._id]') diff --git a/website/views/shared/tasks/task_view/index.jade b/website/views/shared/tasks/task_view/index.jade index 59bf5b92ad..5b5bede142 100644 --- a/website/views/shared/tasks/task_view/index.jade +++ b/website/views/shared/tasks/task_view/index.jade @@ -16,13 +16,13 @@ ui-keypress='{ 13:"score(task, \'down\')" }' ) a(ng-if='task.down', ng-click='applyingAction || score(task,"down")') span.glyphicon.glyphicon-minus - + // Rewards span(ng-if='::task.type=="reward"') input.task-input.reward.visuallyhidden( type='checkbox', ui-keypress='{ 13:"score(task, \'down\')" }' ) - a.money.btn-buy(ng-class='{highValue: task.value >= 1000}', ng-click='score(task, "down")') + a.money.btn-buy(ng-class='{highValue: task.value >= 1000}', ng-click='score(task, "down");') span.shop_gold span.reward-cost {{task.value}}