Group Plans quick wins (#11107)

* WIP(groups): quickish wins

* WIP(groups): two quick wins
1. Don't show task creation button if user is not leader or manager
2. Don't require JS confirm() for approving tasks

* fix(group-plans): allow delete from options button

* fix(group-plans): update tasksOrder when task deleted

* fix(group-tasks): dismiss notification when user takes action

* refactor(tasks): DRY out create button styling

* fix(group-tasks): sync after claiming/unclaiming
This commit is contained in:
Sabe Jones
2019-04-15 10:48:27 -05:00
committed by GitHub
parent 7a5a856ac6
commit 76ae41875d
13 changed files with 150 additions and 117 deletions

View File

@@ -0,0 +1,81 @@
.create-task-area {
position: absolute;
right: 24px;
top: -23px;
z-index: 999;
height: 60px;
}
.slide-tasks-btns-leave-active, .slide-tasks-btns-enter-active {
max-width: 240px;
overflow-x: hidden;
transition: all 0.3s cubic-bezier(0, 1, 0.5, 1);
}
.slide-tasks-btns-enter, .slide-tasks-btns-leave-to {
max-width: 0;
opacity: 0;
}
.rounded-btn {
margin-left: 8px;
background: $white;
width: 48px;
height: 48px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 100px;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
cursor: pointer;
color: $gray-200;
&:hover:not(.create-btn) {
color: $purple-400;
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
}
.svg-icon {
width: 20px;
height: 20px;
&.icon-habit {
width: 24px;
height: 16px;
}
&.icon-daily {
width: 21.6px;
height: 18px;
}
&.icon-todo {
width: 18px;
height: 18px;
}
&.icon-reward {
width: 23.4px;
height: 18px;
}
}
}
.create-btn {
color: $white;
background-color: $green-10;
.svg-icon {
width: 16px;
height: 16px;
transition: transform 0.3s cubic-bezier(0, 1, 0.5, 1);
}
&.open {
background: $gray-200 !important;
.svg-icon {
transform: rotate(-45deg);
}
}
}

View File

@@ -43,10 +43,6 @@ export default {
if (!this.currentGroup) return false; if (!this.currentGroup) return false;
return this.currentGroup.leader === this.user._id; return this.currentGroup.leader === this.user._id;
}, },
isManager () {
if (!this.currentGroup) return false;
return Boolean(this.currentGroup.managers[this.user._id]);
},
}, },
}; };
</script> </script>

View File

@@ -64,15 +64,24 @@
.d-flex.align-items-center .d-flex.align-items-center
span(v-once) {{ $t('filter') }} span(v-once) {{ $t('filter') }}
.svg-icon.filter-icon(v-html="icons.filter") .svg-icon.filter-icon(v-html="icons.filter")
#create-dropdown.col-12.col-md-4 .create-task-area.d-flex(v-if='canCreateTasks')
b-dropdown.float-right(:right="true", :variant="'success'") transition(name="slide-tasks-btns")
.button-label(slot="button-content") .d-flex(v-if="openCreateBtn")
.svg-icon.positive(v-html="icons.positive") .create-task-btn.rounded-btn(
| {{ $t('addTaskToGroupPlan') }} v-for="type in columns",
b-dropdown-item(v-for="type in columns", :key="type", @click="createTask(type)") :key="type",
span.dropdown-icon-item(v-once) @click="createTask(type)",
span.svg-icon.inline(v-html="icons[type]") v-b-tooltip.hover.bottom="$t(type)",
span.text {{$t(type)}} )
.svg-icon(v-html="icons[type]", :class='`icon-${type}`')
#create-task-btn.create-btn.rounded-btn.btn.btn-success(
@click="openCreateBtn = !openCreateBtn",
:class="{open: openCreateBtn}",
)
.svg-icon(v-html="icons.positive")
b-tooltip(target="create-task-btn", placement="bottom", v-if="!openCreateBtn") {{ $t('addTaskToGroupPlan') }}
.row .row
task-column.col-12.col-md-3( task-column.col-12.col-md-3(
v-for="column in columns", v-for="column in columns",
@@ -81,12 +90,14 @@
:taskListOverride='tasksByType[column]', :taskListOverride='tasksByType[column]',
v-on:editTask="editTask", v-on:editTask="editTask",
v-on:loadGroupCompletedTodos="loadGroupCompletedTodos", v-on:loadGroupCompletedTodos="loadGroupCompletedTodos",
v-on:taskDestroyed="taskDestroyed",
:group='group', :group='group',
:searchText="searchText") :searchText="searchText")
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '~client/assets/scss/colors.scss'; @import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/create-task.scss';
.user-tasks-page { .user-tasks-page {
padding-top: 31px; padding-top: 31px;
@@ -339,6 +350,10 @@ export default {
return tagsByType; return tagsByType;
}, },
canCreateTasks () {
if (!this.group) return false;
return this.group.leader && this.group.leader._id === this.user._id || this.group.managers && Boolean(this.group.managers[this.user._id]);
},
}, },
methods: { methods: {
async load () { async load () {
@@ -400,6 +415,7 @@ export default {
}); });
}, },
createTask (type) { createTask (type) {
this.openCreateBtn = false;
this.taskFormPurpose = 'create'; this.taskFormPurpose = 'create';
this.creatingTask = taskDefaults({type, text: ''}, this.user); this.creatingTask = taskDefaults({type, text: ''}, this.user);
this.workingTask = this.creatingTask; this.workingTask = this.creatingTask;

View File

@@ -349,10 +349,11 @@ import creatorIntro from '../creatorIntro';
import InboxModal from '../userMenu/inbox.vue'; import InboxModal from '../userMenu/inbox.vue';
import notificationMenu from './notificationsDropdown'; import notificationMenu from './notificationsDropdown';
import profileModal from '../userMenu/profileModal'; import profileModal from '../userMenu/profileModal';
import reportFlagModal from '../chat/reportFlagModal';
import sendGemsModal from 'client/components/payments/sendGemsModal'; import sendGemsModal from 'client/components/payments/sendGemsModal';
import sync from 'client/mixins/sync';
import userDropdown from './userDropdown'; import userDropdown from './userDropdown';
import reportFlagModal from '../chat/reportFlagModal';
export default { export default {
components: { components: {
@@ -364,6 +365,7 @@ export default {
sendGemsModal, sendGemsModal,
userDropdown, userDropdown,
}, },
mixins: [sync],
data () { data () {
return { return {
isUserDropdownOpen: false, isUserDropdownOpen: false,
@@ -403,14 +405,6 @@ export default {
toggleUserDropdown () { toggleUserDropdown () {
this.isUserDropdownOpen = !this.isUserDropdownOpen; this.isUserDropdownOpen = !this.isUserDropdownOpen;
}, },
async sync () {
this.$root.$emit('habitica::resync-requested');
await Promise.all([
this.$store.dispatch('user:fetch', {forceLoad: true}),
this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}),
]);
this.$root.$emit('habitica::resync-completed');
},
async getUserGroupPlans () { async getUserGroupPlans () {
this.$store.state.groupPlans = await this.$store.dispatch('guilds:getGroupPlans'); this.$store.state.groupPlans = await this.$store.dispatch('guilds:getGroupPlans');
}, },

View File

@@ -4,6 +4,7 @@ base-notification(
:has-icon="false", :has-icon="false",
:notification="notification", :notification="notification",
@click="action", @click="action",
ref="taskApprovalNotification",
) )
div(slot="content") div(slot="content")
div(v-html="notification.data.message") div(v-html="notification.data.message")
@@ -43,12 +44,12 @@ export default {
return; return;
} }
if (!confirm(this.$t('confirmApproval'))) return; await this.$store.dispatch('tasks:approve', {
this.$store.dispatch('tasks:approve', {
taskId: this.notification.data.groupTaskId, taskId: this.notification.data.groupTaskId,
userId: this.notification.data.userId, userId: this.notification.data.userId,
}); });
this.$refs.taskApprovalNotification.remove();
}, },
async needsWork () { async needsWork () {
// Redirect users to the group tasks page if the notification doesn't have data // Redirect users to the group tasks page if the notification doesn't have data
@@ -62,11 +63,13 @@ export default {
if (!confirm(this.$t('confirmNeedsWork'))) return; if (!confirm(this.$t('confirmNeedsWork'))) return;
this.$store.dispatch('tasks:needsWork', { await this.$store.dispatch('tasks:needsWork', {
taskId: this.notification.data.groupTaskId, taskId: this.notification.data.groupTaskId,
userId: this.notification.data.userId, userId: this.notification.data.userId,
}); });
this.$refs.taskApprovalNotification.remove();
}, },
}, },
}; };
</script> </script>

View File

@@ -35,8 +35,10 @@ div
import findIndex from 'lodash/findIndex'; import findIndex from 'lodash/findIndex';
import { mapState } from 'client/libs/store'; import { mapState } from 'client/libs/store';
import approvalModal from './approvalModal'; import approvalModal from './approvalModal';
import sync from 'client/mixins/sync';
export default { export default {
mixins: [sync],
props: ['task', 'group'], props: ['task', 'group'],
components: { components: {
approvalModal, approvalModal,
@@ -91,11 +93,12 @@ export default {
taskId = this.task.group.taskId; taskId = this.task.group.taskId;
} }
this.$store.dispatch('tasks:assignTask', { await this.$store.dispatch('tasks:assignTask', {
taskId, taskId,
userId: this.user._id, userId: this.user._id,
}); });
this.task.group.assignedUsers.push(this.user._id); this.task.group.assignedUsers.push(this.user._id);
this.sync();
}, },
async unassign () { async unassign () {
if (!confirm(this.$t('confirmUnClaim'))) return; if (!confirm(this.$t('confirmUnClaim'))) return;
@@ -106,15 +109,16 @@ export default {
taskId = this.task.group.taskId; taskId = this.task.group.taskId;
} }
this.$store.dispatch('tasks:unassignTask', { await this.$store.dispatch('tasks:unassignTask', {
taskId, taskId,
userId: this.user._id, userId: this.user._id,
}); });
let index = this.task.group.assignedUsers.indexOf(this.user._id); let index = this.task.group.assignedUsers.indexOf(this.user._id);
this.task.group.assignedUsers.splice(index, 1); this.task.group.assignedUsers.splice(index, 1);
this.sync();
}, },
approve () { approve () {
if (!confirm(this.$t('confirmApproval'))) return;
let userIdToApprove = this.task.group.assignedUsers[0]; let userIdToApprove = this.task.group.assignedUsers[0];
this.$store.dispatch('tasks:approve', { this.$store.dispatch('tasks:approve', {
taskId: this.task._id, taskId: this.task._id,

View File

@@ -24,7 +24,6 @@ export default {
props: ['task'], props: ['task'],
methods: { methods: {
approve (index) { approve (index) {
if (!confirm(this.$t('confirmApproval'))) return;
let userIdToApprove = this.task.group.assignedUsers[index]; let userIdToApprove = this.task.group.assignedUsers[index];
this.$store.dispatch('tasks:approve', { this.$store.dispatch('tasks:approve', {
taskId: this.task._id, taskId: this.task._id,

View File

@@ -39,7 +39,7 @@
@update='taskSorted', @update='taskSorted',
@start="isDragging(true)", @start="isDragging(true)",
@end="isDragging(false)", @end="isDragging(false)",
:options='{disabled: activeFilter.label === "scheduled", scrollSensitivity: 64}', :options='{disabled: activeFilter.label === "scheduled" || !isUser, scrollSensitivity: 64}',
) )
task( task(
v-for="task in taskList", v-for="task in taskList",
@@ -48,6 +48,7 @@
@editTask="editTask", @editTask="editTask",
@moveTo="moveTo", @moveTo="moveTo",
:group='group', :group='group',
v-on:taskDestroyed='taskDestroyed'
) )
template(v-if="hasRewardsList") template(v-if="hasRewardsList")
draggable.reward-items( draggable.reward-items(
@@ -691,6 +692,9 @@ export default {
document.documentElement.classList.remove('draggable-cursor'); document.documentElement.classList.remove('draggable-cursor');
} }
}, },
taskDestroyed (task) {
this.$emit('taskDestroyed', task);
},
}, },
}; };
</script> </script>

View File

@@ -17,7 +17,7 @@
.d-flex.justify-content-between .d-flex.justify-content-between
h3.task-title(:class="{ 'has-notes': task.notes }", v-markdown="task.text") h3.task-title(:class="{ 'has-notes': task.notes }", v-markdown="task.text")
menu-dropdown.task-dropdown( menu-dropdown.task-dropdown(
v-if="isUser && !isRunningYesterdailies", v-if="!isRunningYesterdailies",
:right="task.type === 'reward'", :right="task.type === 'reward'",
ref="taskDropdown", ref="taskDropdown",
v-b-tooltip.hover.top="$t('options')" v-b-tooltip.hover.top="$t('options')"
@@ -29,11 +29,11 @@
span.dropdown-icon-item span.dropdown-icon-item
span.svg-icon.inline.edit-icon(v-html="icons.edit") span.svg-icon.inline.edit-icon(v-html="icons.edit")
span.text {{ $t('edit') }} span.text {{ $t('edit') }}
.dropdown-item(@click="moveToTop") .dropdown-item(v-if='isUser', @click="moveToTop")
span.dropdown-icon-item span.dropdown-icon-item
span.svg-icon.inline.push-to-top(v-html="icons.top") span.svg-icon.inline.push-to-top(v-html="icons.top")
span.text {{ $t('taskToTop') }} span.text {{ $t('taskToTop') }}
.dropdown-item(@click="moveToBottom") .dropdown-item(v-if='isUser', @click="moveToBottom")
span.dropdown-icon-item span.dropdown-icon-item
span.svg-icon.inline.push-to-bottom(v-html="icons.bottom") span.svg-icon.inline.push-to-bottom(v-html="icons.bottom")
span.text {{ $t('taskToBottom') }} span.text {{ $t('taskToBottom') }}

View File

@@ -105,6 +105,7 @@
<style lang="scss" scoped> <style lang="scss" scoped>
@import '~client/assets/scss/colors.scss'; @import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/create-task.scss';
.user-tasks-page { .user-tasks-page {
padding-top: 16px; padding-top: 16px;
@@ -118,87 +119,6 @@
margin-bottom: 20px; margin-bottom: 20px;
} }
.create-task-area {
position: absolute;
right: 24px;
top: -40px;
z-index: 999;
height: 60px;
}
.slide-tasks-btns-leave-active, .slide-tasks-btns-enter-active {
max-width: 240px;
overflow-x: hidden;
transition: all 0.3s cubic-bezier(0, 1, 0.5, 1);
}
.slide-tasks-btns-enter, .slide-tasks-btns-leave-to {
max-width: 0;
opacity: 0;
}
.rounded-btn {
margin-left: 8px;
background: $white;
width: 48px;
height: 48px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 100px;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
cursor: pointer;
color: $gray-200;
&:hover:not(.create-btn) {
color: $purple-400;
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
}
.svg-icon {
width: 20px;
height: 20px;
&.icon-habit {
width: 24px;
height: 16px;
}
&.icon-daily {
width: 21.6px;
height: 18px;
}
&.icon-todo {
width: 18px;
height: 18px;
}
&.icon-reward {
width: 23.4px;
height: 18px;
}
}
}
.create-btn {
color: $white;
background-color: $green-10;
.svg-icon {
width: 16px;
height: 16px;
transition: transform 0.3s cubic-bezier(0, 1, 0.5, 1);
}
&.open {
background: $gray-200 !important;
.svg-icon {
transform: rotate(-45deg);
}
}
}
.filter-icon { .filter-icon {
width: 16px; width: 16px;
height: 16px; height: 16px;

View File

@@ -0,0 +1,12 @@
export default {
methods: {
async sync () {
this.$root.$emit('habitica::resync-requested');
await Promise.all([
this.$store.dispatch('user:fetch', {forceLoad: true}),
this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}),
]);
this.$root.$emit('habitica::resync-completed');
},
},
};

View File

@@ -484,7 +484,7 @@
"whatIsGroupManager": "What is a Group Manager?", "whatIsGroupManager": "What is a Group Manager?",
"whatIsGroupManagerDesc": "A Group Manager is a user role that do not have access to the group's billing details, but can create, assign, and approve shared Tasks for the Group's members. Promote Group Managers from the Groups member list.", "whatIsGroupManagerDesc": "A Group Manager is a user role that do not have access to the group's billing details, but can create, assign, and approve shared Tasks for the Group's members. Promote Group Managers from the Groups member list.",
"goToTaskBoard": "Go to Task Board", "goToTaskBoard": "Go to Task Board",
"sharedCompletion": "Shared Completion", "sharedCompletion": "Completion Conditon",
"recurringCompletion": "None - Group task does not complete", "recurringCompletion": "None - Group task does not complete",
"singleCompletion": "Single - Completes when any assigned user finishes", "singleCompletion": "Single - Completes when any assigned user finishes",
"allAssignedCompletion": "All - Completes when all assigned users finish" "allAssignedCompletion": "All - Completes when all assigned users finish"

View File

@@ -1418,6 +1418,10 @@ schema.methods.removeTask = async function groupRemoveTask (task) {
}, { }, {
$set: {'group.broken': 'TASK_DELETED'}, $set: {'group.broken': 'TASK_DELETED'},
}, {multi: true}).exec(); }, {multi: true}).exec();
removeFromArray(group.tasksOrder[`${task.type}s`], task._id);
group.markModified('tasksOrder');
return await group.save();
}; };
// Returns true if the user has reached the spam message limit // Returns true if the user has reached the spam message limit